Skip to content

Commit 024f8b5

Browse files
kumaryash90Krishang Nadgauda
and
Krishang Nadgauda
authored
Airdrop (#242)
* airdrop interfaces and contracts * run prettier * contracts for push and pull airdrops * run prettier * add tests * tests for airdrop claimable * run prettier * forge update * re-organize verifyClaim; remove verifyClaimMerkleProof * add tokenId to ERC1155 TokensClaimed event signature * replace IERC20 transferFrom with CurrencyTransferLib * docs update * run prettier * add native token support + tests to AirdropERC20 * fix AirdropERC1155Claimable tests * fix msg.value check * Fix remaining tests Co-authored-by: Krishang Nadgauda <[email protected]>
1 parent 1cf8dde commit 024f8b5

31 files changed

+3904
-0
lines changed

contracts/airdrop/AirdropERC1155.sol

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.11;
3+
4+
// ========== External imports ==========
5+
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
6+
7+
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
8+
import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol";
9+
10+
// ========== Internal imports ==========
11+
12+
import "../interfaces/airdrop/IAirdropERC1155.sol";
13+
14+
// ========== Features ==========
15+
import "../extension/Ownable.sol";
16+
17+
import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol";
18+
import "../lib/MerkleProof.sol";
19+
20+
contract AirdropERC1155 is
21+
Initializable,
22+
Ownable,
23+
ReentrancyGuardUpgradeable,
24+
ERC2771ContextUpgradeable,
25+
MulticallUpgradeable,
26+
IAirdropERC1155
27+
{
28+
/*///////////////////////////////////////////////////////////////
29+
State variables
30+
//////////////////////////////////////////////////////////////*/
31+
32+
bytes32 private constant MODULE_TYPE = bytes32("AirdropERC1155");
33+
uint256 private constant VERSION = 1;
34+
35+
/*///////////////////////////////////////////////////////////////
36+
Constructor + initializer logic
37+
//////////////////////////////////////////////////////////////*/
38+
39+
constructor() initializer {}
40+
41+
/// @dev Initiliazes the contract, like a constructor.
42+
function initialize(address _defaultAdmin) external initializer {
43+
_setupOwner(_defaultAdmin);
44+
__ReentrancyGuard_init();
45+
}
46+
47+
/*///////////////////////////////////////////////////////////////
48+
Generic contract logic
49+
//////////////////////////////////////////////////////////////*/
50+
51+
/// @dev Returns the type of the contract.
52+
function contractType() external pure returns (bytes32) {
53+
return MODULE_TYPE;
54+
}
55+
56+
/// @dev Returns the version of the contract.
57+
function contractVersion() external pure returns (uint8) {
58+
return uint8(VERSION);
59+
}
60+
61+
/*///////////////////////////////////////////////////////////////
62+
Airdrop logic
63+
//////////////////////////////////////////////////////////////*/
64+
65+
/**
66+
* @notice Lets contract-owner send ERC1155 tokens to a list of addresses.
67+
* @dev The token-owner should approve target tokens to Airdrop contract,
68+
* which acts as operator for the tokens.
69+
*
70+
* @param _tokenAddress Contract address of ERC1155 tokens to air-drop.
71+
* @param _tokenOwner Address from which to transfer tokens.
72+
* @param _recipients List of recipient addresses for the air-drop.
73+
* @param _amounts Quantity of tokens to air-drop, per recipient.
74+
* @param _tokenIds List of ERC1155 token-Ids to drop.
75+
*/
76+
function airdrop(
77+
address _tokenAddress,
78+
address _tokenOwner,
79+
address[] memory _recipients,
80+
uint256[] memory _amounts,
81+
uint256[] memory _tokenIds
82+
) external nonReentrant onlyOwner {
83+
uint256 len = _tokenIds.length;
84+
require(len == _recipients.length && len == _amounts.length, "length mismatch");
85+
86+
IERC1155 token = IERC1155(_tokenAddress);
87+
88+
for (uint256 i = 0; i < len; i++) {
89+
token.safeTransferFrom(_tokenOwner, _recipients[i], _tokenIds[i], _amounts[i], "");
90+
}
91+
}
92+
93+
/*///////////////////////////////////////////////////////////////
94+
Miscellaneous
95+
//////////////////////////////////////////////////////////////*/
96+
97+
/// @dev Returns whether owner can be set in the given execution context.
98+
function _canSetOwner() internal view virtual override returns (bool) {
99+
return msg.sender == owner();
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.11;
3+
4+
// ========== External imports ==========
5+
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
6+
7+
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
8+
import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol";
9+
10+
// ========== Internal imports ==========
11+
12+
import "../interfaces/airdrop/IAirdropERC1155Claimable.sol";
13+
14+
// ========== Features ==========
15+
import "../extension/Ownable.sol";
16+
17+
import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol";
18+
import "../lib/MerkleProof.sol";
19+
20+
contract AirdropERC1155Claimable is
21+
Initializable,
22+
Ownable,
23+
ReentrancyGuardUpgradeable,
24+
ERC2771ContextUpgradeable,
25+
MulticallUpgradeable,
26+
IAirdropERC1155Claimable
27+
{
28+
/*///////////////////////////////////////////////////////////////
29+
State variables
30+
//////////////////////////////////////////////////////////////*/
31+
32+
bytes32 private constant MODULE_TYPE = bytes32("AirdropERC1155Claimable");
33+
uint256 private constant VERSION = 1;
34+
35+
/// @dev address of token being airdropped.
36+
address public airdropTokenAddress;
37+
38+
/// @dev address of owner of tokens being airdropped.
39+
address public tokenOwner;
40+
41+
/// @dev list of tokens to airdrop.
42+
uint256[] public tokenIds;
43+
44+
/// @dev airdrop expiration timestamp.
45+
uint256 public expirationTimestamp;
46+
47+
/*///////////////////////////////////////////////////////////////
48+
Mappings
49+
//////////////////////////////////////////////////////////////*/
50+
51+
/// @dev Mapping from tokenId and claimer address to total number of tokens claimed.
52+
mapping(uint256 => mapping(address => uint256)) public supplyClaimedByWallet;
53+
54+
/// @dev general claim limit for a tokenId if claimer not in allowlist.
55+
mapping(uint256 => uint256) public maxWalletClaimCount;
56+
57+
/// @dev number tokens available to claim for a tokenId.
58+
mapping(uint256 => uint256) public availableAmount;
59+
60+
/// @dev mapping of tokenId to merkle root of the allowlist of addresses eligible to claim.
61+
mapping(uint256 => bytes32) public merkleRoot;
62+
63+
/*///////////////////////////////////////////////////////////////
64+
Constructor + initializer logic
65+
//////////////////////////////////////////////////////////////*/
66+
67+
constructor() initializer {}
68+
69+
/// @dev Initiliazes the contract, like a constructor.
70+
function initialize(
71+
address _defaultAdmin,
72+
address[] memory _trustedForwarders,
73+
address _tokenOwner,
74+
address _airdropTokenAddress,
75+
uint256[] memory _tokenIds,
76+
uint256[] memory _availableAmounts,
77+
uint256 _expirationTimestamp,
78+
uint256[] memory _maxWalletClaimCount,
79+
bytes32[] memory _merkleRoot
80+
) external initializer {
81+
_setupOwner(_defaultAdmin);
82+
__ReentrancyGuard_init();
83+
__ERC2771Context_init(_trustedForwarders);
84+
85+
tokenOwner = _tokenOwner;
86+
airdropTokenAddress = _airdropTokenAddress;
87+
tokenIds = _tokenIds;
88+
expirationTimestamp = _expirationTimestamp;
89+
90+
require(
91+
_maxWalletClaimCount.length == _tokenIds.length &&
92+
_merkleRoot.length == _tokenIds.length &&
93+
_availableAmounts.length == _tokenIds.length,
94+
"length mismatch."
95+
);
96+
97+
for (uint256 i = 0; i < _tokenIds.length; i++) {
98+
merkleRoot[_tokenIds[i]] = _merkleRoot[i];
99+
maxWalletClaimCount[_tokenIds[i]] = _maxWalletClaimCount[i];
100+
availableAmount[_tokenIds[i]] = _availableAmounts[i];
101+
}
102+
}
103+
104+
/*///////////////////////////////////////////////////////////////
105+
Generic contract logic
106+
//////////////////////////////////////////////////////////////*/
107+
108+
/// @dev Returns the type of the contract.
109+
function contractType() external pure returns (bytes32) {
110+
return MODULE_TYPE;
111+
}
112+
113+
/// @dev Returns the version of the contract.
114+
function contractVersion() external pure returns (uint8) {
115+
return uint8(VERSION);
116+
}
117+
118+
/*///////////////////////////////////////////////////////////////
119+
Claim logic
120+
//////////////////////////////////////////////////////////////*/
121+
122+
/**
123+
* @notice Lets an account claim a given quantity of ERC1155 tokens.
124+
*
125+
* @param _receiver The receiver of the tokens to claim.
126+
* @param _quantity The quantity of tokens to claim.
127+
* @param _tokenId Token Id to claim.
128+
* @param _proofs The proof of the claimer's inclusion in the merkle root allowlist
129+
* of the claim conditions that apply.
130+
* @param _proofMaxQuantityForWallet The maximum number of tokens an address included in an
131+
* allowlist can claim.
132+
*/
133+
function claim(
134+
address _receiver,
135+
uint256 _quantity,
136+
uint256 _tokenId,
137+
bytes32[] calldata _proofs,
138+
uint256 _proofMaxQuantityForWallet
139+
) external nonReentrant {
140+
address claimer = _msgSender();
141+
142+
verifyClaim(claimer, _quantity, _tokenId, _proofs, _proofMaxQuantityForWallet);
143+
144+
transferClaimedTokens(_receiver, _quantity, _tokenId);
145+
146+
emit TokensClaimed(_msgSender(), _receiver, _tokenId, _quantity);
147+
}
148+
149+
/// @dev Transfers the tokens being claimed.
150+
function transferClaimedTokens(
151+
address _to,
152+
uint256 _quantityBeingClaimed,
153+
uint256 _tokenId
154+
) internal {
155+
// if transfer claimed tokens is called when `to != msg.sender`, it'd use msg.sender's limits.
156+
// behavior would be similar to `msg.sender` mint for itself, then transfer to `_to`.
157+
supplyClaimedByWallet[_tokenId][_msgSender()] += _quantityBeingClaimed;
158+
availableAmount[_tokenId] -= _quantityBeingClaimed;
159+
160+
IERC1155(airdropTokenAddress).safeTransferFrom(tokenOwner, _to, _tokenId, _quantityBeingClaimed, "");
161+
}
162+
163+
/// @dev Checks a request to claim tokens against the active claim condition's criteria.
164+
function verifyClaim(
165+
address _claimer,
166+
uint256 _quantity,
167+
uint256 _tokenId,
168+
bytes32[] calldata _proofs,
169+
uint256 _proofMaxQuantityForWallet
170+
) public view {
171+
bool isOverride;
172+
173+
bytes32 mroot = merkleRoot[_tokenId];
174+
if (mroot != bytes32(0)) {
175+
(isOverride, ) = MerkleProof.verify(
176+
_proofs,
177+
mroot,
178+
keccak256(abi.encodePacked(_claimer, _proofMaxQuantityForWallet))
179+
);
180+
}
181+
182+
uint256 supplyClaimedAlready = supplyClaimedByWallet[_tokenId][_claimer];
183+
184+
require(_quantity > 0, "Claiming zero tokens");
185+
require(_quantity <= availableAmount[_tokenId], "exceeds available tokens.");
186+
187+
uint256 expTimestamp = expirationTimestamp;
188+
require(expTimestamp == 0 || block.timestamp < expTimestamp, "airdrop expired.");
189+
190+
uint256 claimLimitForWallet = isOverride ? _proofMaxQuantityForWallet : maxWalletClaimCount[_tokenId];
191+
require(_quantity + supplyClaimedAlready <= claimLimitForWallet, "invalid quantity.");
192+
}
193+
194+
/*///////////////////////////////////////////////////////////////
195+
Miscellaneous
196+
//////////////////////////////////////////////////////////////*/
197+
198+
/// @dev Returns whether owner can be set in the given execution context.
199+
function _canSetOwner() internal view virtual override returns (bool) {
200+
return _msgSender() == owner();
201+
}
202+
203+
function _msgSender() internal view virtual override returns (address sender) {
204+
return ERC2771ContextUpgradeable._msgSender();
205+
}
206+
207+
function _msgData() internal view virtual override returns (bytes calldata) {
208+
return ERC2771ContextUpgradeable._msgData();
209+
}
210+
}

0 commit comments

Comments
 (0)