Skip to content

Commit 066ff16

Browse files
PaulRBergmds1
andauthored
feat: add "IMulticall3" interface and "getTokenBalances" method (#271)
* feat: add "IMulticall3" interface chore: fix typos in comments feat: add "getTokenBalances" util refactor: add header separators in "StdUtils" refactor: order functions alphabetically in "StdUtils" * fix: set pragma to ">=0.6.2 <0.9.0" in IMulticall3 fix: add experimental ABIEncoderV2 pragma style: apply "forge fmt" fixes * test: add header separators in "StdUtils.t.sol" test: organize tests alphabetically in "StdUtils.t.sol" * test: add revert test for "getTokenBalances" * Revert "test: add revert test for "getTokenBalances"" This reverts commit c3bedee. * fix/test: fix multicall fork test * refactor: disallow EOAs in "getTokenBalances" test: write "getTokenBalances" test when address is EOA test: test USDC and SHIB holders with "getTokenBalances" * test: add empty array test for "getTokenBalances" chore: delete "console2" log import in "StdUtils.sol" * chore: fix parentheses direction * fix: backwards compatibility with older solc versions * refactor: pull fork call into setUp * fix: codesize check * chore: add comment Co-authored-by: Matt Solomon <[email protected]>
1 parent 7bb0abb commit 066ff16

File tree

3 files changed

+234
-18
lines changed

3 files changed

+234
-18
lines changed

src/StdUtils.sol

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity >=0.6.2 <0.9.0;
33

4+
pragma experimental ABIEncoderV2;
5+
6+
import {IMulticall3} from "./interfaces/IMulticall3.sol";
47
// TODO Remove import.
58
import {VmSafe} from "./Vm.sol";
69

710
abstract contract StdUtils {
11+
/*//////////////////////////////////////////////////////////////////////////
12+
CONSTANTS
13+
//////////////////////////////////////////////////////////////////////////*/
14+
15+
IMulticall3 private constant multicall = IMulticall3(0xcA11bde05977b3631167028862bE2a173976CA11);
816
VmSafe private constant vm = VmSafe(address(uint160(uint256(keccak256("hevm cheat code")))));
917
address private constant CONSOLE2_ADDRESS = 0x000000000000000000636F6e736F6c652e6c6f67;
10-
1118
uint256 private constant INT256_MIN_ABS =
1219
57896044618658097711785492504343953926634992332820282019728792003956564819968;
1320
uint256 private constant UINT256_MAX =
1421
115792089237316195423570985008687907853269984665640564039457584007913129639935;
1522

23+
/*//////////////////////////////////////////////////////////////////////////
24+
INTERNAL FUNCTIONS
25+
//////////////////////////////////////////////////////////////////////////*/
26+
1627
function _bound(uint256 x, uint256 min, uint256 max) internal pure virtual returns (uint256 result) {
1728
require(min <= max, "StdUtils bound(uint256,uint256,uint256): Max is less than min.");
1829
// If x is between min and max, return x directly. This is to ensure that dictionary values
@@ -66,8 +77,13 @@ abstract contract StdUtils {
6677
console2_log("Bound result", vm.toString(result));
6778
}
6879

80+
function bytesToUint(bytes memory b) internal pure virtual returns (uint256) {
81+
require(b.length <= 32, "StdUtils bytesToUint(bytes): Bytes length exceeds 32.");
82+
return abi.decode(abi.encodePacked(new bytes(32 - b.length), b), (uint256));
83+
}
84+
6985
/// @dev Compute the address a contract will be deployed at for a given deployer address and nonce
70-
/// @notice adapated from Solmate implementation (https://github.com/Rari-Capital/solmate/blob/main/src/utils/LibRLP.sol)
86+
/// @notice adapted from Solmate implementation (https://github.com/Rari-Capital/solmate/blob/main/src/utils/LibRLP.sol)
7187
function computeCreateAddress(address deployer, uint256 nonce) internal pure virtual returns (address) {
7288
// forgefmt: disable-start
7389
// The integer zero is treated as an empty byte string, and as a result it only has a length prefix, 0x80, computed via 0x80 + 0.
@@ -100,11 +116,40 @@ abstract contract StdUtils {
100116
return addressFromLast20Bytes(keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, initcodeHash)));
101117
}
102118

103-
function bytesToUint(bytes memory b) internal pure virtual returns (uint256) {
104-
require(b.length <= 32, "StdUtils bytesToUint(bytes): Bytes length exceeds 32.");
105-
return abi.decode(abi.encodePacked(new bytes(32 - b.length), b), (uint256));
119+
// Performs a single call with Multicall3 to query the ERC-20 token balances of the given addresses.
120+
function getTokenBalances(address token, address[] memory addresses)
121+
internal
122+
virtual
123+
returns (uint256[] memory balances)
124+
{
125+
uint256 tokenCodeSize;
126+
assembly {
127+
tokenCodeSize := extcodesize(token)
128+
}
129+
require(tokenCodeSize > 0, "StdUtils getTokenBalances(address,address[]): Token address is not a contract.");
130+
131+
// ABI encode the aggregate call to Multicall3.
132+
uint256 length = addresses.length;
133+
IMulticall3.Call[] memory calls = new IMulticall3.Call[](length);
134+
for (uint256 i = 0; i < length; ++i) {
135+
// 0x70a08231 = bytes4("balanceOf(address)"))
136+
calls[i] = IMulticall3.Call({target: token, callData: abi.encodeWithSelector(0x70a08231, (addresses[i]))});
137+
}
138+
139+
// Make the aggregate call.
140+
(, bytes[] memory returnData) = multicall.aggregate(calls);
141+
142+
// ABI decode the return data and return the balances.
143+
balances = new uint256[](length);
144+
for (uint256 i = 0; i < length; ++i) {
145+
balances[i] = abi.decode(returnData[i], (uint256));
146+
}
106147
}
107148

149+
/*//////////////////////////////////////////////////////////////////////////
150+
PRIVATE FUNCTIONS
151+
//////////////////////////////////////////////////////////////////////////*/
152+
108153
function addressFromLast20Bytes(bytes32 bytesValue) private pure returns (address) {
109154
return address(uint160(uint256(bytesValue)));
110155
}

src/interfaces/IMulticall3.sol

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity >=0.6.2 <0.9.0;
3+
4+
pragma experimental ABIEncoderV2;
5+
6+
interface IMulticall3 {
7+
struct Call {
8+
address target;
9+
bytes callData;
10+
}
11+
12+
struct Call3 {
13+
address target;
14+
bool allowFailure;
15+
bytes callData;
16+
}
17+
18+
struct Call3Value {
19+
address target;
20+
bool allowFailure;
21+
uint256 value;
22+
bytes callData;
23+
}
24+
25+
struct Result {
26+
bool success;
27+
bytes returnData;
28+
}
29+
30+
function aggregate(Call[] calldata calls)
31+
external
32+
payable
33+
returns (uint256 blockNumber, bytes[] memory returnData);
34+
35+
function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData);
36+
37+
function aggregate3Value(Call3Value[] calldata calls) external payable returns (Result[] memory returnData);
38+
39+
function blockAndAggregate(Call[] calldata calls)
40+
external
41+
payable
42+
returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData);
43+
44+
function getBasefee() external view returns (uint256 basefee);
45+
46+
function getBlockHash(uint256 blockNumber) external view returns (bytes32 blockHash);
47+
48+
function getBlockNumber() external view returns (uint256 blockNumber);
49+
50+
function getChainId() external view returns (uint256 chainid);
51+
52+
function getCurrentBlockCoinbase() external view returns (address coinbase);
53+
54+
function getCurrentBlockDifficulty() external view returns (uint256 difficulty);
55+
56+
function getCurrentBlockGasLimit() external view returns (uint256 gaslimit);
57+
58+
function getCurrentBlockTimestamp() external view returns (uint256 timestamp);
59+
60+
function getEthBalance(address addr) external view returns (uint256 balance);
61+
62+
function getLastBlockHash() external view returns (bytes32 blockHash);
63+
64+
function tryAggregate(bool requireSuccess, Call[] calldata calls)
65+
external
66+
payable
67+
returns (Result[] memory returnData);
68+
69+
function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls)
70+
external
71+
payable
72+
returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData);
73+
}

test/StdUtils.t.sol

Lines changed: 111 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,21 @@ pragma solidity >=0.7.0 <0.9.0;
33

44
import "../src/Test.sol";
55

6+
contract StdUtilsMock is StdUtils {
7+
// We deploy a mock version so we can properly test expected reverts.
8+
function getTokenBalances_(address token, address[] memory addresses)
9+
external
10+
returns (uint256[] memory balances)
11+
{
12+
return getTokenBalances(token, addresses);
13+
}
14+
}
15+
616
contract StdUtilsTest is Test {
17+
/*//////////////////////////////////////////////////////////////////////////
18+
BOUND UINT
19+
//////////////////////////////////////////////////////////////////////////*/
20+
721
function testBound() public {
822
assertEq(bound(uint256(5), 0, 4), 0);
923
assertEq(bound(uint256(0), 69, 69), 69);
@@ -74,6 +88,10 @@ contract StdUtilsTest is Test {
7488
bound(num, min, max);
7589
}
7690

91+
/*//////////////////////////////////////////////////////////////////////////
92+
BOUND INT
93+
//////////////////////////////////////////////////////////////////////////*/
94+
7795
function testBoundInt() public {
7896
assertEq(bound(-3, 0, 4), 2);
7997
assertEq(bound(0, -69, -69), -69);
@@ -158,34 +176,114 @@ contract StdUtilsTest is Test {
158176
bound(num, min, max);
159177
}
160178

161-
function testGenerateCreateAddress() external {
179+
/*//////////////////////////////////////////////////////////////////////////
180+
BYTES TO UINT
181+
//////////////////////////////////////////////////////////////////////////*/
182+
183+
function testBytesToUint() external {
184+
bytes memory maxUint = hex"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
185+
bytes memory two = hex"02";
186+
bytes memory millionEther = hex"d3c21bcecceda1000000";
187+
188+
assertEq(bytesToUint(maxUint), type(uint256).max);
189+
assertEq(bytesToUint(two), 2);
190+
assertEq(bytesToUint(millionEther), 1_000_000 ether);
191+
}
192+
193+
function testCannotConvertGT32Bytes() external {
194+
bytes memory thirty3Bytes = hex"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
195+
vm.expectRevert("StdUtils bytesToUint(bytes): Bytes length exceeds 32.");
196+
bytesToUint(thirty3Bytes);
197+
}
198+
199+
/*//////////////////////////////////////////////////////////////////////////
200+
COMPUTE CREATE ADDRESS
201+
//////////////////////////////////////////////////////////////////////////*/
202+
203+
function testComputeCreateAddress() external {
162204
address deployer = 0x6C9FC64A53c1b71FB3f9Af64d1ae3A4931A5f4E9;
163205
uint256 nonce = 14;
164206
address createAddress = computeCreateAddress(deployer, nonce);
165207
assertEq(createAddress, 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45);
166208
}
167209

168-
function testGenerateCreate2Address() external {
210+
/*//////////////////////////////////////////////////////////////////////////
211+
COMPUTE CREATE2 ADDRESS
212+
//////////////////////////////////////////////////////////////////////////*/
213+
214+
function testComputeCreate2Address() external {
169215
bytes32 salt = bytes32(uint256(31415));
170216
bytes32 initcodeHash = keccak256(abi.encode(0x6080));
171217
address deployer = 0x6C9FC64A53c1b71FB3f9Af64d1ae3A4931A5f4E9;
172218
address create2Address = computeCreate2Address(salt, initcodeHash, deployer);
173219
assertEq(create2Address, 0xB147a5d25748fda14b463EB04B111027C290f4d3);
174220
}
221+
}
175222

176-
function testBytesToUint() external {
177-
bytes memory maxUint = hex"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
178-
bytes memory two = hex"02";
179-
bytes memory millionEther = hex"d3c21bcecceda1000000";
223+
contract StdUtilsForkTest is Test {
224+
/*//////////////////////////////////////////////////////////////////////////
225+
GET TOKEN BALANCES
226+
//////////////////////////////////////////////////////////////////////////*/
180227

181-
assertEq(bytesToUint(maxUint), type(uint256).max);
182-
assertEq(bytesToUint(two), 2);
183-
assertEq(bytesToUint(millionEther), 1_000_000 ether);
228+
address internal SHIB = 0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE;
229+
address internal SHIB_HOLDER_0 = 0x855F5981e831D83e6A4b4EBFCAdAa68D92333170;
230+
address internal SHIB_HOLDER_1 = 0x8F509A90c2e47779cA408Fe00d7A72e359229AdA;
231+
address internal SHIB_HOLDER_2 = 0x0e3bbc0D04fF62211F71f3e4C45d82ad76224385;
232+
233+
address internal USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
234+
address internal USDC_HOLDER_0 = 0xDa9CE944a37d218c3302F6B82a094844C6ECEb17;
235+
address internal USDC_HOLDER_1 = 0x3e67F4721E6d1c41a015f645eFa37BEd854fcf52;
236+
237+
function setUp() public {
238+
// All tests of the `getTokenBalances` method are fork tests using live contracts.
239+
vm.createSelectFork({urlOrAlias: "mainnet", blockNumber: 16_428_900});
184240
}
185241

186-
function testCannotConvertGT32Bytes() external {
187-
bytes memory thirty3Bytes = hex"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
188-
vm.expectRevert("StdUtils bytesToUint(bytes): Bytes length exceeds 32.");
189-
bytesToUint(thirty3Bytes);
242+
function testCannotGetTokenBalances_NonTokenContract() external {
243+
// We deploy a mock version so we can properly test the revert.
244+
StdUtilsMock stdUtils = new StdUtilsMock();
245+
246+
// The UniswapV2Factory contract has neither a `balanceOf` function nor a fallback function,
247+
// so the `balanceOf` call should revert.
248+
address token = address(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f);
249+
address[] memory addresses = new address[](1);
250+
addresses[0] = USDC_HOLDER_0;
251+
252+
vm.expectRevert("Multicall3: call failed");
253+
stdUtils.getTokenBalances_(token, addresses);
254+
}
255+
256+
function testCannotGetTokenBalances_EOA() external {
257+
address eoa = vm.addr({privateKey: 1});
258+
address[] memory addresses = new address[](1);
259+
addresses[0] = USDC_HOLDER_0;
260+
vm.expectRevert("StdUtils getTokenBalances(address,address[]): Token address is not a contract.");
261+
getTokenBalances(eoa, addresses);
262+
}
263+
264+
function testGetTokenBalances_Empty() external {
265+
address[] memory addresses = new address[](0);
266+
uint256[] memory balances = getTokenBalances(USDC, addresses);
267+
assertEq(balances.length, 0);
268+
}
269+
270+
function testGetTokenBalances_USDC() external {
271+
address[] memory addresses = new address[](2);
272+
addresses[0] = USDC_HOLDER_0;
273+
addresses[1] = USDC_HOLDER_1;
274+
uint256[] memory balances = getTokenBalances(USDC, addresses);
275+
assertEq(balances[0], 159_000_000_000_000);
276+
assertEq(balances[1], 131_350_000_000_000);
277+
}
278+
279+
function testGetTokenBalances_SHIB() external {
280+
address[] memory addresses = new address[](3);
281+
addresses[0] = SHIB_HOLDER_0;
282+
addresses[1] = SHIB_HOLDER_1;
283+
addresses[2] = SHIB_HOLDER_2;
284+
uint256[] memory balances = getTokenBalances(SHIB, addresses);
285+
assertEq(balances[0], 3_323_256_285_484.42e18);
286+
assertEq(balances[1], 1_271_702_771_149.99999928e18);
287+
assertEq(balances[2], 606_357_106_247e18);
190288
}
191289
}

0 commit comments

Comments
 (0)