Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip updates #425

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@
[submodule "lib/ERC721A"]
path = lib/ERC721A
url = https://github.com/chiru-labs/ERC721A
[submodule "lib/sstore2"]
path = lib/sstore2
url = https://github.com/0xsequence/sstore2
195 changes: 154 additions & 41 deletions contracts/airdrop/AirdropERC721.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,22 @@
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol";

import "lib/sstore2/contracts/SSTORE2.sol";

// ========== Internal imports ==========

import "../interfaces/airdrop/IAirdropERC721.sol";

// ========== Features ==========
import "../extension/Ownable.sol";
import "../extension/PermissionsEnumerable.sol";
import "../extension/BatchAirdropContent.sol";

contract AirdropERC721 is
Initializable,
Ownable,
PermissionsEnumerable,
BatchAirdropContent,
ReentrancyGuardUpgradeable,
MulticallUpgradeable,
IAirdropERC721
Expand All @@ -41,6 +45,8 @@
bytes32 private constant MODULE_TYPE = bytes32("AirdropERC721");
uint256 private constant VERSION = 1;

uint256 private constant CONTENT_COUNT_FOR_POINTER = 1000;

uint256 public payeeCount;
uint256 public processedCount;

Expand All @@ -54,7 +60,7 @@
Constructor + initializer logic
//////////////////////////////////////////////////////////////*/

constructor() initializer {}

Check warning on line 63 in contracts/airdrop/AirdropERC721.sol

View workflow job for this annotation

GitHub Actions / lint

Code contains empty blocks

/// @dev Initiliazes the contract, like a constructor.
function initialize(address _defaultAdmin) external initializer {
Expand Down Expand Up @@ -82,21 +88,49 @@
//////////////////////////////////////////////////////////////*/

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

uint256 currentCount = payeeCount;
payeeCount += len;

AirdropBatch storage batch = _createAirdropBatch(tokenOwner, tokenAddress, currentCount + len);

uint256 size = len > CONTENT_COUNT_FOR_POINTER ? CONTENT_COUNT_FOR_POINTER : len;
AirdropContent[] memory tempContent = new AirdropContent[](size);

uint256 tempContentIndex = 0;
for (uint256 i = 0; i < len; ) {
airdropContent[i + currentCount] = _contents[i];
// airdropContent[i + currentCount] = _contents[i];
tempContent[tempContentIndex++] = _contents[i];

if (tempContentIndex == CONTENT_COUNT_FOR_POINTER) {
address pointer = SSTORE2.write(abi.encode(tempContent));
batch.pointers.push(pointer);

uint256 size = (len - i - 1) > CONTENT_COUNT_FOR_POINTER ? CONTENT_COUNT_FOR_POINTER : (len - i - 1);
tempContent = new AirdropContent[](size);
tempContentIndex = 0;

i += 1;
continue;
}

unchecked {
i += 1;
}
}

if (tempContent.length > 0) {
address pointer = SSTORE2.write(abi.encode(tempContent));
batch.pointers.push(pointer);
}

emit RecipientsAdded(currentCount, currentCount + len);
}

Expand Down Expand Up @@ -126,37 +160,68 @@

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

processedCount += paymentsToProcess;

for (uint256 i = countOfProcessed; i < (countOfProcessed + paymentsToProcess); ) {
AirdropContent memory content = airdropContent[i];
(uint256 _startBatchId, uint256 _endBatchId) = _getBatchesToProcess(
countOfProcessed,
countOfProcessed + paymentsToProcess
);

bool failed;
try
IERC721(content.tokenAddress).safeTransferFrom{ gas: 80_000 }(
content.tokenOwner,
content.recipient,
content.tokenId
)
{} catch {
// revert if failure is due to unapproved tokens
require(
(IERC721(content.tokenAddress).ownerOf(content.tokenId) == content.tokenOwner &&
address(this) == IERC721(content.tokenAddress).getApproved(content.tokenId)) ||
IERC721(content.tokenAddress).isApprovedForAll(content.tokenOwner, address(this)),
"Not owner or approved"
);
processedCount += paymentsToProcess;

// record all other failures, likely originating from recipient accounts
indicesOfFailed.push(i);
failed = true;
uint256 remainingPayments = paymentsToProcess;
for (uint256 i = _startBatchId; i <= _endBatchId; i++) {
AirdropBatch memory batch = _getBatch(i);

uint256 _totalPointers = batch.pointers.length;
uint256 _pointerIdToProcess = batch.pointerIdToProcess;
uint256 _countProcessedInPointer = batch.countProcessedInPointer;

while (_pointerIdToProcess < _totalPointers) {
bytes memory pointerData = SSTORE2.read(batch.pointers[_pointerIdToProcess]);
AirdropContent[] memory contents = abi.decode(pointerData, (AirdropContent[]));

uint256 j = 0;
for (; j < contents.length && remainingPayments > 0; ) {
bool failed;
try
IERC721(batch.tokenAddress).safeTransferFrom{ gas: 80_000 }(
batch.tokenOwner,
contents[j].recipient,
contents[j].tokenId
)
{} catch {

Check warning on line 191 in contracts/airdrop/AirdropERC721.sol

View workflow job for this annotation

GitHub Actions / lint

Code contains empty blocks
// revert if failure is due to unapproved tokens
require(
(IERC721(batch.tokenAddress).ownerOf(contents[j].tokenId) == batch.tokenOwner &&
address(this) == IERC721(batch.tokenAddress).getApproved(contents[j].tokenId)) ||
IERC721(batch.tokenAddress).isApprovedForAll(batch.tokenOwner, address(this)),
"Not owner or approved"
);

// record all other failures, likely originating from recipient accounts
indicesOfFailed.push(j);
failed = true;
}

// emit AirdropPayment(contents[j].recipient, j, failed);

unchecked {
remainingPayments--;
j += 1;
}
}

if (remainingPayments == 0) {
_countProcessedInPointer = j;
break;
}

_pointerIdToProcess += 1;
}

emit AirdropPayment(content.recipient, i, failed);
batch.pointerIdToProcess = _pointerIdToProcess;
batch.countProcessedInPointer = _countProcessedInPointer;

unchecked {
i += 1;
}
_setBatch(i, batch);
}
}

Expand All @@ -167,23 +232,23 @@
*
* @param _contents List containing recipient, tokenId to airdrop.
*/
function airdrop(AirdropContent[] calldata _contents) external nonReentrant onlyRole(DEFAULT_ADMIN_ROLE) {
function airdrop(
address tokenOwner,
address tokenAddress,
AirdropContent[] calldata _contents
) external nonReentrant onlyRole(DEFAULT_ADMIN_ROLE) {
uint256 len = _contents.length;

for (uint256 i = 0; i < len; ) {
bool failed;
try
IERC721(_contents[i].tokenAddress).safeTransferFrom(
_contents[i].tokenOwner,
_contents[i].recipient,
_contents[i].tokenId
)
IERC721(tokenAddress).safeTransferFrom(tokenOwner, _contents[i].recipient, _contents[i].tokenId)
{} catch {

Check warning on line 246 in contracts/airdrop/AirdropERC721.sol

View workflow job for this annotation

GitHub Actions / lint

Code contains empty blocks
// revert if failure is due to unapproved tokens
require(
(IERC721(_contents[i].tokenAddress).ownerOf(_contents[i].tokenId) == _contents[i].tokenOwner &&
address(this) == IERC721(_contents[i].tokenAddress).getApproved(_contents[i].tokenId)) ||
IERC721(_contents[i].tokenAddress).isApprovedForAll(_contents[i].tokenOwner, address(this)),
(IERC721(tokenAddress).ownerOf(_contents[i].tokenId) == tokenOwner &&
address(this) == IERC721(tokenAddress).getApproved(_contents[i].tokenId)) ||
IERC721(tokenAddress).isApprovedForAll(tokenOwner, address(this)),
"Not owner or approved"
);

Expand All @@ -206,14 +271,62 @@
function getAllAirdropPayments(uint256 startId, uint256 endId)
external
view
returns (AirdropContent[] memory contents)
returns (AirdropContentView[] memory airdropContents)
{
require(startId <= endId && endId < payeeCount, "invalid range");

contents = new AirdropContent[](endId - startId + 1);
airdropContents = new AirdropContentView[](endId - startId + 1);

for (uint256 i = startId; i <= endId; i += 1) {
contents[i - startId] = airdropContent[i];
(uint256 _startBatchId, uint256 _endBatchId) = _getBatchesToProcess(startId, endId);

uint256 index = 0;
uint256 startingPointer = type(uint256).max;
for (uint256 i = _startBatchId; i <= _endBatchId; i++) {
AirdropBatch memory batch = _getBatch(i);

uint256 _totalPointers = batch.pointers.length;
uint256 _pointerIdToProcess = 0;

uint256 startIndexInFirstBatch = 0;
if (i == _startBatchId) {
startIndexInFirstBatch = _startBatchId == 0
? startId
: startId - _getBatch(_startBatchId - 1).batchEndIndex;
}

while (_pointerIdToProcess < _totalPointers) {
bytes memory pointerData = SSTORE2.read(batch.pointers[_pointerIdToProcess]);
AirdropContent[] memory contents = abi.decode(pointerData, (AirdropContent[]));

uint256 j = 0;
if (i == _startBatchId && startingPointer != type(uint256).max) {
if (contents.length < startIndexInFirstBatch) {
startIndexInFirstBatch -= contents.length;
_pointerIdToProcess += 1;
continue;
}

j = startIndexInFirstBatch;
startingPointer = _pointerIdToProcess;
}

for (; j < contents.length; ) {
if (index == endId - startId + 1) {
return airdropContents;
}

airdropContents[index].tokenOwner = batch.tokenOwner;
airdropContents[index].tokenAddress = batch.tokenAddress;
airdropContents[index].recipient = contents[j].recipient;
airdropContents[index].tokenId = contents[j].tokenId;
index++;
unchecked {
j += 1;
}
}

_pointerIdToProcess += 1;
}
}
}

Expand Down
92 changes: 92 additions & 0 deletions contracts/extension/BatchAirdropContent.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

/// @author thirdweb

contract BatchAirdropContent {
uint256[] private batchIds;

struct AirdropBatch {
address tokenOwner;
address tokenAddress;
uint256 batchEndIndex;
address[] pointers;
uint256 pointerIdToProcess;
uint256 countProcessedInPointer;
}

mapping(uint256 => AirdropBatch) private airdropBatch;

function getBatchCount() public view returns (uint256) {
return batchIds.length;
}

function _getBatchId(uint256 _checkIndex) internal view returns (uint256 batchId, uint256 index) {
uint256 numOfBatches = getBatchCount();
uint256[] memory indices = batchIds;

for (uint256 i = 0; i < numOfBatches; i += 1) {
if (_checkIndex < indices[i]) {
index = i;
batchId = indices[i];

return (batchId, index);
}
}

revert("Invalid tokenId");
}

function _setBatch(uint256 _batchId, AirdropBatch memory _batch) internal {
airdropBatch[_batchId] = _batch;
}

function _getBatch(uint256 index) internal view returns (AirdropBatch memory) {
return airdropBatch[index];
}

function _getBatchesToProcess(uint256 _startCount, uint256 _endCount)
internal
view
returns (uint256 startBatchId, uint256 endBatchId)
{
uint256[] memory _batchIds = batchIds;
uint256 batchCount = _batchIds.length;

for (uint256 i = 0; i < batchCount; i += 1) {
if (_startCount < _batchIds[i]) {
startBatchId = i;
break;
}
}

for (uint256 i = 0; i < batchCount; i += 1) {
if (_endCount < _batchIds[i]) {
endBatchId = i;
break;
}
}
}

function _createAirdropBatch(
address _tokenOwner,
address _tokenAddress,
uint256 _payeeCount
) internal returns (AirdropBatch storage) {
uint256 len = batchIds.length;
batchIds.push(_payeeCount);

AirdropBatch memory batch = AirdropBatch({
tokenOwner: _tokenOwner,
tokenAddress: _tokenAddress,
batchEndIndex: _payeeCount,
pointers: new address[](0),
pointerIdToProcess: 0,
countProcessedInPointer: 0
});

airdropBatch[len] = batch;

return airdropBatch[len];
}
}
Loading
Loading