Incorrectly casting interface experience
— Learning — 2 min read
A surprising result that was hinted at by an issue on the Cooler audit is that a call to a wrongly casted interface's function may revert.
We all knew that the function selector used to determine which function will be executed on a contract is the first four bytes of the hash of its signature: keccak256(signature)[:4]
.
The signature being the name of the function and the type of its arguments, i.e. transfer(address,uint256)
for an ERC20 token transfer function. The signature does not take into account the returned
value type.
It is logical that a call using the wrong signature would fail. But it is less intuitive that a call to an interface providing the wrong return value also fails.
The tested contract is the following:
1pragma solidity 0.8.17;2
3
4contract ERC20 {5 function transfer(address to, uint256 amount) public returns (bool) {6 return true;7 }8}9
10contract WrongERC20 {11 bool public worked;12
13 function transfer(address to, uint256 amount) public {14 worked = true;15 }16}17
18contract Test {19 ERC20 public token;20
21 constructor(address _token) {22 token = ERC20(_token); // initialized with WrongERC20 address23 }24
25 function callTransfer() public {26 token.transfer(address(this), 0); // will revert27 }28}
The Test
contract is initialized with the address of a WrongERC20
contract.
When callTransfer() is called, the call reverts and the transaction fails.
I believe the reason is that the code of Test
attempts to decode the return value of token.transfer()
as a boolean and reverts when it fails to do so (because WrongERC20.transfer()
returns no value).
This further justifies why if I switch the return values around and make ERC20.transfer()
return nothing and WrongERC20.transfer()
return a boolean, the call to Test.callTransfer()
no longer
reverts.
1pragma solidity 0.8.17;2
3
4contract ERC20 {5 function transfer(address to, uint256 amount) public {6 return true;7 }8}9
10contract WrongERC20 {11 bool public worked;12
13 function transfer(address to, uint256 amount) public returns (bool) {14 worked = true;15 }16}17
18contract Test {19 ERC20 public token;20
21 constructor(address _token) {22 token = ERC20(_token); // initialized with WrongERC20 address23 }24
25 function callTransfer() public {26 token.transfer(address(this), 0); // will not revert27 }28}
In the above example, when Test
is constructed with the address of a WrongERC20
contract, the call to callTransfer()
does not revert.