Skip to content
TwitterGithub

Incorrectly casting interface experience

Learning2 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 address
23 }
24
25 function callTransfer() public {
26 token.transfer(address(this), 0); // will revert
27 }
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 address
23 }
24
25 function callTransfer() public {
26 token.transfer(address(this), 0); // will not revert
27 }
28}

In the above example, when Test is constructed with the address of a WrongERC20 contract, the call to callTransfer() does not revert.