Skip to content

Commit

Permalink
⚡️ Pack for ETH airdropper
Browse files Browse the repository at this point in the history
  • Loading branch information
Philogy committed Nov 2, 2023
1 parent c89190c commit 7c2685f
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 49 deletions.
6 changes: 3 additions & 3 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
28 changes: 13 additions & 15 deletions src/GasliteDrop.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
}
}
4 changes: 2 additions & 2 deletions src/utils/DropPackLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
70 changes: 41 additions & 29 deletions test/GasliteDrop.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}
Expand All @@ -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.
Expand All @@ -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();
}
}

0 comments on commit 7c2685f

Please sign in to comment.