diff --git a/README.md b/README.md index 2bf3f8b..eac823f 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,15 @@ Find the up-to-date contract addresses [here][contract-addresses]. ## Implementation Table -| Contract | Implemented Interfaces | -| ------------------ | ------------------------------------------------------- | -| `Biomapper` | [`IGenerationChangeEvents`], [`IProveUniquenessEvents`] | -| `BiomapperLog` | [`IBiomapperLogRead`] | -| `BridgedBiomapper` | [`IBridgedBiomapperRead`], [`IBridgeBiomappingEvents`] | +| Contract | Implemented Interfaces | +| ------------------ | ------------------------------------------------------------------------ | +| `Biomapper` | [`IGenerationChangeEvents`], [`IProveUniquenessEvents`] | +| `BiomapperLog` | [`IBiomapperLogRead`], [`IBiomapperLogAddressesPerGenerationEnumerator`] | +| `BridgedBiomapper` | [`IBridgedBiomapperRead`], [`IBridgeBiomappingEvents`] | [`IBiomapperLogRead`]: core/IBiomapperLogRead.sol/interface.IBiomapperLogRead.html [`IBridgedBiomapperRead`]: core/IBridgedBiomapperRead.sol/interface.IBridgedBiomapperRead.html +[`IBiomapperLogAddressesPerGenerationEnumerator`]: core/IBiomapperLogAddressesPerGenerationEnumerator.sol/interface.IBiomapperLogAddressesPerGenerationEnumerator.html [`IGenerationChangeEvents`]: events/IGenerationChangeEvents.sol/interface.IGenerationChangeEvents.html [`IProveUniquenessEvents`]: events/IProveUniquenessEvents.sol/interface.IProveUniquenessEvents.html [`IBridgeBiomappingEvents`]: events/IBridgeBiomappingEvents.sol/interface.IBridgeBiomappingEvents.html @@ -48,6 +49,7 @@ Import the dependencies from the `@biomapper-sdk` like this: ```solidity import {IBiomapperLogRead} from "@biomapper-sdk/core/IBiomapperLogRead.sol"; import {IBridgedBiomapperRead} from "@biomapper-sdk/core/IBridgedBiomapperRead.sol"; +import {IBiomapperLogAddressesPerGenerationEnumerator} from "@biomapper-sdk/core/IBiomapperLogAddressesPerGenerationEnumerator.sol"; import {BiomapperLogLib} from "@biomapper-sdk/libraries/BiomapperLogLib.sol"; import {BridgedBiomapperLib} from "@biomapper-sdk/libraries/BridgedBiomapperLib.sol"; ``` @@ -65,6 +67,7 @@ Import the dependencies from `biomapper-sdk` like this: ```solidity import {IBiomapperLogRead} from "biomapper-sdk/core/IBiomapperLogRead.sol"; import {IBridgedBiomapperRead} from "biomapper-sdk/core/IBridgedBiomapperRead.sol"; +import {IBiomapperLogAddressesPerGenerationEnumerator} from "biomapper-sdk/core/IBiomapperLogAddressesPerGenerationEnumerator.sol"; import {BiomapperLogLib} from "biomapper-sdk/libraries/BiomapperLogLib.sol"; import {BridgedBiomapperLib} from "biomapper-sdk/libraries/BridgedBiomapperLib.sol"; ``` diff --git a/core/IBiomapperLogAddressesPerGenerationEnumerator.sol b/core/IBiomapperLogAddressesPerGenerationEnumerator.sol new file mode 100644 index 0000000..9e52d56 --- /dev/null +++ b/core/IBiomapperLogAddressesPerGenerationEnumerator.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IBiomapperLogAddressesPerGenerationEnumerator { + /** + * @dev Retrieves all biomapped accounts within a specified generation. + * @param generationPtr The block number marking the start of the generation to get the list of biomapped accounts. + * @param cursor The starting index of the page. For the first request, set `cursor` to address(0). + * @param maxPageSize The maximum number of elements to return in this call (also soft-capped in the contract). + * @return nextCursor The starting index for the next page of results. + * @return biomappedAccounts An array of addresses that were biomapped within a specified generation. + * + * Notes: + * - For the first request, set `cursor` to address(0) to start from the beginning of the dataset. + * - If `nextCursor` is address(0), all available elements have been retrieved, indicating the end of the dataset. + * - There is a soft cap on the max page size that is implementation-dependent. + */ + function listAddressesPerGeneration( + uint256 generationPtr, + address cursor, + uint256 maxPageSize + ) + external + view + returns (address nextCursor, address[] memory biomappedAccounts); +} diff --git a/core/package.json b/core/package.json index 0c57b03..70a8539 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@biomapper-sdk/core", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "type": "module", "files": [ diff --git a/events/package.json b/events/package.json index fb4829b..50c0f94 100644 --- a/events/package.json +++ b/events/package.json @@ -1,6 +1,6 @@ { "name": "@biomapper-sdk/events", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "type": "module", "files": [ diff --git a/examples/proactive-sybil-resistant-airdrop/contracts/ProactiveSybilResistantAirdrop.sol b/examples/proactive-sybil-resistant-airdrop/contracts/ProactiveSybilResistantAirdrop.sol new file mode 100644 index 0000000..690171f --- /dev/null +++ b/examples/proactive-sybil-resistant-airdrop/contracts/ProactiveSybilResistantAirdrop.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IBiomapperLogRead} from "@biomapper-sdk/core/IBiomapperLogRead.sol"; +import {IBiomapperLogAddressesPerGenerationEnumerator} from "@biomapper-sdk/core/IBiomapperLogAddressesPerGenerationEnumerator.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/** + * @title Proactive Sybil-Resistant Airdrop + * @dev A contract for conducting a Sybil-resistant airdrop by sending tokens to all biomapped users. + */ +contract ProactiveSybilResistantAirdrop { + using SafeERC20 for IERC20; + + IERC20 public immutable ERC20_TOKEN; // The ERC20 token being airdropped + address public immutable TOKEN_VAULT; // The address holding the tokens for the airdrop + uint256 public immutable AMOUNT_PER_USER; // The amount of tokens to send to each user + IBiomapperLogAddressesPerGenerationEnumerator + public immutable BIOMAPPER_LOG; // The contract for retrieving unique users list + uint256 public immutable MAX_USERS_PER_AIRDROP_TICK; // Maximum amount of users to get tokens for each function call + uint256 public immutable GENERATION_PTR; // Current generation pointer at the moment of contract deployment + + address public nextAccountToGetAirdrop; // The cursor for enumertor + bool public airdropCompleted; // The completion flag + + /** + * @dev Constructor to initialize the contract with required parameters. + * @param tokenAddress The address of the ERC20 token being airdropped. + * @param tokenVault The address holding the tokens for the airdrop. + * @param amountPerUser The amount of tokens to send to each user. + * @param biomapperLogAddress The address of the contract for retrieving unique users list. + * @param maxUsersPerAirdropTick The address of the contract for checking uniqueness of users. + */ + constructor( + address tokenAddress, + address tokenVault, + uint256 amountPerUser, + address biomapperLogAddress, + uint256 maxUsersPerAirdropTick + ) { + ERC20_TOKEN = IERC20(tokenAddress); + TOKEN_VAULT = tokenVault; + AMOUNT_PER_USER = amountPerUser; + BIOMAPPER_LOG = IBiomapperLogAddressesPerGenerationEnumerator( + biomapperLogAddress + ); + MAX_USERS_PER_AIRDROP_TICK = maxUsersPerAirdropTick; + GENERATION_PTR = IBiomapperLogRead(biomapperLogAddress) + .generationsHead(); + } + + event AirdropIsCompleted(); + + /** + * @dev Send tokens to biomapped users in the set generation, no more than `MAX_USERS_PER_AIRDROP_TICK` users per call. + * @return needsMoreTicks The list of biomapped accounts is not exhausted, the airdrop is not completed. + */ + function airdropTick() public returns (bool needsMoreTicks) { + require(!airdropCompleted, "Airdrop is completed"); + + (address nextCursor, address[] memory biomappedAccounts) = BIOMAPPER_LOG + .listAddressesPerGeneration( + GENERATION_PTR, + nextAccountToGetAirdrop, + MAX_USERS_PER_AIRDROP_TICK + ); + + for (uint index = 0; index < biomappedAccounts.length; index++) { + ERC20_TOKEN.safeTransferFrom( + TOKEN_VAULT, + biomappedAccounts[index], + AMOUNT_PER_USER + ); + } + + nextAccountToGetAirdrop = nextCursor; + + if (nextCursor == address(0)) { + airdropCompleted = true; + emit AirdropIsCompleted(); + return false; + } + + return true; + } + + /** + * @dev Send tokens to all biomapped users in the set generation. + * This function may fail due to excessive gas usage, call `airdropTick` in multiple transactions instead. + */ + function airdrop() external { + require(!airdropCompleted, "Airdrop is completed"); + + while (airdropTick()) {} + } +} diff --git a/libraries/package.json b/libraries/package.json index 7cf559a..9fc9714 100644 --- a/libraries/package.json +++ b/libraries/package.json @@ -1,6 +1,6 @@ { "name": "@biomapper-sdk/libraries", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "type": "module", "files": [ diff --git a/mock/package.json b/mock/package.json index 50d9ba6..d63f4c4 100644 --- a/mock/package.json +++ b/mock/package.json @@ -1,6 +1,6 @@ { "name": "@biomapper-sdk/mock", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "type": "module", "files": [ diff --git a/tests/hardhat/contracts/CanImport.sol b/tests/hardhat/contracts/CanImport.sol index 4a8240e..27a9679 100644 --- a/tests/hardhat/contracts/CanImport.sol +++ b/tests/hardhat/contracts/CanImport.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.24; import {IBiomapperLogRead} from "@biomapper-sdk/core/IBiomapperLogRead.sol"; +import {IBiomapperLogAddressesPerGenerationEnumerator} from "@biomapper-sdk/core/IBiomapperLogAddressesPerGenerationEnumerator.sol"; import {BiomapperLogLib} from "@biomapper-sdk/libraries/BiomapperLogLib.sol"; import {BridgedBiomapperLib} from "@biomapper-sdk/libraries/BridgedBiomapperLib.sol"; import {IGenerationChangeEvents} from "@biomapper-sdk/events/IGenerationChangeEvents.sol";