-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ airdrop: stream
EXA
to eligible accounts
- Loading branch information
1 parent
0558b53
commit 0fa8b19
Showing
4 changed files
with
147 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@exactly/protocol": patch | ||
--- | ||
|
||
✨ airdrop: stream `EXA` to eligible accounts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |