Skip to content

Commit

Permalink
✨ airdrop: stream EXA to eligible accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
santichez authored and cruzdanilo committed Jul 23, 2023
1 parent 0558b53 commit 0fa8b19
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/dull-comics-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/protocol": patch
---

✨ airdrop: stream `EXA` to eligible accounts
3 changes: 3 additions & 0 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
AirdropTest:testClaimSingleRoot() (gas: 670460)
AirdropTest:testClaimTwiceShouldRevert() (gas: 661206)
AirdropTest:testEmitClaim() (gas: 661076)
AuditorTest:testAccountShortfall() (gas: 159721)
AuditorTest:testAccountShortfallRevert() (gas: 183495)
AuditorTest:testBorrowMPValidation() (gas: 159377)
Expand Down
71 changes: 71 additions & 0 deletions contracts/periphery/Airdrop.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.17;

import { ERC20 } from "solmate/src/tokens/ERC20.sol";
import { MerkleProofLib } from "solmate/src/utils/MerkleProofLib.sol";
import { SafeTransferLib } from "solmate/src/utils/SafeTransferLib.sol";

contract Airdrop {
using SafeTransferLib for ERC20;
using MerkleProofLib for bytes32[];

ERC20 public immutable exa;
bytes32 public immutable root;
ISablierV2LockupLinear public immutable sablier;

mapping(address => bool) public claimed;
mapping(address => uint256) public streamIds;

constructor(ERC20 exa_, bytes32 root_, ISablierV2LockupLinear sablier_) {
exa = exa_;
root = root_;
sablier = sablier_;

exa.safeApprove(address(sablier), type(uint256).max);
}

function claim(uint128 amount, bytes32[] calldata proof) external returns (uint256 streamId) {
assert(!claimed[msg.sender]);
assert(proof.verify(root, keccak256(abi.encodePacked(msg.sender, amount))));

claimed[msg.sender] = true;
streamIds[msg.sender] = streamId = sablier.createWithDurations(
ISablierV2LockupLinear.CreateWithDurations({
sender: address(this),
recipient: msg.sender,
totalAmount: amount,
asset: exa,
cancelable: false,
durations: ISablierV2LockupLinear.Durations({ cliff: 0, total: 5 * 4 weeks }),
broker: ISablierV2LockupLinear.Broker({ account: address(0), fee: 0 })
})
);
emit Claim(msg.sender, amount, streamId);
}

event Claim(address indexed account, uint128 amount, uint256 streamId);
}

interface ISablierV2LockupLinear {
struct Durations {
uint40 cliff;
uint40 total;
}

struct Broker {
address account;
uint256 fee;
}

struct CreateWithDurations {
address sender;
address recipient;
uint128 totalAmount;
ERC20 asset;
bool cancelable;
Durations durations;
Broker broker;
}

function createWithDurations(CreateWithDurations calldata params) external returns (uint256 streamId);
}
68 changes: 68 additions & 0 deletions test/solidity/Airdrop.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.17;

import { Vm } from "forge-std/Vm.sol";
import { Test, stdError } from "forge-std/Test.sol";
import { MockERC20 } from "solmate/src/test/utils/mocks/MockERC20.sol";
import { Airdrop, ISablierV2LockupLinear } from "../../contracts/periphery/Airdrop.sol";
import { console2 as console } from "forge-std/console2.sol";

contract AirdropTest is Test {
MockERC20 internal exa;
Airdrop internal airdrop;
ISablierV2LockupLinear internal constant sablier = ISablierV2LockupLinear(0xB923aBdCA17Aed90EB5EC5E407bd37164f632bFD);

function setUp() external {
vm.createSelectFork(vm.envString("OPTIMISM_NODE"), 106_835_444);

exa = new MockERC20("EXA", "EXA", 18);
}

function testClaimSingleRoot() external {
address account = 0x8967782Fb0917bab83F13Bd17db3b41C700b368D;
uint128 amount = 420 ether;
bytes32 root = keccak256(abi.encodePacked(account, amount));
bytes32[] memory proof = new bytes32[](0);

airdrop = new Airdrop(exa, root, sablier);
exa.mint(address(airdrop), 1_000_000 ether);

vm.expectRevert(stdError.assertionError);
vm.prank(account);
airdrop.claim(amount + 1, proof);

vm.expectRevert(stdError.assertionError);
vm.prank(account);
airdrop.claim(amount - 1, proof);

vm.prank(account);
uint256 streamId = airdrop.claim(amount, proof);
assertGt(streamId, 0);
assertEq(airdrop.streamIds(account), streamId);
}

function testEmitClaim() external {
uint128 amount = 1 ether;
bytes32 root = keccak256(abi.encodePacked(address(this), amount));
airdrop = new Airdrop(exa, root, sablier);
exa.mint(address(airdrop), 1_000_000 ether);

vm.expectEmit(true, true, true, false, address(airdrop));
emit Claim(address(this), 1 ether, 4);
airdrop.claim(1 ether, new bytes32[](0));
}

function testClaimTwiceShouldRevert() external {
uint128 amount = 1 ether;
bytes32 root = keccak256(abi.encodePacked(address(this), amount));
airdrop = new Airdrop(exa, root, sablier);
exa.mint(address(airdrop), 1_000_000 ether);

airdrop.claim(1 ether, new bytes32[](0));

vm.expectRevert(stdError.assertionError);
airdrop.claim(1 ether, new bytes32[](0));
}

event Claim(address indexed account, uint128 amount, uint256 streamId);
}

0 comments on commit 0fa8b19

Please sign in to comment.