Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -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)
105 changes: 53 additions & 52 deletions src/GasliteDrop.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
pragma solidity 0.8.19;

// forgefmt: disable-start
/**

bbbbbbbb dddddddd
b::::::b d::::::d
b::::::b d::::::d
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
17 changes: 9 additions & 8 deletions src/GasliteSplitter.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
pragma solidity 0.8.19;

// forgefmt: disable-start
/**

bbbbbbbb dddddddd
b::::::b d::::::d
b::::::b d::::::d
Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions src/utils/DropPackLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {SafeCastLib} from "@solady/utils/SafeCastLib.sol";

/// @author philogy <https://github.com/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));
}
}
96 changes: 71 additions & 25 deletions test/GasliteDrop.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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();
}
}