什么空投合约“空投合约”(AirdropContract)是指专门用于自动向一组地址发送代币或NFT的智能合约:https://learnblockchain.cn/shawn_shaw
“空投合约”(Airdrop Contract
)是指专门用于自动向一组地址发送代币或 NFT
的智能合约。常用与项目早期免费向参与者发送奖励,激励用户参与项目,获取流量。
加密货币历史上资金量最大的空投项目是 Hyperliquid
的 HYPE
代币分发。该项目于 2024
年 11
月向其社区分发了 3.1
亿枚 HYPE
代币,占总供应量的 31%
。在短短几周内,这些代币的价值飙升至超过 108
亿美元,创下了空投历史上的新纪录。
下面我将以以太坊上的空投合约构建说起,使用以太坊
ERC20
合约来讲解智能合约中发放空投的三种实现方式。
空投合约的实现方式分别有三种。
批量铸造:项目方直接通过调用合约中的 mint
函数,主动给满足条件的参与者批量铸造代币。但是这种方式,虽然简单易懂,但在参与者较多的情况下,通常需要消耗海量 gas
。极不经济。并且这种方式发送空投,权限全由项目方控制,中心化程度较高。
默克尔证明:这种方式通过在智能合约中存储一个默克尔根,链下维护默克尔树。默克尔树及默克尔证明原理请看默克尔树 用户领取空投,传入默克尔证明和空投地址即可完成空投地址的认证。
默克尔证明的好处是,链上只需存储默克尔树根即可,通过节省合约的存储空间来降低 gas
消耗。链下维护默克尔树。在验证空投资格的时候,传入空投地址和相应的默克尔证明,通过合约内的计算即可完成空投的发放。缺点是,一旦部署合约后,合约中的默克尔根无法修改,没办法动态增加空投地址。
gas
,因为链上无需存储任何数据,签名全程在链下执行。并且,数字签名的方式还能实现空投地址的动态增加。下面,我们来分别讲解三种方案的实现流程。
链上合约:
编写一个 dropForBatch
函数,只允许合约拥有者调用,使用 for
循环对代币进行 mint
。完成空投发送。
/*方案一:项目方批量发送空投
只能项目方(合约拥有者)调用
*/
function dropForBatch(
address[] calldata spenders,
uint256[] calldata values
) external onlyOwner() {
require(spenders.length == values.length, "Lengths of Addresses and Amounts NOT EQUAL");
/*循环批量给用户 mint */
for (uint i = 0; i < spenders.length; i++) {
_mint(spenders[i], values[i]);
}
}
链下调用:
构造 receivers
地址数组和 amounts
地址数组,直接发起调用即可。
address[] memory receivers = new address[](2);
uint256[] memory amounts = new uint256[](2);
receivers[0] = user1;
receivers[1] = user2;
amounts[0] = 100 ether;
amounts[1] = 200 ether;
airdrop.dropForBatch(receivers, amounts);
链上合约:
在链上,最主要是要保存一个默克尔树的树根,根据传入的地址和 proof
来构造出这个树根则说明地址是正确的。
/*空投默克尔树根 hash*/
bytes32 immutable public rootHash;
调用方法上,先校验地址是否已空投过,然后使用 OZ
代码库中的 MerkleProof
库来验证 proof
和 leafHash
是否能构建出 rootHash
。如果能,则说明这个 leaf
(空投地址)有资格领取空投。
/*
方案二:默克尔证明
用 MerkleProof 库判断 (spender + value) 和 proof 构建的 recoverRoothash 是否和 rootHash 一致
*/
function dropForMerkleProof(
address spender,
uint256 value,
bytes32[] calldata proof
) external {
/*已经空投过*/
require(!mintedAddress[spender],"Aleardy minted!");
/*用 地址 + 数量 构建叶子节点 hash*/
bytes32 leafHash = keccak256(abi.encodePacked(spender,value));
bool inAirdrop = MerkleProof.verify(proof,rootHash,leafHash);
/*校验默克尔证明是否正确*/
require(inAirdrop,"Invalid merkle proof!");
mintedAddress[spender] = true;
_mint(spender, value);
}
链下调用:
链下部分,我们首先要保存着一棵完整的默克尔树(前端有相应的 js
库可以处理,后面代码仅构造一个非常简单的二层树),有了这棵树之后,我们可以在这棵树中找到某个空投地址的 hash
(在叶子节点上)。然后我们使用这个地址的 hash
,往上去寻找其兄弟节点的 hash
,构造出一个 proof
数组。然后使用空投地址、数量、proof 数组
,发送到合约中即可。
/*这里仅用 1 个兄弟节点替代,
默克尔树较深的肯定不止一个,
leaf2 为 leaf1 的兄弟节点
*/
proof = new bytes32[](1);
proof[0] = leaf2;
token.dropForMerkleProof(user1, 100 ether, proof);
链上合约:
链上合约部分,首先是使用到 ECDSA
的库来对 messageHash
和 signature
来恢复出地址,如果此地址等于我们的合约管理员的地址。则说明这个被签名的地址(参数中的 spender
)有资格领取空投合约。(注意:这里未考虑签名重放攻击的隐患。理论上,需要设置 nonce
、chainId
、deadline
)
/*方案三:使用数字签名验证空投人是否有权限
未针对签名可重放攻击,
需要避免签名可重放攻击需加上 nonce、chainId、deadline 来构建消息进行签名
*/
function dropForSignature(
address spender,
uint256 value,
bytes calldata signature
) external {
require(!mintedAddress[spender], "Already minted!");
/*恢复 messageHash*/
bytes32 dataHash = keccak256(abi.encodePacked(spender,value));
bytes32 messageHash = MessageHashUtils.toEthSignedMessageHash(dataHash);
/*从 messageHash 和 signature中恢复地址*/
address recovered = ECDSA.recover(messageHash, signature);
require(owner() == recovered,"verify signature fail");
mintedAddress[spender] = true;
_mint(spender,value);
}
链下调用:
链下调用部分,当然是首先需要使用合约管理员的私钥对 spender
(user1
) 地址签发一个允许空投的签名(参数为 spender
地址、代币数量
)。然后使用 spender
地址、代币数量
、签名
直接调用空投合约即可。
uint256 amount = 100 ether;
// 构造签名消息
bytes32 dataHash = keccak256(abi.encodePacked(user1, amount));
bytes32 messageHash = MessageHashUtils.toEthSignedMessageHash(dataHash);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, messageHash);
bytes memory signature = abi.encodePacked(r, s, v);
// 伪造成 user1 调用合约领取
vm.prank(user1);
token.dropForSignature(user1, amount, signature);
空投合约 Airdrop1.sol
contract Airdrop1 is ERC20, Ownable {
constructor(string memory _name, string memory _symbol) ERC20(_name,_symbol) Ownable(msg.sender) {
}
/*方案一:项目方批量发送空投
只能项目方(合约拥有者)调用
*/
function dropForBatch(
address[] calldata spenders,
uint256[] calldata values
) external onlyOwner() {
require(spenders.length == values.length, "Lengths of Addresses and Amounts NOT EQUAL");
/*循环批量给用户 mint */
for (uint i = 0; i < spenders.length; i++) {
_mint(spenders[i], values[i]);
}
}
}
- 测试 Airdrop1.t.sol
contract Airdrop1Test is Test {
Airdrop1 public airdrop;
address public owner;
address public user1 = address(0x1);
address public user2 = address(0x2);
function setUp() public {
owner = address(this);
airdrop = new Airdrop1("TestToken", "TTK");
}
function testBatchDrop() public {
address[] memory receivers = new address[](2);
uint256[] memory amounts = new uint256[](2);
receivers[0] = user1;
receivers[1] = user2;
amounts[0] = 100 ether;
amounts[1] = 200 ether;
airdrop.dropForBatch(receivers, amounts);
assertEq(airdrop.balanceOf(user1), 100 ether);
assertEq(airdrop.balanceOf(user2), 200 ether);
}
}
- **测试命令**
```js
forge test --match-path "./test/airdrop/Airdrop1.t.sol" -vvvv
链上合约 Airdrop2.sol
contract Airdrop2 is ERC20 {
/*空投默克尔树根 hash*/
bytes32 immutable public rootHash;
/*记录已空投的地址*/
mapping(address => bool) public mintedAddress;
constructor(string memory _name, string memory _symbol, bytes32 _rootHash) ERC20(_name,_symbol){
rootHash = _rootHash;
}
/*
方案二:默克尔证明
用 MerkleProof 库判断 (spender + value) 和 proof 构建的 recoverRoothash 是否和 rootHash 一致
*/
function dropForMerkleProof(
address spender,
uint256 value,
bytes32[] calldata proof
) external {
/*已经空投过*/
require(!mintedAddress[spender],"Aleardy minted!");
/*用 地址 + 数量 构建叶子节点 hash*/
bytes32 leafHash = keccak256(abi.encodePacked(spender,value));
bool inAirdrop = MerkleProof.verify(proof,rootHash,leafHash);
/*校验默克尔证明是否正确*/
require(inAirdrop,"Invalid merkle proof!");
mintedAddress[spender] = true;
_mint(spender, value);
}
}
测试 Airdrop2.t.sol
contract Airdrop2Test is Test {
Airdrop2 public token;
address user1 = address(0x1);
address user2 = address(0x2);
address user3 = address(0x3); // 非空投地址
bytes32 public root;
bytes32[] public proof;
bytes32 public leaf1;
bytes32 public leaf2;
function setUp() public {
// 构建 Merkle Tree (2个地址)
address[] memory recipients = new address[](2);
uint256[] memory amounts = new uint256[](2);
recipients[0] = user1;
recipients[1] = user2;
amounts[0] = 100 ether;
amounts[1] = 200 ether;
bytes32[] memory leaves = new bytes32[](2);
leaves[0] = keccak256(abi.encodePacked(recipients[0], amounts[0]));
leaves[1] = keccak256(abi.encodePacked(recipients[1], amounts[1]));
// 构建 Merkle Tree 手动计算 root (简单两层)
// 若已排序:hash(left, right)
leaf1 = leaves[0];
leaf2 = leaves[1];
if (leaf1 < leaf2) {
root = keccak256(abi.encodePacked(leaf1, leaf2));
} else {
root = keccak256(abi.encodePacked(leaf2, leaf1));
}
console.log("==== root =====");
console.logBytes32(root);
token = new Airdrop2("AirdropToken", "ATK", root);
}
/*有效 proof*/
function testAirdropWithValidProof() public {
vm.prank(user1);
/*这里仅用 1 个兄弟节点替代,
默克尔树较深的肯定不止一个,
leaf2 为 leaf1 的兄弟节点
*/
proof = new bytes32[](1);
proof[0] = leaf2;
token.dropForMerkleProof(user1, 100 ether, proof);
assertEq(token.balanceOf(user1), 100 ether);
assertTrue(token.mintedAddress(user1));
}
/*无效地址*/
function testAirdropWithWrongProof() public {
vm.prank(user3);
proof = new bytes32[](1);
proof[0] = leaf2;
vm.expectRevert("Invalid merkle proof!");
token.dropForMerkleProof(user3, 300 ether, proof); // 错误地址 + 错误 proof
}
}
forge test --match-path "./test/airdrop/Airdrop2.t.sol" -vvv
链上合约 Airdrop3.sol
contract Airdrop3 is ERC20, Ownable{
mapping(address => bool) public mintedAddress;
constructor(string memory _name, string memory _symbol) ERC20(_name,_symbol) Ownable(msg.sender){
}
/*方案三:使用数字签名验证空投人是否有权限
未针对签名可重放攻击,
需要避免签名可重放攻击需加上 nonce、chainId、deadline 来构建消息进行签名
*/
function dropForSignature(
address spender,
uint256 value,
bytes calldata signature
) external {
require(!mintedAddress[spender], "Already minted!");
/*恢复 messageHash*/
bytes32 dataHash = keccak256(abi.encodePacked(spender,value));
bytes32 messageHash = MessageHashUtils.toEthSignedMessageHash(dataHash);
/*从 messageHash 和 signature中恢复地址*/
address recovered = ECDSA.recover(messageHash, signature);
require(owner() == recovered,"verify signature fail");
mintedAddress[spender] = true;
_mint(spender,value);
}
}
测试 Airdrop3.t.sol
contract Airdrop3Test is Test {
Airdrop3 public token;
address public owner;
uint256 public ownerPrivateKey;
address public user1;
uint256 public user1PrivateKey;
function setUp() public {
ownerPrivateKey = 0xAAAAA;
owner = vm.addr(ownerPrivateKey); // owner 拥有合约
vm.prank(owner);
token = new Airdrop3("AirdropToken", "ADT");
// 创建 user1 地址
user1PrivateKey = 0xA11CE; // 只是个示例私钥
user1 = vm.addr(user1PrivateKey);
}
/*测试成功签名领取空投*/
function testSuccessfulAirdropBySignature() public {
uint256 amount = 100 ether;
// 构造签名消息
bytes32 dataHash = keccak256(abi.encodePacked(user1, amount));
bytes32 messageHash = MessageHashUtils.toEthSignedMessageHash(dataHash);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, messageHash);
bytes memory signature = abi.encodePacked(r, s, v);
// 伪造成 user1 调用合约领取
vm.prank(user1);
token.dropForSignature(user1, amount, signature);
// 校验领取是否成功
assertEq(token.balanceOf(user1), amount);
assertTrue(token.mintedAddress(user1));
}
/*测试验签失败*/
function testAirdropBySignatureFail() public {
uint256 amount = 100 ether;
// 构造签名消息
bytes32 dataHash = keccak256(abi.encodePacked(user1, amount));
bytes32 messageHash = MessageHashUtils.toEthSignedMessageHash(dataHash);
/*给个错误的私钥去签名*/
(uint8 v, bytes32 r, bytes32 s) = vm.sign(user1PrivateKey, messageHash);
bytes memory signature = abi.encodePacked(r, s, v);
// 伪造成 user1 调用合约领取
vm.prank(user1);
vm.expectRevert("verify signature fail");
token.dropForSignature(user1, amount, signature);
}
}
测试命令
forge test --match-path "./test/airdrop/Airdrop3.t.sol" -vvv
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!