From a14aa5484c0183ece4341f24248be0a144b33633 Mon Sep 17 00:00:00 2001 From: Andres Adjimann Date: Thu, 1 Jun 2023 13:35:43 -0300 Subject: [PATCH 1/3] feat: add signed multigivaway package --- packages/giveaway/.eslintignore | 16 + packages/giveaway/.eslintrc.js | 41 + packages/giveaway/.gitignore | 16 + packages/giveaway/.prettierignore | 16 + packages/giveaway/.prettierrc.js | 16 + packages/giveaway/.solcover.js | 6 + packages/giveaway/.solhint.json | 21 + packages/giveaway/README.md | 33 + .../giveaway/contracts/ERC2771Handler.sol | 39 + .../giveaway/contracts/SignedMultiGiveaway.md | 421 ++++++ .../contracts/SignedMultiGiveaway.sol | 398 ++++++ .../contracts/SignedMultiGiveawayBase.sol | 160 +++ .../contracts/test/FakeMintableERC1155.sol | 20 + .../contracts/test/FakeMintableERC20.sol | 15 + .../contracts/test/FakeMintableERC721.sol | 15 + packages/giveaway/hardhat.config.ts | 21 + packages/giveaway/package.json | 71 + packages/giveaway/test/fixtures.ts | 260 ++++ packages/giveaway/test/signature.ts | 194 +++ packages/giveaway/test/signedMultiGiveaway.ts | 1207 +++++++++++++++++ packages/giveaway/tsconfig.json | 16 + 21 files changed, 3002 insertions(+) create mode 100644 packages/giveaway/.eslintignore create mode 100644 packages/giveaway/.eslintrc.js create mode 100644 packages/giveaway/.gitignore create mode 100644 packages/giveaway/.prettierignore create mode 100644 packages/giveaway/.prettierrc.js create mode 100644 packages/giveaway/.solcover.js create mode 100644 packages/giveaway/.solhint.json create mode 100644 packages/giveaway/README.md create mode 100644 packages/giveaway/contracts/ERC2771Handler.sol create mode 100644 packages/giveaway/contracts/SignedMultiGiveaway.md create mode 100644 packages/giveaway/contracts/SignedMultiGiveaway.sol create mode 100644 packages/giveaway/contracts/SignedMultiGiveawayBase.sol create mode 100644 packages/giveaway/contracts/test/FakeMintableERC1155.sol create mode 100644 packages/giveaway/contracts/test/FakeMintableERC20.sol create mode 100644 packages/giveaway/contracts/test/FakeMintableERC721.sol create mode 100644 packages/giveaway/hardhat.config.ts create mode 100644 packages/giveaway/package.json create mode 100644 packages/giveaway/test/fixtures.ts create mode 100644 packages/giveaway/test/signature.ts create mode 100644 packages/giveaway/test/signedMultiGiveaway.ts create mode 100644 packages/giveaway/tsconfig.json diff --git a/packages/giveaway/.eslintignore b/packages/giveaway/.eslintignore new file mode 100644 index 0000000000..a77e23ebbf --- /dev/null +++ b/packages/giveaway/.eslintignore @@ -0,0 +1,16 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +# Hardhat files +cache +artifacts + +# generated docs +generated-markups + +# editors +.idea diff --git a/packages/giveaway/.eslintrc.js b/packages/giveaway/.eslintrc.js new file mode 100644 index 0000000000..e734de4058 --- /dev/null +++ b/packages/giveaway/.eslintrc.js @@ -0,0 +1,41 @@ +const path = require('path'); +const tsconfigPath = path.join(__dirname, 'tsconfig.json'); +module.exports = { + root: true, + extends: [ + 'eslint:recommended', + 'plugin:mocha/recommended', + 'plugin:prettier/recommended', + ], + parserOptions: { + ecmaVersion: 2020, + }, + plugins: ['mocha'], + env: { + commonjs: true, + node: true, + mocha: true, + }, + overrides: [ + { + files: ['*.ts'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: [tsconfigPath], + ecmaVersion: 2020, + sourceType: 'module', + }, + plugins: ['mocha', '@typescript-eslint'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:mocha/recommended', + 'plugin:prettier/recommended', + ], + rules: { + '@typescript-eslint/no-misused-promises': 'error', + '@typescript-eslint/no-floating-promises': 'error', + }, + }, + ], +}; diff --git a/packages/giveaway/.gitignore b/packages/giveaway/.gitignore new file mode 100644 index 0000000000..a77e23ebbf --- /dev/null +++ b/packages/giveaway/.gitignore @@ -0,0 +1,16 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +# Hardhat files +cache +artifacts + +# generated docs +generated-markups + +# editors +.idea diff --git a/packages/giveaway/.prettierignore b/packages/giveaway/.prettierignore new file mode 100644 index 0000000000..a77e23ebbf --- /dev/null +++ b/packages/giveaway/.prettierignore @@ -0,0 +1,16 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +# Hardhat files +cache +artifacts + +# generated docs +generated-markups + +# editors +.idea diff --git a/packages/giveaway/.prettierrc.js b/packages/giveaway/.prettierrc.js new file mode 100644 index 0000000000..9e89cdeaff --- /dev/null +++ b/packages/giveaway/.prettierrc.js @@ -0,0 +1,16 @@ +module.exports = { + singleQuote: true, + bracketSpacing: false, + plugins: ['prettier-plugin-solidity'], + overrides: [ + { + files: '*.sol', + options: { + printWidth: 120, + tabWidth: 4, + singleQuote: false, + explicitTypes: 'always', + }, + }, + ], +}; diff --git a/packages/giveaway/.solcover.js b/packages/giveaway/.solcover.js new file mode 100644 index 0000000000..b6b105c5af --- /dev/null +++ b/packages/giveaway/.solcover.js @@ -0,0 +1,6 @@ +module.exports = { + mocha: { + grep: '@skip-on-coverage', // Find everything with this tag + invert: true, // Run the grep's inverse set. + }, +}; diff --git a/packages/giveaway/.solhint.json b/packages/giveaway/.solhint.json new file mode 100644 index 0000000000..9fb97ba3de --- /dev/null +++ b/packages/giveaway/.solhint.json @@ -0,0 +1,21 @@ +{ + "extends": "solhint:recommended", + "plugins": ["prettier"], + "rules": { + "prettier/prettier": [ + "error", + { + "endOfLine": "auto" + } + ], + "code-complexity": ["error", 7], + "compiler-version": ["error", "^0.8.0"], + "const-name-snakecase": "off", + "func-name-mixedcase": "off", + "constructor-syntax": "error", + "func-visibility": ["error", {"ignoreConstructors": true}], + "not-rely-on-time": "off", + "no-inline-assembly": "off", + "reason-string": ["warn", {"maxLength": 64}] + } +} diff --git a/packages/giveaway/README.md b/packages/giveaway/README.md new file mode 100644 index 0000000000..14efa124d6 --- /dev/null +++ b/packages/giveaway/README.md @@ -0,0 +1,33 @@ +# Signed Giveaways + +This is a hardhat project, see [https://hardhat.org](https://hardhat.org) + +The main contract gives rewards in any ERC20, ERC721 or ERC1155 when the backend authorize it via message signing. + +The message is composed of: + +- A list of signatures. If the contract holt too much value more than one signature is needed. Ideally the systems that + sign must be independent. +- A list of claim ids used by the backend to avoid double spending. +- Expiration the expiration time of the message in unix timestamp. After the expiration the message cannot be used + anymore. +- From: usually the rewards must be transferred to the contract and this value is address(this), if a strategy with + approve and transfer is used this is the address of the source. +- To: the destination address that will get the rewards. +- Claims: A list of union structs (ClaimEntry) that include: token type (ERC20, ERC721, ERC1155, etc), token address and + a data field that depending on the token type may have: amount, tokenId, etc. + +# Usage + +```shell +yarn hardhat help +yarn hardhat test +yarn hardhat coverage +REPORT_GAS=true yarn hardhat test +yarn hardhat markup +``` + +# Deployment + +This package exports the contract source code, for deployments see: [@sandbox-smart-contract/deploy](../deploy) package. + diff --git a/packages/giveaway/contracts/ERC2771Handler.sol b/packages/giveaway/contracts/ERC2771Handler.sol new file mode 100644 index 0000000000..7d657126af --- /dev/null +++ b/packages/giveaway/contracts/ERC2771Handler.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +// solhint-disable-next-line compiler-version +pragma solidity 0.8.18; + +/// @dev minimal ERC2771 handler to keep bytecode-size down +/// based on: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.6.0/contracts/metatx/ERC2771Context.sol +/// with an initializer for proxies and a mutable forwarder + +abstract contract ERC2771Handler { + address internal _trustedForwarder; + + function __ERC2771Handler_initialize(address forwarder) internal { + _trustedForwarder = forwarder; + } + + function isTrustedForwarder(address forwarder) public view returns (bool) { + return forwarder == _trustedForwarder; + } + + function getTrustedForwarder() external view returns (address) { + return _trustedForwarder; + } + + function trustedForwarder() external view returns (address) { + return _trustedForwarder; + } + + function _msgSender() internal view virtual returns (address sender) { + if (isTrustedForwarder(msg.sender)) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + // solhint-disable-next-line no-inline-assembly + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return msg.sender; + } + } +} diff --git a/packages/giveaway/contracts/SignedMultiGiveaway.md b/packages/giveaway/contracts/SignedMultiGiveaway.md new file mode 100644 index 0000000000..31cfb1e459 --- /dev/null +++ b/packages/giveaway/contracts/SignedMultiGiveaway.md @@ -0,0 +1,421 @@ +# Audience + +The intended audience for .md documentation is auditors, internal developers and external developer contributors. + +# Features + +This contract give rewards in any ERC20, ERC721 or ERC1155 when the backend authorize it via message signing. The +message is composed of: + +- A list of signatures. If the contract holt too much value more than one signature is needed. Ideally the systems that + sign must be independent. +- A list of claim ids used by the backend to avoid double spending. +- Expiration the expiration time of the message in unix timestamp. After the expiration the message cannot be used + anymore. +- From: usually the rewards must be transferred to the contract and this value is address(this), if a strategy with + approve and transfer is used this is the address of the source. +- To: the destination address that will get the rewards. +- Claims: A list of union structs (ClaimEntry) that include: token type (ERC20, ERC721, ERC1155, etc), token address and + a data field that depending on the token type may have: amount, tokenId, etc. + +```solidity +Signature[] calldata sigs, +uint256[] calldata claimIds, +uint256 expiration, +address from, // if different from address(this) then must be used with approve +address to, +ClaimEntry[] calldata claims +``` + +## Roles + +- SIGNERS: This role corresponds to the addresses authorized to sign claims. +- DEFAULT_ADMIN_ROLE: This role can grant and revoke access to the other roles also it is in charge of setting the + limits and un-pausing the contract. +- BACKOFFICE_ADMIN: Addresses in this role help the admin but with fewer privileges. They can revoke claims, pause the + contract in the case of an emergency (but cannot unpause it). + +## Limits + +The contract implement some limits on the amount that can be claim at once, this doesn't protect us completely but at +least it will force the attacker to send a lot of transactions. The limits can be imposed over a certain token address +and for ERC1155 a token address plus token id. + +We have the following limits: + +- Number of signatures: number of backend signatures needed to approve a claim, this can be used to have a multi-sig + structure on the backend side (default is 1). +- Max entries per claim: maximum amount of claim entries (token transferred) that can be done in one claim (default is + 1). +- Max wei per claim: maximum amount of tokens transferred for each claim entry. + +## Structs info + +### PerTokenLimitData + +```solidity +struct PerTokenLimitData { + uint256 maxWeiPerClaim; +} +``` + + +### LimitData + +```solidity +struct LimitData { + uint128 numberOfSignaturesNeeded; + uint128 maxClaimEntries; +} +``` + + +### BatchClaimData + +```solidity +struct BatchClaimData { + SignedMultiGiveawayBase.Signature[] sigs; + uint256[] claimIds; + uint256 expiration; + address from; + address to; + SignedMultiGiveawayBase.ClaimEntry[] claims; +} +``` + + +## Events info + +### Claimed + +```solidity +event Claimed(uint256[] claimIds, address indexed from, address indexed to, SignedMultiGiveawayBase.ClaimEntry[] claims, address operator) +``` + + +### RevokedClaims + +```solidity +event RevokedClaims(uint256[] claimIds, address operator) +``` + + +### AssetsRecovered + +```solidity +event AssetsRecovered(address to, SignedMultiGiveawayBase.ClaimEntry[] claims, address operator) +``` + + +### MaxWeiPerClaimSet + +```solidity +event MaxWeiPerClaimSet(address token, uint256 tokenId, uint256 maxWeiPerClaim, address operator) +``` + + +### NumberOfSignaturesNeededSet + +```solidity +event NumberOfSignaturesNeededSet(uint256 numberOfSignaturesNeeded, address operator) +``` + + +### MaxClaimEntriesSet + +```solidity +event MaxClaimEntriesSet(uint256 maxClaimEntries, address operator) +``` + + +## Constants info + +### BACKOFFICE_ROLE (0x6406156d) + +```solidity +bytes32 constant BACKOFFICE_ROLE = keccak256("BACKOFFICE_ROLE") +``` + +this role is for addresses that help the admin. Can pause the contract, butF, only the admin can unpause it. +## Modifiers info + +### onlyAdmin + +```solidity +modifier onlyAdmin() +``` + + +### onlyBackoffice + +```solidity +modifier onlyBackoffice() +``` + + +## Functions info + +### initialize (0x485cc955) + +```solidity +function initialize(address trustedForwarder_, address admin_) + external + initializer +``` + + +### claim (0xbec74704) + +```solidity +function claim( + SignedMultiGiveawayBase.Signature[] calldata sigs, + uint256[] calldata claimIds, + uint256 expiration, + address from, + address to, + SignedMultiGiveawayBase.ClaimEntry[] calldata claims +) external whenNotPaused +``` + +verifies the ERC712 signatures and transfer tokens from the source user to the destination user. + + +Parameters: + +| Name | Type | Description | +| :------- | :------------------------------------------ | :--------------------------------------------------------------- | +| sigs | struct SignedMultiGiveawayBase.Signature[] | signature part (v,r,s) the array of signatures M in N of M sigs | +| claimIds | uint256[] | unique claim ids, used by the backend to avoid double spending | +| from | address | source user | +| to | address | destination user | +| claims | struct SignedMultiGiveawayBase.ClaimEntry[] | list of tokens to do transfer | + +### batchClaim (0x4ac48bc1) + +```solidity +function batchClaim(SignedMultiGiveaway.BatchClaimData[] calldata batch) + external + whenNotPaused +``` + +does a lot of claims in batch + + +Parameters: + +| Name | Type | Description | +| :---- | :------------------------------------------ | :----------------------------------- | +| batch | struct SignedMultiGiveaway.BatchClaimData[] | an array of args to the claim method | + +### recoverAssets (0x2bea1a5d) + +```solidity +function recoverAssets( + address to, + SignedMultiGiveawayBase.ClaimEntry[] calldata claims +) external onlyAdmin +``` + +let the admin recover tokens from the contract + + +Parameters: + +| Name | Type | Description | +| :----- | :------------------------------------------ | :----------------------------------------- | +| to | address | destination address of the recovered fund | +| claims | struct SignedMultiGiveawayBase.ClaimEntry[] | list of the tokens to transfer | + +### revokeClaims (0xe5dac0d0) + +```solidity +function revokeClaims(uint256[] calldata claimIds) external onlyBackoffice +``` + +let the admin revoke some claims so they cannot be used anymore + + +Parameters: + +| Name | Type | Description | +| :------- | :-------- | :------------------------------- | +| claimIds | uint256[] | and array of claim Ids to revoke | + +### pause (0x8456cb59) + +```solidity +function pause() external onlyBackoffice +``` + +Triggers stopped state. No mre claims are accepted. +### unpause (0x3f4ba83a) + +```solidity +function unpause() external onlyAdmin +``` + +Returns to the normal state. Accept claims. +### setNumberOfSignaturesNeeded (0x2ed5c3a7) + +```solidity +function setNumberOfSignaturesNeeded(uint128 numberOfSignaturesNeeded) + external + onlyAdmin +``` + +set the global limits of the contract + + +Parameters: + +| Name | Type | Description | +| :----------------------- | :------ | :------------------------------------------------------------ | +| numberOfSignaturesNeeded | uint128 | number of signatures needed to approve a claim (default to 1) | + +### setMaxClaimEntries (0xe0999632) + +```solidity +function setMaxClaimEntries(uint128 maxClaimEntries) external onlyAdmin +``` + +set the global limits of the contract + + +Parameters: + +| Name | Type | Description | +| :-------------- | :------ | :------------------------------------------------------------------------------------- | +| maxClaimEntries | uint128 | maximum number of entries in a claim (amount of transfers) that can be claimed at once | + +### setMaxWeiPerClaim (0x2b31cf6f) + +```solidity +function setMaxWeiPerClaim( + address token, + uint256 tokenId, + uint256 maxWeiPerClaim +) external onlyAdmin +``` + +set the limits per token and tokenId + +even tokenId is kind of inconsistent for tokenType!=ERC1155 it doesn't harm + +Parameters: + +| Name | Type | Description | +| :------------- | :------ | :------------------------------------------------------------ | +| token | address | the token to which will assign the limit | +| tokenId | uint256 | for ERC1155 is the id of the token, else it must be zero | +| maxWeiPerClaim | uint256 | the max amount per each claim, for example 0.01eth per claim | + +### isClaimed (0x9e34070f) + +```solidity +function isClaimed(uint256 claimId) external view virtual returns (bool) +``` + +return true if already claimed + + +Return values: + +| Name | Type | Description | +| :--- | :--- | :-------------- | +| [0] | bool | true if claimed | + +### verifySignature (0xe4564c5b) + +```solidity +function verifySignature( + SignedMultiGiveawayBase.Signature calldata sig, + uint256[] calldata claimIds, + uint256 expiration, + address from, + address to, + SignedMultiGiveawayBase.ClaimEntry[] calldata claims +) external view virtual returns (address) +``` + +verifies a ERC712 signature for the Claim data type. + + +Parameters: + +| Name | Type | Description | +| :--------- | :------------------------------------------ | :--------------------------------------- | +| sig | struct SignedMultiGiveawayBase.Signature | signature part (v,r,s) | +| claimIds | uint256[] | unique id used to avoid double spending | +| expiration | uint256 | expiration timestamp | +| from | address | source user | +| to | address | destination user | +| claims | struct SignedMultiGiveawayBase.ClaimEntry[] | list of tokens to do transfer | + + +Return values: + +| Name | Type | Description | +| :--- | :------ | :--------------------------------------------------- | +| [0] | address | the recovered address must match the signing address | + +### domainSeparator (0xf698da25) + +```solidity +function domainSeparator() public view virtual returns (bytes32) +``` + +EIP712 domain separator + + +Return values: + +| Name | Type | Description | +| :--- | :------ | :------------------------------- | +| [0] | bytes32 | the hash of the domain separator | + +### getNumberOfSignaturesNeeded (0xdbbac9cc) + +```solidity +function getNumberOfSignaturesNeeded() external view returns (uint256) +``` + +get the needed number of signatures to approve a claim +### getMaxClaimEntries (0x4423c4bc) + +```solidity +function getMaxClaimEntries() external view returns (uint256) +``` + +get the maximum claim entries per claim +### getMaxWeiPerClaim (0xaa5a047d) + +```solidity +function getMaxWeiPerClaim(address token, uint256 tokenId) + external + view + returns (uint256) +``` + +get maximum Weis that can be claimed at once + +even tokenId is kind of inconsistent for tokenType!=ERC1155 it doesn't harm + +Parameters: + +| Name | Type | Description | +| :------ | :------ | :------------------------------------------ | +| token | address | the token contract address | +| tokenId | uint256 | inf ERC1155 the token id else must be zero | + +### supportsInterface (0x01ffc9a7) + +```solidity +function supportsInterface(bytes4 interfaceId) + public + view + virtual + override + returns (bool) +``` + +See {IERC165-supportsInterface}. diff --git a/packages/giveaway/contracts/SignedMultiGiveaway.sol b/packages/giveaway/contracts/SignedMultiGiveaway.sol new file mode 100644 index 0000000000..9b146ec69e --- /dev/null +++ b/packages/giveaway/contracts/SignedMultiGiveaway.sol @@ -0,0 +1,398 @@ +//SPDX-License-Identifier: MIT +// solhint-disable-next-line compiler-version +pragma solidity 0.8.18; + +import {SignedMultiGiveawayBase} from "./SignedMultiGiveawayBase.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {IERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; +import {IERC1155Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {ERC2771Handler} from "./ERC2771Handler.sol"; +import { + ERC1155HolderUpgradeable, + ERC1155ReceiverUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155HolderUpgradeable.sol"; +import { + ERC721HolderUpgradeable, + IERC721ReceiverUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol"; +import { + AccessControlEnumerableUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; + +/// @title This contract give rewards in any ERC20, ERC721 or ERC1155 when the backend authorize it via message signing. +/// @dev The whole contract is split in the base one this implementation to facilitate the reading and split +/// @dev the signature checking code. +/// @dev This contract support meta transactions. +/// @dev This contract is final, don't inherit form it. +contract SignedMultiGiveaway is + SignedMultiGiveawayBase, + PausableUpgradeable, + ERC2771Handler, + ERC1155HolderUpgradeable, + ERC721HolderUpgradeable +{ + /// @notice limits applied for each claim per token + struct PerTokenLimitData { + uint256 maxWeiPerClaim; // maximum amount of wei per each individual claim, 0 => check disabled + } + + /// @dev global limits that affect the whole contract behaviour + struct LimitData { + uint128 numberOfSignaturesNeeded; // Amount of signatures needed minus one to approve a message, 0 => 1 signature + uint128 maxClaimEntries; // Maximum amount of claims per message minus one, 0 => 1 claim entry pero claim + } + + /// @dev args of claim, used to pass an array to batchClaim + struct BatchClaimData { + Signature[] sigs; + uint256[] claimIds; + uint256 expiration; + address from; // address(this) + address to; + ClaimEntry[] claims; + } + + /// @dev this role is for addresses that help the admin. Can pause the contract, butF, only the admin can unpause it. + bytes32 public constant BACKOFFICE_ROLE = keccak256("BACKOFFICE_ROLE"); + + /// @dev configurable global limits for the contract. + LimitData private _limits; + + /// @dev limits applied to each claim per token and tokenId (most useful for EIP1155 tokens) + /// @dev Token -> id -> Limit + mapping(address => mapping(uint256 => PerTokenLimitData)) private _perTokenLimitData; + + event Claimed(uint256[] claimIds, address indexed from, address indexed to, ClaimEntry[] claims, address operator); + event RevokedClaims(uint256[] claimIds, address operator); + event AssetsRecovered(address to, ClaimEntry[] claims, address operator); + event MaxWeiPerClaimSet(address token, uint256 tokenId, uint256 maxWeiPerClaim, address operator); + event NumberOfSignaturesNeededSet(uint256 numberOfSignaturesNeeded, address operator); + event MaxClaimEntriesSet(uint256 maxClaimEntries, address operator); + + modifier onlyAdmin() { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "only admin"); + _; + } + + modifier onlyBackoffice() { + require(hasRole(BACKOFFICE_ROLE, _msgSender()), "only backoffice"); + _; + } + + function initialize(address trustedForwarder_, address admin_) external initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __ERC1155Receiver_init_unchained(); + __ERC1155Holder_init_unchained(); + __ERC721Holder_init_unchained(); + __AccessControl_init_unchained(); + __EIP712_init_unchained(name, version); + __Pausable_init_unchained(); + __ERC2771Handler_initialize(trustedForwarder_); + _setupRole(DEFAULT_ADMIN_ROLE, admin_); + _setupRole(BACKOFFICE_ROLE, admin_); + } + + /// @notice verifies the ERC712 signatures and transfer tokens from the source user to the destination user. + /// @param sigs signature part (v,r,s) the array of signatures M in N of M sigs + /// @param claimIds unique claim ids, used by the backend to avoid double spending + /// @param from source user + /// @param to destination user + /// @param claims list of tokens to do transfer + function claim( + Signature[] calldata sigs, + uint256[] calldata claimIds, + uint256 expiration, + address from, // if different from address(this) then must be used with approve + address to, + ClaimEntry[] calldata claims + ) external whenNotPaused { + _claim(_limits.numberOfSignaturesNeeded + 1, sigs, claimIds, expiration, from, to, claims); + _transfer(from, to, claims); + emit Claimed(claimIds, from, to, claims, _msgSender()); + } + + /// @notice does a lot of claims in batch + /// @param batch an array of args to the claim method + function batchClaim(BatchClaimData[] calldata batch) external whenNotPaused { + uint256 len = batch.length; + require(len > 0, "invalid len"); + address sender = _msgSender(); + for (uint256 i; i < len; i++) { + BatchClaimData calldata c = batch[i]; + _claim(_limits.numberOfSignaturesNeeded + 1, c.sigs, c.claimIds, c.expiration, c.from, c.to, c.claims); + _transfer(c.from, c.to, c.claims); + emit Claimed(c.claimIds, c.from, c.to, c.claims, sender); + } + } + + /// @notice let the admin recover tokens from the contract + /// @param to destination address of the recovered fund + /// @param claims list of the tokens to transfer + function recoverAssets(address to, ClaimEntry[] calldata claims) external onlyAdmin { + _transfer(address(this), to, claims); + emit AssetsRecovered(to, claims, _msgSender()); + } + + /// @notice let the admin revoke some claims so they cannot be used anymore + /// @param claimIds and array of claim Ids to revoke + function revokeClaims(uint256[] calldata claimIds) external onlyBackoffice { + _revokeClaims(claimIds); + emit RevokedClaims(claimIds, _msgSender()); + } + + /// @notice Triggers stopped state. No mre claims are accepted. + function pause() external onlyBackoffice { + _pause(); + } + + /// @notice Returns to the normal state. Accept claims. + function unpause() external onlyAdmin { + _unpause(); + } + + /// @notice set the global limits of the contract + /// @param numberOfSignaturesNeeded number of signatures needed to approve a claim (default to 1) + function setNumberOfSignaturesNeeded(uint128 numberOfSignaturesNeeded) external onlyAdmin { + require(numberOfSignaturesNeeded > 0, "invalid numberOfSignaturesNeeded"); + _limits = LimitData({ + numberOfSignaturesNeeded: numberOfSignaturesNeeded - 1, + maxClaimEntries: _limits.maxClaimEntries + }); + emit NumberOfSignaturesNeededSet(numberOfSignaturesNeeded, _msgSender()); + } + + /// @notice set the global limits of the contract + /// @param maxClaimEntries maximum number of entries in a claim (amount of transfers) that can be claimed at once + function setMaxClaimEntries(uint128 maxClaimEntries) external onlyAdmin { + require(maxClaimEntries > 0, "invalid maxClaimEntries"); + _limits = LimitData({ + numberOfSignaturesNeeded: _limits.numberOfSignaturesNeeded, + maxClaimEntries: maxClaimEntries - 1 + }); + emit MaxClaimEntriesSet(maxClaimEntries, _msgSender()); + } + + /// @notice set the limits per token and tokenId + /// @param token the token to which will assign the limit + /// @param tokenId for ERC1155 is the id of the token, else it must be zero + /// @param maxWeiPerClaim the max amount per each claim, for example 0.01eth per claim + /// @dev even tokenId is kind of inconsistent for tokenType!=ERC1155 it doesn't harm + function setMaxWeiPerClaim( + address token, + uint256 tokenId, + uint256 maxWeiPerClaim + ) external onlyAdmin { + require(token != address(0), "invalid token address"); + _perTokenLimitData[token][tokenId].maxWeiPerClaim = maxWeiPerClaim; + emit MaxWeiPerClaimSet(token, tokenId, maxWeiPerClaim, _msgSender()); + } + + /// @notice return true if already claimed + /// @return true if claimed + function isClaimed(uint256 claimId) external view virtual returns (bool) { + return _isClaimed(claimId); + } + + /// @notice verifies a ERC712 signature for the Claim data type. + /// @param sig signature part (v,r,s) + /// @param claimIds unique id used to avoid double spending + /// @param expiration expiration timestamp + /// @param from source user + /// @param to destination user + /// @param claims list of tokens to do transfer + /// @return the recovered address must match the signing address + function verifySignature( + Signature calldata sig, + uint256[] calldata claimIds, + uint256 expiration, + address from, + address to, + ClaimEntry[] calldata claims + ) external view virtual returns (address) { + return _verifySignature(sig, claimIds, expiration, from, to, claims); + } + + /// @notice EIP712 domain separator + /// @return the hash of the domain separator + function domainSeparator() public view virtual returns (bytes32) { + return _domainSeparatorV4(); + } + + /// @notice get the needed number of signatures to approve a claim + function getNumberOfSignaturesNeeded() external view returns (uint256) { + return _limits.numberOfSignaturesNeeded + 1; + } + + /// @notice get the maximum claim entries per claim + function getMaxClaimEntries() external view returns (uint256) { + return _limits.maxClaimEntries + 1; + } + + /// @notice get maximum Weis that can be claimed at once + /// @param token the token contract address + /// @param tokenId inf ERC1155 the token id else must be zero + /// @dev even tokenId is kind of inconsistent for tokenType!=ERC1155 it doesn't harm + function getMaxWeiPerClaim(address token, uint256 tokenId) external view returns (uint256) { + return _perTokenLimitData[token][tokenId].maxWeiPerClaim; + } + + /// @dev See {IERC165-supportsInterface}. + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(AccessControlEnumerableUpgradeable, ERC1155ReceiverUpgradeable) + returns (bool) + { + return (interfaceId == type(IERC721ReceiverUpgradeable).interfaceId) || super.supportsInterface(interfaceId); + } + + function _transfer( + address from, + address to, + ClaimEntry[] calldata claims + ) internal { + uint256 len = claims.length; + require(len <= _limits.maxClaimEntries + 1, "too many claims"); + for (uint256 i; i < len; i++) { + _transferEntry(from, to, claims[i]); + } + } + + // solhint-disable code-complexity + function _transferEntry( + address from, + address to, + ClaimEntry calldata claimEntry + ) internal { + if (claimEntry.tokenType == TokenType.ERC20) { + _transferERC20(from, to, claimEntry); + } else if (claimEntry.tokenType == TokenType.ERC721) { + _transferERC721(from, to, claimEntry); + } else if (claimEntry.tokenType == TokenType.ERC721_BATCH) { + _transferERC721Batch(from, to, claimEntry); + } else if (claimEntry.tokenType == TokenType.ERC721_SAFE) { + _transferERC721Safe(from, to, claimEntry); + } else if (claimEntry.tokenType == TokenType.ERC721_SAFE_BATCH) { + _transferERC721SafeBatch(from, to, claimEntry); + } else if (claimEntry.tokenType == TokenType.ERC1155) { + _transferERC1155(from, to, claimEntry); + } else if (claimEntry.tokenType == TokenType.ERC1155_BATCH) { + _transferERC1155Batch(from, to, claimEntry); + } else { + revert("invalid token type"); + } + } + + function _transferERC20( + address from, + address to, + ClaimEntry calldata claimEntry + ) internal { + address tokenAddress = claimEntry.tokenAddress; + uint256 amount = abi.decode(claimEntry.data, (uint256)); + _checkLimits(_perTokenLimitData[tokenAddress][0], amount); + if (from == address(this)) { + require(IERC20Upgradeable(tokenAddress).transfer(to, amount), "transfer failed"); + } else { + require(IERC20Upgradeable(tokenAddress).transferFrom(from, to, amount), "transfer failed"); + } + } + + function _transferERC721( + address from, + address to, + ClaimEntry calldata claimEntry + ) internal { + address tokenAddress = claimEntry.tokenAddress; + uint256 tokenId = abi.decode(claimEntry.data, (uint256)); + // We want a global limit, not per tokenId. + _checkLimits(_perTokenLimitData[tokenAddress][0], 1); + IERC721Upgradeable(tokenAddress).transferFrom(from, to, tokenId); + } + + function _transferERC721Batch( + address from, + address to, + ClaimEntry calldata claimEntry + ) internal { + address tokenAddress = claimEntry.tokenAddress; + uint256[] memory tokenIds = abi.decode(claimEntry.data, (uint256[])); + uint256 len = tokenIds.length; + // We want a global limit, not per tokenId. + _checkLimits(_perTokenLimitData[tokenAddress][0], len); + for (uint256 i; i < len; i++) { + IERC721Upgradeable(tokenAddress).transferFrom(from, to, tokenIds[i]); + } + } + + function _transferERC721Safe( + address from, + address to, + ClaimEntry calldata claimEntry + ) internal { + address tokenAddress = claimEntry.tokenAddress; + uint256 tokenId = abi.decode(claimEntry.data, (uint256)); + // We want a global limit, not per tokenId. + _checkLimits(_perTokenLimitData[tokenAddress][0], 1); + IERC721Upgradeable(tokenAddress).safeTransferFrom(from, to, tokenId); + } + + function _transferERC721SafeBatch( + address from, + address to, + ClaimEntry calldata claimEntry + ) internal { + address tokenAddress = claimEntry.tokenAddress; + uint256[] memory tokenIds = abi.decode(claimEntry.data, (uint256[])); + uint256 len = tokenIds.length; + // We want a global limit, not per tokenId. + _checkLimits(_perTokenLimitData[tokenAddress][0], len); + for (uint256 i; i < len; i++) { + IERC721Upgradeable(tokenAddress).safeTransferFrom(from, to, tokenIds[i]); + } + } + + function _transferERC1155( + address from, + address to, + ClaimEntry calldata claimEntry + ) internal { + address tokenAddress = claimEntry.tokenAddress; + (uint256 tokenId, uint256 amount, bytes memory data) = abi.decode(claimEntry.data, (uint256, uint256, bytes)); + _checkLimits(_perTokenLimitData[tokenAddress][tokenId], amount); + IERC1155Upgradeable(tokenAddress).safeTransferFrom(from, to, tokenId, amount, data); + } + + function _transferERC1155Batch( + address from, + address to, + ClaimEntry calldata claimEntry + ) internal { + address tokenAddress = claimEntry.tokenAddress; + (uint256[] memory ids, uint256[] memory amounts, bytes memory data) = + abi.decode(claimEntry.data, (uint256[], uint256[], bytes)); + + uint256 len = ids.length; + require(len > 0, "invalid data len"); + require(len == amounts.length, "invalid data"); + for (uint256 i; i < len; i++) { + _checkLimits(_perTokenLimitData[tokenAddress][ids[i]], amounts[i]); + } + IERC1155Upgradeable(tokenAddress).safeBatchTransferFrom(from, to, ids, amounts, data); + } + + function _checkLimits(PerTokenLimitData storage limits, uint256 amount) internal view { + require(amount > 0, "invalid amount"); + if (limits.maxWeiPerClaim > 0) { + require(amount < limits.maxWeiPerClaim, "checkLimits, amount too high"); + } + } + + function _msgSender() internal view override(ContextUpgradeable, ERC2771Handler) returns (address sender) { + return ERC2771Handler._msgSender(); + } +} diff --git a/packages/giveaway/contracts/SignedMultiGiveawayBase.sol b/packages/giveaway/contracts/SignedMultiGiveawayBase.sol new file mode 100644 index 0000000000..a5f2173aa0 --- /dev/null +++ b/packages/giveaway/contracts/SignedMultiGiveawayBase.sol @@ -0,0 +1,160 @@ +//SPDX-License-Identifier: MIT +// solhint-disable-next-line compiler-version +pragma solidity 0.8.18; + +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; +import {ECDSAUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; +import { + AccessControlEnumerableUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; + +/// @title This contract give rewards in any ERC20, ERC721 or ERC1155 when the backend authorize it via message signing. +/// @dev The whole contract is split in this base one and implementation to facilitate the reading and split +/// @dev the signature checking code +/// @dev This contract support meta transactions. +abstract contract SignedMultiGiveawayBase is EIP712Upgradeable, AccessControlEnumerableUpgradeable { + struct Signature { + uint8 v; + bytes32 r; + bytes32 s; + } + + enum TokenType {INVALID, ERC20, ERC721, ERC721_BATCH, ERC721_SAFE, ERC721_SAFE_BATCH, ERC1155, ERC1155_BATCH} + /// @dev this is a union type, data depends on the tokenType it can be amount, amount + tokenId, etc. + struct ClaimEntry { + TokenType tokenType; + address tokenAddress; + bytes data; + } + + string public constant name = "Sandbox SignedMultiGiveaway"; + string public constant version = "1.0"; + + /// @dev the address of the signers authorized to sign messages + bytes32 public constant SIGNER_ROLE = keccak256("SIGNER_ROLE"); + + bytes32 public constant CLAIM_ENTRY_TYPEHASH = + keccak256("ClaimEntry(uint256 tokenType,address tokenAddress,bytes data)"); + bytes32 public constant CLAIM_TYPEHASH = + keccak256( + "Claim(uint256[] claimIds,uint256 expiration,address from,address to,ClaimEntry[] claims)ClaimEntry(uint256 tokenType,address tokenAddress,bytes data)" + ); + + uint256[49] private __preGap; + /// @dev claimId => true if already claimed + mapping(uint256 => bool) private _claimed; + + /// @notice verifies a ERC712 signature and mint a new NFT for the buyer. + /// @param sigs signature part + /// @param claimIds unique claim ids + /// @param from source user + /// @param to destination user + /// @param claims list of tokens to do transfer + function _claim( + uint256 numberOfSignatures, + Signature[] calldata sigs, + uint256[] calldata claimIds, + uint256 expiration, + address from, + address to, + ClaimEntry[] calldata claims + ) internal virtual { + if (expiration != 0) { + require(block.timestamp < expiration, "expired"); + } + for (uint256 i; i < claimIds.length; i++) { + require(!_claimed[claimIds[i]], "already claimed"); + _claimed[claimIds[i]] = true; + } + bytes32 digest = _digest(claimIds, expiration, from, to, claims); + _checkSig(numberOfSignatures, digest, sigs); + } + + /// @notice let the admin revoke some claims so they cannot be used anymore + /// @param claimIds and array of claim Ids to revoke + function _revokeClaims(uint256[] calldata claimIds) internal { + for (uint256 i; i < claimIds.length; i++) { + _claimed[claimIds[i]] = true; + } + } + + function _checkSig( + uint256 numberOfSignatures, + bytes32 digest, + Signature[] calldata sigs + ) internal virtual { + require(numberOfSignatures == sigs.length, "not enough signatures"); + address lastSig = address(0); + for (uint256 i; i < numberOfSignatures; i++) { + address signer = _recover(digest, sigs[i]); + require(hasRole(SIGNER_ROLE, signer), "invalid signer"); + // Signers must be different and sorted in incremental order. + require(lastSig < signer, "invalid order"); + lastSig = signer; + } + } + + /// @notice verifies a ERC712 signature for the Claim data type. + /// @param sig signature part (v,r,s) + /// @param claimIds unique id used to avoid double spending + /// @param expiration expiration timestamp + /// @param from source user + /// @param to destination user + /// @param claims list of tokens to do transfer + /// @return the recovered address must match the signing address + function _verifySignature( + Signature calldata sig, + uint256[] calldata claimIds, + uint256 expiration, + address from, + address to, + ClaimEntry[] calldata claims + ) internal view virtual returns (address) { + bytes32 digest = _digest(claimIds, expiration, from, to, claims); + return _recover(digest, sig); + } + + /// @notice return true if already claimed + /// @return true if claimed + function _isClaimed(uint256 claimId) internal view virtual returns (bool) { + return _claimed[claimId]; + } + + function _digest( + uint256[] calldata claimIds, + uint256 expiration, + address from, + address to, + ClaimEntry[] calldata claims + ) internal view virtual returns (bytes32) { + bytes32 structHash = + keccak256(abi.encode(CLAIM_TYPEHASH, _hashClaimIds(claimIds), expiration, from, to, _hashClaims(claims))); + return _hashTypedDataV4(structHash); + } + + function _recover(bytes32 digest, Signature calldata sig) internal view virtual returns (address) { + return ECDSAUpgradeable.recover(digest, sig.v, sig.r, sig.s); + } + + function _hashClaimIds(uint256[] calldata claimIds) internal pure returns (bytes32 hash) { + return keccak256(abi.encodePacked(claimIds)); + } + + function _hashClaims(ClaimEntry[] calldata claims) internal pure returns (bytes32 hash) { + bytes32[] memory claimHashes = new bytes32[](claims.length); + for (uint256 i; i < claims.length; i++) { + ClaimEntry calldata claimEntry = claims[i]; + claimHashes[i] = keccak256( + abi.encode( + CLAIM_ENTRY_TYPEHASH, + claimEntry.tokenType, + claimEntry.tokenAddress, + keccak256(claimEntry.data) + ) + ); + } + return keccak256(abi.encodePacked(claimHashes)); + } + + uint256[49] private __postGap; +} diff --git a/packages/giveaway/contracts/test/FakeMintableERC1155.sol b/packages/giveaway/contracts/test/FakeMintableERC1155.sol new file mode 100644 index 0000000000..cbb67aad60 --- /dev/null +++ b/packages/giveaway/contracts/test/FakeMintableERC1155.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +/* solhint-disable no-empty-blocks */ +pragma solidity ^0.8; + +import {ERC1155Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; + +contract FakeMintableERC1155 is ERC1155Upgradeable { + constructor() initializer { + super.__ERC1155_init("http://test.test"); + } + + function mint( + address account, + uint256 id, + uint256 amount, + bytes memory data + ) public { + _mint(account, id, amount, data); + } +} diff --git a/packages/giveaway/contracts/test/FakeMintableERC20.sol b/packages/giveaway/contracts/test/FakeMintableERC20.sol new file mode 100644 index 0000000000..eea355bcb1 --- /dev/null +++ b/packages/giveaway/contracts/test/FakeMintableERC20.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +/* solhint-disable no-empty-blocks */ +pragma solidity ^0.8; + +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; + +contract FakeMintableERC20 is ERC20Upgradeable { + constructor() initializer { + super.__ERC20_init("MINE", "MINE"); + } + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } +} diff --git a/packages/giveaway/contracts/test/FakeMintableERC721.sol b/packages/giveaway/contracts/test/FakeMintableERC721.sol new file mode 100644 index 0000000000..fc96816f12 --- /dev/null +++ b/packages/giveaway/contracts/test/FakeMintableERC721.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +/* solhint-disable no-empty-blocks */ +pragma solidity ^0.8; + +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; + +contract FakeMintableERC721 is ERC721Upgradeable { + constructor() initializer { + super.__ERC721_init("MINFT", "MINFT"); + } + + function safeMint(address to, uint256 tokenId) public { + _safeMint(to, tokenId); + } +} diff --git a/packages/giveaway/hardhat.config.ts b/packages/giveaway/hardhat.config.ts new file mode 100644 index 0000000000..e5c93426f2 --- /dev/null +++ b/packages/giveaway/hardhat.config.ts @@ -0,0 +1,21 @@ +import '@nomicfoundation/hardhat-toolbox'; +import '@dlsl/hardhat-markup'; + +import {HardhatUserConfig} from 'hardhat/config'; + +const config: HardhatUserConfig = { + solidity: { + compilers: [ + { + version: '0.8.18', + settings: { + optimizer: { + enabled: true, + runs: 2000, + }, + }, + }, + ], + }, +}; +export default config; diff --git a/packages/giveaway/package.json b/packages/giveaway/package.json new file mode 100644 index 0000000000..1be6ebf857 --- /dev/null +++ b/packages/giveaway/package.json @@ -0,0 +1,71 @@ +{ + "name": "@sandbox-smart-contracts/giveaway", + "version": "0.0.1", + "description": "Contracts used to airdrop tokens", + "license": "MIT", + + "main": "index.js", + "directories": { + "contracts": "contracts" + }, + "scripts": { + "lint": "eslint --max-warnings 0 \"**/*.{js,ts}\" && solhint --max-warnings 0 \"contracts/**/*.sol\"", + "lint:fix": "eslint --fix \"**/*.{js,ts}\" && solhint --fix \"contracts/**/*.sol\"", + "format": "prettier --check \"**/*.{ts,js,sol}\"", + "format:fix": "prettier --write \"**/*.{ts,js,sol}\"", + "test": "dotenv -- cross-env NODE_OPTIONS=\"--max-old-space-size=8192 node --unhandled-rejections=strict\" HARDHAT_COMPILE=true NODE_ENV=test hardhat test", + "coverage": "dotenv -- cross-env NODE_OPTIONS=\"--max-old-space-size=8192 node --unhandled-rejections=strict\" HARDHAT_COMPILE=true NODE_ENV=test hardhat coverage --testfiles 'test/*.ts''test/*.js'", + "hardhat": "hardhat", + "compile": "hardhat compile" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/thesandboxgame/sandbox-smart-contracts.git" + }, + "bugs": { + "url": "https://github.com/thesandboxgame/sandbox-smart-contracts/issues" + }, + "homepage": "https://github.com/thesandboxgame/sandbox-smart-contracts#readme", + "mocha": { + "require": "hardhat/register", + "timeout": 40000, + "_": [ + "test/**/*.ts" + ] + }, + "devDependencies": { + "@dlsl/hardhat-markup": "^1.0.0-rc.11", + "@ethersproject/abi": "^5.7.0", + "@ethersproject/providers": "^5.7.2", + "@nomicfoundation/hardhat-chai-matchers": "1", + "@nomicfoundation/hardhat-network-helpers": "^1.0.8", + "@nomicfoundation/hardhat-toolbox": "^2.0.2", + "@nomiclabs/hardhat-ethers": "^2.2.3", + "@nomiclabs/hardhat-etherscan": "^3.1.7", + "@openzeppelin/contracts-upgradeable": "^4.9.0", + "@typechain/ethers-v5": "^11.0.0", + "@typechain/hardhat": "^7.0.0", + "@typescript-eslint/eslint-plugin": "^5.59.8", + "@typescript-eslint/parser": "^5.59.8", + "chai": "^4.3.7", + "cross-env": "^7.0.3", + "dotenv": "^16.1.3", + "dotenv-cli": "^7.2.1", + "eslint": "^8.41.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-mocha": "^10.1.0", + "eslint-plugin-prettier": "^4.2.1", + "eth-sig-util": "^3.0.1", + "ethers": "^5.0.0", + "hardhat": "^2.14.1", + "hardhat-gas-reporter": "^1.0.9", + "prettier": "2.0.5", + "prettier-plugin-solidity": "1.0.0-beta.11", + "solhint": "^3.4.1", + "solhint-plugin-prettier": "^0.0.5", + "solidity-coverage": "^0.8.2", + "ts-node": "^10.9.1", + "typechain": "^8.2.0", + "typescript": "^5.0.4" + } +} diff --git a/packages/giveaway/test/fixtures.ts b/packages/giveaway/test/fixtures.ts new file mode 100644 index 0000000000..f00f578a2b --- /dev/null +++ b/packages/giveaway/test/fixtures.ts @@ -0,0 +1,260 @@ +import {BigNumber, BigNumberish, Contract, Signer} from 'ethers'; +import { + Claim, + compareClaim, + ERC1155BatchClaim, + ERC1155Claim, + ERC20Claim, + getClaimEntires, + signedMultiGiveawaySignature, + TokenType, +} from './signature'; +import {ethers} from 'hardhat'; +import {expect} from 'chai'; + +async function deploy(name: string, users: Signer[] = []): Promise { + const Contract = await ethers.getContractFactory(name); + const contract = await Contract.deploy(); + await contract.deployed(); + const ret = []; + for (const s of users) { + ret.push(await contract.connect(s)); + } + ret.push(contract); + return ret; +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export async function setupSignedMultiGiveaway() { + const [ + , + deployer, + upgradeAdmin, + trustedForwarder, + admin, + backofficeAdmin, + seller, + signer, + other, + dest, + ] = await ethers.getSigners(); + const [sandToken, sandTokenAsOther] = await deploy('FakeMintableERC20', [ + deployer, + other, + ]); + const [landToken] = await deploy('FakeMintableERC721', [deployer]); + const [assetToken] = await deploy('FakeMintableERC1155', [deployer]); + const [ + contractAsDeployer, + contract, + contractAsAdmin, + contractAsBackofficeAdmin, + signedGiveaway, + ] = await deploy('SignedMultiGiveaway', [ + deployer, + other, + admin, + backofficeAdmin, + ]); + + // Initialize + await contractAsDeployer.initialize(trustedForwarder.address, admin.address); + // Grant roles. + const signerRole = await contractAsAdmin.SIGNER_ROLE(); + await contractAsAdmin.grantRole(signerRole, signer.address); + const backofficeRole = await contractAsAdmin.BACKOFFICE_ROLE(); + await contractAsAdmin.grantRole(backofficeRole, backofficeAdmin.address); + + interface ERC721Balance { + tokenType: TokenType.ERC721 | TokenType.ERC721_SAFE; + owner: string; + } + + interface ERC721BatchBalance { + tokenType: TokenType.ERC721_BATCH | TokenType.ERC721_SAFE_BATCH; + owners: string[]; + } + + type Balance = + | ERC20Claim + | ERC1155Claim + | ERC1155BatchClaim + | ERC721Balance + | ERC721BatchBalance; + + async function checkBalances( + source: string, + pre: Balance[], + preDest: Balance[], + claims: Claim[] + ) { + const pos = await balances(source, claims); + const postDest = await balances(dest.address, claims); + for (const [idx, c] of claims.entries()) { + switch (c.tokenType) { + case TokenType.ERC20: + case TokenType.ERC1155: + { + const r = pre[idx] as ERC20Claim; + const rDest = preDest[idx] as ERC20Claim; + const s = pos[idx] as ERC20Claim; + const sDest = postDest[idx] as ERC20Claim; + expect(s.amount).to.be.equal( + BigNumber.from(r.amount).sub(c.amount) + ); + expect(sDest.amount).to.be.equal( + BigNumber.from(rDest.amount).add(c.amount) + ); + } + break; + case TokenType.ERC721: + { + const r = pre[idx] as ERC721Balance; + const s = pos[idx] as ERC721Balance; + expect(r.owner).to.be.equal(contract.address); + expect(s.owner).to.be.equal(dest.address); + } + break; + } + } + } + + async function balances( + address: string, + claims: Claim[] + ): Promise { + const ret: Balance[] = []; + for (const c of claims) { + switch (c.tokenType) { + case TokenType.ERC20: + ret.push({...c, amount: await c.token.balanceOf(address)}); + break; + case TokenType.ERC721: + case TokenType.ERC721_SAFE: + ret.push({...c, owner: await c.token.ownerOf(c.tokenId)}); + break; + case TokenType.ERC721_BATCH: + case TokenType.ERC721_SAFE_BATCH: + { + const owners = []; + for (const tokenId of c.tokenIds) { + owners.push(await c.token.ownerOf(tokenId)); + } + ret.push({...c, owners}); + } + break; + case TokenType.ERC1155: + ret.push({...c, amount: await c.token.balanceOf(address, c.tokenId)}); + break; + case TokenType.ERC1155_BATCH: + { + const amounts = []; + for (const tokenId of c.tokenIds) { + amounts.push(await c.token.balanceOf(address, tokenId)); + } + ret.push({...c, amounts}); + } + break; + default: + throw new Error('balances: invalid token type ' + c.tokenType); + } + } + return ret; + } + + async function mintToContract(address: string, claims: Claim[]) { + for (const c of claims) { + switch (c.tokenType) { + case TokenType.ERC20: + await c.token.mint(address, c.amount); + break; + case TokenType.ERC721: + case TokenType.ERC721_SAFE: + await c.token.safeMint(address, c.tokenId); + break; + case TokenType.ERC721_BATCH: + case TokenType.ERC721_SAFE_BATCH: + { + for (const tokenId of c.tokenIds) { + await c.token.safeMint(address, tokenId); + } + } + break; + case TokenType.ERC1155: + await c.token.mint(address, c.tokenId, c.amount, c.data); + break; + case TokenType.ERC1155_BATCH: + { + for (const [idx, tokenId] of c.tokenIds.entries()) { + await c.token.mint(address, tokenId, c.amounts[idx], c.data); + } + } + break; + default: + throw new Error('mintToContract: invalid token type ' + c.tokenType); + } + } + } + + return { + mintToContract, + balances, + checkBalances, + signAndClaim: async ( + claimIds: BigNumberish[], + claims: Claim[], + signerUser = signer, + expiration = 0 + ) => { + await mintToContract(contract.address, claims); + const pre = await balances(contract.address, claims); + const preDest = await balances(dest.address, claims); + const {v, r, s} = await signedMultiGiveawaySignature( + contract, + signerUser.address, + claimIds, + expiration, + contract.address, + dest.address, + getClaimEntires(claims) + ); + await expect( + contract.claim( + [{v, r, s}], + claimIds, + expiration, + contract.address, + dest.address, + getClaimEntires(claims) + ) + ) + .to.emit(contract, 'Claimed') + .withArgs( + claimIds, + contract.address, + dest.address, + compareClaim(claims), + other.address + ); + await checkBalances(contract.address, pre, preDest, claims); + }, + signedGiveaway, + contract, + contractAsDeployer, + contractAsAdmin, + contractAsBackofficeAdmin, + sandToken, + sandTokenAsOther, + landToken, + assetToken, + deployer, + upgradeAdmin, + trustedForwarder, + admin, + backofficeAdmin, + seller, + signer, + other, + dest, + }; +} diff --git a/packages/giveaway/test/signature.ts b/packages/giveaway/test/signature.ts new file mode 100644 index 0000000000..044f4ba7d6 --- /dev/null +++ b/packages/giveaway/test/signature.ts @@ -0,0 +1,194 @@ +import {BigNumberish, Contract} from 'ethers'; +import {Signature} from '@ethersproject/bytes'; +import {ethers} from 'hardhat'; +import {signTypedData_v4} from 'eth-sig-util'; +import {BytesLike, Hexable} from '@ethersproject/bytes/src.ts/index'; + +export enum TokenType { + INVALID, + ERC20, + ERC721, + ERC721_BATCH, + ERC721_SAFE, + ERC721_SAFE_BATCH, + ERC1155, + ERC1155_BATCH, +} + +export type ClaimEntry = { + tokenType: TokenType; + tokenAddress: string; + data: string; +}; + +export function compareClaim(a: Claim[]): (b: ClaimEntry[]) => boolean { + return (b: ClaimEntry[]) => + a.every( + (x, idx) => + x.tokenType === b[idx].tokenType && + x.token.address === b[idx].tokenAddress && + getClaimData(x) === b[idx].data + ); +} + +export interface ClaimEntryWithContract { + tokenType: TokenType; + token: Contract; +} + +// For testing +export interface InvalidClaim extends ClaimEntryWithContract { + tokenType: TokenType.INVALID; + data: string; +} + +export interface ERC20Claim extends ClaimEntryWithContract { + tokenType: TokenType.ERC20; + amount: BigNumberish; +} + +export interface ERC721Claim extends ClaimEntryWithContract { + tokenType: TokenType.ERC721 | TokenType.ERC721_SAFE; + tokenId: BigNumberish; +} + +export interface ERC721BatchClaim extends ClaimEntryWithContract { + tokenType: TokenType.ERC721_BATCH | TokenType.ERC721_SAFE_BATCH; + tokenIds: BigNumberish[]; +} + +export interface ERC1155Claim extends ClaimEntryWithContract { + tokenType: TokenType.ERC1155; + amount: BigNumberish; + tokenId: BigNumberish; + data: BytesLike | Hexable | number; +} + +export interface ERC1155BatchClaim extends ClaimEntryWithContract { + tokenType: TokenType.ERC1155_BATCH; + amounts: BigNumberish[]; + tokenIds: BigNumberish[]; + data: BytesLike | Hexable | number; +} + +export type Claim = + | InvalidClaim + | ERC20Claim + | ERC721Claim + | ERC721BatchClaim + | ERC1155Claim + | ERC1155BatchClaim; +export const getClaimData = function (claim: Claim): string { + switch (claim.tokenType) { + case TokenType.ERC20: + return ethers.utils.defaultAbiCoder.encode(['uint256'], [claim.amount]); + case TokenType.ERC721: + case TokenType.ERC721_SAFE: + return ethers.utils.defaultAbiCoder.encode(['uint256'], [claim.tokenId]); + case TokenType.ERC721_BATCH: + case TokenType.ERC721_SAFE_BATCH: + return ethers.utils.defaultAbiCoder.encode( + ['uint256[]'], + [claim.tokenIds] + ); + case TokenType.ERC1155: + return ethers.utils.defaultAbiCoder.encode( + ['uint256', 'uint256', 'bytes'], + [ + claim.tokenId, + claim.amount.toString(), + ethers.utils.arrayify(claim.data), + ] + ); + case TokenType.ERC1155_BATCH: + return ethers.utils.defaultAbiCoder.encode( + ['uint256[]', 'uint256[]', 'bytes'], + [claim.tokenIds, claim.amounts, ethers.utils.arrayify(claim.data)] + ); + default: + throw new Error('Invalid type:' + (claim as Claim).tokenType); + } +}; + +export function getClaimEntires(claims: Claim[]): ClaimEntry[] { + return claims.map((x) => ({ + tokenType: x.tokenType, + tokenAddress: x.token.address, + data: getClaimData(x), + })); +} + +export const signedMultiGiveawaySignature = async function ( + contract: Contract, + signer: string, + claimIds: BigNumberish[], + expiration: number, + from: string, + to: string, + claims: ClaimEntry[], + privateKey = '' +): Promise { + const chainId = (await contract.provider.getNetwork()).chainId; + + const data = { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + ClaimEntry: [ + {name: 'tokenType', type: 'uint256'}, + {name: 'tokenAddress', type: 'address'}, + {name: 'data', type: 'bytes'}, + ], + Claim: [ + {name: 'claimIds', type: 'uint256[]'}, + {name: 'expiration', type: 'uint256'}, + {name: 'from', type: 'address'}, + {name: 'to', type: 'address'}, + {name: 'claims', type: 'ClaimEntry[]'}, + ], + }, + primaryType: 'Claim', + domain: { + name: 'Sandbox SignedMultiGiveaway', + version: '1.0', + chainId: chainId.toString(), + verifyingContract: contract.address, + }, + message: { + claimIds: claimIds.map((x) => x.toString()), + expiration, + from, + to, + claims, + }, + } as never; + + let signature; + if (privateKey) { + signature = signTypedData_v4(ethers.utils.arrayify(privateKey) as Buffer, { + data, + }); + } else { + signature = await ethers.provider.send('eth_signTypedData_v4', [ + signer, + data, + ]); + } + return ethers.utils.splitSignature(signature); +}; diff --git a/packages/giveaway/test/signedMultiGiveaway.ts b/packages/giveaway/test/signedMultiGiveaway.ts new file mode 100644 index 0000000000..437c3cfbc5 --- /dev/null +++ b/packages/giveaway/test/signedMultiGiveaway.ts @@ -0,0 +1,1207 @@ +import {ethers} from 'hardhat'; +import { + defaultAbiCoder, + keccak256, + solidityPack, + toUtf8Bytes, +} from 'ethers/lib/utils'; +import {BigNumber, constants} from 'ethers'; +import { + Claim, + ClaimEntry, + getClaimEntires, + signedMultiGiveawaySignature, + TokenType, +} from './signature'; +import {expect} from 'chai'; +import {loadFixture, time} from '@nomicfoundation/hardhat-network-helpers'; +import {setupSignedMultiGiveaway} from './fixtures'; + +describe('SignedMultiGiveaway.sol', function () { + describe('initialization', function () { + it('interfaces', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + const interfaces = { + IERC165: '0x01ffc9a7', + IAccessControl: '0x7965db0b', + IERC721Receiver: '0x150b7a02', + IERC1155Receiver: '0x4e2312e0', + IAccessControlEnumerable: '0x5a05180f', + }; + for (const i of Object.values(interfaces)) { + expect(await fixtures.contract.supportsInterface(i)).to.be.true; + } + // for coverage + expect(await fixtures.contract.supportsInterface('0xffffffff')).to.be + .false; + }); + }); + describe('roles', function () { + it('admin', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const defaultAdminRole = await fixtures.contract.DEFAULT_ADMIN_ROLE(); + expect( + await fixtures.contract.hasRole( + defaultAdminRole, + fixtures.admin.address + ) + ).to.be.true; + }); + + it('backoffice admin', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const backofficeRole = await fixtures.contract.BACKOFFICE_ROLE(); + expect( + await fixtures.contract.hasRole(backofficeRole, fixtures.admin.address) + ).to.be.true; + expect( + await fixtures.contract.hasRole( + backofficeRole, + fixtures.backofficeAdmin.address + ) + ).to.be.true; + }); + + it('signer', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const signerRole = await fixtures.contract.SIGNER_ROLE(); + expect( + await fixtures.contract.hasRole(signerRole, fixtures.signer.address) + ).to.be.true; + }); + }); + + describe('claim', function () { + it('should be able to claim', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + ]; + const expiration = (await time.latest()) + 60 * 60 * 24; + await fixtures.signAndClaim( + [claimId], + claims, + fixtures.signer, + expiration + ); + expect(await fixtures.contract.isClaimed(claimId)).to.be.true; + }); + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('simulate a call so we can compare in the backend tests', async function () { + const to = '0x5085e160fefF4DEfaa7F69782C3c70f4fB6d2F3e'; + const token = await ethers.getContractAt( + 'FakeMintableERC20', + '0x3205278179ebfF7cB75b010909fA45a9F20dF7Fa' + ); + // 0xa7D35eBFC30c849280E11be50bfd8fBE6F1c55AA + const pk = + '0x9f7bd9216e2afbc15eb7744fb15af5b74715a929b1f8bed316543fd9df69c87e'; + const signer = new ethers.Wallet(pk, ethers.provider); + const contract = await ethers.getContractAt( + 'SignedMultiGiveaway', + '0xaBe4FF8922a4476E94d3A8960021e4d3C7127721', + signer + ); + const claimIds = [0x123]; + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token, + amount: '0x123', + }, + { + tokenType: TokenType.ERC721, + token, + tokenId: '0x123', + }, + { + tokenType: TokenType.ERC721_BATCH, + token, + tokenIds: ['0x123', '0x124'], + }, + { + tokenType: TokenType.ERC1155, + token, + tokenId: '0x123', + amount: '0x123', + data: [], + }, + { + tokenType: TokenType.ERC1155_BATCH, + token, + tokenIds: ['0x123', '0x124'], + amounts: ['0x123', '0x124'], + data: [], + }, + ]; + const expiration = 0; + const from = contract.address; + const {v, r, s} = await signedMultiGiveawaySignature( + contract, + signer.address, + claimIds, + expiration, + from, + to, + getClaimEntires(claims), + pk + ); + const ret = await contract.populateTransaction.claim( + [{v, r, s}], + claimIds, + expiration, + from, + to, + getClaimEntires(claims) + ); + console.log(ret); + }); + it('should be able to claim multiple tokens', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + { + tokenType: TokenType.ERC721, + token: fixtures.landToken, + tokenId: 123, + }, + { + tokenType: TokenType.ERC721_SAFE, + token: fixtures.landToken, + tokenId: 124, + }, + { + tokenType: TokenType.ERC721_BATCH, + token: fixtures.landToken, + tokenIds: [125, 126], + }, + { + tokenType: TokenType.ERC721_SAFE_BATCH, + token: fixtures.landToken, + tokenIds: [127, 128], + }, + { + tokenType: TokenType.ERC1155, + token: fixtures.assetToken, + tokenId: 456, + amount, + data: [], + }, + { + tokenType: TokenType.ERC1155_BATCH, + token: fixtures.assetToken, + tokenIds: [457, 458], + amounts: [12, 13], + data: [], + }, + ]; + await fixtures.contractAsAdmin.setMaxClaimEntries(claims.length); + await fixtures.signAndClaim([claimId], claims); + }); + it('should be able to claim with approve', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + ]; + await fixtures.sandToken.mint(fixtures.other.address, amount); + await fixtures.sandTokenAsOther.approve( + fixtures.contract.address, + amount + ); + const pre = await fixtures.balances(fixtures.other.address, claims); + const preDest = await fixtures.balances(fixtures.dest.address, claims); + const sig = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.signer.address, + [claimId], + 0, + fixtures.other.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + await fixtures.contract.claim( + [sig], + [claimId], + 0, + fixtures.other.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + await fixtures.checkBalances( + fixtures.other.address, + pre, + preDest, + claims + ); + }); + + describe('multiple signatures', function () { + it('should be able to claim with N signatures', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + ]; + await fixtures.mintToContract(fixtures.contract.address, claims); + const pre = await fixtures.balances(fixtures.contract.address, claims); + const preDest = await fixtures.balances(fixtures.dest.address, claims); + const sig1 = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.signer.address, + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + const signerRole = await fixtures.contractAsAdmin.SIGNER_ROLE(); + await fixtures.contractAsAdmin.grantRole( + signerRole, + fixtures.other.address + ); + const sig2 = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.other.address, + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + await fixtures.contractAsAdmin.setNumberOfSignaturesNeeded(2); + await fixtures.contract.claim( + [sig1, sig2], + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + await fixtures.checkBalances( + fixtures.contract.address, + pre, + preDest, + claims + ); + }); + it('signatures must be in order other < signer', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + ]; + await fixtures.mintToContract(fixtures.contract.address, claims); + const sig1 = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.signer.address, + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + const signerRole = await fixtures.contractAsAdmin.SIGNER_ROLE(); + await fixtures.contractAsAdmin.grantRole( + signerRole, + fixtures.other.address + ); + const sig2 = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.other.address, + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + await fixtures.contractAsAdmin.setNumberOfSignaturesNeeded(2); + + // sigs must have the right order signer < other + await expect( + fixtures.contract.claim( + [sig2, sig1], + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ) + ).to.revertedWith('invalid order'); + }); + }); + it('should fail to claim if amount is zero', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const amount = 0; + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + ]; + await expect(fixtures.signAndClaim([claimId], claims)).to.be.revertedWith( + 'invalid amount' + ); + }); + it('should be fail to claim ERC1155 in batch if wrong len', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + await expect( + fixtures.signAndClaim( + [claimId], + [ + { + tokenType: TokenType.ERC1155_BATCH, + token: fixtures.assetToken, + tokenIds: [], + amounts: [], + data: [], + }, + ] + ) + ).to.revertedWith('invalid data len'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC1155_BATCH, + token: fixtures.assetToken, + tokenIds: [12], + amounts: [], + data: [], + }, + ]; + const {v, r, s} = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.signer.address, + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + await expect( + fixtures.contract.claim( + [{v, r, s}], + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ) + ).to.revertedWith('invalid data'); + }); + + it('should fail to claim the same id twice', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount: ethers.utils.parseEther('5'), + }, + ]; + await fixtures.signAndClaim([1, 2, 3, claimId], claims); + await expect( + fixtures.signAndClaim([claimId, 4, 5, 6], claims) + ).to.be.revertedWith('already claimed'); + }); + it('should fail to claim if the signature is wrong', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount: ethers.utils.parseEther('5'), + }, + ]; + await fixtures.mintToContract(fixtures.contract.address, claims); + const {v, r, s} = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.signer.address, + [claimId.add(1)], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + await expect( + fixtures.contract.claim( + [{v, r, s}], + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ) + ).to.be.revertedWith('invalid signer'); + }); + it('should fail to mint if the signer is invalid', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount: ethers.utils.parseEther('5'), + }, + ]; + await expect( + fixtures.signAndClaim([claimId], claims, fixtures.other) + ).to.be.revertedWith('invalid signer'); + }); + + it('claim with metaTX trusted forwarder', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + ]; + await fixtures.mintToContract(fixtures.contract.address, claims); + const pre = await fixtures.balances(fixtures.contract.address, claims); + const preDest = await fixtures.balances(fixtures.dest.address, claims); + const {v, r, s} = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.signer.address, + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + + const contractAsTrustedForwarder = await fixtures.signedGiveaway.connect( + fixtures.trustedForwarder + ); + const txData = await contractAsTrustedForwarder.populateTransaction.claim( + [{v, r, s}], + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + // The msg.sender goes at the end. + txData.data = solidityPack( + ['bytes', 'address'], + [txData.data, fixtures.other.address] + ); + await contractAsTrustedForwarder.signer.sendTransaction(txData); + await fixtures.checkBalances( + fixtures.contract.address, + pre, + preDest, + claims + ); + }); + + it('should be able to batch claim', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const baseClaimId = BigNumber.from(0x123); + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + { + tokenType: TokenType.ERC721, + token: fixtures.landToken, + tokenId: 123, + }, + { + tokenType: TokenType.ERC1155, + token: fixtures.assetToken, + tokenId: 456, + amount, + data: [], + }, + ]; + await fixtures.mintToContract(fixtures.contract.address, claims); + const pre = await fixtures.balances(fixtures.contract.address, claims); + const preDest = await fixtures.balances(fixtures.dest.address, claims); + const args = []; + for (const [i, c] of claims.entries()) { + const claimId = baseClaimId.add(i); + const {v, r, s} = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.signer.address, + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires([c]) + ); + args.push({ + sigs: [{v, r, s}], + claimIds: [claimId], + expiration: 0, + from: fixtures.contract.address, + to: fixtures.dest.address, + claims: getClaimEntires([c]), + }); + } + await fixtures.contract.batchClaim(args); + await fixtures.checkBalances( + fixtures.contract.address, + pre, + preDest, + claims + ); + }); + }); + + describe('recoverAssets', function () { + it('admin should be able to recover assets', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + ]; + await fixtures.mintToContract(fixtures.contract.address, claims); + const pre = BigNumber.from( + await fixtures.sandToken.balanceOf(fixtures.contract.address) + ); + await fixtures.contractAsAdmin.recoverAssets( + fixtures.other.address, + getClaimEntires(claims) + ); + const pos = BigNumber.from( + await fixtures.sandToken.balanceOf(fixtures.contract.address) + ); + expect(pos).to.be.equal(pre.sub(amount)); + }); + it('should fail to recover assets if not admin', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount: ethers.utils.parseEther('5'), + }, + ]; + await fixtures.mintToContract(fixtures.contract.address, claims); + await expect( + fixtures.contract.recoverAssets( + fixtures.other.address, + getClaimEntires(claims) + ) + ).to.be.revertedWith('only admin'); + await expect( + fixtures.contractAsBackofficeAdmin.recoverAssets( + fixtures.other.address, + getClaimEntires(claims) + ) + ).to.be.revertedWith('only admin'); + }); + }); + + describe('revoke', function () { + it('should fail to revoke if not admin', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + await expect( + fixtures.contract.revokeClaims([claimId]) + ).to.be.revertedWith('only backoffice'); + }); + it('should fail to claim if the id was revoked', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount: ethers.utils.parseEther('5'), + }, + ]; + await fixtures.contractAsBackofficeAdmin.revokeClaims([claimId]); + await expect(fixtures.signAndClaim([claimId], claims)).to.be.revertedWith( + 'already claimed' + ); + }); + }); + describe('pause', function () { + it('should fail to pause if not admin', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + await expect(fixtures.contract.pause()).to.be.revertedWith( + 'only backoffice' + ); + }); + it('should fail to unpause if not admin', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + await fixtures.contractAsAdmin.pause(); + await expect(fixtures.contract.unpause()).to.be.revertedWith( + 'only admin' + ); + }); + it('should fail to claim if paused by backoffice admin', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount: ethers.utils.parseEther('5'), + }, + ]; + await fixtures.contractAsBackofficeAdmin.pause(); + await expect(fixtures.signAndClaim([claimId], claims)).to.be.revertedWith( + 'Pausable: paused' + ); + }); + it('should fail to batchClaim if paused by backoffice admin', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + const baseClaimId = BigNumber.from(0x123); + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + { + tokenType: TokenType.ERC721, + token: fixtures.landToken, + tokenId: 123, + }, + { + tokenType: TokenType.ERC1155, + token: fixtures.assetToken, + tokenId: 456, + amount, + data: [], + }, + ]; + await fixtures.mintToContract(fixtures.contract.address, claims); + const args = []; + for (const [i, c] of claims.entries()) { + const claimId = baseClaimId.add(i); + const {v, r, s} = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.signer.address, + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires([c]) + ); + args.push({ + sigs: [{v, r, s}], + claimIds: [claimId], + expiration: 0, + from: fixtures.contract.address, + to: fixtures.dest.address, + claims: getClaimEntires([c]), + }); + } + await fixtures.contractAsBackofficeAdmin.pause(); + await expect(fixtures.contract.batchClaim(args)).to.be.revertedWith( + 'Pausable: paused' + ); + }); + + it('should be able to claim sand after pause/unpause', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount: ethers.utils.parseEther('5'), + }, + ]; + await fixtures.contractAsAdmin.pause(); + await expect(fixtures.signAndClaim([claimId], claims)).to.be.revertedWith( + 'Pausable: paused' + ); + + await fixtures.contractAsAdmin.unpause(); + + await fixtures.signAndClaim([claimId], claims); + }); + }); + + describe('fixed limits', function () { + it('admin should be able to set limits', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + expect( + await fixtures.contractAsAdmin.getNumberOfSignaturesNeeded() + ).to.be.equal(1); + await expect(fixtures.contractAsAdmin.setNumberOfSignaturesNeeded(2)) + .to.emit(fixtures.contract, 'NumberOfSignaturesNeededSet') + .withArgs(2, fixtures.admin.address); + expect( + await fixtures.contractAsAdmin.getNumberOfSignaturesNeeded() + ).to.be.equal(2); + + expect(await fixtures.contractAsAdmin.getMaxClaimEntries()).to.be.equal( + 1 + ); + await expect(fixtures.contractAsAdmin.setMaxClaimEntries(2)) + .to.emit(fixtures.contract, 'MaxClaimEntriesSet') + .withArgs(2, fixtures.admin.address); + expect(await fixtures.contractAsAdmin.getMaxClaimEntries()).to.be.equal( + 2 + ); + + expect( + await fixtures.contractAsAdmin.getMaxWeiPerClaim( + fixtures.sandToken.address, + 12 + ) + ).to.be.equal(0); + await expect( + fixtures.contractAsAdmin.setMaxWeiPerClaim( + fixtures.sandToken.address, + 12, + 2 + ) + ) + .to.emit(fixtures.contract, 'MaxWeiPerClaimSet') + .withArgs(fixtures.sandToken.address, 12, 2, fixtures.admin.address); + expect( + await fixtures.contractAsAdmin.getMaxWeiPerClaim( + fixtures.sandToken.address, + 12 + ) + ).to.be.equal(2); + }); + + it('others should fail to set limits', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + await expect( + fixtures.contractAsBackofficeAdmin.setMaxClaimEntries(1) + ).to.be.revertedWith('only admin'); + await expect( + fixtures.contractAsBackofficeAdmin.setNumberOfSignaturesNeeded(1) + ).to.be.revertedWith('only admin'); + await expect(fixtures.contract.setMaxClaimEntries(1)).to.be.revertedWith( + 'only admin' + ); + await expect( + fixtures.contract.setNumberOfSignaturesNeeded(1) + ).to.be.revertedWith('only admin'); + await expect( + fixtures.contract.setMaxWeiPerClaim(fixtures.sandToken.address, 12, 2) + ).to.be.revertedWith('only admin'); + }); + + it('numberOfSignaturesNeeded should be grater than 0', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + await expect( + fixtures.contractAsAdmin.setNumberOfSignaturesNeeded(0) + ).to.be.revertedWith('invalid numberOfSignaturesNeeded'); + }); + it('maxClaimEntries should be grater than 0', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + await expect( + fixtures.contractAsAdmin.setMaxClaimEntries(0) + ).to.be.revertedWith('invalid maxClaimEntries'); + }); + + it('should fail to claim if over max entries', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + { + tokenType: TokenType.ERC721, + token: fixtures.landToken, + tokenId: 123, + }, + { + tokenType: TokenType.ERC1155, + token: fixtures.assetToken, + tokenId: 456, + amount, + data: [], + }, + ]; + await fixtures.contractAsAdmin.setMaxClaimEntries(2); + expect(await fixtures.contractAsAdmin.getMaxClaimEntries()).to.be.equal( + 2 + ); + await expect(fixtures.signAndClaim([claimId], claims)).to.revertedWith( + 'too many claims' + ); + }); + + it('should fail to claim if not enough signatures', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + ]; + await fixtures.contractAsAdmin.setNumberOfSignaturesNeeded(2); + expect( + await fixtures.contractAsAdmin.getNumberOfSignaturesNeeded() + ).to.be.equal(2); + await expect(fixtures.signAndClaim([claimId], claims)).to.revertedWith( + 'not enough signatures' + ); + }); + it('signatures should expire', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + ]; + const expiration = (await time.latest()) + 60 * 60; + await time.increase(60 * 60 * 24); + await expect( + fixtures.signAndClaim([claimId], claims, fixtures.signer, expiration) + ).to.revertedWith('expired'); + }); + it('should fail to claim if over maxPerClaim per token', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const maxPerClaim = 10; + await fixtures.contractAsAdmin.setMaxWeiPerClaim( + fixtures.sandToken.address, + 0, + maxPerClaim + ); + const claimId = BigNumber.from(0x123); + await expect( + fixtures.signAndClaim( + [claimId], + [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount: maxPerClaim + 1, + }, + ] + ) + ).to.be.revertedWith('checkLimits, amount too high'); + }); + it('should success to claim if maxPerClaim is !=0 but amount is bellow maxPerClaim per token', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const maxPerClaim = 1000; + await fixtures.contractAsAdmin.setMaxWeiPerClaim( + fixtures.sandToken.address, + 0, + maxPerClaim + ); + const claimId = BigNumber.from(0x123); + await fixtures.signAndClaim( + [claimId], + [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount: maxPerClaim - 1, + }, + ] + ); + }); + + it('should fail to claim if over maxPerClaim per token per token id (ERC1155)', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const tokenId = 0x1233; + const maxPerClaim = 10; + await fixtures.contractAsAdmin.setMaxWeiPerClaim( + fixtures.assetToken.address, + tokenId, + maxPerClaim + ); + const claimId = BigNumber.from(0x123); + await expect( + fixtures.signAndClaim( + [claimId], + [ + { + tokenType: TokenType.ERC1155, + token: fixtures.assetToken, + tokenId, + amount: maxPerClaim + 1, + data: [], + }, + ] + ) + ).to.be.revertedWith('checkLimits, amount too high'); + }); + }); + + describe('coverage', function () { + it('a valid signature must verify correctly', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount: ethers.utils.parseEther('5'), + }, + ]; + await fixtures.mintToContract(fixtures.contract.address, claims); + const {v, r, s} = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.signer.address, + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + + expect( + await fixtures.contract.verifySignature( + {v, r, s}, + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ) + ).to.equal(fixtures.signer.address); + }); + it('check the trusted forwarder', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + expect(await fixtures.contract.getTrustedForwarder()).to.be.equal( + fixtures.trustedForwarder.address + ); + expect(await fixtures.contract.trustedForwarder()).to.be.equal( + fixtures.trustedForwarder.address + ); + }); + it('check the domain separator', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const typeHash = keccak256( + toUtf8Bytes( + 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' + ) + ); + const hashedName = ethers.utils.keccak256( + toUtf8Bytes('Sandbox SignedMultiGiveaway') + ); + const versionHash = ethers.utils.keccak256(toUtf8Bytes('1.0')); + const network = await fixtures.contract.provider.getNetwork(); + const domainSeparator = ethers.utils.keccak256( + defaultAbiCoder.encode( + ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], + [ + typeHash, + hashedName, + versionHash, + network.chainId, + fixtures.contract.address, + ] + ) + ); + expect(await fixtures.contract.domainSeparator()).to.be.equal( + domainSeparator + ); + }); + it('should fail to initialize twice', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + await expect( + fixtures.contractAsDeployer.initialize( + fixtures.trustedForwarder.address, + fixtures.admin.address + ) + ).to.revertedWith('Initializable: contract is already initialized'); + }); + it('should fail if batch len is wrong', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + await expect(fixtures.contract.batchClaim([])).to.revertedWith( + 'invalid len' + ); + }); + it('should fail if token address is zero', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + await expect( + fixtures.contractAsAdmin.setMaxWeiPerClaim(constants.AddressZero, 12, 2) + ).to.be.revertedWith('invalid token address'); + }); + it('should fail if invalid token type', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const claims: ClaimEntry[] = [ + { + tokenType: 0, + tokenAddress: fixtures.sandToken.address, + data: '0x', + }, + ]; + const {v, r, s} = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.signer.address, + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + claims + ); + await expect( + fixtures.contract.claim( + [{v, r, s}], + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + claims + ) + ).to.be.revertedWith('invalid token type'); + }); + it('should fail to claim with no balance', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + ]; + const sig = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.signer.address, + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + await expect( + fixtures.contract.claim( + [sig], + [claimId], + 0, + fixtures.contract.address, + fixtures.dest.address, + getClaimEntires(claims) + ) + ).to.revertedWith('ERC20: transfer amount exceeds balance'); + }); + it('should fail to claim with approve when no balance', async function () { + const fixtures = await loadFixture(setupSignedMultiGiveaway); + + const claimId = BigNumber.from(0x123); + const amount = ethers.utils.parseEther('5'); + const claims: Claim[] = [ + { + tokenType: TokenType.ERC20, + token: fixtures.sandToken, + amount, + }, + ]; + await fixtures.sandTokenAsOther.approve( + fixtures.contract.address, + amount + ); + const sig = await signedMultiGiveawaySignature( + fixtures.contract, + fixtures.signer.address, + [claimId], + 0, + fixtures.other.address, + fixtures.dest.address, + getClaimEntires(claims) + ); + await expect( + fixtures.contract.claim( + [sig], + [claimId], + 0, + fixtures.other.address, + fixtures.dest.address, + getClaimEntires(claims) + ) + ).to.revertedWith('ERC20: transfer amount exceeds balance'); + }); + }); +}); diff --git a/packages/giveaway/tsconfig.json b/packages/giveaway/tsconfig.json new file mode 100644 index 0000000000..d642e9f3fe --- /dev/null +++ b/packages/giveaway/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": [ + "hardhat.config.ts", + "./typechain-types", + "./test" + ] +} From 51a8b088672515c19b6db02a49a3d7cead0284b6 Mon Sep 17 00:00:00 2001 From: Andres Adjimann Date: Fri, 2 Jun 2023 13:57:42 -0300 Subject: [PATCH 2/3] feat: update solhint and prettier packages --- packages/giveaway/.prettierrc.js | 1 - .../contracts/SignedMultiGiveaway.sol | 90 +++++-------------- .../contracts/SignedMultiGiveawayBase.sol | 26 +++--- .../contracts/test/FakeMintableERC1155.sol | 7 +- packages/giveaway/package.json | 14 +-- 5 files changed, 44 insertions(+), 94 deletions(-) diff --git a/packages/giveaway/.prettierrc.js b/packages/giveaway/.prettierrc.js index 9e89cdeaff..60dbb58db3 100644 --- a/packages/giveaway/.prettierrc.js +++ b/packages/giveaway/.prettierrc.js @@ -9,7 +9,6 @@ module.exports = { printWidth: 120, tabWidth: 4, singleQuote: false, - explicitTypes: 'always', }, }, ], diff --git a/packages/giveaway/contracts/SignedMultiGiveaway.sol b/packages/giveaway/contracts/SignedMultiGiveaway.sol index 9b146ec69e..086d435f32 100644 --- a/packages/giveaway/contracts/SignedMultiGiveaway.sol +++ b/packages/giveaway/contracts/SignedMultiGiveaway.sol @@ -9,17 +9,9 @@ import {IERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC7 import {IERC1155Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import {ERC2771Handler} from "./ERC2771Handler.sol"; -import { - ERC1155HolderUpgradeable, - ERC1155ReceiverUpgradeable -} from "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155HolderUpgradeable.sol"; -import { - ERC721HolderUpgradeable, - IERC721ReceiverUpgradeable -} from "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol"; -import { - AccessControlEnumerableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; +import {ERC1155HolderUpgradeable, ERC1155ReceiverUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155HolderUpgradeable.sol"; +import {ERC721HolderUpgradeable, IERC721ReceiverUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol"; +import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; /// @title This contract give rewards in any ERC20, ERC721 or ERC1155 when the backend authorize it via message signing. /// @dev The whole contract is split in the base one this implementation to facilitate the reading and split @@ -180,11 +172,7 @@ contract SignedMultiGiveaway is /// @param tokenId for ERC1155 is the id of the token, else it must be zero /// @param maxWeiPerClaim the max amount per each claim, for example 0.01eth per claim /// @dev even tokenId is kind of inconsistent for tokenType!=ERC1155 it doesn't harm - function setMaxWeiPerClaim( - address token, - uint256 tokenId, - uint256 maxWeiPerClaim - ) external onlyAdmin { + function setMaxWeiPerClaim(address token, uint256 tokenId, uint256 maxWeiPerClaim) external onlyAdmin { require(token != address(0), "invalid token address"); _perTokenLimitData[token][tokenId].maxWeiPerClaim = maxWeiPerClaim; emit MaxWeiPerClaimSet(token, tokenId, maxWeiPerClaim, _msgSender()); @@ -240,21 +228,13 @@ contract SignedMultiGiveaway is } /// @dev See {IERC165-supportsInterface}. - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(AccessControlEnumerableUpgradeable, ERC1155ReceiverUpgradeable) - returns (bool) - { + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(AccessControlEnumerableUpgradeable, ERC1155ReceiverUpgradeable) returns (bool) { return (interfaceId == type(IERC721ReceiverUpgradeable).interfaceId) || super.supportsInterface(interfaceId); } - function _transfer( - address from, - address to, - ClaimEntry[] calldata claims - ) internal { + function _transfer(address from, address to, ClaimEntry[] calldata claims) internal { uint256 len = claims.length; require(len <= _limits.maxClaimEntries + 1, "too many claims"); for (uint256 i; i < len; i++) { @@ -263,11 +243,7 @@ contract SignedMultiGiveaway is } // solhint-disable code-complexity - function _transferEntry( - address from, - address to, - ClaimEntry calldata claimEntry - ) internal { + function _transferEntry(address from, address to, ClaimEntry calldata claimEntry) internal { if (claimEntry.tokenType == TokenType.ERC20) { _transferERC20(from, to, claimEntry); } else if (claimEntry.tokenType == TokenType.ERC721) { @@ -287,11 +263,7 @@ contract SignedMultiGiveaway is } } - function _transferERC20( - address from, - address to, - ClaimEntry calldata claimEntry - ) internal { + function _transferERC20(address from, address to, ClaimEntry calldata claimEntry) internal { address tokenAddress = claimEntry.tokenAddress; uint256 amount = abi.decode(claimEntry.data, (uint256)); _checkLimits(_perTokenLimitData[tokenAddress][0], amount); @@ -302,11 +274,7 @@ contract SignedMultiGiveaway is } } - function _transferERC721( - address from, - address to, - ClaimEntry calldata claimEntry - ) internal { + function _transferERC721(address from, address to, ClaimEntry calldata claimEntry) internal { address tokenAddress = claimEntry.tokenAddress; uint256 tokenId = abi.decode(claimEntry.data, (uint256)); // We want a global limit, not per tokenId. @@ -314,11 +282,7 @@ contract SignedMultiGiveaway is IERC721Upgradeable(tokenAddress).transferFrom(from, to, tokenId); } - function _transferERC721Batch( - address from, - address to, - ClaimEntry calldata claimEntry - ) internal { + function _transferERC721Batch(address from, address to, ClaimEntry calldata claimEntry) internal { address tokenAddress = claimEntry.tokenAddress; uint256[] memory tokenIds = abi.decode(claimEntry.data, (uint256[])); uint256 len = tokenIds.length; @@ -329,11 +293,7 @@ contract SignedMultiGiveaway is } } - function _transferERC721Safe( - address from, - address to, - ClaimEntry calldata claimEntry - ) internal { + function _transferERC721Safe(address from, address to, ClaimEntry calldata claimEntry) internal { address tokenAddress = claimEntry.tokenAddress; uint256 tokenId = abi.decode(claimEntry.data, (uint256)); // We want a global limit, not per tokenId. @@ -341,11 +301,7 @@ contract SignedMultiGiveaway is IERC721Upgradeable(tokenAddress).safeTransferFrom(from, to, tokenId); } - function _transferERC721SafeBatch( - address from, - address to, - ClaimEntry calldata claimEntry - ) internal { + function _transferERC721SafeBatch(address from, address to, ClaimEntry calldata claimEntry) internal { address tokenAddress = claimEntry.tokenAddress; uint256[] memory tokenIds = abi.decode(claimEntry.data, (uint256[])); uint256 len = tokenIds.length; @@ -356,25 +312,19 @@ contract SignedMultiGiveaway is } } - function _transferERC1155( - address from, - address to, - ClaimEntry calldata claimEntry - ) internal { + function _transferERC1155(address from, address to, ClaimEntry calldata claimEntry) internal { address tokenAddress = claimEntry.tokenAddress; (uint256 tokenId, uint256 amount, bytes memory data) = abi.decode(claimEntry.data, (uint256, uint256, bytes)); _checkLimits(_perTokenLimitData[tokenAddress][tokenId], amount); IERC1155Upgradeable(tokenAddress).safeTransferFrom(from, to, tokenId, amount, data); } - function _transferERC1155Batch( - address from, - address to, - ClaimEntry calldata claimEntry - ) internal { + function _transferERC1155Batch(address from, address to, ClaimEntry calldata claimEntry) internal { address tokenAddress = claimEntry.tokenAddress; - (uint256[] memory ids, uint256[] memory amounts, bytes memory data) = - abi.decode(claimEntry.data, (uint256[], uint256[], bytes)); + (uint256[] memory ids, uint256[] memory amounts, bytes memory data) = abi.decode( + claimEntry.data, + (uint256[], uint256[], bytes) + ); uint256 len = ids.length; require(len > 0, "invalid data len"); diff --git a/packages/giveaway/contracts/SignedMultiGiveawayBase.sol b/packages/giveaway/contracts/SignedMultiGiveawayBase.sol index a5f2173aa0..4ffc4ff726 100644 --- a/packages/giveaway/contracts/SignedMultiGiveawayBase.sol +++ b/packages/giveaway/contracts/SignedMultiGiveawayBase.sol @@ -4,9 +4,7 @@ pragma solidity 0.8.18; import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; import {ECDSAUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; -import { - AccessControlEnumerableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; +import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; /// @title This contract give rewards in any ERC20, ERC721 or ERC1155 when the backend authorize it via message signing. /// @dev The whole contract is split in this base one and implementation to facilitate the reading and split @@ -19,7 +17,16 @@ abstract contract SignedMultiGiveawayBase is EIP712Upgradeable, AccessControlEnu bytes32 s; } - enum TokenType {INVALID, ERC20, ERC721, ERC721_BATCH, ERC721_SAFE, ERC721_SAFE_BATCH, ERC1155, ERC1155_BATCH} + enum TokenType { + INVALID, + ERC20, + ERC721, + ERC721_BATCH, + ERC721_SAFE, + ERC721_SAFE_BATCH, + ERC1155, + ERC1155_BATCH + } /// @dev this is a union type, data depends on the tokenType it can be amount, amount + tokenId, etc. struct ClaimEntry { TokenType tokenType; @@ -78,11 +85,7 @@ abstract contract SignedMultiGiveawayBase is EIP712Upgradeable, AccessControlEnu } } - function _checkSig( - uint256 numberOfSignatures, - bytes32 digest, - Signature[] calldata sigs - ) internal virtual { + function _checkSig(uint256 numberOfSignatures, bytes32 digest, Signature[] calldata sigs) internal virtual { require(numberOfSignatures == sigs.length, "not enough signatures"); address lastSig = address(0); for (uint256 i; i < numberOfSignatures; i++) { @@ -127,8 +130,9 @@ abstract contract SignedMultiGiveawayBase is EIP712Upgradeable, AccessControlEnu address to, ClaimEntry[] calldata claims ) internal view virtual returns (bytes32) { - bytes32 structHash = - keccak256(abi.encode(CLAIM_TYPEHASH, _hashClaimIds(claimIds), expiration, from, to, _hashClaims(claims))); + bytes32 structHash = keccak256( + abi.encode(CLAIM_TYPEHASH, _hashClaimIds(claimIds), expiration, from, to, _hashClaims(claims)) + ); return _hashTypedDataV4(structHash); } diff --git a/packages/giveaway/contracts/test/FakeMintableERC1155.sol b/packages/giveaway/contracts/test/FakeMintableERC1155.sol index cbb67aad60..459b447700 100644 --- a/packages/giveaway/contracts/test/FakeMintableERC1155.sol +++ b/packages/giveaway/contracts/test/FakeMintableERC1155.sol @@ -9,12 +9,7 @@ contract FakeMintableERC1155 is ERC1155Upgradeable { super.__ERC1155_init("http://test.test"); } - function mint( - address account, - uint256 id, - uint256 amount, - bytes memory data - ) public { + function mint(address account, uint256 id, uint256 amount, bytes memory data) public { _mint(account, id, amount, data); } } diff --git a/packages/giveaway/package.json b/packages/giveaway/package.json index 1be6ebf857..5571dfd361 100644 --- a/packages/giveaway/package.json +++ b/packages/giveaway/package.json @@ -3,7 +3,6 @@ "version": "0.0.1", "description": "Contracts used to airdrop tokens", "license": "MIT", - "main": "index.js", "directories": { "contracts": "contracts" @@ -43,8 +42,11 @@ "@nomiclabs/hardhat-ethers": "^2.2.3", "@nomiclabs/hardhat-etherscan": "^3.1.7", "@openzeppelin/contracts-upgradeable": "^4.9.0", - "@typechain/ethers-v5": "^11.0.0", - "@typechain/hardhat": "^7.0.0", + "@typechain/ethers-v5": "^10.1.0", + "@typechain/hardhat": "^6.1.2", + "@types/chai": "^4.3.5", + "@types/mocha": "^10.0.1", + "@types/node": "^20.2.5", "@typescript-eslint/eslint-plugin": "^5.59.8", "@typescript-eslint/parser": "^5.59.8", "chai": "^4.3.7", @@ -59,13 +61,13 @@ "ethers": "^5.0.0", "hardhat": "^2.14.1", "hardhat-gas-reporter": "^1.0.9", - "prettier": "2.0.5", - "prettier-plugin-solidity": "1.0.0-beta.11", + "prettier": "^2.8.8", + "prettier-plugin-solidity": "^1.1.3", "solhint": "^3.4.1", "solhint-plugin-prettier": "^0.0.5", "solidity-coverage": "^0.8.2", "ts-node": "^10.9.1", "typechain": "^8.2.0", - "typescript": "^5.0.4" + "typescript": "5.0.4" } } From d2b5afda70c4d076624cb658e4622141ccd77e52 Mon Sep 17 00:00:00 2001 From: Andres Adjimann Date: Thu, 8 Jun 2023 16:08:30 -0300 Subject: [PATCH 3/3] fix: simplify npm scripts for test and coverage --- packages/giveaway/package.json | 8 +- yarn.lock | 869 +++++++++++++++++++++++++++++++-- 2 files changed, 843 insertions(+), 34 deletions(-) diff --git a/packages/giveaway/package.json b/packages/giveaway/package.json index 5571dfd361..814d245bb5 100644 --- a/packages/giveaway/package.json +++ b/packages/giveaway/package.json @@ -1,7 +1,7 @@ { "name": "@sandbox-smart-contracts/giveaway", "version": "0.0.1", - "description": "Contracts used to airdrop tokens", + "description": "Contracts used for token claims", "license": "MIT", "main": "index.js", "directories": { @@ -12,8 +12,8 @@ "lint:fix": "eslint --fix \"**/*.{js,ts}\" && solhint --fix \"contracts/**/*.sol\"", "format": "prettier --check \"**/*.{ts,js,sol}\"", "format:fix": "prettier --write \"**/*.{ts,js,sol}\"", - "test": "dotenv -- cross-env NODE_OPTIONS=\"--max-old-space-size=8192 node --unhandled-rejections=strict\" HARDHAT_COMPILE=true NODE_ENV=test hardhat test", - "coverage": "dotenv -- cross-env NODE_OPTIONS=\"--max-old-space-size=8192 node --unhandled-rejections=strict\" HARDHAT_COMPILE=true NODE_ENV=test hardhat coverage --testfiles 'test/*.ts''test/*.js'", + "test": "hardhat test", + "coverage": "hardhat coverage --testfiles 'test/*.ts''test/*.js'", "hardhat": "hardhat", "compile": "hardhat compile" }, @@ -50,9 +50,7 @@ "@typescript-eslint/eslint-plugin": "^5.59.8", "@typescript-eslint/parser": "^5.59.8", "chai": "^4.3.7", - "cross-env": "^7.0.3", "dotenv": "^16.1.3", - "dotenv-cli": "^7.2.1", "eslint": "^8.41.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-mocha": "^10.1.0", diff --git a/yarn.lock b/yarn.lock index 59d2974293..8964f566fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -173,6 +173,37 @@ __metadata: languageName: node linkType: hard +"@dlsl/hardhat-markup@npm:^1.0.0-rc.11": + version: 1.0.0-rc.12 + resolution: "@dlsl/hardhat-markup@npm:1.0.0-rc.12" + dependencies: + json2md: ^2.0.0 + prettier-plugin-solidity: ^1.1.3 + solidity-ast: ^0.4.49 + peerDependencies: + hardhat: ^2.10.0 + checksum: 7139c712f84d02c2bcc8ec0393e3bb3e20121af6e9401a5158833c0ceebef02643d4f89ae6047b7c431cde937a8a3bec277842576c1c7ce2cc84ccd1f145b190 + languageName: node + linkType: hard + +"@eslint-community/eslint-utils@npm:^4.2.0": + version: 4.4.0 + resolution: "@eslint-community/eslint-utils@npm:4.4.0" + dependencies: + eslint-visitor-keys: ^3.3.0 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: cdfe3ae42b4f572cbfb46d20edafe6f36fc5fb52bf2d90875c58aefe226892b9677fef60820e2832caf864a326fe4fc225714c46e8389ccca04d5f9288aabd22 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.4.0": + version: 4.5.1 + resolution: "@eslint-community/regexpp@npm:4.5.1" + checksum: 6d901166d64998d591fab4db1c2f872981ccd5f6fe066a1ad0a93d4e11855ecae6bfb76660869a469563e8882d4307228cebd41142adb409d182f2966771e57e + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^0.4.3": version: 0.4.3 resolution: "@eslint/eslintrc@npm:0.4.3" @@ -190,6 +221,30 @@ __metadata: languageName: node linkType: hard +"@eslint/eslintrc@npm:^2.0.3": + version: 2.0.3 + resolution: "@eslint/eslintrc@npm:2.0.3" + dependencies: + ajv: ^6.12.4 + debug: ^4.3.2 + espree: ^9.5.2 + globals: ^13.19.0 + ignore: ^5.2.0 + import-fresh: ^3.2.1 + js-yaml: ^4.1.0 + minimatch: ^3.1.2 + strip-json-comments: ^3.1.1 + checksum: ddc51f25f8524d8231db9c9bf03177e503d941a332e8d5ce3b10b09241be4d5584a378a529a27a527586bfbccf3031ae539eb891352033c340b012b4d0c81d92 + languageName: node + linkType: hard + +"@eslint/js@npm:8.42.0": + version: 8.42.0 + resolution: "@eslint/js@npm:8.42.0" + checksum: 750558843ac458f7da666122083ee05306fc087ecc1e5b21e7e14e23885775af6c55bcc92283dff1862b7b0d8863ec676c0f18c7faf1219c722fe91a8ece56b6 + languageName: node + linkType: hard + "@ethereumjs/block@npm:^3.5.1": version: 3.6.3 resolution: "@ethereumjs/block@npm:3.6.3" @@ -242,7 +297,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/abi@npm:5.7.0, @ethersproject/abi@npm:^5.0.0-beta.146, @ethersproject/abi@npm:^5.1.2, @ethersproject/abi@npm:^5.4.0, @ethersproject/abi@npm:^5.6.3, @ethersproject/abi@npm:^5.7.0": +"@ethersproject/abi@npm:5.7.0, @ethersproject/abi@npm:^5.0.0-beta.146, @ethersproject/abi@npm:^5.0.9, @ethersproject/abi@npm:^5.1.2, @ethersproject/abi@npm:^5.4.0, @ethersproject/abi@npm:^5.6.3, @ethersproject/abi@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/abi@npm:5.7.0" dependencies: @@ -660,6 +715,17 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/config-array@npm:^0.11.10": + version: 0.11.10 + resolution: "@humanwhocodes/config-array@npm:0.11.10" + dependencies: + "@humanwhocodes/object-schema": ^1.2.1 + debug: ^4.1.1 + minimatch: ^3.0.5 + checksum: 1b1302e2403d0e35bc43e66d67a2b36b0ad1119efc704b5faff68c41f791a052355b010fb2d27ef022670f550de24cd6d08d5ecf0821c16326b7dcd0ee5d5d8a + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.5.0": version: 0.5.0 resolution: "@humanwhocodes/config-array@npm:0.5.0" @@ -671,7 +737,14 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^1.2.0": +"@humanwhocodes/module-importer@npm:^1.0.1": + version: 1.0.1 + resolution: "@humanwhocodes/module-importer@npm:1.0.1" + checksum: 0fd22007db8034a2cdf2c764b140d37d9020bbfce8a49d3ec5c05290e77d4b0263b1b972b752df8c89e5eaa94073408f2b7d977aed131faf6cf396ebb5d7fb61 + languageName: node + linkType: hard + +"@humanwhocodes/object-schema@npm:^1.2.0, @humanwhocodes/object-schema@npm:^1.2.1": version: 1.2.1 resolution: "@humanwhocodes/object-schema@npm:1.2.1" checksum: a824a1ec31591231e4bad5787641f59e9633827d0a2eaae131a288d33c9ef0290bd16fda8da6f7c0fcb014147865d12118df10db57f27f41e20da92369fcb3f1 @@ -755,7 +828,7 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.walk@npm:^1.2.3": +"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" dependencies: @@ -923,7 +996,7 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/hardhat-chai-matchers@npm:^1.0.6": +"@nomicfoundation/hardhat-chai-matchers@npm:1, @nomicfoundation/hardhat-chai-matchers@npm:^1.0.6": version: 1.0.6 resolution: "@nomicfoundation/hardhat-chai-matchers@npm:1.0.6" dependencies: @@ -941,6 +1014,44 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/hardhat-network-helpers@npm:^1.0.8": + version: 1.0.8 + resolution: "@nomicfoundation/hardhat-network-helpers@npm:1.0.8" + dependencies: + ethereumjs-util: ^7.1.4 + peerDependencies: + hardhat: ^2.9.5 + checksum: cf865301fa7a8cebf5c249bc872863d2e69f0f3d14cceadbc5d5761bd97745f38fdb17c9074d46ef0d3a75748f43c0e14d37a54a09ae3b7e0e981c7f437c8553 + languageName: node + linkType: hard + +"@nomicfoundation/hardhat-toolbox@npm:^2.0.2": + version: 2.0.2 + resolution: "@nomicfoundation/hardhat-toolbox@npm:2.0.2" + peerDependencies: + "@ethersproject/abi": ^5.4.7 + "@ethersproject/providers": ^5.4.7 + "@nomicfoundation/hardhat-chai-matchers": ^1.0.0 + "@nomicfoundation/hardhat-network-helpers": ^1.0.0 + "@nomiclabs/hardhat-ethers": ^2.0.0 + "@nomiclabs/hardhat-etherscan": ^3.0.0 + "@typechain/ethers-v5": ^10.1.0 + "@typechain/hardhat": ^6.1.2 + "@types/chai": ^4.2.0 + "@types/mocha": ">=9.1.0" + "@types/node": ">=12.0.0" + chai: ^4.2.0 + ethers: ^5.4.7 + hardhat: ^2.11.0 + hardhat-gas-reporter: ^1.0.8 + solidity-coverage: ^0.8.1 + ts-node: ">=8.0.0" + typechain: ^8.1.0 + typescript: ">=4.5.0" + checksum: a2eafb709acbabe40de4871c4e8684a03098f045dba4fc6c6e9281358d072f386a668488c109e2a36b8eade01dc4c4f9e8a76fa45c92591857c590c6e19f1ae7 + languageName: node + linkType: hard + "@nomicfoundation/solidity-analyzer-darwin-arm64@npm:0.1.1": version: 0.1.1 resolution: "@nomicfoundation/solidity-analyzer-darwin-arm64@npm:0.1.1" @@ -1050,6 +1161,16 @@ __metadata: languageName: node linkType: hard +"@nomiclabs/hardhat-ethers@npm:^2.2.3": + version: 2.2.3 + resolution: "@nomiclabs/hardhat-ethers@npm:2.2.3" + peerDependencies: + ethers: ^5.0.0 + hardhat: ^2.0.0 + checksum: 72321317e55eb510306e04c42353c5f7ceb42d086fc76cc740120da6e1635b7ad5bbf23a8d6b02bd590754adcf646618933111624085ab249b1ff3482e773226 + languageName: node + linkType: hard + "@nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers@0.3.0-beta.7": version: 0.3.0-beta.7 resolution: "hardhat-deploy-ethers@npm:0.3.0-beta.7" @@ -1060,7 +1181,7 @@ __metadata: languageName: node linkType: hard -"@nomiclabs/hardhat-etherscan@npm:^3.0.3": +"@nomiclabs/hardhat-etherscan@npm:^3.0.3, @nomiclabs/hardhat-etherscan@npm:^3.1.7": version: 3.1.7 resolution: "@nomiclabs/hardhat-etherscan@npm:3.1.7" dependencies: @@ -1218,7 +1339,7 @@ __metadata: languageName: node linkType: hard -"@openzeppelin/contracts-upgradeable@npm:^4.8.2": +"@openzeppelin/contracts-upgradeable@npm:^4.8.2, @openzeppelin/contracts-upgradeable@npm:^4.9.0": version: 4.9.1 resolution: "@openzeppelin/contracts-upgradeable@npm:4.9.1" checksum: 4dff468b7de51701da8f46ca16e44831109e2602e80c96d68e3fba8ea1ae4ac498e60d38829e0d2d8494bb8cfc274adc7adc2dab566b929319dfe76a5c3af017 @@ -1318,6 +1439,47 @@ __metadata: languageName: unknown linkType: soft +"@sandbox-smart-contracts/giveaway@workspace:packages/giveaway": + version: 0.0.0-use.local + resolution: "@sandbox-smart-contracts/giveaway@workspace:packages/giveaway" + dependencies: + "@dlsl/hardhat-markup": ^1.0.0-rc.11 + "@ethersproject/abi": ^5.7.0 + "@ethersproject/providers": ^5.7.2 + "@nomicfoundation/hardhat-chai-matchers": 1 + "@nomicfoundation/hardhat-network-helpers": ^1.0.8 + "@nomicfoundation/hardhat-toolbox": ^2.0.2 + "@nomiclabs/hardhat-ethers": ^2.2.3 + "@nomiclabs/hardhat-etherscan": ^3.1.7 + "@openzeppelin/contracts-upgradeable": ^4.9.0 + "@typechain/ethers-v5": ^10.1.0 + "@typechain/hardhat": ^6.1.2 + "@types/chai": ^4.3.5 + "@types/mocha": ^10.0.1 + "@types/node": ^20.2.5 + "@typescript-eslint/eslint-plugin": ^5.59.8 + "@typescript-eslint/parser": ^5.59.8 + chai: ^4.3.7 + dotenv: ^16.1.3 + eslint: ^8.41.0 + eslint-config-prettier: ^8.8.0 + eslint-plugin-mocha: ^10.1.0 + eslint-plugin-prettier: ^4.2.1 + eth-sig-util: ^3.0.1 + ethers: ^5.0.0 + hardhat: ^2.14.1 + hardhat-gas-reporter: ^1.0.9 + prettier: ^2.8.8 + prettier-plugin-solidity: ^1.1.3 + solhint: ^3.4.1 + solhint-plugin-prettier: ^0.0.5 + solidity-coverage: ^0.8.2 + ts-node: ^10.9.1 + typechain: ^8.2.0 + typescript: 5.0.4 + languageName: unknown + linkType: soft + "@scure/base@npm:~1.1.0": version: 1.1.1 resolution: "@scure/base@npm:1.1.1" @@ -1451,7 +1613,7 @@ __metadata: languageName: node linkType: hard -"@solidity-parser/parser@npm:^0.14.0": +"@solidity-parser/parser@npm:^0.14.0, @solidity-parser/parser@npm:^0.14.1": version: 0.14.5 resolution: "@solidity-parser/parser@npm:0.14.5" dependencies: @@ -1561,6 +1723,38 @@ __metadata: languageName: node linkType: hard +"@typechain/ethers-v5@npm:^10.1.0": + version: 10.2.1 + resolution: "@typechain/ethers-v5@npm:10.2.1" + dependencies: + lodash: ^4.17.15 + ts-essentials: ^7.0.1 + peerDependencies: + "@ethersproject/abi": ^5.0.0 + "@ethersproject/providers": ^5.0.0 + ethers: ^5.1.3 + typechain: ^8.1.1 + typescript: ">=4.3.0" + checksum: 852da4b1ff368ef87251111a5d50077de3d0fc12c519529269a74223740f8bda89297e67a5eb6c1f5b04ee23119566d6cbccf58264d32a83132be0f328a58d22 + languageName: node + linkType: hard + +"@typechain/hardhat@npm:^6.1.2": + version: 6.1.6 + resolution: "@typechain/hardhat@npm:6.1.6" + dependencies: + fs-extra: ^9.1.0 + peerDependencies: + "@ethersproject/abi": ^5.4.7 + "@ethersproject/providers": ^5.4.7 + "@typechain/ethers-v5": ^10.2.1 + ethers: ^5.4.7 + hardhat: ^2.9.9 + typechain: ^8.1.1 + checksum: f214bebf7860956230478cb92696ba757829cfd9dc65ac99c3bc7e539378310318d92b009054186f446595c8ffc1a81e9c6d028da0eb04253253049ea1b6e8d3 + languageName: node + linkType: hard + "@types/abstract-leveldown@npm:*": version: 7.2.1 resolution: "@types/abstract-leveldown@npm:7.2.1" @@ -1607,7 +1801,7 @@ __metadata: languageName: node linkType: hard -"@types/chai@npm:*, @types/chai@npm:^4.2.11": +"@types/chai@npm:*, @types/chai@npm:^4.2.11, @types/chai@npm:^4.3.5": version: 4.3.5 resolution: "@types/chai@npm:4.3.5" checksum: c8f26a88c6b5b53a3275c7f5ff8f107028e3cbb9ff26795fff5f3d9dea07106a54ce9e2dce5e40347f7c4cc35657900aaf0c83934a25a1ae12e61e0f5516e431 @@ -1668,7 +1862,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.7": +"@types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.9": version: 7.0.12 resolution: "@types/json-schema@npm:7.0.12" checksum: 00239e97234eeb5ceefb0c1875d98ade6e922bfec39dd365ec6bd360b5c2f825e612ac4f6e5f1d13601b8b30f378f15e6faa805a3a732f4a1bbe61915163d293 @@ -1723,6 +1917,13 @@ __metadata: languageName: node linkType: hard +"@types/mocha@npm:^10.0.1": + version: 10.0.1 + resolution: "@types/mocha@npm:10.0.1" + checksum: 224ea9fce7b1734ccdb9aa99a622d902a538ce1847bca7fd22c5fb38adcf3ed536f50f48f587085db988a4bb3c2eb68f4b98e1cd6a38bc5547bd3bbbedc54495 + languageName: node + linkType: hard + "@types/mocha@npm:^8.0.2": version: 8.2.3 resolution: "@types/mocha@npm:8.2.3" @@ -1730,7 +1931,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*": +"@types/node@npm:*, @types/node@npm:^20.2.5": version: 20.2.5 resolution: "@types/node@npm:20.2.5" checksum: 38ce7c7e9d76880dc632f71d71e0d5914fcda9d5e9a7095d6c339abda55ca4affb0f2a882aeb29398f8e09d2c5151f0b6586c81c8ccdfe529c34b1ea3337425e @@ -1781,6 +1982,13 @@ __metadata: languageName: node linkType: hard +"@types/prettier@npm:^2.1.1": + version: 2.7.3 + resolution: "@types/prettier@npm:2.7.3" + checksum: 705384209cea6d1433ff6c187c80dcc0b95d99d5c5ce21a46a9a58060c527973506822e428789d842761e0280d25e3359300f017fbe77b9755bc772ab3dc2f83 + languageName: node + linkType: hard + "@types/qs@npm:^6.2.31, @types/qs@npm:^6.9.7": version: 6.9.7 resolution: "@types/qs@npm:6.9.7" @@ -1816,6 +2024,13 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7.3.12": + version: 7.5.0 + resolution: "@types/semver@npm:7.5.0" + checksum: 0a64b9b9c7424d9a467658b18dd70d1d781c2d6f033096a6e05762d20ebbad23c1b69b0083b0484722aabf35640b78ccc3de26368bcae1129c87e9df028a22e2 + languageName: node + linkType: hard + "@types/through@npm:*": version: 0.0.30 resolution: "@types/through@npm:0.0.30" @@ -1847,6 +2062,30 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/eslint-plugin@npm:^5.59.8": + version: 5.59.9 + resolution: "@typescript-eslint/eslint-plugin@npm:5.59.9" + dependencies: + "@eslint-community/regexpp": ^4.4.0 + "@typescript-eslint/scope-manager": 5.59.9 + "@typescript-eslint/type-utils": 5.59.9 + "@typescript-eslint/utils": 5.59.9 + debug: ^4.3.4 + grapheme-splitter: ^1.0.4 + ignore: ^5.2.0 + natural-compare-lite: ^1.4.0 + semver: ^7.3.7 + tsutils: ^3.21.0 + peerDependencies: + "@typescript-eslint/parser": ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: bd2428e307085d7fa6699913b6e61d65eb450bbcd26f884390cbf16722b80e1d80dc289c72774be1cdffd022744894204c3242f40ba3ffdfa05d3f210c4130bb + languageName: node + linkType: hard + "@typescript-eslint/experimental-utils@npm:4.33.0": version: 4.33.0 resolution: "@typescript-eslint/experimental-utils@npm:4.33.0" @@ -1880,6 +2119,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/parser@npm:^5.59.8": + version: 5.59.9 + resolution: "@typescript-eslint/parser@npm:5.59.9" + dependencies: + "@typescript-eslint/scope-manager": 5.59.9 + "@typescript-eslint/types": 5.59.9 + "@typescript-eslint/typescript-estree": 5.59.9 + debug: ^4.3.4 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 69b07d0a5bc6e1d24d23916c057ea9f2f53a0e7fb6dabadff92987c299640edee2c013fb93269322c7124e87b5c515529001397eae33006dfb40e1dcdf1902d7 + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:4.33.0": version: 4.33.0 resolution: "@typescript-eslint/scope-manager@npm:4.33.0" @@ -1890,6 +2146,33 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:5.59.9": + version: 5.59.9 + resolution: "@typescript-eslint/scope-manager@npm:5.59.9" + dependencies: + "@typescript-eslint/types": 5.59.9 + "@typescript-eslint/visitor-keys": 5.59.9 + checksum: 362c22662d844440a7e14223d8cc0722f77ff21ad8f78deb0ee3b3f21de01b8846bf25fbbf527544677e83d8ff48008b3f7d40b39ddec55994ea4a1863e9ec0a + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:5.59.9": + version: 5.59.9 + resolution: "@typescript-eslint/type-utils@npm:5.59.9" + dependencies: + "@typescript-eslint/typescript-estree": 5.59.9 + "@typescript-eslint/utils": 5.59.9 + debug: ^4.3.4 + tsutils: ^3.21.0 + peerDependencies: + eslint: "*" + peerDependenciesMeta: + typescript: + optional: true + checksum: 6bc2619c5024c152b181eff1f44c9b5e7d0fc75ce9403f03b39d59fc1e13191b2fbaf6730f26a1caae22922ac47489f39c2cebccdd713588f6963169ed2a7958 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:4.33.0": version: 4.33.0 resolution: "@typescript-eslint/types@npm:4.33.0" @@ -1897,6 +2180,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:5.59.9": + version: 5.59.9 + resolution: "@typescript-eslint/types@npm:5.59.9" + checksum: 283f8fee1ee590eeccc2e0fcd3526c856c4b1e2841af2cdcd09eeac842a42cfb32f6bc8b40385380f3dbc3ee29da30f1819115eedf9e16f69ff5a160aeddd8fa + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:4.33.0": version: 4.33.0 resolution: "@typescript-eslint/typescript-estree@npm:4.33.0" @@ -1915,6 +2205,42 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:5.59.9": + version: 5.59.9 + resolution: "@typescript-eslint/typescript-estree@npm:5.59.9" + dependencies: + "@typescript-eslint/types": 5.59.9 + "@typescript-eslint/visitor-keys": 5.59.9 + debug: ^4.3.4 + globby: ^11.1.0 + is-glob: ^4.0.3 + semver: ^7.3.7 + tsutils: ^3.21.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: c0c9b81f20a2a4337f07bc3ccdc9c1dabd765f59096255ed9a149e91e5c9517b25c2b6655f8f073807cfc13500c7451fbd9bb62e5e572c07cc07945ab042db89 + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:5.59.9": + version: 5.59.9 + resolution: "@typescript-eslint/utils@npm:5.59.9" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@types/json-schema": ^7.0.9 + "@types/semver": ^7.3.12 + "@typescript-eslint/scope-manager": 5.59.9 + "@typescript-eslint/types": 5.59.9 + "@typescript-eslint/typescript-estree": 5.59.9 + eslint-scope: ^5.1.1 + semver: ^7.3.7 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 22ec5962886de7dcf65f99c37aad9fb189a3bef6b2b07c81887fb82a0e8bf137246da58e64fb02141352285708440be13acd7f6db1ca19e96f86724813ac4646 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:4.33.0": version: 4.33.0 resolution: "@typescript-eslint/visitor-keys@npm:4.33.0" @@ -1925,6 +2251,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:5.59.9": + version: 5.59.9 + resolution: "@typescript-eslint/visitor-keys@npm:5.59.9" + dependencies: + "@typescript-eslint/types": 5.59.9 + eslint-visitor-keys: ^3.3.0 + checksum: 2909ce761f7fe546592cd3c43e33263d8a5fa619375fd2fdffbc72ffc33e40d6feacafb28c79f36c638fcc2225048e7cc08c61cbac6ca63723dc68610d80e3e6 + languageName: node + linkType: hard + "@ungap/promise-all-settled@npm:1.1.2": version: 1.1.2 resolution: "@ungap/promise-all-settled@npm:1.1.2" @@ -2053,7 +2389,7 @@ __metadata: languageName: node linkType: hard -"acorn-jsx@npm:^5.3.1": +"acorn-jsx@npm:^5.3.1, acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" peerDependencies: @@ -2078,7 +2414,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.4.1": +"acorn@npm:^8.4.1, acorn@npm:^8.8.0": version: 8.8.2 resolution: "acorn@npm:8.8.2" bin: @@ -2331,6 +2667,20 @@ __metadata: languageName: node linkType: hard +"array-back@npm:^3.0.1, array-back@npm:^3.1.0": + version: 3.1.0 + resolution: "array-back@npm:3.1.0" + checksum: 7205004fcd0f9edd926db921af901b083094608d5b265738d0290092f9822f73accb468e677db74c7c94ef432d39e5ed75a7b1786701e182efb25bbba9734209 + languageName: node + linkType: hard + +"array-back@npm:^4.0.1, array-back@npm:^4.0.2": + version: 4.0.2 + resolution: "array-back@npm:4.0.2" + checksum: f30603270771eeb54e5aad5f54604c62b3577a18b6db212a7272b2b6c32049121b49431f656654790ed1469411e45f387e7627c0de8fd0515995cc40df9b9294 + languageName: node + linkType: hard + "array-buffer-byte-length@npm:^1.0.0": version: 1.0.0 resolution: "array-buffer-byte-length@npm:1.0.0" @@ -3048,7 +3398,7 @@ __metadata: languageName: node linkType: hard -"chai@npm:^4.2.0": +"chai@npm:^4.2.0, chai@npm:^4.3.7": version: 4.3.7 resolution: "chai@npm:4.3.7" dependencies: @@ -3406,6 +3756,30 @@ __metadata: languageName: node linkType: hard +"command-line-args@npm:^5.1.1": + version: 5.2.1 + resolution: "command-line-args@npm:5.2.1" + dependencies: + array-back: ^3.1.0 + find-replace: ^3.0.0 + lodash.camelcase: ^4.3.0 + typical: ^4.0.0 + checksum: e759519087be3cf2e86af8b9a97d3058b4910cd11ee852495be881a067b72891f6a32718fb685ee6d41531ab76b2b7bfb6602f79f882cd4b7587ff1e827982c7 + languageName: node + linkType: hard + +"command-line-usage@npm:^6.1.0": + version: 6.1.3 + resolution: "command-line-usage@npm:6.1.3" + dependencies: + array-back: ^4.0.2 + chalk: ^2.4.2 + table-layout: ^1.0.2 + typical: ^5.2.0 + checksum: 8261d4e5536eb0bcddee0ec5e89c05bb2abd18e5760785c8078ede5020bc1c612cbe28eb6586f5ed4a3660689748e5aaad4a72f21566f4ef39393694e2fa1a0b + languageName: node + linkType: hard + "commander@npm:3.0.2": version: 3.0.2 resolution: "commander@npm:3.0.2" @@ -3766,7 +4140,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3": +"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -3838,6 +4212,13 @@ __metadata: languageName: node linkType: hard +"deep-extend@npm:~0.6.0": + version: 0.6.0 + resolution: "deep-extend@npm:0.6.0" + checksum: 7be7e5a8d468d6b10e6a67c3de828f55001b6eb515d014f7aeb9066ce36bd5717161eb47d6a0f7bed8a9083935b465bc163ee2581c8b128d29bf61092fdf57a7 + languageName: node + linkType: hard + "deep-is@npm:^0.1.3, deep-is@npm:~0.1.2, deep-is@npm:~0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -4003,6 +4384,15 @@ __metadata: languageName: node linkType: hard +"difflib@npm:^0.2.4": + version: 0.2.4 + resolution: "difflib@npm:0.2.4" + dependencies: + heap: ">= 0.2.0" + checksum: 4f4237b026263ce7471b77d9019b901c2f358a7da89401a80a84a8c3cdc1643a8e70b7495ccbe686cb4d95492eaf5dac119cd9ecbffe5f06bfc175fbe5c20a27 + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -4056,6 +4446,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.1.3": + version: 16.1.4 + resolution: "dotenv@npm:16.1.4" + checksum: c1b2e13df4d374a6a29e134c56c7b040ba20500677fe8b9939ea654f3b3badb9aaa0b172e40e4dfa1233a4177dbb8fb79d84cc79a50ac9c9641fe2ad98c14876 + languageName: node + linkType: hard + "dotenv@npm:^8.1.0, dotenv@npm:^8.2.0": version: 8.6.0 resolution: "dotenv@npm:8.6.0" @@ -4422,6 +4819,29 @@ __metadata: languageName: node linkType: hard +"eslint-config-prettier@npm:^8.8.0": + version: 8.8.0 + resolution: "eslint-config-prettier@npm:8.8.0" + peerDependencies: + eslint: ">=7.0.0" + bin: + eslint-config-prettier: bin/cli.js + checksum: 1e94c3882c4d5e41e1dcfa2c368dbccbfe3134f6ac7d40101644d3bfbe3eb2f2ffac757f3145910b5eacf20c0e85e02b91293d3126d770cbf3dc390b3564681c + languageName: node + linkType: hard + +"eslint-plugin-mocha@npm:^10.1.0": + version: 10.1.0 + resolution: "eslint-plugin-mocha@npm:10.1.0" + dependencies: + eslint-utils: ^3.0.0 + rambda: ^7.1.0 + peerDependencies: + eslint: ">=7.0.0" + checksum: 67c063ba190fe8ab3186baaf800a375e9f16a17f69deaac2ea0d1825f6e4260f9a56bd510ceb2ffbe6644d7090beda0efbd2ab7824e4852ce2abee53a1086179 + languageName: node + linkType: hard + "eslint-plugin-mocha@npm:^8.0.0": version: 8.2.0 resolution: "eslint-plugin-mocha@npm:8.2.0" @@ -4434,6 +4854,21 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-prettier@npm:^4.2.1": + version: 4.2.1 + resolution: "eslint-plugin-prettier@npm:4.2.1" + dependencies: + prettier-linter-helpers: ^1.0.0 + peerDependencies: + eslint: ">=7.28.0" + prettier: ">=2.0.0" + peerDependenciesMeta: + eslint-config-prettier: + optional: true + checksum: b9e839d2334ad8ec7a5589c5cb0f219bded260839a857d7a486997f9870e95106aa59b8756ff3f37202085ebab658de382b0267cae44c3a7f0eb0bcc03a4f6d6 + languageName: node + linkType: hard + "eslint-scope@npm:^5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" @@ -4444,6 +4879,16 @@ __metadata: languageName: node linkType: hard +"eslint-scope@npm:^7.2.0": + version: 7.2.0 + resolution: "eslint-scope@npm:7.2.0" + dependencies: + esrecurse: ^4.3.0 + estraverse: ^5.2.0 + checksum: 64591a2d8b244ade9c690b59ef238a11d5c721a98bcee9e9f445454f442d03d3e04eda88e95a4daec558220a99fa384309d9faae3d459bd40e7a81b4063980ae + languageName: node + linkType: hard + "eslint-utils@npm:^2.1.0": version: 2.1.0 resolution: "eslint-utils@npm:2.1.0" @@ -4478,6 +4923,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1": + version: 3.4.1 + resolution: "eslint-visitor-keys@npm:3.4.1" + checksum: f05121d868202736b97de7d750847a328fcfa8593b031c95ea89425333db59676ac087fa905eba438d0a3c5769632f828187e0c1a0d271832a2153c1d3661c2c + languageName: node + linkType: hard + "eslint@npm:^7.7.0": version: 7.32.0 resolution: "eslint@npm:7.32.0" @@ -4528,6 +4980,55 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^8.41.0": + version: 8.42.0 + resolution: "eslint@npm:8.42.0" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@eslint-community/regexpp": ^4.4.0 + "@eslint/eslintrc": ^2.0.3 + "@eslint/js": 8.42.0 + "@humanwhocodes/config-array": ^0.11.10 + "@humanwhocodes/module-importer": ^1.0.1 + "@nodelib/fs.walk": ^1.2.8 + ajv: ^6.10.0 + chalk: ^4.0.0 + cross-spawn: ^7.0.2 + debug: ^4.3.2 + doctrine: ^3.0.0 + escape-string-regexp: ^4.0.0 + eslint-scope: ^7.2.0 + eslint-visitor-keys: ^3.4.1 + espree: ^9.5.2 + esquery: ^1.4.2 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^6.0.1 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + globals: ^13.19.0 + graphemer: ^1.4.0 + ignore: ^5.2.0 + import-fresh: ^3.0.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + is-path-inside: ^3.0.3 + js-yaml: ^4.1.0 + json-stable-stringify-without-jsonify: ^1.0.1 + levn: ^0.4.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 + natural-compare: ^1.4.0 + optionator: ^0.9.1 + strip-ansi: ^6.0.1 + strip-json-comments: ^3.1.0 + text-table: ^0.2.0 + bin: + eslint: bin/eslint.js + checksum: 07105397b5f2ff4064b983b8971e8c379ec04b1dfcc9d918976b3e00377189000161dac991d82ba14f8759e466091b8c71146f602930ca810c290ee3fcb3faf0 + languageName: node + linkType: hard + "espree@npm:^7.3.0, espree@npm:^7.3.1": version: 7.3.1 resolution: "espree@npm:7.3.1" @@ -4539,6 +5040,17 @@ __metadata: languageName: node linkType: hard +"espree@npm:^9.5.2": + version: 9.5.2 + resolution: "espree@npm:9.5.2" + dependencies: + acorn: ^8.8.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^3.4.1 + checksum: 6506289d6eb26471c0b383ee24fee5c8ae9d61ad540be956b3127be5ce3bf687d2ba6538ee5a86769812c7c552a9d8239e8c4d150f9ea056c6d5cbe8399c03c1 + languageName: node + linkType: hard + "esprima@npm:2.5.x": version: 2.5.0 resolution: "esprima@npm:2.5.0" @@ -4579,7 +5091,7 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.4.0": +"esquery@npm:^1.4.0, esquery@npm:^1.4.2": version: 1.5.0 resolution: "esquery@npm:1.5.0" dependencies: @@ -4830,7 +5342,7 @@ __metadata: languageName: node linkType: hard -"ethers@npm:^5.7.1, ethers@npm:^5.7.2": +"ethers@npm:^5.0.0, ethers@npm:^5.7.1, ethers@npm:^5.7.2": version: 5.7.2 resolution: "ethers@npm:5.7.2" dependencies: @@ -5144,6 +5656,15 @@ __metadata: languageName: node linkType: hard +"find-replace@npm:^3.0.0": + version: 3.0.0 + resolution: "find-replace@npm:3.0.0" + dependencies: + array-back: ^3.0.1 + checksum: 6b04bcfd79027f5b84aa1dfe100e3295da989bdac4b4de6b277f4d063e78f5c9e92ebc8a1fec6dd3b448c924ba404ee051cc759e14a3ee3e825fa1361025df08 + languageName: node + linkType: hard + "find-up@npm:3.0.0, find-up@npm:^3.0.0": version: 3.0.0 resolution: "find-up@npm:3.0.0" @@ -5153,7 +5674,7 @@ __metadata: languageName: node linkType: hard -"find-up@npm:5.0.0": +"find-up@npm:5.0.0, find-up@npm:^5.0.0": version: 5.0.0 resolution: "find-up@npm:5.0.0" dependencies: @@ -5365,7 +5886,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^7.0.1": +"fs-extra@npm:^7.0.0, fs-extra@npm:^7.0.1": version: 7.0.1 resolution: "fs-extra@npm:7.0.1" dependencies: @@ -5387,7 +5908,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^9.0.1": +"fs-extra@npm:^9.0.1, fs-extra@npm:^9.1.0": version: 9.1.0 resolution: "fs-extra@npm:9.1.0" dependencies: @@ -5655,6 +6176,15 @@ __metadata: languageName: node linkType: hard +"glob-parent@npm:^6.0.2": + version: 6.0.2 + resolution: "glob-parent@npm:6.0.2" + dependencies: + is-glob: ^4.0.3 + checksum: c13ee97978bef4f55106b71e66428eb1512e71a7466ba49025fc2aec59a5bfb0954d5abd58fc5ee6c9b076eef4e1f6d3375c2e964b88466ca390da4419a786a8 + languageName: node + linkType: hard + "glob@npm:5.x, glob@npm:^5.0.15, glob@npm:^5.0.3": version: 5.0.15 resolution: "glob@npm:5.0.15" @@ -5710,6 +6240,20 @@ __metadata: languageName: node linkType: hard +"glob@npm:7.1.7": + version: 7.1.7 + resolution: "glob@npm:7.1.7" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^3.0.4 + once: ^1.3.0 + path-is-absolute: ^1.0.0 + checksum: b61f48973bbdcf5159997b0874a2165db572b368b931135832599875919c237fc05c12984e38fe828e69aa8a921eb0e8a4997266211c517c9cfaae8a93988bb8 + languageName: node + linkType: hard + "glob@npm:7.2.0": version: 7.2.0 resolution: "glob@npm:7.2.0" @@ -5793,7 +6337,7 @@ __metadata: languageName: node linkType: hard -"globals@npm:^13.6.0, globals@npm:^13.9.0": +"globals@npm:^13.19.0, globals@npm:^13.6.0, globals@npm:^13.9.0": version: 13.20.0 resolution: "globals@npm:13.20.0" dependencies: @@ -5827,7 +6371,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.0.3": +"globby@npm:^11.0.3, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -5968,6 +6512,20 @@ __metadata: languageName: node linkType: hard +"grapheme-splitter@npm:^1.0.4": + version: 1.0.4 + resolution: "grapheme-splitter@npm:1.0.4" + checksum: 0c22ec54dee1b05cd480f78cf14f732cb5b108edc073572c4ec205df4cd63f30f8db8025afc5debc8835a8ddeacf648a1c7992fe3dcd6ad38f9a476d84906620 + languageName: node + linkType: hard + +"graphemer@npm:^1.4.0": + version: 1.4.0 + resolution: "graphemer@npm:1.4.0" + checksum: bab8f0be9b568857c7bec9fda95a89f87b783546d02951c40c33f84d05bb7da3fd10f863a9beb901463669b6583173a8c8cc6d6b306ea2b9b9d5d3d943c3a673 + languageName: node + linkType: hard + "graphql@npm:^15.4.0": version: 15.8.0 resolution: "graphql@npm:15.8.0" @@ -6074,7 +6632,7 @@ __metadata: languageName: node linkType: hard -"hardhat-gas-reporter@npm:^1.0.4": +"hardhat-gas-reporter@npm:^1.0.4, hardhat-gas-reporter@npm:^1.0.9": version: 1.0.9 resolution: "hardhat-gas-reporter@npm:1.0.9" dependencies: @@ -6087,7 +6645,7 @@ __metadata: languageName: node linkType: hard -"hardhat@npm:^2.12.5": +"hardhat@npm:^2.12.5, hardhat@npm:^2.14.1": version: 2.15.0 resolution: "hardhat@npm:2.15.0" dependencies: @@ -6278,6 +6836,13 @@ __metadata: languageName: node linkType: hard +"heap@npm:>= 0.2.0": + version: 0.2.7 + resolution: "heap@npm:0.2.7" + checksum: b0f3963a799e02173f994c452921a777f2b895b710119df999736bfed7477235c2860c423d9aea18a9f3b3d065cb1114d605c208cfcb8d0ac550f97ec5d28cb0 + languageName: node + linkType: hard + "hmac-drbg@npm:^1.0.1": version: 1.0.1 resolution: "hmac-drbg@npm:1.0.1" @@ -6498,6 +7063,13 @@ __metadata: languageName: node linkType: hard +"indento@npm:^1.1.13": + version: 1.1.13 + resolution: "indento@npm:1.1.13" + checksum: 37523498f57b531c81d4f1366defa9559169f1a4c3e1e9901947439df39e2fd65dfbbc52253e61e9288b1cc78bee7194c008c619ccb8fd39f4fb5dc8d0e20ded + languageName: node + linkType: hard + "infer-owner@npm:^1.0.4": version: 1.0.4 resolution: "infer-owner@npm:1.0.4" @@ -6725,7 +7297,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:~4.0.1": +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -6771,6 +7343,13 @@ __metadata: languageName: node linkType: hard +"is-path-inside@npm:^3.0.3": + version: 3.0.3 + resolution: "is-path-inside@npm:3.0.3" + checksum: abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 + languageName: node + linkType: hard + "is-plain-obj@npm:^2.1.0": version: 2.1.0 resolution: "is-plain-obj@npm:2.1.0" @@ -7103,6 +7682,15 @@ __metadata: languageName: node linkType: hard +"json2md@npm:^2.0.0": + version: 2.0.0 + resolution: "json2md@npm:2.0.0" + dependencies: + indento: ^1.1.13 + checksum: f83aba0684939101234dbcab53f164af58a0229fb61ca00782821e4c48f76881cc93634dc563f552da90792ec98bb58369778a56389762136584b259259876fe + languageName: node + linkType: hard + "json5@npm:^2.1.0, json5@npm:^2.2.2": version: 2.2.3 resolution: "json5@npm:2.2.3" @@ -7434,6 +8022,13 @@ __metadata: languageName: node linkType: hard +"lodash.camelcase@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.camelcase@npm:4.3.0" + checksum: cb9227612f71b83e42de93eccf1232feeb25e705bdb19ba26c04f91e885bfd3dd5c517c4a97137658190581d3493ea3973072ca010aab7e301046d90740393d1 + languageName: node + linkType: hard + "lodash.merge@npm:^4.6.2": version: 4.6.2 resolution: "lodash.merge@npm:4.6.2" @@ -7812,7 +8407,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:2 || 3, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1": +"minimatch@npm:2 || 3, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -8057,6 +8652,41 @@ __metadata: languageName: node linkType: hard +"mocha@npm:7.1.2": + version: 7.1.2 + resolution: "mocha@npm:7.1.2" + dependencies: + ansi-colors: 3.2.3 + browser-stdout: 1.3.1 + chokidar: 3.3.0 + debug: 3.2.6 + diff: 3.5.0 + escape-string-regexp: 1.0.5 + find-up: 3.0.0 + glob: 7.1.3 + growl: 1.10.5 + he: 1.2.0 + js-yaml: 3.13.1 + log-symbols: 3.0.0 + minimatch: 3.0.4 + mkdirp: 0.5.5 + ms: 2.1.1 + node-environment-flags: 1.0.6 + object.assign: 4.1.0 + strip-json-comments: 2.0.1 + supports-color: 6.0.0 + which: 1.3.1 + wide-align: 1.1.3 + yargs: 13.3.2 + yargs-parser: 13.1.2 + yargs-unparser: 1.6.0 + bin: + _mocha: bin/_mocha + mocha: bin/mocha + checksum: 0fc9ad0dd79e43a34de03441634f58e8a3d211af4cdbcd56de150ec99f7aff3b8678bd5aeb41f82115f7df4199a24f7bb372f65e5bcba133b41a5310dee908bd + languageName: node + linkType: hard + "mocha@npm:^10.0.0": version: 10.2.0 resolution: "mocha@npm:10.2.0" @@ -8309,6 +8939,13 @@ __metadata: languageName: node linkType: hard +"natural-compare-lite@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare-lite@npm:1.4.0" + checksum: 5222ac3986a2b78dd6069ac62cbb52a7bf8ffc90d972ab76dfe7b01892485d229530ed20d0c62e79a6b363a663b273db3bde195a1358ce9e5f779d4453887225 + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -9125,6 +9762,19 @@ __metadata: languageName: node linkType: hard +"prettier-plugin-solidity@npm:^1.1.3": + version: 1.1.3 + resolution: "prettier-plugin-solidity@npm:1.1.3" + dependencies: + "@solidity-parser/parser": ^0.16.0 + semver: ^7.3.8 + solidity-comments-extractor: ^0.0.7 + peerDependencies: + prettier: ">=2.3.0 || >=3.0.0-alpha.0" + checksum: d5aadfa411a4d983a2bd204048726fd91fbcaffbfa26d818ef0d6001fb65f82d0eae082e935e96c79e65e09ed979b186311ddb8c38be2f0ce5dd5f5265df77fe + languageName: node + linkType: hard + "prettier@npm:2.0.5": version: 2.0.5 resolution: "prettier@npm:2.0.5" @@ -9134,7 +9784,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.2.1, prettier@npm:^2.8.3": +"prettier@npm:^2.2.1, prettier@npm:^2.3.1, prettier@npm:^2.8.3, prettier@npm:^2.8.8": version: 2.8.8 resolution: "prettier@npm:2.8.8" bin: @@ -9316,6 +9966,13 @@ __metadata: languageName: node linkType: hard +"rambda@npm:^7.1.0": + version: 7.5.0 + resolution: "rambda@npm:7.5.0" + checksum: ad608a9a4160d0b6b0921047cea1329276bf239ff58d439135288712dcdbbf0df47c76591843ad249d89e7c5a9109ce86fe099aa54aef0dc0aa92a9b4dd1b8eb + languageName: node + linkType: hard + "ramda@npm:^0.27.1": version: 0.27.2 resolution: "ramda@npm:0.27.2" @@ -9460,6 +10117,13 @@ __metadata: languageName: node linkType: hard +"reduce-flatten@npm:^2.0.0": + version: 2.0.0 + resolution: "reduce-flatten@npm:2.0.0" + checksum: 64393ef99a16b20692acfd60982d7fdbd7ff8d9f8f185c6023466444c6dd2abb929d67717a83cec7f7f8fb5f46a25d515b3b2bf2238fdbfcdbfd01d2a9e73cb8 + languageName: node + linkType: hard + "regexp.prototype.flags@npm:^1.4.3": version: 1.5.0 resolution: "regexp.prototype.flags@npm:1.5.0" @@ -9936,7 +10600,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.2.1, semver@npm:^7.3.4, semver@npm:^7.3.5": +"semver@npm:^7.2.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8": version: 7.5.1 resolution: "semver@npm:7.5.1" dependencies: @@ -10219,7 +10883,7 @@ __metadata: languageName: node linkType: hard -"solhint@npm:^3.3.4": +"solhint@npm:^3.3.4, solhint@npm:^3.4.1": version: 3.4.1 resolution: "solhint@npm:3.4.1" dependencies: @@ -10250,6 +10914,13 @@ __metadata: languageName: node linkType: hard +"solidity-ast@npm:^0.4.49": + version: 0.4.49 + resolution: "solidity-ast@npm:0.4.49" + checksum: f5b0354ddfa882346cf12d33f79c6123796a07637b248ceb9cfeec9f81540e270407f6fca660cf75666e1ba1866270319ab3fbe54b01491dbd35adffd1405243 + languageName: node + linkType: hard + "solidity-comments-extractor@npm:^0.0.7": version: 0.0.7 resolution: "solidity-comments-extractor@npm:0.0.7" @@ -10285,6 +10956,38 @@ __metadata: languageName: node linkType: hard +"solidity-coverage@npm:^0.8.2": + version: 0.8.2 + resolution: "solidity-coverage@npm:0.8.2" + dependencies: + "@ethersproject/abi": ^5.0.9 + "@solidity-parser/parser": ^0.14.1 + chalk: ^2.4.2 + death: ^1.1.0 + detect-port: ^1.3.0 + difflib: ^0.2.4 + fs-extra: ^8.1.0 + ghost-testrpc: ^0.0.2 + global-modules: ^2.0.0 + globby: ^10.0.1 + jsonschema: ^1.2.4 + lodash: ^4.17.15 + mocha: 7.1.2 + node-emoji: ^1.10.0 + pify: ^4.0.1 + recursive-readdir: ^2.2.2 + sc-istanbul: ^0.4.5 + semver: ^7.3.4 + shelljs: ^0.8.3 + web3-utils: ^1.3.6 + peerDependencies: + hardhat: ^2.11.0 + bin: + solidity-coverage: plugins/bin.js + checksum: 489f73d56a1279f2394b7a14db315532884895baa00a4016e68a4e5be0eddca90a95cb3322e6a0b15e67f2d9003b9413ee24c1c61d78f558f5a2e1e233840825 + languageName: node + linkType: hard + "source-map-support@npm:^0.5.13, source-map-support@npm:^0.5.16, source-map-support@npm:^0.5.17": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" @@ -10394,6 +11097,13 @@ __metadata: languageName: node linkType: hard +"string-format@npm:^2.0.0": + version: 2.0.0 + resolution: "string-format@npm:2.0.0" + checksum: dada2ef95f6d36c66562c673d95315f80457fa7dce2f3609a2e75d1190b98c88319028cf0a5b6c043d01c18d581b2641579f79480584ba030d6ac6fceb30bc55 + languageName: node + linkType: hard + "string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -10655,6 +11365,18 @@ __metadata: languageName: node linkType: hard +"table-layout@npm:^1.0.2": + version: 1.0.2 + resolution: "table-layout@npm:1.0.2" + dependencies: + array-back: ^4.0.1 + deep-extend: ~0.6.0 + typical: ^5.2.0 + wordwrapjs: ^4.0.0 + checksum: 8f41b5671f101a5195747ec1727b1d35ea2cd5bf85addda11cc2f4b36892db9696ce3c2c7334b5b8a122505b34d19135fede50e25678df71b0439e0704fd953f + languageName: node + linkType: hard + "table@npm:^6.0.9, table@npm:^6.8.0, table@npm:^6.8.1": version: 6.8.1 resolution: "table@npm:6.8.1" @@ -10808,6 +11530,20 @@ __metadata: languageName: node linkType: hard +"ts-command-line-args@npm:^2.2.0": + version: 2.5.1 + resolution: "ts-command-line-args@npm:2.5.1" + dependencies: + chalk: ^4.1.0 + command-line-args: ^5.1.1 + command-line-usage: ^6.1.0 + string-format: ^2.0.0 + bin: + write-markdown: dist/write-markdown.js + checksum: 7c0a7582e94f1d2160e3dd379851ec4f1758bc673ccd71bae07f839f83051b6b83e0ae14325c2d04ea728e5bde7b7eacfd2ab060b8fd4b8ab29e0bbf77f6c51e + languageName: node + linkType: hard + "ts-essentials@npm:^1.0.2": version: 1.0.4 resolution: "ts-essentials@npm:1.0.4" @@ -10815,6 +11551,15 @@ __metadata: languageName: node linkType: hard +"ts-essentials@npm:^7.0.1": + version: 7.0.3 + resolution: "ts-essentials@npm:7.0.3" + peerDependencies: + typescript: ">=3.7.0" + checksum: 74d75868acf7f8b95e447d8b3b7442ca21738c6894e576df9917a352423fde5eb43c5651da5f78997da6061458160ae1f6b279150b42f47ccc58b73e55acaa2f + languageName: node + linkType: hard + "ts-node@npm:^10.9.1": version: 10.9.1 resolution: "ts-node@npm:10.9.1" @@ -11016,6 +11761,28 @@ __metadata: languageName: node linkType: hard +"typechain@npm:^8.2.0": + version: 8.2.0 + resolution: "typechain@npm:8.2.0" + dependencies: + "@types/prettier": ^2.1.1 + debug: ^4.3.1 + fs-extra: ^7.0.0 + glob: 7.1.7 + js-sha3: ^0.8.0 + lodash: ^4.17.15 + mkdirp: ^1.0.4 + prettier: ^2.3.1 + ts-command-line-args: ^2.2.0 + ts-essentials: ^7.0.1 + peerDependencies: + typescript: ">=4.3.0" + bin: + typechain: dist/cli/cli.js + checksum: 8591d333fda0e31172f4d9e0a8e23c24eee446ce3719989bd48e63f84a975917bb2f853ecaf616193ad7f3964e7c42fe3b1fc5abb69f4446794f465505f6c1a7 + languageName: node + linkType: hard + "typed-array-length@npm:^1.0.4": version: 1.0.4 resolution: "typed-array-length@npm:1.0.4" @@ -11043,6 +11810,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:5.0.4": + version: 5.0.4 + resolution: "typescript@npm:5.0.4" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 82b94da3f4604a8946da585f7d6c3025fff8410779e5bde2855ab130d05e4fd08938b9e593b6ebed165bda6ad9292b230984f10952cf82f0a0ca07bbeaa08172 + languageName: node + linkType: hard + "typescript@npm:^4.0.5": version: 4.9.5 resolution: "typescript@npm:4.9.5" @@ -11053,6 +11830,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@5.0.4#~builtin": + version: 5.0.4 + resolution: "typescript@patch:typescript@npm%3A5.0.4#~builtin::version=5.0.4&hash=b5f058" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: d26b6ba97b6d163c55dbdffd9bbb4c211667ebebc743accfeb2c8c0154aace7afd097b51165a72a5bad2cf65a4612259344ff60f8e642362aa1695c760d303ac + languageName: node + linkType: hard + "typescript@patch:typescript@^4.0.5#~builtin": version: 4.9.5 resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=289587" @@ -11063,6 +11850,20 @@ __metadata: languageName: node linkType: hard +"typical@npm:^4.0.0": + version: 4.0.0 + resolution: "typical@npm:4.0.0" + checksum: a242081956825328f535e6195a924240b34daf6e7fdb573a1809a42b9f37fb8114fa99c7ab89a695e0cdb419d4149d067f6723e4b95855ffd39c6c4ca378efb3 + languageName: node + linkType: hard + +"typical@npm:^5.2.0": + version: 5.2.0 + resolution: "typical@npm:5.2.0" + checksum: ccaeb151a9a556291b495571ca44c4660f736fb49c29314bbf773c90fad92e9485d3cc2b074c933866c1595abbbc962f2b8bfc6e0f52a8c6b0cdd205442036ac + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4": version: 3.17.4 resolution: "uglify-js@npm:3.17.4" @@ -11792,7 +12593,7 @@ __metadata: languageName: node linkType: hard -"web3-utils@npm:1.10.0, web3-utils@npm:^1.3.0": +"web3-utils@npm:1.10.0, web3-utils@npm:^1.3.0, web3-utils@npm:^1.3.6": version: 1.10.0 resolution: "web3-utils@npm:1.10.0" dependencies: @@ -11985,6 +12786,16 @@ __metadata: languageName: node linkType: hard +"wordwrapjs@npm:^4.0.0": + version: 4.0.1 + resolution: "wordwrapjs@npm:4.0.1" + dependencies: + reduce-flatten: ^2.0.0 + typical: ^5.2.0 + checksum: 3d927f3c95d0ad990968da54c0ad8cde2801d8e91006cd7474c26e6b742cc8557250ce495c9732b2f9db1f903601cb74ec282e0f122ee0d02d7abe81e150eea8 + languageName: node + linkType: hard + "workerpool@npm:6.1.0": version: 6.1.0 resolution: "workerpool@npm:6.1.0"