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

Add functionStaticCall and functionDelegateCall methods to Address library #2333

Merged
merged 15 commits into from
Sep 17, 2020
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 3.3.0 (unreleased)

* `Address`: added `functionStaticCall` and `functionDelegateCall`, similar to the existing `functionCall`. ([#2333](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2333))

## 3.2.0 (2020-09-10)

### New features
Expand Down
12 changes: 11 additions & 1 deletion 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 {
string public sharedAnswer;

event CallReturnValue(string data);

function isContract(address account) external view returns (bool) {
Expand All @@ -17,13 +19,21 @@ contract AddressImpl {

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)));
}

function functionStaticCall(address target, bytes calldata data) external {
bytes memory returnData = Address.functionStaticCall(target, data);
emit CallReturnValue(abi.decode(returnData, (string)));
}

function functionDelegateCall(address target, bytes calldata data) external {
bytes memory returnData = Address.functionDelegateCall(target, data);
emit CallReturnValue(abi.decode(returnData, (string)));
}

Expand Down
10 changes: 10 additions & 0 deletions contracts/mocks/CallReceiverMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pragma solidity ^0.6.0;

contract CallReceiverMock {
string public sharedAnswer;

event MockFunctionCalled();

Expand All @@ -20,6 +21,10 @@ contract CallReceiverMock {
return "0x1234";
}

function mockStaticFunction() public pure returns (string memory) {
return "0x1234";
}

function mockFunctionRevertsNoReason() public payable {
revert();
}
Expand All @@ -37,4 +42,9 @@ contract CallReceiverMock {
_array.push(i);
}
}

function mockFunctionWritesStorage() public returns (string memory) {
sharedAnswer = "42";
return "0x1234";
}
}
58 changes: 53 additions & 5 deletions contracts/utils/Address.sol
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ library Address {
* _Available since v3.1._
*/
function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) {
return _functionCallWithValue(target, data, 0, errorMessage);
return functionCallWithValue(target, data, 0, errorMessage);
}

/**
Expand All @@ -113,14 +113,62 @@ library Address {
*/
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");
return _functionCallWithValue(target, data, value, errorMessage);
require(isContract(target), "Address: call to non-contract");

// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returndata) = target.call{ value: value }(data);
return _verifyCallResult(success, returndata, 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");
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
return functionStaticCall(target, data, "Address: low-level static call failed");
}

/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(address target, bytes memory data, string memory errorMessage) internal view returns (bytes memory) {
require(isContract(target), "Address: static call to non-contract");

// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returndata) = target.staticcall(data);
return _verifyCallResult(success, returndata, errorMessage);
}

/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.3._
*/
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}

/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.3._
*/
function functionDelegateCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) {
require(isContract(target), "Address: delegate call to non-contract");

// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returndata) = target.call{ value: weiValue }(data);
(bool success, bytes memory returndata) = target.delegatecall(data);
return _verifyCallResult(success, returndata, errorMessage);
}

function _verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) private pure returns(bytes memory) {
if (success) {
return returndata;
} else {
Expand Down
106 changes: 105 additions & 1 deletion test/utils/Address.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ describe('Address', function () {
// which cause a mockFunctionOutOfGas function to crash Ganache and the
// subsequent tests before running out of gas.
it('reverts when the called function runs out of gas', async function () {
this.timeout(10000);
if (coverage) { return this.skip(); }
const abiEncodedCall = web3.eth.abi.encodeFunctionCall({
name: 'mockFunctionOutOfGas',
Expand All @@ -154,7 +155,7 @@ describe('Address', function () {
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({
Expand Down Expand Up @@ -285,4 +286,107 @@ describe('Address', function () {
});
});
});

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

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

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

expectEvent(receipt, 'CallReturnValue', { data: '0x1234' });
});

it('reverts on a non-static function', async function () {
const abiEncodedCall = web3.eth.abi.encodeFunctionCall({
name: 'mockFunction',
type: 'function',
inputs: [],
}, []);

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

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

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

it('reverts when address is not a contract', async function () {
const [ recipient ] = accounts;
const abiEncodedCall = web3.eth.abi.encodeFunctionCall({
name: 'mockFunction',
type: 'function',
inputs: [],
}, []);
await expectRevert(
this.mock.functionStaticCall(recipient, abiEncodedCall),
'Address: static call to non-contract',
);
});
});

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

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

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

expectEvent(receipt, 'CallReturnValue', { data: '0x1234' });

expect(await this.mock.sharedAnswer()).to.equal('42');
});

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

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

it('reverts when address is not a contract', async function () {
const [ recipient ] = accounts;
const abiEncodedCall = web3.eth.abi.encodeFunctionCall({
name: 'mockFunction',
type: 'function',
inputs: [],
}, []);
await expectRevert(
this.mock.functionDelegateCall(recipient, abiEncodedCall),
'Address: delegate call to non-contract',
);
});
});
});