Skip to content

Commit 49887e2

Browse files
Airdrop updates + audit fixes (#324)
* add stateless airdrop functions * failsafe * reset functionality for airdrops * failsafe for erc20 airdrop * fix tests * revert for unapproved tokens * add internal fn for currency transfer with bool return val * getter for cancelled payments * tests * [H-1] addRecipients adds empty airdrops on subsequent calls * [M-1] Fails for non-compliant ERC20 tokens * [Q-1] Unused library SafeERC20 * [M-2] resetRecipients() can exceed block gas limit * [L-1] No event emitted within resetRecipients() * [M-3] processPayments() can be griefed * [G-3] Use unchecked blocks to reduce gas * [G-4] Unnecessary to emit airdrop contents * [M-1] Fails for non-compliant ERC20 tokens: revised fix --------- Co-authored-by: Krishang <krishang@thirdweb.com>
1 parent 768a1eb commit 49887e2

15 files changed

+1887
-205
lines changed

contracts/airdrop/AirdropERC1155.sol

Lines changed: 120 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ contract AirdropERC1155 is
4444
uint256 public payeeCount;
4545
uint256 public processedCount;
4646

47-
uint256[] private indicesOfFailed;
47+
uint256[] public indicesOfFailed;
4848

4949
mapping(uint256 => AirdropContent) private airdropContent;
5050

51+
CancelledPayments[] public cancelledPaymentIndices;
52+
5153
/*///////////////////////////////////////////////////////////////
5254
Constructor + initializer logic
5355
//////////////////////////////////////////////////////////////*/
@@ -80,41 +82,82 @@ contract AirdropERC1155 is
8082
//////////////////////////////////////////////////////////////*/
8183

8284
///@notice Lets contract-owner set up an airdrop of ERC721 NFTs to a list of addresses.
83-
function addAirdropRecipients(AirdropContent[] calldata _contents) external onlyRole(DEFAULT_ADMIN_ROLE) {
85+
function addRecipients(AirdropContent[] calldata _contents) external onlyRole(DEFAULT_ADMIN_ROLE) {
8486
uint256 len = _contents.length;
8587
require(len > 0, "No payees provided.");
8688

8789
uint256 currentCount = payeeCount;
8890
payeeCount += len;
8991

90-
for (uint256 i = currentCount; i < len; i += 1) {
91-
airdropContent[i] = _contents[i];
92+
for (uint256 i = 0; i < len; ) {
93+
airdropContent[i + currentCount] = _contents[i];
94+
95+
unchecked {
96+
i += 1;
97+
}
9298
}
9399

94-
emit RecipientsAdded(_contents);
100+
emit RecipientsAdded(currentCount, currentCount + len);
101+
}
102+
103+
///@notice Lets contract-owner cancel any pending payments.
104+
function cancelPendingPayments(uint256 numberOfPaymentsToCancel) external onlyRole(DEFAULT_ADMIN_ROLE) {
105+
uint256 countOfProcessed = processedCount;
106+
107+
// increase processedCount by the specified count -- all pending payments in between will be treated as cancelled.
108+
uint256 newProcessedCount = countOfProcessed + numberOfPaymentsToCancel;
109+
require(newProcessedCount <= payeeCount, "Exceeds total payees.");
110+
processedCount = newProcessedCount;
111+
112+
CancelledPayments memory range = CancelledPayments({
113+
startIndex: countOfProcessed,
114+
endIndex: newProcessedCount - 1
115+
});
116+
117+
cancelledPaymentIndices.push(range);
118+
119+
emit PaymentsCancelledByAdmin(countOfProcessed, newProcessedCount - 1);
95120
}
96121

97122
/// @notice Lets contract-owner send ERC721 NFTs to a list of addresses.
98-
function airdrop(uint256 paymentsToProcess) external nonReentrant {
123+
function processPayments(uint256 paymentsToProcess) external nonReentrant onlyRole(DEFAULT_ADMIN_ROLE) {
99124
uint256 totalPayees = payeeCount;
100125
uint256 countOfProcessed = processedCount;
101126

102127
require(countOfProcessed + paymentsToProcess <= totalPayees, "invalid no. of payments");
103128

104129
processedCount += paymentsToProcess;
105130

106-
for (uint256 i = countOfProcessed; i < (countOfProcessed + paymentsToProcess); i += 1) {
131+
for (uint256 i = countOfProcessed; i < (countOfProcessed + paymentsToProcess); ) {
107132
AirdropContent memory content = airdropContent[i];
108133

109-
IERC1155(content.tokenAddress).safeTransferFrom(
110-
content.tokenOwner,
111-
content.recipient,
112-
content.tokenId,
113-
content.amount,
114-
""
115-
);
116-
117-
emit AirdropPayment(content.recipient, content);
134+
bool failed;
135+
try
136+
IERC1155(content.tokenAddress).safeTransferFrom{ gas: 80_000 }(
137+
content.tokenOwner,
138+
content.recipient,
139+
content.tokenId,
140+
content.amount,
141+
""
142+
)
143+
{} catch {
144+
// revert if failure is due to unapproved tokens
145+
require(
146+
IERC1155(content.tokenAddress).balanceOf(content.tokenOwner, content.tokenId) >= content.amount &&
147+
IERC1155(content.tokenAddress).isApprovedForAll(content.tokenOwner, address(this)),
148+
"Not balance or approved"
149+
);
150+
151+
// record and continue for all other failures, likely originating from recipient accounts
152+
indicesOfFailed.push(i);
153+
failed = true;
154+
}
155+
156+
emit AirdropPayment(content.recipient, i, failed);
157+
158+
unchecked {
159+
i += 1;
160+
}
118161
}
119162
}
120163

@@ -123,21 +166,38 @@ contract AirdropERC1155 is
123166
* @dev The token-owner should approve target tokens to Airdrop contract,
124167
* which acts as operator for the tokens.
125168
*
126-
* @param _tokenAddress Contract address of ERC1155 tokens to air-drop.
127-
* @param _tokenOwner Address from which to transfer tokens.
128169
* @param _contents List containing recipient, tokenId and amounts to airdrop.
129170
*/
130-
function airdrop(
131-
address _tokenAddress,
132-
address _tokenOwner,
133-
AirdropContent[] calldata _contents
134-
) external nonReentrant onlyOwner {
171+
function airdrop(AirdropContent[] calldata _contents) external nonReentrant onlyRole(DEFAULT_ADMIN_ROLE) {
135172
uint256 len = _contents.length;
136173

137-
IERC1155 token = IERC1155(_tokenAddress);
138-
139-
for (uint256 i = 0; i < len; i++) {
140-
token.safeTransferFrom(_tokenOwner, _contents[i].recipient, _contents[i].tokenId, _contents[i].amount, "");
174+
for (uint256 i = 0; i < len; ) {
175+
bool failed;
176+
try
177+
IERC1155(_contents[i].tokenAddress).safeTransferFrom(
178+
_contents[i].tokenOwner,
179+
_contents[i].recipient,
180+
_contents[i].tokenId,
181+
_contents[i].amount,
182+
""
183+
)
184+
{} catch {
185+
// revert if failure is due to unapproved tokens
186+
require(
187+
IERC1155(_contents[i].tokenAddress).balanceOf(_contents[i].tokenOwner, _contents[i].tokenId) >=
188+
_contents[i].amount &&
189+
IERC1155(_contents[i].tokenAddress).isApprovedForAll(_contents[i].tokenOwner, address(this)),
190+
"Not balance or approved"
191+
);
192+
193+
failed = true;
194+
}
195+
196+
emit StatelessAirdrop(_contents[i].recipient, _contents[i], failed);
197+
198+
unchecked {
199+
i += 1;
200+
}
141201
}
142202
}
143203

@@ -146,38 +206,45 @@ contract AirdropERC1155 is
146206
//////////////////////////////////////////////////////////////*/
147207

148208
/// @notice Returns all airdrop payments set up -- pending, processed or failed.
149-
function getAllAirdropPayments() external view returns (AirdropContent[] memory contents) {
150-
uint256 count = payeeCount;
151-
contents = new AirdropContent[](count);
209+
function getAllAirdropPayments(uint256 startId, uint256 endId)
210+
external
211+
view
212+
returns (AirdropContent[] memory contents)
213+
{
214+
require(startId <= endId && endId < payeeCount, "invalid range");
152215

153-
for (uint256 i = 0; i < count; i += 1) {
154-
contents[i] = airdropContent[i];
216+
contents = new AirdropContent[](endId - startId + 1);
217+
218+
for (uint256 i = startId; i <= endId; i += 1) {
219+
contents[i - startId] = airdropContent[i];
155220
}
156221
}
157222

158223
/// @notice Returns all pending airdrop payments.
159-
function getAllAirdropPaymentsPending() external view returns (AirdropContent[] memory contents) {
160-
uint256 endCount = payeeCount;
161-
uint256 startCount = processedCount;
162-
contents = new AirdropContent[](endCount - startCount);
224+
function getAllAirdropPaymentsPending(uint256 startId, uint256 endId)
225+
external
226+
view
227+
returns (AirdropContent[] memory contents)
228+
{
229+
require(startId <= endId && endId < payeeCount, "invalid range");
230+
231+
uint256 processed = processedCount;
232+
if (processed == payeeCount) {
233+
return contents;
234+
}
235+
236+
if (startId < processed) {
237+
startId = processed;
238+
}
239+
contents = new AirdropContent[](endId - startId + 1);
163240

164241
uint256 idx;
165-
for (uint256 i = startCount; i < endCount; i += 1) {
242+
for (uint256 i = startId; i <= endId; i += 1) {
166243
contents[idx] = airdropContent[i];
167244
idx += 1;
168245
}
169246
}
170247

171-
/// @notice Returns all pending airdrop processed.
172-
function getAllAirdropPaymentsProcessed() external view returns (AirdropContent[] memory contents) {
173-
uint256 count = processedCount;
174-
contents = new AirdropContent[](count);
175-
176-
for (uint256 i = 0; i < count; i += 1) {
177-
contents[i] = airdropContent[i];
178-
}
179-
}
180-
181248
/// @notice Returns all pending airdrop failed.
182249
function getAllAirdropPaymentsFailed() external view returns (AirdropContent[] memory contents) {
183250
uint256 count = indicesOfFailed.length;
@@ -188,12 +255,17 @@ contract AirdropERC1155 is
188255
}
189256
}
190257

258+
/// @notice Returns all blocks of cancelled payments as an array of index range.
259+
function getCancelledPaymentIndices() external view returns (CancelledPayments[] memory) {
260+
return cancelledPaymentIndices;
261+
}
262+
191263
/*///////////////////////////////////////////////////////////////
192264
Miscellaneous
193265
//////////////////////////////////////////////////////////////*/
194266

195267
/// @dev Returns whether owner can be set in the given execution context.
196268
function _canSetOwner() internal view virtual override returns (bool) {
197-
return msg.sender == owner();
269+
return hasRole(DEFAULT_ADMIN_ROLE, msg.sender);
198270
}
199271
}

0 commit comments

Comments
 (0)