-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathAirdropDistributor.sol
127 lines (104 loc) · 4.63 KB
/
AirdropDistributor.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "../interfaces/ITokenLocker.sol";
import "../interfaces/IVault.sol";
interface IClaimCallback {
function claimCallback(address claimant, uint256 amount) external returns (bool success);
}
/**
@title Prisma veCRV Airdrop Distributor
@notice Distributes PRISMA to veCRV holders that voted in favor of
Prisma's initial Curve governance proposal.
@dev Airdropped PRISMA tokens are given as a one year locked position.
Distribution is via a merkle proof. The proof and script used
to create are available on Github: https://github.com/prisma-fi
*/
contract AirdropDistributor is Ownable {
using Address for address;
bytes32 public merkleRoot;
uint256 public canClaimUntil;
mapping(uint256 => uint256) private claimedBitMap;
mapping(address receiver => address callback) public claimCallback;
IERC20 public immutable token;
ITokenLocker public immutable locker;
address public immutable vault;
uint256 private immutable lockToTokenRatio;
uint256 private immutable CLAIM_LOCK_WEEKS;
uint256 public constant CLAIM_DURATION = 13 weeks;
event Claimed(address indexed claimant, address indexed receiver, uint256 index, uint256 amount);
event MerkleRootSet(bytes32 root, uint256 canClaimUntil);
constructor(IERC20 _token, ITokenLocker _locker, address _vault, uint256 lockWeeks) {
token = _token;
locker = _locker;
vault = _vault;
CLAIM_LOCK_WEEKS = lockWeeks;
lockToTokenRatio = _locker.lockToTokenRatio();
}
function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner {
require(merkleRoot == bytes32(0), "merkleRoot already set");
merkleRoot = _merkleRoot;
canClaimUntil = block.timestamp + CLAIM_DURATION;
emit MerkleRootSet(_merkleRoot, canClaimUntil);
}
function sweepUnclaimedTokens() external {
require(merkleRoot != bytes32(0), "merkleRoot not set");
require(block.timestamp > canClaimUntil, "Claims still active");
uint256 amount = token.allowance(vault, address(this));
require(amount > 0, "Nothing to sweep");
token.transferFrom(vault, address(this), amount);
token.approve(vault, amount);
IPrismaVault(vault).increaseUnallocatedSupply(amount);
}
function isClaimed(uint256 index) public view returns (bool) {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
uint256 claimedWord = claimedBitMap[claimedWordIndex];
uint256 mask = (1 << claimedBitIndex);
return claimedWord & mask == mask;
}
function _setClaimed(uint256 index) private {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
claimedBitMap[claimedWordIndex] = claimedBitMap[claimedWordIndex] | (1 << claimedBitIndex);
}
/**
@dev `amount` is after dividing by `locker.lockToTokenRatio()`
*/
function claim(
address claimant,
address receiver,
uint256 index,
uint256 amount,
bytes32[] calldata merkleProof
) external {
if (msg.sender != claimant) {
require(msg.sender == owner(), "onlyOwner");
require(claimant.isContract(), "Claimant must be a contract");
}
require(merkleRoot != bytes32(0), "merkleRoot not set");
require(block.timestamp < canClaimUntil, "Claims period has finished");
require(!isClaimed(index), "Already claimed");
bytes32 node = keccak256(abi.encodePacked(index, claimant, amount));
require(MerkleProof.verifyCalldata(merkleProof, merkleRoot, node), "Invalid proof");
_setClaimed(index);
token.transferFrom(vault, address(this), amount * lockToTokenRatio);
locker.lock(receiver, amount, CLAIM_LOCK_WEEKS);
if (claimant != receiver) {
address callback = claimCallback[receiver];
if (callback != address(0)) IClaimCallback(callback).claimCallback(claimant, amount);
}
emit Claimed(claimant, receiver, index, amount * lockToTokenRatio);
}
/**
@notice Set a claim callback contract
@dev When set, claims directed to the caller trigger a callback to this address
*/
function setClaimCallback(address _callback) external returns (bool) {
claimCallback[msg.sender] = _callback;
return true;
}
}