From 495410bc7e1c5f140013ada20418dc221ac698f5 Mon Sep 17 00:00:00 2001 From: Philippe Dumonet Date: Wed, 1 Nov 2023 23:34:25 +0100 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=93=9D=20Add=20gas=20snapshot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gas-snapshot | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .gas-snapshot diff --git a/.gas-snapshot b/.gas-snapshot new file mode 100644 index 0000000..49bc3d7 --- /dev/null +++ b/.gas-snapshot @@ -0,0 +1,14 @@ +GasliteDropTest:test_airdropERC20() (gas: 3976642) +GasliteDropTest:test_airdropERC721() (gas: 31311895) +GasliteDropTest:test_airdropETH() (gas: 7929106) +GasliteSplitterTest:test_splitterConstructorState() (gas: 56775) +GasliteSplitterTest:test_splitterSplitETH() (gas: 219346) +GasliteSplitterTest:test_splitterSplitETHBalanceZero() (gas: 10807) +GasliteSplitterTest:test_splitterSplitETHReleaseRoyalty() (gas: 887376) +GasliteSplitterTest:test_splitterSplitETHUnevenShares() (gas: 836669) +GasliteSplitterTest:test_splitterSplitToken() (gas: 209542) +GasliteSplitterTest:test_splitterSplitTokenBalanceZero() (gas: 19291) +GasliteSplitterTest:test_splitterSplitTokenReleaseRoyalty() (gas: 871495) +GasliteSplitterTest:test_splitterSplitTokenUnevenShares() (gas: 826866) +NFTSplitterTest:test_mintWithEth() (gas: 195380) +NFTSplitterTest:test_mintWithToken() (gas: 195080) \ No newline at end of file From 5a10c2f87e67dc8d6c766889782a284fce2627e9 Mon Sep 17 00:00:00 2001 From: Philippe Dumonet Date: Wed, 1 Nov 2023 23:42:12 +0100 Subject: [PATCH 2/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Clean=20ascii=20art=20?= =?UTF-8?q?and=20disable=20`forge=20fmt`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/GasliteDrop.sol | 18 ++++++++++-------- src/GasliteSplitter.sol | 17 +++++++++-------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/GasliteDrop.sol b/src/GasliteDrop.sol index 08c7f09..579afa3 100644 --- a/src/GasliteDrop.sol +++ b/src/GasliteDrop.sol @@ -1,7 +1,8 @@ pragma solidity 0.8.19; +// forgefmt: disable-start /** - + bbbbbbbb dddddddd b::::::b d::::::d b::::::b d::::::d @@ -19,14 +20,15 @@ g:::::::ggggg:::::ga::::a a:::::as:::::ssss::::::s b:::::bbbbbb::::::ba: g::::::::::::::::ga:::::aaaa::::::as::::::::::::::s b::::::::::::::::b a:::::aaaa::::::a d:::::::::::::::::d gg::::::::::::::g a::::::::::aa:::as:::::::::::ss b:::::::::::::::b a::::::::::aa:::a d:::::::::ddd::::d gggggggg::::::g aaaaaaaaaa aaaa sssssssssss bbbbbbbbbbbbbbbb aaaaaaaaaa aaaa ddddddddd ddddd - g:::::g -gggggg g:::::g -g:::::gg gg:::::g - g::::::ggg:::::::g - gg:::::::::::::g - ggg::::::ggg - gggggg + g:::::g +gggggg g:::::g +g:::::gg gg:::::g + g::::::ggg:::::::g + gg:::::::::::::g + ggg::::::ggg + gggggg */ +// forgefmt: disable-end /// @title GasliteDrop /// @notice Turbo gas optimized bulk transfers of ERC20, ERC721, and ETH diff --git a/src/GasliteSplitter.sol b/src/GasliteSplitter.sol index dc9fc99..bec778f 100644 --- a/src/GasliteSplitter.sol +++ b/src/GasliteSplitter.sol @@ -1,7 +1,7 @@ pragma solidity 0.8.19; +// forgefmt: disable-start /** - bbbbbbbb dddddddd b::::::b d::::::d b::::::b d::::::d @@ -19,14 +19,15 @@ g:::::::ggggg:::::ga::::a a:::::as:::::ssss::::::s b:::::bbbbbb::::::ba: g::::::::::::::::ga:::::aaaa::::::as::::::::::::::s b::::::::::::::::b a:::::aaaa::::::a d:::::::::::::::::d gg::::::::::::::g a::::::::::aa:::as:::::::::::ss b:::::::::::::::b a::::::::::aa:::a d:::::::::ddd::::d gggggggg::::::g aaaaaaaaaa aaaa sssssssssss bbbbbbbbbbbbbbbb aaaaaaaaaa aaaa ddddddddd ddddd - g:::::g -gggggg g:::::g -g:::::gg gg:::::g - g::::::ggg:::::::g - gg:::::::::::::g - ggg::::::ggg - gggggg + g:::::g +gggggg g:::::g +g:::::gg gg:::::g + g::::::ggg:::::::g + gg:::::::::::::g + ggg::::::ggg + gggggg */ +// forgefmt: disable-end /// @title GasliteSplitter /// @notice Turbo gas optimized payment splitter From dad53c2d78c958f23d7bc62ca60a4b2d955a0aa1 Mon Sep 17 00:00:00 2001 From: Philippe Dumonet Date: Wed, 1 Nov 2023 23:52:32 +0100 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Optimize=20`airdropERC?= =?UTF-8?q?20`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/GasliteDrop.sol | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/GasliteDrop.sol b/src/GasliteDrop.sol index 579afa3..9251f51 100644 --- a/src/GasliteDrop.sol +++ b/src/GasliteDrop.sol @@ -85,23 +85,24 @@ contract GasliteDrop { uint256 _totalAmount ) external payable { assembly { - // Check that the number of addresses matches the number of amounts - if iszero(eq(_amounts.length, _addresses.length)) { revert(0, 0) } + // Minimize branches by bundling error checks, makes successful case cheaper and failure + // case much more expensive. + let noError := eq(_amounts.length, _addresses.length) - // transferFrom(address from, address to, uint256 amount) - mstore(0x00, hex"23b872dd") + // Packed `transfer(address to, uint256 amount)` [0xa9059cbb] + // and `transferFrom(address from, address to, uint256 amount)` [0x23b872dd], to avoid + // extra mstore later. Right shifted to minimize bytecode size (no added runtime gas + // used). + mstore(0x00, 0xa9059cbb23b872dd) // from address - mstore(0x04, caller()) + mstore(0x20, caller()) // to address (this contract) - mstore(0x24, address()) + mstore(0x40, address()) // total amount - mstore(0x44, _totalAmount) + mstore(0x60, _totalAmount) // transfer total amount to this contract - if iszero(call(gas(), _token, 0, 0x00, 0x64, 0, 0)) { revert(0, 0) } - - // transfer(address to, uint256 value) - mstore(0x00, hex"a9059cbb") + noError := and(noError, call(gas(), _token, 0, 0x1c, 0x64, 0, 0)) // end of array let end := add(_addresses.offset, shl(5, _addresses.length)) @@ -111,16 +112,19 @@ contract GasliteDrop { // Loop through the addresses for { let addressOffset := _addresses.offset } 1 {} { // to address - mstore(0x04, calldataload(addressOffset)) + mstore(0x1c, calldataload(addressOffset)) // amount - mstore(0x24, calldataload(sub(addressOffset, diff))) + mstore(0x3c, calldataload(sub(addressOffset, diff))) // transfer the tokens - if iszero(call(gas(), _token, 0, 0x00, 0x64, 0, 0)) { revert(0, 0) } + noError := and(noError, call(gas(), _token, 0, 0x18, 0x44, 0, 0)) // increment the address offset addressOffset := add(addressOffset, 0x20) // if addressOffset >= end, break if iszero(lt(addressOffset, end)) { break } } + + // Check final error flag. + if iszero(noError) { revert(0, 0) } } } From 771fc9a85138668ff02390876c64e6bdec5f96ec Mon Sep 17 00:00:00 2001 From: Philippe Dumonet Date: Thu, 2 Nov 2023 00:12:47 +0100 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=94=A8=20Add=20balance=20checking=20t?= =?UTF-8?q?o=20ERC20=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gas-snapshot | 6 ++--- test/GasliteDrop.t.sol | 52 ++++++++++++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 49bc3d7..0a0ef2a 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,6 +1,6 @@ -GasliteDropTest:test_airdropERC20() (gas: 3976642) -GasliteDropTest:test_airdropERC721() (gas: 31311895) -GasliteDropTest:test_airdropETH() (gas: 7929106) +GasliteDropTest:test_airdropERC20() (gas: 26948781) +GasliteDropTest:test_airdropERC721() (gas: 31309664) +GasliteDropTest:test_airdropETH() (gas: 7940881) GasliteSplitterTest:test_splitterConstructorState() (gas: 56775) GasliteSplitterTest:test_splitterSplitETH() (gas: 219346) GasliteSplitterTest:test_splitterSplitETHBalanceZero() (gas: 10807) diff --git a/test/GasliteDrop.t.sol b/test/GasliteDrop.t.sol index 330466b..4420e9e 100644 --- a/test/GasliteDrop.t.sol +++ b/test/GasliteDrop.t.sol @@ -3,26 +3,30 @@ pragma solidity 0.8.19; import {GasliteDrop} from "./../src/GasliteDrop.sol"; import {NFT} from "./../test/utils/NFT.sol"; import {Token} from "./../test/utils/Token.sol"; +import {LibPRNG} from "@solady/utils/LibPRNG.sol"; import "forge-std/Test.sol"; contract GasliteDropTest is Test { + using LibPRNG for LibPRNG.PRNG; + GasliteDrop gasliteDrop; NFT nft; Token token; - address user = vm.addr(0x1); + address immutable sender = makeAddr("sender"); uint256 quantity = 1000; uint256 value = quantity * 0.001 ether; + uint256 internal constant MAX_ERC20_BATCH_DROP = 1000; + function setUp() public { nft = new NFT(); token = new Token(); - token.transfer(user, quantity); gasliteDrop = new GasliteDrop(); } function test_airdropERC721() public { - vm.startPrank(user); - nft.batchMint(address(user), quantity); + vm.startPrank(sender); + nft.batchMint(address(sender), quantity); uint256[] memory tokenIds = new uint256[](quantity); address[] memory recipients = new address[](quantity); @@ -38,22 +42,42 @@ contract GasliteDropTest is Test { } function test_airdropERC20() public { - vm.startPrank(user); - token.approve(address(gasliteDrop), quantity); + // Fixed inputs for gas comparison. + test_fuzzedAirdropERC20(quantity, uint(keccak256("gas bad"))); + } - address[] memory recipients = new address[](quantity); - uint256[] memory amounts = new uint256[](quantity); - for (uint256 i = 0; i < quantity; i++) { - recipients[i] = vm.addr(2); - amounts[i] = 1; + function test_fuzzedAirdropERC20(uint256 totalRecipients, uint256 initialRng) public { + totalRecipients = bound(totalRecipients, 0, MAX_ERC20_BATCH_DROP); + LibPRNG.PRNG memory rng = LibPRNG.PRNG({state: initialRng}); + + // Setup. + uint256 total = 0; + address[] memory recipients = new address[](totalRecipients); + uint256[] memory amounts = new uint256[](totalRecipients); + for (uint256 i = 0; i < totalRecipients; i++) { + recipients[i] = address(uint160(rng.next())); + // Constrain to 96-bits for packing later + uint256 amount = uint96(rng.next()); + total += amount; + amounts[i] = amount; } - gasliteDrop.airdropERC20(address(token), recipients, amounts, quantity); + deal(address(token), sender, total); + + // Interaction. + vm.startPrank(sender); + token.approve(address(gasliteDrop), type(uint256).max); + gasliteDrop.airdropERC20(address(token), recipients, amounts, total); vm.stopPrank(); + + // Checks. + for (uint256 i = 0; i < totalRecipients; i++) { + assertEq(token.balanceOf(recipients[i]), amounts[i]); + } } function test_airdropETH() public { - payable(user).transfer(value); - vm.startPrank(user); + payable(sender).transfer(value); + vm.startPrank(sender); address[] memory recipients = new address[](quantity); uint256[] memory amounts = new uint256[](quantity); From efb0b8a3b38aa559fa83ec7cae3c9590707736b3 Mon Sep 17 00:00:00 2001 From: Philippe Dumonet Date: Thu, 2 Nov 2023 00:16:50 +0100 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Optimize=20error=20che?= =?UTF-8?q?ck=20in=20`airdropERC20`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/GasliteDrop.sol | 19 +++++++++---------- test/GasliteDrop.t.sol | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/GasliteDrop.sol b/src/GasliteDrop.sol index 9251f51..3220622 100644 --- a/src/GasliteDrop.sol +++ b/src/GasliteDrop.sol @@ -85,15 +85,14 @@ contract GasliteDrop { uint256 _totalAmount ) external payable { assembly { - // Minimize branches by bundling error checks, makes successful case cheaper and failure - // case much more expensive. - let noError := eq(_amounts.length, _addresses.length) - // Packed `transfer(address to, uint256 amount)` [0xa9059cbb] // and `transferFrom(address from, address to, uint256 amount)` [0x23b872dd], to avoid - // extra mstore later. Right shifted to minimize bytecode size (no added runtime gas - // used). - mstore(0x00, 0xa9059cbb23b872dd) + // extra mstore later. Also set "no error" flag at 0 to `true`. + mstore(0x00, 0x010000000000000000000000000000000000000000000000a9059cbb23b872dd) + + // If comparison fails sets "no error" flag at byte 0 to 0, else just writes to 1. + mstore8(eq(_amounts.length, _addresses.length), 0) + // from address mstore(0x20, caller()) // to address (this contract) @@ -102,7 +101,7 @@ contract GasliteDrop { mstore(0x60, _totalAmount) // transfer total amount to this contract - noError := and(noError, call(gas(), _token, 0, 0x1c, 0x64, 0, 0)) + mstore8(call(gas(), _token, 0, 0x1c, 0x64, 0, 0), 0) // end of array let end := add(_addresses.offset, shl(5, _addresses.length)) @@ -116,7 +115,7 @@ contract GasliteDrop { // amount mstore(0x3c, calldataload(sub(addressOffset, diff))) // transfer the tokens - noError := and(noError, call(gas(), _token, 0, 0x18, 0x44, 0, 0)) + mstore8(call(gas(), _token, 0, 0x18, 0x44, 0, 0), 0) // increment the address offset addressOffset := add(addressOffset, 0x20) // if addressOffset >= end, break @@ -124,7 +123,7 @@ contract GasliteDrop { } // Check final error flag. - if iszero(noError) { revert(0, 0) } + if iszero(byte(0, mload(0))) { revert(0, 0) } } } diff --git a/test/GasliteDrop.t.sol b/test/GasliteDrop.t.sol index 4420e9e..e8e4f10 100644 --- a/test/GasliteDrop.t.sol +++ b/test/GasliteDrop.t.sol @@ -43,7 +43,7 @@ contract GasliteDropTest is Test { function test_airdropERC20() public { // Fixed inputs for gas comparison. - test_fuzzedAirdropERC20(quantity, uint(keccak256("gas bad"))); + test_fuzzedAirdropERC20(quantity, uint256(keccak256("gas bad"))); } function test_fuzzedAirdropERC20(uint256 totalRecipients, uint256 initialRng) public { From c89190ceb221c5a6093645f44dff11b2861ca87a Mon Sep 17 00:00:00 2001 From: Philippe Dumonet Date: Thu, 2 Nov 2023 00:52:46 +0100 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Add=20calldata=20packi?= =?UTF-8?q?ng=20to=20`airdropERC20`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/GasliteDrop.sol | 46 ++++++++++++++++++--------------------- src/utils/DropPackLib.sol | 17 +++++++++++++++ test/GasliteDrop.t.sol | 16 +++++++++++--- 3 files changed, 51 insertions(+), 28 deletions(-) create mode 100644 src/utils/DropPackLib.sol diff --git a/src/GasliteDrop.sol b/src/GasliteDrop.sol index 3220622..ad3feb5 100644 --- a/src/GasliteDrop.sol +++ b/src/GasliteDrop.sol @@ -75,23 +75,16 @@ contract GasliteDrop { /// @notice Airdrop ERC20 tokens to a list of addresses /// @param _token The address of the ERC20 contract - /// @param _addresses The addresses to airdrop to - /// @param _amounts The amounts to airdrop + /// @param _packedRecipients Recipient address packed with 96-bit amount (recipient ++ amount) /// @param _totalAmount The total amount to airdrop - function airdropERC20( - address _token, - address[] calldata _addresses, - uint256[] calldata _amounts, - uint256 _totalAmount - ) external payable { + function airdropERC20(address _token, bytes32[] calldata _packedRecipients, uint256 _totalAmount) + external + payable + { assembly { - // Packed `transfer(address to, uint256 amount)` [0xa9059cbb] - // and `transferFrom(address from, address to, uint256 amount)` [0x23b872dd], to avoid - // extra mstore later. Also set "no error" flag at 0 to `true`. - mstore(0x00, 0x010000000000000000000000000000000000000000000000a9059cbb23b872dd) - - // If comparison fails sets "no error" flag at byte 0 to 0, else just writes to 1. - mstore8(eq(_amounts.length, _addresses.length), 0) + // Puts selector of `transferFrom(address from, address to, uint256 amount)` in memory + // together with the default value for the "no error" flag (true i.e. `1`). + mstore(0x00, 0x0100000000000000000000000000000000000000000000000000000023b872dd) // from address mstore(0x20, caller()) @@ -104,22 +97,25 @@ contract GasliteDrop { mstore8(call(gas(), _token, 0, 0x1c, 0x64, 0, 0), 0) // end of array - let end := add(_addresses.offset, shl(5, _addresses.length)) - // diff = _addresses.offset - _amounts.offset - let diff := sub(_addresses.offset, _amounts.offset) + let end := add(_packedRecipients.offset, shl(5, _packedRecipients.length)) + + // Puts selector of `transfer(address to, uint256 amount)` in memory with touching the + // "no error" flag. + mstore(0x01, 0xa9059cbb00) // Loop through the addresses - for { let addressOffset := _addresses.offset } 1 {} { - // to address - mstore(0x1c, calldataload(addressOffset)) + for { let recipientsOffset := _packedRecipients.offset } 1 {} { + let packedRecipient := calldataload(recipientsOffset) + // to address (shifted left by 12 bytes) + mstore(0x2c, packedRecipient) // amount - mstore(0x3c, calldataload(sub(addressOffset, diff))) + mstore(0x40, and(0xffffffffffffffffffffffff, packedRecipient)) // transfer the tokens - mstore8(call(gas(), _token, 0, 0x18, 0x44, 0, 0), 0) + mstore8(call(gas(), _token, 0, 0x1c, 0x44, 0, 0), 0) // increment the address offset - addressOffset := add(addressOffset, 0x20) + recipientsOffset := add(recipientsOffset, 0x20) // if addressOffset >= end, break - if iszero(lt(addressOffset, end)) { break } + if iszero(lt(recipientsOffset, end)) { break } } // Check final error flag. diff --git a/src/utils/DropPackLib.sol b/src/utils/DropPackLib.sol new file mode 100644 index 0000000..110b99f --- /dev/null +++ b/src/utils/DropPackLib.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {SafeCastLib} from "@solady/utils/SafeCastLib.sol"; + +/// @author philogy +library DropPackLib { + using SafeCastLib for uint256; + + function packERC20Recipient(address recipient, uint256 amount) internal pure returns (bytes32) { + bytes32(abi.encodePacked(recipient, amount.toUint96())); + } + + function packETHRecipient(address recipient, uint256 amount) internal pure returns (bytes32) { + bytes32(abi.encodePacked(amount.toUint96(), recipient)); + } +} diff --git a/test/GasliteDrop.t.sol b/test/GasliteDrop.t.sol index e8e4f10..dd6f3de 100644 --- a/test/GasliteDrop.t.sol +++ b/test/GasliteDrop.t.sol @@ -4,6 +4,7 @@ import {GasliteDrop} from "./../src/GasliteDrop.sol"; import {NFT} from "./../test/utils/NFT.sol"; import {Token} from "./../test/utils/Token.sol"; import {LibPRNG} from "@solady/utils/LibPRNG.sol"; +import {DropPackLib} from "../src/utils/DropPackLib.sol"; import "forge-std/Test.sol"; contract GasliteDropTest is Test { @@ -47,6 +48,7 @@ contract GasliteDropTest is Test { } function test_fuzzedAirdropERC20(uint256 totalRecipients, uint256 initialRng) public { + vm.pauseGasMetering(); totalRecipients = bound(totalRecipients, 0, MAX_ERC20_BATCH_DROP); LibPRNG.PRNG memory rng = LibPRNG.PRNG({state: initialRng}); @@ -54,25 +56,33 @@ contract GasliteDropTest is Test { uint256 total = 0; address[] memory recipients = new address[](totalRecipients); uint256[] memory amounts = new uint256[](totalRecipients); + bytes32[] memory packedRecipients = new bytes32[] (totalRecipients); for (uint256 i = 0; i < totalRecipients; i++) { - recipients[i] = address(uint160(rng.next())); - // Constrain to 96-bits for packing later + address recipient = address(uint160(rng.next())); + recipients[i] = recipient; + // Constrain to 96-bits for packing. uint256 amount = uint96(rng.next()); total += amount; amounts[i] = amount; + packedRecipients[i] = DropPackLib.packERC20Recipient(recipient, amount); } deal(address(token), sender, total); // Interaction. vm.startPrank(sender); token.approve(address(gasliteDrop), type(uint256).max); - gasliteDrop.airdropERC20(address(token), recipients, amounts, total); + + vm.resumeGasMetering(); + gasliteDrop.airdropERC20(address(token), packedRecipients, total); + vm.pauseGasMetering(); + vm.stopPrank(); // Checks. for (uint256 i = 0; i < totalRecipients; i++) { assertEq(token.balanceOf(recipients[i]), amounts[i]); } + vm.resumeGasMetering(); } function test_airdropETH() public { From 7c2685f794c4124bef1be648be8ffa8069c4c81f Mon Sep 17 00:00:00 2001 From: Philippe Dumonet Date: Thu, 2 Nov 2023 01:13:06 +0100 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Pack=20for=20ETH=20air?= =?UTF-8?q?dropper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gas-snapshot | 6 ++-- src/GasliteDrop.sol | 28 ++++++++-------- src/utils/DropPackLib.sol | 4 +-- test/GasliteDrop.t.sol | 70 +++++++++++++++++++++++---------------- 4 files changed, 59 insertions(+), 49 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 0a0ef2a..39259fa 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,6 +1,6 @@ -GasliteDropTest:test_airdropERC20() (gas: 26948781) -GasliteDropTest:test_airdropERC721() (gas: 31309664) -GasliteDropTest:test_airdropETH() (gas: 7940881) +GasliteDropTest:test_airdropERC20() (gas: 25017908) +GasliteDropTest:test_airdropERC721() (gas: 31207172) +GasliteDropTest:test_airdropETH() (gas: 34560500) GasliteSplitterTest:test_splitterConstructorState() (gas: 56775) GasliteSplitterTest:test_splitterSplitETH() (gas: 219346) GasliteSplitterTest:test_splitterSplitETHBalanceZero() (gas: 10807) diff --git a/src/GasliteDrop.sol b/src/GasliteDrop.sol index ad3feb5..158991d 100644 --- a/src/GasliteDrop.sol +++ b/src/GasliteDrop.sol @@ -124,31 +124,29 @@ contract GasliteDrop { } /// @notice Airdrop ETH to a list of addresses - /// @param _addresses The addresses to airdrop to - /// @param _amounts The amounts to airdrop - function airdropETH(address[] calldata _addresses, uint256[] calldata _amounts) external payable { + /// @param _packedRecipients Recipient address packed with 96-bit amount (amount ++ recipient) + function airdropETH(bytes32[] calldata _packedRecipients) external payable { assembly { - // Check that the number of addresses matches the number of amounts - if iszero(eq(_amounts.length, _addresses.length)) { revert(0, 0) } + // Assumes byte 0 in memory is 0 (default) i.e. scratch space untouched so far. // iterator - let i := _addresses.offset + let offset := _packedRecipients.offset // end of array - let end := add(i, shl(5, _addresses.length)) - // diff = _addresses.offset - _amounts.offset - let diff := sub(_amounts.offset, _addresses.offset) + let end := add(offset, shl(5, _packedRecipients.length)) // Loop through the addresses for {} 1 {} { - // transfer the ETH - if iszero(call(gas(), calldataload(i), calldataload(add(i, diff)), 0x00, 0x00, 0x00, 0x00)) { - revert(0x00, 0x00) - } + let packedRecipient := calldataload(offset) + // transfer the ETH, byte 0 is error flag (0 = no error, 1 = error) + mstore8(call(gas(), packedRecipient, shr(160, packedRecipient), 0x00, 0x00, 0x00, 0x00), 1) // increment the iterator - i := add(i, 0x20) + offset := add(offset, 0x20) // if i >= end, break - if eq(end, i) { break } + if eq(end, offset) { break } } + + // Check error flag. + if byte(0, mload(0x00)) { revert(0x0, 0x0) } } } } diff --git a/src/utils/DropPackLib.sol b/src/utils/DropPackLib.sol index 110b99f..fb7b2ff 100644 --- a/src/utils/DropPackLib.sol +++ b/src/utils/DropPackLib.sol @@ -8,10 +8,10 @@ library DropPackLib { using SafeCastLib for uint256; function packERC20Recipient(address recipient, uint256 amount) internal pure returns (bytes32) { - bytes32(abi.encodePacked(recipient, amount.toUint96())); + return bytes32(abi.encodePacked(recipient, amount.toUint96())); } function packETHRecipient(address recipient, uint256 amount) internal pure returns (bytes32) { - bytes32(abi.encodePacked(amount.toUint96(), recipient)); + return bytes32(abi.encodePacked(amount.toUint96(), recipient)); } } diff --git a/test/GasliteDrop.t.sol b/test/GasliteDrop.t.sol index dd6f3de..9082343 100644 --- a/test/GasliteDrop.t.sol +++ b/test/GasliteDrop.t.sol @@ -14,10 +14,10 @@ contract GasliteDropTest is Test { NFT nft; Token token; address immutable sender = makeAddr("sender"); - uint256 quantity = 1000; - uint256 value = quantity * 0.001 ether; uint256 internal constant MAX_ERC20_BATCH_DROP = 1000; + uint256 internal constant MAX_ETH_BATCH_DROP = 1000; + uint256 internal constant MAX_ERC721_BATCH_DROP = 1000; function setUp() public { nft = new NFT(); @@ -27,11 +27,11 @@ contract GasliteDropTest is Test { function test_airdropERC721() public { vm.startPrank(sender); - nft.batchMint(address(sender), quantity); + nft.batchMint(address(sender), MAX_ERC721_BATCH_DROP); - uint256[] memory tokenIds = new uint256[](quantity); - address[] memory recipients = new address[](quantity); - for (uint256 i = 0; i < quantity; i++) { + uint256[] memory tokenIds = new uint256[](MAX_ERC721_BATCH_DROP); + address[] memory recipients = new address[](MAX_ERC721_BATCH_DROP); + for (uint256 i = 0; i < MAX_ERC721_BATCH_DROP; i++) { tokenIds[i] = i; recipients[i] = vm.addr(2); } @@ -43,21 +43,15 @@ contract GasliteDropTest is Test { } function test_airdropERC20() public { - // Fixed inputs for gas comparison. - test_fuzzedAirdropERC20(quantity, uint256(keccak256("gas bad"))); - } - - function test_fuzzedAirdropERC20(uint256 totalRecipients, uint256 initialRng) public { vm.pauseGasMetering(); - totalRecipients = bound(totalRecipients, 0, MAX_ERC20_BATCH_DROP); - LibPRNG.PRNG memory rng = LibPRNG.PRNG({state: initialRng}); + LibPRNG.PRNG memory rng = LibPRNG.PRNG({state: uint(keccak256("gas bad (erc20 test)"))}); // Setup. uint256 total = 0; - address[] memory recipients = new address[](totalRecipients); - uint256[] memory amounts = new uint256[](totalRecipients); - bytes32[] memory packedRecipients = new bytes32[] (totalRecipients); - for (uint256 i = 0; i < totalRecipients; i++) { + address[] memory recipients = new address[](MAX_ERC20_BATCH_DROP); + uint256[] memory amounts = new uint256[](MAX_ERC20_BATCH_DROP); + bytes32[] memory packedRecipients = new bytes32[] (MAX_ERC20_BATCH_DROP); + for (uint256 i = 0; i < MAX_ERC20_BATCH_DROP; i++) { address recipient = address(uint160(rng.next())); recipients[i] = recipient; // Constrain to 96-bits for packing. @@ -68,35 +62,53 @@ contract GasliteDropTest is Test { } deal(address(token), sender, total); - // Interaction. vm.startPrank(sender); token.approve(address(gasliteDrop), type(uint256).max); + // Interaction. vm.resumeGasMetering(); gasliteDrop.airdropERC20(address(token), packedRecipients, total); - vm.pauseGasMetering(); + vm.pauseGasMetering(); vm.stopPrank(); - // Checks. - for (uint256 i = 0; i < totalRecipients; i++) { + for (uint256 i = 0; i < MAX_ERC20_BATCH_DROP; i++) { assertEq(token.balanceOf(recipients[i]), amounts[i]); } vm.resumeGasMetering(); } function test_airdropETH() public { - payable(sender).transfer(value); - vm.startPrank(sender); + vm.pauseGasMetering(); + LibPRNG.PRNG memory rng = LibPRNG.PRNG({state: uint(keccak256("gas bad (erc20 test)"))}); - address[] memory recipients = new address[](quantity); - uint256[] memory amounts = new uint256[](quantity); - for (uint256 i = 0; i < quantity; i++) { - recipients[i] = vm.addr(2); - amounts[i] = 0.001 ether; + // Setup. + uint256 total = 0; + address[] memory recipients = new address[](MAX_ETH_BATCH_DROP); + uint256[] memory amounts = new uint256[](MAX_ETH_BATCH_DROP); + bytes32[] memory packedRecipients = new bytes32[] (MAX_ETH_BATCH_DROP); + for (uint256 i = 0; i < MAX_ETH_BATCH_DROP; i++) { + address recipient = address(uint160(rng.next())); + recipients[i] = recipient; + // Constrain to 96-bits for packing. + uint256 amount = uint96(rng.next()); + total += amount; + amounts[i] = amount; + packedRecipients[i] = DropPackLib.packETHRecipient(recipient, amount); } + startHoax(sender, total); + vm.resumeGasMetering(); - gasliteDrop.airdropETH{value: value}(recipients, amounts); + // Interaction + gasliteDrop.airdropETH{value: total}(packedRecipients); + + vm.pauseGasMetering(); vm.stopPrank(); + + // Checks. + for (uint256 i = 0; i < MAX_ETH_BATCH_DROP; i++) { + assertEq(recipients[i].balance, amounts[i]); + } + vm.resumeGasMetering(); } } From 8e0979d56ade1d06e7548ea8edc83ffce062ee32 Mon Sep 17 00:00:00 2001 From: Philippe Dumonet Date: Thu, 2 Nov 2023 01:17:43 +0100 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=A5=A2=20Optimize=20`airdropETH`=20er?= =?UTF-8?q?ror=20flag=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gas-snapshot | 2 +- src/GasliteDrop.sol | 4 +++- test/GasliteDrop.t.sol | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 39259fa..00d8ec5 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,6 +1,6 @@ GasliteDropTest:test_airdropERC20() (gas: 25017908) GasliteDropTest:test_airdropERC721() (gas: 31207172) -GasliteDropTest:test_airdropETH() (gas: 34560500) +GasliteDropTest:test_airdropETH() (gas: 34560497) GasliteSplitterTest:test_splitterConstructorState() (gas: 56775) GasliteSplitterTest:test_splitterSplitETH() (gas: 219346) GasliteSplitterTest:test_splitterSplitETHBalanceZero() (gas: 10807) diff --git a/src/GasliteDrop.sol b/src/GasliteDrop.sol index 158991d..ad534b1 100644 --- a/src/GasliteDrop.sol +++ b/src/GasliteDrop.sol @@ -146,7 +146,9 @@ contract GasliteDrop { } // Check error flag. - if byte(0, mload(0x00)) { revert(0x0, 0x0) } + if iszero(lt(mload(0x00), 0x0100000000000000000000000000000000000000000000000000000000000000)) { + revert(0x0, 0x0) + } } } } diff --git a/test/GasliteDrop.t.sol b/test/GasliteDrop.t.sol index 9082343..ed9b72d 100644 --- a/test/GasliteDrop.t.sol +++ b/test/GasliteDrop.t.sol @@ -44,7 +44,7 @@ contract GasliteDropTest is Test { function test_airdropERC20() public { vm.pauseGasMetering(); - LibPRNG.PRNG memory rng = LibPRNG.PRNG({state: uint(keccak256("gas bad (erc20 test)"))}); + LibPRNG.PRNG memory rng = LibPRNG.PRNG({state: uint256(keccak256("gas bad (erc20 test)"))}); // Setup. uint256 total = 0; @@ -80,7 +80,7 @@ contract GasliteDropTest is Test { function test_airdropETH() public { vm.pauseGasMetering(); - LibPRNG.PRNG memory rng = LibPRNG.PRNG({state: uint(keccak256("gas bad (erc20 test)"))}); + LibPRNG.PRNG memory rng = LibPRNG.PRNG({state: uint256(keccak256("gas bad (erc20 test)"))}); // Setup. uint256 total = 0;