diff --git a/.gas-snapshot b/.gas-snapshot new file mode 100644 index 0000000..00d8ec5 --- /dev/null +++ b/.gas-snapshot @@ -0,0 +1,14 @@ +GasliteDropTest:test_airdropERC20() (gas: 25017908) +GasliteDropTest:test_airdropERC721() (gas: 31207172) +GasliteDropTest:test_airdropETH() (gas: 34560497) +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 diff --git a/src/GasliteDrop.sol b/src/GasliteDrop.sol index 08c7f09..ad534b1 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 @@ -73,80 +75,79 @@ 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 { - // Check that the number of addresses matches the number of amounts - if iszero(eq(_amounts.length, _addresses.length)) { revert(0, 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) - // transferFrom(address from, address to, uint256 amount) - mstore(0x00, hex"23b872dd") // 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") + 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(0x04, calldataload(addressOffset)) + for { let recipientsOffset := _packedRecipients.offset } 1 {} { + let packedRecipient := calldataload(recipientsOffset) + // to address (shifted left by 12 bytes) + mstore(0x2c, packedRecipient) // amount - mstore(0x24, calldataload(sub(addressOffset, diff))) + mstore(0x40, and(0xffffffffffffffffffffffff, packedRecipient)) // transfer the tokens - if iszero(call(gas(), _token, 0, 0x00, 0x64, 0, 0)) { revert(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. + if iszero(byte(0, mload(0))) { revert(0, 0) } } } /// @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 iszero(lt(mload(0x00), 0x0100000000000000000000000000000000000000000000000000000000000000)) { + revert(0x0, 0x0) } } } 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 diff --git a/src/utils/DropPackLib.sol b/src/utils/DropPackLib.sol new file mode 100644 index 0000000..fb7b2ff --- /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) { + return bytes32(abi.encodePacked(recipient, amount.toUint96())); + } + + function packETHRecipient(address recipient, uint256 amount) internal pure returns (bytes32) { + return bytes32(abi.encodePacked(amount.toUint96(), recipient)); + } +} diff --git a/test/GasliteDrop.t.sol b/test/GasliteDrop.t.sol index 330466b..ed9b72d 100644 --- a/test/GasliteDrop.t.sol +++ b/test/GasliteDrop.t.sol @@ -3,30 +3,35 @@ 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 {DropPackLib} from "../src/utils/DropPackLib.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); - uint256 quantity = 1000; - uint256 value = quantity * 0.001 ether; + address immutable sender = makeAddr("sender"); + + 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(); 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), 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); } @@ -38,31 +43,72 @@ contract GasliteDropTest is Test { } function test_airdropERC20() public { - vm.startPrank(user); - token.approve(address(gasliteDrop), quantity); + vm.pauseGasMetering(); + LibPRNG.PRNG memory rng = LibPRNG.PRNG({state: uint256(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] = 1; + // Setup. + uint256 total = 0; + 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. + uint256 amount = uint96(rng.next()); + total += amount; + amounts[i] = amount; + packedRecipients[i] = DropPackLib.packERC20Recipient(recipient, amount); } - gasliteDrop.airdropERC20(address(token), recipients, amounts, quantity); + deal(address(token), sender, total); + + vm.startPrank(sender); + token.approve(address(gasliteDrop), type(uint256).max); + + // Interaction. + vm.resumeGasMetering(); + gasliteDrop.airdropERC20(address(token), packedRecipients, total); + + vm.pauseGasMetering(); vm.stopPrank(); + // Checks. + for (uint256 i = 0; i < MAX_ERC20_BATCH_DROP; i++) { + assertEq(token.balanceOf(recipients[i]), amounts[i]); + } + vm.resumeGasMetering(); } function test_airdropETH() public { - payable(user).transfer(value); - vm.startPrank(user); + vm.pauseGasMetering(); + LibPRNG.PRNG memory rng = LibPRNG.PRNG({state: uint256(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(); + + // Interaction + gasliteDrop.airdropETH{value: total}(packedRecipients); - gasliteDrop.airdropETH{value: value}(recipients, amounts); + vm.pauseGasMetering(); vm.stopPrank(); + + // Checks. + for (uint256 i = 0; i < MAX_ETH_BATCH_DROP; i++) { + assertEq(recipients[i].balance, amounts[i]); + } + vm.resumeGasMetering(); } }