From ce381d43dbb4d85259fad81ed62e421e8a3563a6 Mon Sep 17 00:00:00 2001 From: Logan Nguyen Date: Tue, 7 Nov 2023 12:23:38 -0600 Subject: [PATCH] feat: added ContractCaller solidity example contract (#534) (#535) feat: added ContractCaller solidity example contract Signed-off-by: Logan Nguyen --- .../yul/contract-caller/ContractCaller.sol | 106 ++++++++++++ contracts/yul/math-coverage/MatchCoverage.sol | 2 +- test/yul/contract-caller/ContractCaller.js | 158 ++++++++++++++++++ 3 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 contracts/yul/contract-caller/ContractCaller.sol create mode 100644 test/yul/contract-caller/ContractCaller.js diff --git a/contracts/yul/contract-caller/ContractCaller.sol b/contracts/yul/contract-caller/ContractCaller.sol new file mode 100644 index 000000000..1d921627c --- /dev/null +++ b/contracts/yul/contract-caller/ContractCaller.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +contract TargetContract { + uint256 count; + + constructor(uint256 _count) { + count = _count; + } + + function setCount(uint256 _count) external { + count = _count; + } + + function getCount() external view returns (uint256) { + return count; + } +} + + +contract ContractCaller { + uint256 public count; + + event CallResult(bool success); + event CallReturnedData(uint256 count); + + /// call(g, a, v, in, insize, out, outsize) + /// `g`: Amount of gas to be provided for the execution of called contract + /// `a`: Address of the called contract + /// `v`: callvalue() (a.k.a. msg.value) + /// `in`: Input data that will be provided to the called contract + /// `insize`: Input data size + /// `out`: Output data produced by the called contract + /// `outsize`: Output data size + function call(uint256 gasLimit, address payable _targetContractAddress, bytes memory input) external payable { + bool success; + uint256 returnedData; + + assembly { + let returnedDataPlaceholder := mload(0x40) // load the data at free memory pointer + + success := call(gasLimit, _targetContractAddress, callvalue(), add(input, 0x20), mload(input), returnedDataPlaceholder, 0x20) + + returnedData := mload(returnedDataPlaceholder) + } + + emit CallResult(success); + emit CallReturnedData(returnedData); + } + + /// staticcall(g, a, in, insize, out, outsize) - identical to `call` but do not allow state modifications + /// `g`: Amount of gas to be provided for the execution of called contract + /// `a`: Address of the called contract + /// `in`: Input data that will be provided to the called contract + /// `insize`: Input data size + /// `out`: Output data produced by the called contract + /// `outsize`: Output data size + function staticcall(uint256 gasLimit, address payable _targetContractAddress, bytes memory input) external { + bool success; + uint256 returnedData; + + assembly { + let returnedDataPlaceholder := mload(0x40) // load the data at free memory pointer + + success := staticcall(gasLimit, _targetContractAddress, add(input, 0x20), mload(input), returnedDataPlaceholder, 0x20) + + returnedData := mload(returnedDataPlaceholder) + } + + emit CallResult(success); + emit CallReturnedData(returnedData); + } + + + /// callcode(g, a, v, in, insize, out, outsize) - identical to `call` but only use the code from a and stay in the context of the current contract otherwise + /// `g`: Amount of gas to be provided for the execution of called contract + /// `a`: Address of the called contract + /// `in`: Input data that will be provided to the called contract + /// `insize`: Input data size + /// `out`: Output data produced by the called contract + /// `outsize`: Output data size + function callCode(uint256 gasLimit, address payable _targetContractAddress, bytes memory input) external payable { + bool success; + assembly { + /// @notice callcode uses the code from `_targetContractAddress` to update current contract's states + success := callcode(gasLimit, _targetContractAddress, callvalue(), add(input, 0x20), mload(input), 0, 0) + } + emit CallResult(success); + } + + /// delegatecall(g, a, in, insize, out, outsize) - identical to `callcode` but also keep caller and callvalue + /// `g`: Amount of gas to be provided for the execution of called contract + /// `a`: Address of the called contract + /// `in`: Input data that will be provided to the called contract + /// `insize`: Input data size + /// `out`: Output data produced by the called contract + /// `outsize`: Output data size + function delegateCall(uint256 gasLimit, address payable _targetContractAddress, bytes memory input) external { + bool success; + assembly { + /// @notice delegatecall uses the code from `_targetContractAddress` to update current contract's states + success := delegatecall(gasLimit, _targetContractAddress, add(input, 0x20), mload(input), 0, 0) + } + emit CallResult(success); + } +} diff --git a/contracts/yul/math-coverage/MatchCoverage.sol b/contracts/yul/math-coverage/MatchCoverage.sol index 8c661e7c2..e08591cb8 100644 --- a/contracts/yul/math-coverage/MatchCoverage.sol +++ b/contracts/yul/math-coverage/MatchCoverage.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; contract MathCoverage { diff --git a/test/yul/contract-caller/ContractCaller.js b/test/yul/contract-caller/ContractCaller.js new file mode 100644 index 000000000..8b0eaf12f --- /dev/null +++ b/test/yul/contract-caller/ContractCaller.js @@ -0,0 +1,158 @@ +/*- + * + * Hedera Smart Contracts + * + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +const { expect } = require('chai') +const { ethers } = require('hardhat') + +describe('@solidityequiv5 Contract Caller Tests', async () => { + let contractCaller, targetContract, getCountEncodedSig, setCountEncodedSig + const COUNT_A = 3 + const GAS = 1_000_000 + const INITIAL_COUNT = 9 + + beforeEach(async () => { + // deploy contracts + const contractCallerFactory = await ethers.getContractFactory( + 'ContractCaller' + ) + const targetContractFactory = await ethers.getContractFactory( + 'TargetContract' + ) + contractCaller = await contractCallerFactory.deploy() + targetContract = await targetContractFactory.deploy(INITIAL_COUNT) + + // prepare encoded function signatures + getCountEncodedSig = + targetContract.interface.encodeFunctionData('getCount()') + + setCountEncodedSig = targetContract.interface.encodeFunctionData( + 'setCount(uint256)', + [COUNT_A] + ) + }) + + it('Should execute call(g, a, v, in, insize, out, outsize)', async () => { + // prepare transactions + const callSetCountTx = await contractCaller.call( + GAS, + targetContract.address, + setCountEncodedSig + ) + const callGetCountTx = await contractCaller.call( + GAS, + targetContract.address, + getCountEncodedSig + ) + + // wait for the receipts + const callSetCountReceipt = await callSetCountTx.wait() + const callGetCountReceipt = await callGetCountTx.wait() + + // extract events + const [callSetCountResult] = callSetCountReceipt.events.map( + (e) => e.event === 'CallResult' && e + )[0].args + const [callGetCountResult] = callGetCountReceipt.events.map( + (e) => e.event === 'CallResult' && e + )[0].args + const [callGetCountReturnedData] = callGetCountReceipt.events.map( + (e) => e.event === 'CallReturnedData' && e + )[1].args + + // assertion + expect(callSetCountResult).to.be.true + expect(callGetCountResult).to.be.true + expect(callGetCountReturnedData).to.eq(COUNT_A) + }) + + it('Should execute staticcall(g, a, in, insize, out, outsize)', async () => { + // prepare transactions + const callGetCountTx = await contractCaller.staticcall( + GAS, + targetContract.address, + getCountEncodedSig + ) + + // wait for the receipts + const callGetCountReceipt = await callGetCountTx.wait() + + // extract events + const [callGetCountResult] = callGetCountReceipt.events.map( + (e) => e.event === 'CallResult' && e + )[0].args + const [callGetCountReturnedData] = callGetCountReceipt.events.map( + (e) => e.event === 'CallReturnedData' && e + )[1].args + + // assertion + expect(callGetCountResult).to.be.true + expect(callGetCountReturnedData).to.eq(INITIAL_COUNT) + }) + + it('Should execute callcode(g, a, v, in, insize, out, outsize)', async () => { + // prepare transactions + const callSetCountTx = await contractCaller.callCode( + GAS, + targetContract.address, + setCountEncodedSig + ) + + // wait for the receipts + const callSetCountReceipt = await callSetCountTx.wait() + + // extract events + const [callSetCountResult] = callSetCountReceipt.events.map( + (e) => e.event === 'CallResult' && e + )[0].args + + // get storage count within ContractCaller contract + const storageCount = await contractCaller.count() + + // @notice since callcode use the code from `targetContract` to update `ContractCaller` contract + // => `storageCount` is expected to equal `COUNT_A` + expect(storageCount).to.eq(COUNT_A) + expect(callSetCountResult).to.be.true + }) + + it('Should execute delegatecall(g, a, in, insize, out, outsize)', async () => { + // prepare transactions + const callSetCountTx = await contractCaller.delegateCall( + GAS, + targetContract.address, + setCountEncodedSig + ) + + // wait for the receipts + const callSetCountReceipt = await callSetCountTx.wait() + + // extract events + const [callSetCountResult] = callSetCountReceipt.events.map( + (e) => e.event === 'CallResult' && e + )[0].args + + // get storage count within ContractCaller contract + const storageCount = await contractCaller.count() + + // @notice since callcode use the code from `targetContract` to update `ContractCaller` contract + // => `storageCount` is expected to equal `COUNT_A` + expect(storageCount).to.eq(COUNT_A) + expect(callSetCountResult).to.be.true + }) +})