Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add wrapper function for low level calls #2264

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions contracts/mocks/AddressImpl.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ pragma solidity ^0.6.0;
import "../utils/Address.sol";

contract AddressImpl {
event CallReturnValue(string data);

function isContract(address account) external view returns (bool) {
return Address.isContract(account);
}
Expand All @@ -13,8 +15,16 @@ contract AddressImpl {
Address.sendValue(receiver, amount);
}

function functionCall(address target, bytes calldata data) external returns (bytes memory) {
return Address.functionCall(target, data);
function functionCall(address target, bytes calldata data) external {
bytes memory returnData = Address.functionCall(target, data);

emit CallReturnValue(abi.decode(returnData, (string)));
}

function functionCallWithValue(address target, bytes calldata data, uint256 value) external payable {
bytes memory returnData = Address.functionCallWithValue(target, data, value);

emit CallReturnValue(abi.decode(returnData, (string)));
}

// sendValue's tests require the contract to hold Ether
Expand Down
32 changes: 28 additions & 4 deletions contracts/mocks/CallReceiverMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,38 @@
pragma solidity ^0.6.0;

contract CallReceiverMock {

event MockFunctionCalled();

function mockFunction() public {

uint256[] private _array;

function mockFunction() public payable returns (string memory) {
emit MockFunctionCalled();

return "0x1234";
}

function mockFunctionNonPayable() public returns (string memory) {
emit MockFunctionCalled();

return "0x1234";
}

function mockFunctionReverts() public pure {
function mockFunctionRevertsNoReason() public payable {
revert();
}

function mockFunctionRevertsReason() public payable {
revert("CallReceiverMock: reverting");
}

function mockFunctionThrows() public payable {
assert(false);
}

function mockFunctionOutOfGas() public payable {
for (uint256 i = 0; ; ++i) {
_array.push(i);
}
}
}
65 changes: 51 additions & 14 deletions contracts/utils/Address.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,30 +59,69 @@ library Address {
}

/**
* @dev Replacement for Solidity's low-level `call`: performs a low-level
* call with `data` to the target address `target`. Returns the `returndata`
* provided by the low-level call.
* @dev Performs a Solidity function call using a low level `call`. A
* plain`call` is an unsafe replacement for a function call: use this
* function instead.
*
* The call is not executed if the target address is not a contract.
* If `target` reverts with a revert reason, it is bubbled up by this
* function (like regular Solidity function calls).
*
* Requirements:
*
* - `target` must be a contract.
* - calling `target` with `data` must not revert.
*/
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCall(target, data, "Address: low-level call failed");
}

/**
* @dev Replacement for Solidity's low-level `call`: performs a low-level
* call with `data` to the target address `target`. Returns the `returndata`
* provided by the low-level call. Uses `errorMessage` as default revert message.

* The call is not executed if the target address is not a contract.
* @dev Same as {Address-functionCall-address-bytes-}, but with
* `errorMessage` as a custom revert reason when `target` reverts.
nventuro marked this conversation as resolved.
Show resolved Hide resolved
*/
function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) {
return _functionCallWithValue(target, data, 0, errorMessage);
}

/**
* @dev Performs a Solidity function call using a low level `call`,
* transferring `value` wei. A plain`call` is an unsafe replacement for a
* function call: use this function instead.
*
* If `target` reverts with a revert reason, it is bubbled up by this
* function (like regular Solidity function calls).
*
* Requirements:
*
* - `target` must be a contract.
* - the calling contract must have an ETH balance of at least `value`.
* - calling `target` with `data` must not revert.
*/
function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}

/**
* @dev Same as {Address-functionCallWithValue-address-bytes-uint256-}, but
* with `errorMessage` as a custom revert reason when `target` reverts.
nventuro marked this conversation as resolved.
Show resolved Hide resolved
*/
function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
frangio marked this conversation as resolved.
Show resolved Hide resolved
return _functionCallWithValue(target, data, value, errorMessage);
}

function _functionCallWithValue(address target, bytes memory data, uint256 weiValue, string memory errorMessage) private returns (bytes memory) {
require(isContract(target), "Address: call to non-contract");

// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returndata) = target.call(data);
if (!success) {
(bool success, bytes memory returndata) = target.call{ value: weiValue }(data);
if (success) {
return returndata;
} else {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly

// solhint-disable-next-line no-inline-assembly
assembly {
let returndata_size := mload(returndata)
Expand All @@ -91,8 +130,6 @@ library Address {
} else {
revert(errorMessage);
}
} else {
return returndata;
}
}
}
}
138 changes: 132 additions & 6 deletions test/utils/Address.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { expect } = require('chai');

const AddressImpl = contract.fromArtifact('AddressImpl');
const EtherReceiver = contract.fromArtifact('EtherReceiverMock');
const CallReceiver = contract.fromArtifact('CallReceiverMock');
const CallReceiverMock = contract.fromArtifact('CallReceiverMock');

describe('Address', function () {
const [ recipient, other ] = accounts;
Expand Down Expand Up @@ -94,7 +94,7 @@ describe('Address', function () {

describe('functionCall', function () {
beforeEach(async function () {
this.contractRecipient = await CallReceiver.new();
this.contractRecipient = await CallReceiverMock.new();
});

context('with valid contract receiver', function () {
Expand All @@ -104,16 +104,59 @@ describe('Address', function () {
type: 'function',
inputs: [],
}, []);
const { tx } = await this.mock.functionCall(this.contractRecipient.address, abiEncodedCall);
await expectEvent.inTransaction(tx, CallReceiver, 'MockFunctionCalled');

const receipt = await this.mock.functionCall(this.contractRecipient.address, abiEncodedCall);

expectEvent(receipt, 'CallReturnValue', { data: '0x1234' });
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
});

it('reverts when the called function reverts with no reason', async function () {
const abiEncodedCall = web3.eth.abi.encodeFunctionCall({
name: 'mockFunctionRevertsNoReason',
type: 'function',
inputs: [],
}, []);

await expectRevert(
this.mock.functionCall(this.contractRecipient.address, abiEncodedCall),
'Address: low-level call failed'
);
});

it('reverts when the called function reverts, bubbling up the revert reason', async function () {
const abiEncodedCall = web3.eth.abi.encodeFunctionCall({
name: 'mockFunctionRevertsReason',
type: 'function',
inputs: [],
}, []);

await expectRevert(
this.mock.functionCall(this.contractRecipient.address, abiEncodedCall),
'CallReceiverMock: reverting'
);
});

it('reverts when the called function reverts', async function () {
it('reverts when the called function runs out of gas', async function () {
const abiEncodedCall = web3.eth.abi.encodeFunctionCall({
name: 'mockFunctionReverts',
name: 'mockFunctionOutOfGas',
type: 'function',
inputs: [],
}, []);

await expectRevert(
this.mock.functionCall(this.contractRecipient.address, abiEncodedCall),
'Address: low-level call failed'
);
}).timeout(5000);

it('reverts when the called function throws', async function () {
const abiEncodedCall = web3.eth.abi.encodeFunctionCall({
name: 'mockFunctionThrows',
type: 'function',
inputs: [],
}, []);

await expectRevert(
this.mock.functionCall(this.contractRecipient.address, abiEncodedCall),
'Address: low-level call failed'
Expand All @@ -126,6 +169,7 @@ describe('Address', function () {
type: 'function',
inputs: [],
}, []);

await expectRevert(
this.mock.functionCall(this.contractRecipient.address, abiEncodedCall),
'Address: low-level call failed'
Expand All @@ -145,4 +189,86 @@ describe('Address', function () {
});
});
});

describe('functionCallWithValue', function () {
beforeEach(async function () {
this.contractRecipient = await CallReceiverMock.new();
});

context('with zero value', function () {
it('calls the requested function', async function () {
const abiEncodedCall = web3.eth.abi.encodeFunctionCall({
name: 'mockFunction',
type: 'function',
inputs: [],
}, []);

const receipt = await this.mock.functionCallWithValue(this.contractRecipient.address, abiEncodedCall, 0);

expectEvent(receipt, 'CallReturnValue', { data: '0x1234' });
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
});
});

context('with non-zero value', function () {
const amount = ether('1.2');

it('reverts if insufficient sender balance', async function () {
const abiEncodedCall = web3.eth.abi.encodeFunctionCall({
name: 'mockFunction',
type: 'function',
inputs: [],
}, []);

await expectRevert(
this.mock.functionCallWithValue(this.contractRecipient.address, abiEncodedCall, amount),
'Address: insufficient balance for call'
);
});

it('calls the requested function with existing value', async function () {
nventuro marked this conversation as resolved.
Show resolved Hide resolved
const abiEncodedCall = web3.eth.abi.encodeFunctionCall({
name: 'mockFunction',
type: 'function',
inputs: [],
}, []);

await send.ether(other, this.mock.address, amount);
const receipt = await this.mock.functionCallWithValue(this.contractRecipient.address, abiEncodedCall, amount);

expectEvent(receipt, 'CallReturnValue', { data: '0x1234' });
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
});

it('calls the requested function with transaction funds', async function () {
const abiEncodedCall = web3.eth.abi.encodeFunctionCall({
name: 'mockFunction',
type: 'function',
inputs: [],
}, []);

expect(await balance.current(this.mock.address)).to.be.bignumber.equal('0');
const receipt = await this.mock.functionCallWithValue(
this.contractRecipient.address, abiEncodedCall, amount, { from: other, value: amount }
);

expectEvent(receipt, 'CallReturnValue', { data: '0x1234' });
await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled');
});

it('reverts when calling non-payable functions', async function () {
const abiEncodedCall = web3.eth.abi.encodeFunctionCall({
name: 'mockFunctionNonPayable',
type: 'function',
inputs: [],
}, []);

await send.ether(other, this.mock.address, amount);
await expectRevert(
this.mock.functionCallWithValue(this.contractRecipient.address, abiEncodedCall, amount),
'Address: low-level call with value failed'
);
});
});
});
});