1. Đặt vấn đề
Giả sử chúng ta sắp phát hành 1 đồng ERC-20 trên Ethereum và cần Airdrop cho 1 cơ số người dùng ban đầu, nhằm tăng số người giữ token cũng như quảng bá rộng hơn cho đồng token sắp phát hành.
Về cơ bản, chúng ta sẽ cần thực hiện các bước sau để xây dựng tính năng Airdrop:
- Tất nhiên là phải deploy contract token ERC-20 trước.
- Viết contract Airdrop: Contract lưu danh sách các địa chỉ đăng ký nhận token. Sau khi hết đợt đăng ký Airdrop thì người dùng có thể gọi đến contract để claim token.
- Ngoài ra chúng ta có thể xây dựng thêm bot (vd như bot Telegram) để tracking người dùng với điều kiện follow page, share bài viết. v..v để đăng ký được Airdrop.
Merkle Airdrop
Với cách thiết kế contract Airdrop ở trên, chúng ta có 1 vấn đề. Với các đợt Airdrop, số người đăng ký nhận token tùy theo quy mô có thể dao động từ hàng trăm, hàng nghìn hay thậm chí là hàng vạn người.
Phí lưu trữ cũng như phí giao dịch của Ethereum hiện tại không hề rẻ một chút nào, các bạn hãy tưởng tượng 1 array trong contract lưu đến hàng ngàn địa chỉ đăng ký thì sẽ rất tốn kém, chưa kể đến các giao dịch thêm mới địa chỉ vào array nữa. Nói chung, giải pháp thiết kế contract Airdrop như trên là không hề tối ưu một chút nào cho ví tiền của bạn.
Với Merkle Airdrop, chúng ta sẽ không phải lo lắng việc phải lưu 1 lượng lớn địa chỉ đăng ký vào contract nữa, trong khi đó vẫn đảm bảo được việc xác minh xem địa chỉ claim đã đăng ký trước đó hay chưa ? Từ đó tiết kiệm được rất nhiều chi phí trong việc Airdrop.
2. Cơ sở lý thuyết
Merkle Tree
Merkle Tree đơn giản là một cấu trúc dữ liệu dạng cây nhị phân, giá trị của các nút, các lá là mã hash của dữ liệu.
Để tạo ra một Merkle Tree, từ dữ liệu chúng ta có, dùng hàm hash để tính toán ra giá trị hash tương ứng của dữ liệu, các giá trị này sẽ là nút lá của cây. Tiếp tục hash các giá trị liền kề nhau đến khi còn 1 giá trị hash duy nhất (Gốc của cây Merkle). Hình bên dưới mô tả cách mà một Merkle Tree được tính toán như thế nào ?
Merkle Tree giúp việc xác minh, kiểm tra tính toàn vẹn dữ liệu trong khi chỉ tốn 1 lượng nhỏ không gian lưu trữ (do mã hash có kích thước bé). Trong Blockchain, Merkle Tree được dùng rất phổ biến nhằm xác minh các giao dịch (Dùng trong Bitcoin, Ethereum, v..v)
Merkle Proof
Merkle Proof dùng để kiểm tra xem dữ liệu đầu vào có thuộc Merkle Tree hay không mà không cần phải tiết lộ các dữ liệu tất cả dữ liệu tạo thành Merkle Tree.
Chúng ta cùng xem qua ví dụ được minh họa ở hình trên để có thể nắm rõ Merkle Proof là gì ? Trong ví dụ này chúng ta cần chứng minh rằng dữ liệu K thuộc Merkle Tree. Ta cần tính Hash của K rồi leo dần lên gốc của Merkle Tree, nếu giá trị của gốc Merkle Tree tính được trùng với giá trị Merkle Root cho trước thì chứng tỏ K thuộc Merkle Tree.
Thay vì phải dùng tất cả data từ A-P để tính toán lại Merkle Root xem có giống Merkle Root ban đầu không ? Ta sẽ chỉ cần lấy các nút sau của cây để chứng K thuộc Merkle Tree.
- Hash của L từ đó tính được hash KL
- Hash của HJ từ đó tính được hash IJKL
- Hash của MNOP từ đó tính được hash IJKLMNOP
- Hash của ABCDEFGH
Từ đó ta hoàn toàn tính được Merkle Root mà chỉ cần biết 4 giá trị nút trong Merkle Tree
3. Merkle Airdrop
Luồng cơ bản mà chúng ta sẽ implement Airdrop như sau
- Cho người dùng đăng ký airdrop và lưu danh sách dưới dạng như sau (Chúng ta có thể lưu ở server, cloud hay IPFS gì đó tùy ý). Giá trị thứ nhất là địa chỉ đăng ký và thứ 2 là số lượng token airdrop cho địa chỉ đó.
1 2 3 4 | 0x19171a5da52276b6a034CB859ddA1e905739F8B2 10000000000000000000 0x04d1eC716Fe9AC219D59b9E4f0D64D3B4339642e 10000000000000000000 0x14C06EC9402f7CD52dd0AF02979a350EAF133F76 10000000000000000000 |
- Sau khi kết thúc thời gian đăng ký airdrop, từ danh sách ở trên, chúng ta tính toán ra Merkle Root và lưu trên smart contract.
- Dựa vào số lượng người đăng ký airdrop, chúng ta sẽ gửi số lượng token ERC-20 tương ứng vào smart contract Merkle Airdrop để có thể airdrop cho người dùng.
- Người dùng sau đó sẽ gọi đến contract Merkle Airdrop để claim về lượng token đã đăng ký. Dựa vào Merkle Proof, contract sẽ tính toán liệu xem địa chỉ này đã đăng ký airdrop hay chưa và số lượng token claim có thỏa mãn hay không ? Nếu đúng thì contract sẽ gửi lượng token tương ứng cho người dùng.
Ví dụ cụ thể
Bên chúng tôi cũng đã có xây dựng 1 trang Airdrop và cho đến nay vẫn đang hoạt động khá ổn.
Đăng ký: Người dùng sẽ đăng ký nhận airdrop thông qua BOT trên Telegram với một số điều kiện như join box chat Telegram, follow twitter hay retweet. Khi người dùng hoàn thành các bước đăng ký thì con BOT sẽ gọi đến server Node.js và lưu thông tin của người dùng vào MongoDB.
Claim: Sau khi đóng đăng ký airdrop và cho phép người dùng claim token. Người dùng sẽ vào trang airdrop. Server sẽ tính toán và trả về Merkle Proof dựa trên address của người dùng. Sau đó, người dùng ký giao dịch gọi đến smart contract để claim, nếu người dùng đã đăng ký trước đó thì sẽ nhận được token khi hoàn thành giao dịch.
4. Smart contract
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 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | <span class="token comment">// MerkleAirdrop.sol</span> pragma solidity <span class="token operator">^</span><span class="token number">0.6</span><span class="token number">.0</span><span class="token punctuation">;</span> pragma experimental ABIEncoderV2<span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token string">"@openzeppelin/contracts/cryptography/MerkleProof.sol"</span><span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token string">"@openzeppelin/contracts/token/ERC20/IERC20.sol"</span><span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token string">"@openzeppelin/contracts/token/ERC20/SafeERC20.sol"</span><span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token string">"@openzeppelin/contracts/math/SafeMath.sol"</span><span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token string">"@openzeppelin/contracts/proxy/Initializable.sol"</span><span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token string">"@openzeppelin/contracts/access/Ownable.sol"</span><span class="token punctuation">;</span> contract PhoneAirdrop is Ownable <span class="token punctuation">{</span> using SafeERC20 <span class="token keyword">for</span> <span class="token constant">IERC20</span><span class="token punctuation">;</span> using SafeMath <span class="token keyword">for</span> uint256<span class="token punctuation">;</span> event <span class="token function">Claimed</span><span class="token punctuation">(</span>address claimant<span class="token punctuation">,</span> uint256 week<span class="token punctuation">,</span> uint256 balance<span class="token punctuation">)</span><span class="token punctuation">;</span> event <span class="token function">TrancheAdded</span><span class="token punctuation">(</span>uint256 tranche<span class="token punctuation">,</span> bytes32 merkleRoot<span class="token punctuation">,</span> uint256 totalAmount<span class="token punctuation">)</span><span class="token punctuation">;</span> event <span class="token function">TrancheExpired</span><span class="token punctuation">(</span>uint256 tranche<span class="token punctuation">)</span><span class="token punctuation">;</span> event <span class="token function">RemovedFunder</span><span class="token punctuation">(</span>address indexed _address<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token constant">IERC20</span> <span class="token keyword">public</span> token<span class="token punctuation">;</span> <span class="token function">mapping</span><span class="token punctuation">(</span><span class="token parameter">uint256</span> <span class="token operator">=></span> bytes32<span class="token punctuation">)</span> <span class="token keyword">public</span> merkleRoots<span class="token punctuation">;</span> <span class="token function">mapping</span><span class="token punctuation">(</span><span class="token parameter">uint256</span> <span class="token operator">=></span> <span class="token function">mapping</span><span class="token punctuation">(</span><span class="token parameter">address</span> <span class="token operator">=></span> bool<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">public</span> claimed<span class="token punctuation">;</span> uint256 <span class="token keyword">public</span> tranches<span class="token punctuation">;</span> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token constant">IERC20</span> _token<span class="token punctuation">)</span> <span class="token keyword">public</span> <span class="token punctuation">{</span> token <span class="token operator">=</span> _token<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">seedNewAllocations</span><span class="token punctuation">(</span><span class="token parameter">bytes32 _merkleRoot<span class="token punctuation">,</span> uint256 _totalAllocation</span><span class="token punctuation">)</span> <span class="token keyword">public</span> onlyOwner <span class="token function">returns</span> <span class="token punctuation">(</span><span class="token parameter">uint256 trancheId</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> token<span class="token punctuation">.</span><span class="token function">safeTransferFrom</span><span class="token punctuation">(</span>msg<span class="token punctuation">.</span>sender<span class="token punctuation">,</span> <span class="token function">address</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">,</span> _totalAllocation<span class="token punctuation">)</span><span class="token punctuation">;</span> trancheId <span class="token operator">=</span> tranches<span class="token punctuation">;</span> merkleRoots<span class="token punctuation">[</span>trancheId<span class="token punctuation">]</span> <span class="token operator">=</span> _merkleRoot<span class="token punctuation">;</span> tranches <span class="token operator">=</span> tranches<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span> emit <span class="token function">TrancheAdded</span><span class="token punctuation">(</span>trancheId<span class="token punctuation">,</span> _merkleRoot<span class="token punctuation">,</span> _totalAllocation<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">expireTranche</span><span class="token punctuation">(</span><span class="token parameter">uint256 _trancheId</span><span class="token punctuation">)</span> <span class="token keyword">public</span> onlyOwner <span class="token punctuation">{</span> merkleRoots<span class="token punctuation">[</span>_trancheId<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">bytes32</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span> emit <span class="token function">TrancheExpired</span><span class="token punctuation">(</span>_trancheId<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">claimWeek</span><span class="token punctuation">(</span> <span class="token parameter">address _liquidityProvider<span class="token punctuation">,</span> uint256 _tranche<span class="token punctuation">,</span> uint256 _balance<span class="token punctuation">,</span> bytes32<span class="token punctuation">[</span><span class="token punctuation">]</span> memory _merkleProof</span> <span class="token punctuation">)</span> <span class="token keyword">public</span> <span class="token punctuation">{</span> <span class="token function">_claimWeek</span><span class="token punctuation">(</span>_liquidityProvider<span class="token punctuation">,</span> _tranche<span class="token punctuation">,</span> _balance<span class="token punctuation">,</span> _merkleProof<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">_disburse</span><span class="token punctuation">(</span>_liquidityProvider<span class="token punctuation">,</span> _balance<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">claimWeeks</span><span class="token punctuation">(</span> <span class="token parameter">address _liquidityProvider<span class="token punctuation">,</span> uint256<span class="token punctuation">[</span><span class="token punctuation">]</span> memory _tranches<span class="token punctuation">,</span> uint256<span class="token punctuation">[</span><span class="token punctuation">]</span> memory _balances<span class="token punctuation">,</span> bytes32<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token punctuation">]</span> memory _merkleProofs</span> <span class="token punctuation">)</span> <span class="token keyword">public</span> <span class="token punctuation">{</span> uint256 len <span class="token operator">=</span> _tranches<span class="token punctuation">.</span>length<span class="token punctuation">;</span> <span class="token function">require</span><span class="token punctuation">(</span>len <span class="token operator">==</span> _balances<span class="token punctuation">.</span>length <span class="token operator">&&</span> len <span class="token operator">==</span> _merkleProofs<span class="token punctuation">.</span>length<span class="token punctuation">,</span> <span class="token string">"Mismatching inputs"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> uint256 totalBalance <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> <span class="token keyword">for</span><span class="token punctuation">(</span>uint256 i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator"><</span> len<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">_claimWeek</span><span class="token punctuation">(</span>_liquidityProvider<span class="token punctuation">,</span> _tranches<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">,</span> _balances<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">,</span> _merkleProofs<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> totalBalance <span class="token operator">=</span> totalBalance<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span>_balances<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">_disburse</span><span class="token punctuation">(</span>_liquidityProvider<span class="token punctuation">,</span> totalBalance<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">verifyClaim</span><span class="token punctuation">(</span> <span class="token parameter">address _liquidityProvider<span class="token punctuation">,</span> uint256 _tranche<span class="token punctuation">,</span> uint256 _balance<span class="token punctuation">,</span> bytes32<span class="token punctuation">[</span><span class="token punctuation">]</span> memory _merkleProof</span> <span class="token punctuation">)</span> <span class="token keyword">public</span> view <span class="token function">returns</span> <span class="token punctuation">(</span><span class="token parameter">bool valid</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">_verifyClaim</span><span class="token punctuation">(</span>_liquidityProvider<span class="token punctuation">,</span> _tranche<span class="token punctuation">,</span> _balance<span class="token punctuation">,</span> _merkleProof<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">_claimWeek</span><span class="token punctuation">(</span> <span class="token parameter">address _liquidityProvider<span class="token punctuation">,</span> uint256 _tranche<span class="token punctuation">,</span> uint256 _balance<span class="token punctuation">,</span> bytes32<span class="token punctuation">[</span><span class="token punctuation">]</span> memory _merkleProof</span> <span class="token punctuation">)</span> <span class="token keyword">private</span> <span class="token punctuation">{</span> <span class="token function">require</span><span class="token punctuation">(</span>_tranche <span class="token operator"><</span> tranches<span class="token punctuation">,</span> <span class="token string">"Week cannot be in the future"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token operator">!</span>claimed<span class="token punctuation">[</span>_tranche<span class="token punctuation">]</span><span class="token punctuation">[</span>_liquidityProvider<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token string">"LP has already claimed"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token function">_verifyClaim</span><span class="token punctuation">(</span>_liquidityProvider<span class="token punctuation">,</span> _tranche<span class="token punctuation">,</span> _balance<span class="token punctuation">,</span> _merkleProof<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token string">"Incorrect merkle proof"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> claimed<span class="token punctuation">[</span>_tranche<span class="token punctuation">]</span><span class="token punctuation">[</span>_liquidityProvider<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> emit <span class="token function">Claimed</span><span class="token punctuation">(</span>_liquidityProvider<span class="token punctuation">,</span> _tranche<span class="token punctuation">,</span> _balance<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">_verifyClaim</span><span class="token punctuation">(</span> <span class="token parameter">address _liquidityProvider<span class="token punctuation">,</span> uint256 _tranche<span class="token punctuation">,</span> uint256 _balance<span class="token punctuation">,</span> bytes32<span class="token punctuation">[</span><span class="token punctuation">]</span> memory _merkleProof</span> <span class="token punctuation">)</span> <span class="token keyword">private</span> view <span class="token function">returns</span> <span class="token punctuation">(</span><span class="token parameter">bool valid</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> bytes32 leaf <span class="token operator">=</span> <span class="token function">keccak256</span><span class="token punctuation">(</span>abi<span class="token punctuation">.</span><span class="token function">encodePacked</span><span class="token punctuation">(</span>_liquidityProvider<span class="token punctuation">,</span> _balance<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> MerkleProof<span class="token punctuation">.</span><span class="token function">verify</span><span class="token punctuation">(</span>_merkleProof<span class="token punctuation">,</span> merkleRoots<span class="token punctuation">[</span>_tranche<span class="token punctuation">]</span><span class="token punctuation">,</span> leaf<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">_disburse</span><span class="token punctuation">(</span><span class="token parameter">address _liquidityProvider<span class="token punctuation">,</span> uint256 _balance</span><span class="token punctuation">)</span> <span class="token keyword">private</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>_balance <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> token<span class="token punctuation">.</span><span class="token function">safeTransfer</span><span class="token punctuation">(</span>_liquidityProvider<span class="token punctuation">,</span> _balance<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token function">revert</span><span class="token punctuation">(</span><span class="token string">"No balance would be transferred - not going to waste your gas"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Chúng ta cùng tìm hiểu một chút về logic của contract Merkle Airdrop
- Biến
tranches
lưu id của đợt Airdrop (chúng ta có thể mở nhiều đợt airdrop khác nhau) - Mapping
merkleRoots
lưu giá trị Merkle Root của đợt Airdrop tương ứng. - Mapping
claimed
dùng để check xem trong đợt airdrop cụ thể thì địa chỉ đó đã claim hay chưa ? - Hàm
seedNewAllocations
là hàm init đợt Airdrop, sau khi kết thúc đăng ký airdrop thì owner của contract sẽ gọi đến hàm này để chuyển token vào contract cũng như lưu giá trị Merkle Root. - Hàm private
_claimWeek
sẽ check các điều kiện xem địa chỉ của user đã claim hay chưa ? idtranches
có hợp lệ hay không ? - Hàm
_verifyClaim
sẽ dựa vào Merkle Proof người dùng gửi lên để tính toán xem địa chỉ có đúng là đã đăng ký airdrop hay chưa ? - Cuối cùng là hàm
_disburse
là hàm sẽ gửi token từ contract đến cho người dùng khi tất cả các điều kiện đã được thỏa mãn.