From 164dceaeae985f6b88c3cbf38ac46caa159ab276 Mon Sep 17 00:00:00 2001 From: Sebastian Camacho Date: Wed, 27 Sep 2023 01:14:04 +0200 Subject: [PATCH] move faucet contract to new faucet package --- .../core/test/faucet/faucetsERC1155.test.ts | 271 --------------- packages/core/test/faucet/fixtures.ts | 63 +--- packages/faucets/.eslintignore | 16 + packages/faucets/.eslintrc.js | 41 +++ packages/faucets/.gitignore | 15 + packages/faucets/.prettierignore | 16 + packages/faucets/.prettierrc.js | 15 + packages/faucets/.solcover.js | 7 + packages/faucets/.solhint.json | 15 + packages/faucets/README.md | 75 +++++ .../contracts}/FaucetsERC1155.md | 0 .../contracts}/FaucetsERC1155.sol | 10 +- packages/faucets/contracts/mock/FakeAsset.sol | 12 + packages/faucets/docs.example | 21 ++ packages/faucets/hardhat.config.ts | 21 ++ packages/faucets/package.json | 61 ++++ packages/faucets/test/faucetsERC1155.ts | 318 ++++++++++++++++++ packages/faucets/test/fixtures.ts | 71 ++++ packages/faucets/tsconfig.json | 16 + yarn.lock | 65 +++- 20 files changed, 788 insertions(+), 341 deletions(-) delete mode 100644 packages/core/test/faucet/faucetsERC1155.test.ts create mode 100644 packages/faucets/.eslintignore create mode 100644 packages/faucets/.eslintrc.js create mode 100644 packages/faucets/.gitignore create mode 100644 packages/faucets/.prettierignore create mode 100644 packages/faucets/.prettierrc.js create mode 100644 packages/faucets/.solcover.js create mode 100644 packages/faucets/.solhint.json create mode 100644 packages/faucets/README.md rename packages/{core/src/solc_0.8/faucet => faucets/contracts}/FaucetsERC1155.md (100%) rename packages/{core/src/solc_0.8/faucet => faucets/contracts}/FaucetsERC1155.sol (97%) create mode 100644 packages/faucets/contracts/mock/FakeAsset.sol create mode 100644 packages/faucets/docs.example create mode 100644 packages/faucets/hardhat.config.ts create mode 100644 packages/faucets/package.json create mode 100644 packages/faucets/test/faucetsERC1155.ts create mode 100644 packages/faucets/test/fixtures.ts create mode 100644 packages/faucets/tsconfig.json diff --git a/packages/core/test/faucet/faucetsERC1155.test.ts b/packages/core/test/faucet/faucetsERC1155.test.ts deleted file mode 100644 index 18d5a15ddb..0000000000 --- a/packages/core/test/faucet/faucetsERC1155.test.ts +++ /dev/null @@ -1,271 +0,0 @@ -import {ethers} from 'hardhat'; -import {expect} from '../chai-setup'; -import {setupFaucetERC1155} from './fixtures'; -import {BigNumber, Contract, Signer} from 'ethers'; -import {increaseTime} from '../utils'; - -describe('FaucetsERC1155', function () { - let faucetsERC1155: Contract; - let mockAssetERC1155: Contract; - let owner: Signer; - let user1: Signer; - let erc1155TokenIds: Array = []; - - const erc1155Amounts = [100000, 50]; - const faucetPeriod = 3600; - const faucetLimit = 100; - - beforeEach(async function () { - const setup = await setupFaucetERC1155(); - faucetsERC1155 = setup.faucetsERC1155; - mockAssetERC1155 = setup.mockAssetERC1155; - const {mintAssetERC1155} = setup; - - [owner, user1] = await ethers.getSigners(); - const ownerAddress = await owner.getAddress(); - - const {tokenId: tokenIdA} = await mintAssetERC1155({ - creatorAddress: ownerAddress, - packId: 1, - hash: - '0x78b9f42c22c3c8b260b781578da3151e8200c741c6b7437bafaff5a9df9b403e', - supply: erc1155Amounts[0], - ownerAddress: ownerAddress, - data: '0x', - }); - - const {tokenId: tokenIdB} = await mintAssetERC1155({ - creatorAddress: ownerAddress, - packId: 2, - hash: - '0x78b9f42c22c3c8b260b781578da3151e8200c741c6b7437bafaff5a9df9b404e', - supply: erc1155Amounts[1], - ownerAddress: ownerAddress, - data: '0x', - }); - - erc1155TokenIds = [tokenIdA, tokenIdB]; - - await faucetsERC1155 - .connect(owner) - .addFaucet( - mockAssetERC1155.address, - faucetPeriod, - faucetLimit, - erc1155TokenIds - ); - await mockAssetERC1155 - .connect(owner) - .safeBatchTransferFrom( - ownerAddress, - faucetsERC1155.address, - erc1155TokenIds, - erc1155Amounts, - '0x' - ); - }); - - it('Should allow a user to claim ERC1155 tokens', async function () { - await faucetsERC1155 - .connect(user1) - .claim(mockAssetERC1155.address, erc1155TokenIds[0], faucetLimit); - const user1Balance = await mockAssetERC1155.balanceOf( - await user1.getAddress(), - erc1155TokenIds[0] - ); - expect(user1Balance).to.equal(faucetLimit); - }); - - it('Should not allow a user to claim more than the limit', async function () { - await expect( - faucetsERC1155 - .connect(user1) - .claim(mockAssetERC1155.address, erc1155TokenIds[0], faucetLimit + 1) - ).to.be.revertedWith('Faucets: AMOUNT_TOO_HIGH'); - }); - - it('Should not allow a user to claim before the faucet period expires', async function () { - await faucetsERC1155 - .connect(user1) - .claim(mockAssetERC1155.address, erc1155TokenIds[0], faucetLimit); - await expect( - faucetsERC1155 - .connect(user1) - .claim(mockAssetERC1155.address, erc1155TokenIds[0], faucetLimit) - ).to.be.revertedWith('Faucets: CLAIM_PERIOD_NOT_PASSED'); - }); - - it('Should allow a user to claim after the faucet period expires', async function () { - await faucetsERC1155 - .connect(user1) - .claim(mockAssetERC1155.address, erc1155TokenIds[0], faucetLimit); - - await increaseTime(faucetPeriod + 1, true); - - await faucetsERC1155 - .connect(user1) - .claim(mockAssetERC1155.address, erc1155TokenIds[0], faucetLimit); - const user1Balance = await mockAssetERC1155.balanceOf( - await user1.getAddress(), - erc1155TokenIds[0] - ); - expect(user1Balance).to.equal(faucetLimit * 2); - }); - - it("Should not allow a user to claim if the faucet doesn't have enough tokens", async function () { - const highAmount = erc1155Amounts[1] + 1; - await expect( - faucetsERC1155 - .connect(user1) - .claim(mockAssetERC1155.address, erc1155TokenIds[1], highAmount) - ).to.be.revertedWith('Faucets: BALANCE_IS_NOT_ENOUGH'); - }); - - it("Should correctly get the faucet's period", async function () { - const period = await faucetsERC1155.getPeriod(mockAssetERC1155.address); - expect(period).to.equal(faucetPeriod); - }); - - it('Should allow the owner to set a new faucet period', async function () { - const newPeriod = 7200; - await faucetsERC1155 - .connect(owner) - .setPeriod(mockAssetERC1155.address, newPeriod); - - const updatedPeriod = await faucetsERC1155.getPeriod( - mockAssetERC1155.address - ); - expect(updatedPeriod).to.equal(newPeriod); - }); - - it('Should not allow a non-owner to set a new faucet period', async function () { - const newPeriod = 7200; - await expect( - faucetsERC1155 - .connect(user1) - .setPeriod(mockAssetERC1155.address, newPeriod) - ).to.be.revertedWith('Ownable: caller is not the owner'); - }); - - it("Should allow the owner to withdraw all the faucet's tokens", async function () { - const faucetBalanceBefore = await mockAssetERC1155.balanceOf( - faucetsERC1155.address, - erc1155TokenIds[0] - ); - expect(faucetBalanceBefore).to.be.gt(0); - - await faucetsERC1155 - .connect(owner) - .withdraw( - mockAssetERC1155.address, - await owner.getAddress(), - erc1155TokenIds - ); - - const faucetBalanceAfter = await mockAssetERC1155.balanceOf( - faucetsERC1155.address, - erc1155TokenIds[0] - ); - const ownerBalanceAfter = await mockAssetERC1155.balanceOf( - await owner.getAddress(), - erc1155TokenIds[0] - ); - - expect(faucetBalanceAfter).to.equal(0); - expect(ownerBalanceAfter).to.equal(faucetBalanceBefore); - }); - - it("Should not allow a non-owner to withdraw the faucet's tokens", async function () { - await expect( - faucetsERC1155 - .connect(user1) - .withdraw( - mockAssetERC1155.address, - await user1.getAddress(), - erc1155TokenIds - ) - ).to.be.revertedWith('Ownable: caller is not the owner'); - }); - - it('Should allow a user to batch claim multiple ERC1155 tokens from a single faucet', async function () { - const claimAmounts = [faucetLimit, erc1155Amounts[1] - 1]; - - await faucetsERC1155 - .connect(user1) - .claimBatch(mockAssetERC1155.address, erc1155TokenIds, claimAmounts); - - const user1BalanceTokenA = await mockAssetERC1155.balanceOf( - await user1.getAddress(), - erc1155TokenIds[0] - ); - const user1BalanceTokenB = await mockAssetERC1155.balanceOf( - await user1.getAddress(), - erc1155TokenIds[1] - ); - - expect(user1BalanceTokenA).to.equal(claimAmounts[0]); - expect(user1BalanceTokenB).to.equal(claimAmounts[1]); - }); - - it('Should not allow a user to batch claim amounts greater than the set limits', async function () { - const excessiveAmounts = [faucetLimit + 1, erc1155Amounts[1] + 1]; - - await expect( - faucetsERC1155 - .connect(user1) - .claimBatch(mockAssetERC1155.address, erc1155TokenIds, excessiveAmounts) - ).to.be.revertedWith('Faucets: AMOUNT_TOO_HIGH'); - }); - - it('Should not allow a user to batch claim before the faucet period expires', async function () { - await faucetsERC1155 - .connect(user1) - .claimBatch(mockAssetERC1155.address, erc1155TokenIds, [faucetLimit, 10]); - - await expect( - faucetsERC1155 - .connect(user1) - .claimBatch(mockAssetERC1155.address, erc1155TokenIds, [ - faucetLimit, - 10, - ]) - ).to.be.revertedWith('Faucets: CLAIM_PERIOD_NOT_PASSED'); - }); - - it('Should allow a user to batch claim after the faucet period expires', async function () { - await faucetsERC1155 - .connect(user1) - .claimBatch(mockAssetERC1155.address, erc1155TokenIds, [faucetLimit, 10]); - - await increaseTime(faucetPeriod + 1, true); - - await faucetsERC1155 - .connect(user1) - .claimBatch(mockAssetERC1155.address, erc1155TokenIds, [faucetLimit, 10]); - - const user1BalanceTokenA = await mockAssetERC1155.balanceOf( - await user1.getAddress(), - erc1155TokenIds[0] - ); - const user1BalanceTokenB = await mockAssetERC1155.balanceOf( - await user1.getAddress(), - erc1155TokenIds[1] - ); - - expect(user1BalanceTokenA).to.equal(faucetLimit * 2); - expect(user1BalanceTokenB).to.equal(20); - }); - - it('Should revert if trying to batch claim for tokens not in the faucet', async function () { - const notInFaucetTokenId = BigNumber.from('9999'); - await expect( - faucetsERC1155 - .connect(user1) - .claimBatch( - mockAssetERC1155.address, - [notInFaucetTokenId], - [faucetLimit] - ) - ).to.be.revertedWith('Faucets: TOKEN_DOES_NOT_EXIST'); - }); -}); diff --git a/packages/core/test/faucet/fixtures.ts b/packages/core/test/faucet/fixtures.ts index 41e5c3415f..37f4c45f80 100644 --- a/packages/core/test/faucet/fixtures.ts +++ b/packages/core/test/faucet/fixtures.ts @@ -2,10 +2,9 @@ import { ethers, getNamedAccounts, getUnnamedAccounts, - deployments, } from 'hardhat'; import {BigNumber} from 'ethers'; -import {withSnapshot, expectEventWithArgs} from '../utils'; +import {withSnapshot} from '../utils'; export const setupFaucet = withSnapshot(['Faucet'], async function () { const {sandAdmin, sandBeneficiary, deployer} = await getNamedAccounts(); @@ -28,63 +27,3 @@ export const setupFaucet = withSnapshot(['Faucet'], async function () { deadline, }; }); - -export const setupFaucetERC1155 = withSnapshot([], async function () { - const [owner] = await ethers.getSigners(); - const {deploy} = deployments; - - const Faucet = await ethers.getContractFactory('FaucetsERC1155'); - const faucetsERC1155 = await Faucet.deploy(owner.getAddress()); - await faucetsERC1155.deployed(); - - const ERC1155ERC721HelperLib = await deploy('ERC1155ERC721Helper', { - from: owner.address, - log: true, - skipIfAlreadyDeployed: true, - }); - const MockAssetERC1155 = await ethers.getContractFactory('MockAssetERC1155', { - libraries: { - ERC1155ERC721Helper: ERC1155ERC721HelperLib.address, - }, - }); - const mockAssetERC1155 = await MockAssetERC1155.deploy(); - await mockAssetERC1155.deployed(); - - async function mintAssetERC1155({ - creatorAddress, - packId, - hash, - supply, - ownerAddress, - data, - }: { - creatorAddress: string; - packId: number; - hash: string; - supply: number; - ownerAddress: string; - data: string; - }) { - const receipt = await mockAssetERC1155.mintWithOutBouncerCheck( - creatorAddress, - packId, - hash, - supply, - ownerAddress, - data - ); - const transferEvent = await expectEventWithArgs( - mockAssetERC1155, - receipt, - 'TransferSingle' - ); - const tokenId = transferEvent.args[3]; - return {tokenId}; - } - - return { - faucetsERC1155, - mockAssetERC1155, - mintAssetERC1155, - }; -}); diff --git a/packages/faucets/.eslintignore b/packages/faucets/.eslintignore new file mode 100644 index 0000000000..a77e23ebbf --- /dev/null +++ b/packages/faucets/.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/faucets/.eslintrc.js b/packages/faucets/.eslintrc.js new file mode 100644 index 0000000000..e734de4058 --- /dev/null +++ b/packages/faucets/.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/faucets/.gitignore b/packages/faucets/.gitignore new file mode 100644 index 0000000000..04e3058747 --- /dev/null +++ b/packages/faucets/.gitignore @@ -0,0 +1,15 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +# Hardhat files +cache +artifacts +./artifacts +./cache +./typechain + +deployments \ No newline at end of file diff --git a/packages/faucets/.prettierignore b/packages/faucets/.prettierignore new file mode 100644 index 0000000000..a77e23ebbf --- /dev/null +++ b/packages/faucets/.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/faucets/.prettierrc.js b/packages/faucets/.prettierrc.js new file mode 100644 index 0000000000..60dbb58db3 --- /dev/null +++ b/packages/faucets/.prettierrc.js @@ -0,0 +1,15 @@ +module.exports = { + singleQuote: true, + bracketSpacing: false, + plugins: ['prettier-plugin-solidity'], + overrides: [ + { + files: '*.sol', + options: { + printWidth: 120, + tabWidth: 4, + singleQuote: false, + }, + }, + ], +}; diff --git a/packages/faucets/.solcover.js b/packages/faucets/.solcover.js new file mode 100644 index 0000000000..f6c4e5445d --- /dev/null +++ b/packages/faucets/.solcover.js @@ -0,0 +1,7 @@ +module.exports = { + mocha: { + grep: '@skip-on-coverage', // Find everything with this tag + invert: true, // Run the grep's inverse set. + }, + skipFiles: ['/mock'], +}; diff --git a/packages/faucets/.solhint.json b/packages/faucets/.solhint.json new file mode 100644 index 0000000000..f89501544f --- /dev/null +++ b/packages/faucets/.solhint.json @@ -0,0 +1,15 @@ +{ + "extends": "solhint:recommended", + "plugins": ["prettier"], + "rules": { + "prettier/prettier": [ + "error", + { + "endOfLine": "auto" + } + ], + "compiler-version": ["error", "^0.8.0"], + "func-visibility": ["error", {"ignoreConstructors": true}], + "custom-errors": "off" + } +} diff --git a/packages/faucets/README.md b/packages/faucets/README.md new file mode 100644 index 0000000000..be0d21d0fb --- /dev/null +++ b/packages/faucets/README.md @@ -0,0 +1,75 @@ +# + +*Include a high level description of your package here* + +This example project is based on the template generated by hardhat when run in an empty directory. + +This project demonstrates a basic Hardhat use case. It comes with a sample contract, a sample test fixture for that contract, +and tests. + +## Creating a new package + +You can copy-paste this example package: `cp -a packages/example-hardhat packages/` + +## Running the project locally + +Install dependencies with `yarn` + +Testing: Use `yarn test` inside `packages/` to run tests locally inside this package + +For testing from root (with workspace feature) use: `yarn workspace @sandbox-smart-contracts/ test` + +Coverage: Run `yarn coverage` + +Formatting: Run `yarn prettier` to check and `yarn prettier:fix` to fix formatting errors + +Linting: Run `yarn lint` to check and `yarn lint:fix` to fix static analysis errors + +## Package structure and minimum standards + +#### A NOTE ON DEPENDENCIES + +1. Add whatever dependencies you like inside your package; this template is for hardhat usage. OpenZeppelin contracts + are highly recommended and should be installed as a dev dependency +2. For most Pull Requests there should be minimum changes to `yarn.lock` at root level +3. Changes to root-level dependencies are permissible, however they should not be downgraded +4. Take care to run `yarn` before pushing your changes +5. You shouldn't need to install dotenv since you won't be deploying inside this package (see below) + +#### UNIT TESTING + +1. Unit tests are to be added in `packages//test` +2. Coverage must meet minimum requirements for CI to pass +3. `getSigners` return an array of addresses, the first one is the default `deployer` for contracts, under no + circumstances should tests be written as `deployer` +4. It's permissible to create mock contracts at `packages//contracts/mock` e.g. for third-party contracts +5. Tests must not rely on any deploy scripts from the `deploy` package; your contracts must be deployed inside the test + fixture. See `test/fixtures.ts` + +# Deployment + +Each package must unit-test the contracts by running everything inside the `hardhat node`. Deployment to "real" +networks, configuration of our environment and integration tests must be done inside the `deploy` package. + +The `deploy` package only imports `.sol` files. The idea is to recompile everything inside it and manage the entire +deploy strategy from one place. + +1. Your deploy scripts should not be included inside `packages/`: deploy scripts live inside `packages/deploy/` +2. The `deploy` package doesn't use the hardhat config file from the specific package. Instead, it + uses `packages/deploy/hardhat.config.ts` +3. You will need to review `packages/deploy/hardhat.config.ts` and update it as needed for any new namedAccounts you + added to your package +4. When it comes to deploy time, it is preferred to include deploy scripts and end-to-end tests as a separate PR +5. The named accounts inside the `deploy` package must use the "real-life" values +6. Refer to the readme at `packages/deploy` to learn more about importing your package + +#### INTEGRATION TESTING + +1. End-to-end tests live at `packages/deploy/` +2. You must add end-to-end tests ahead of deploying your package. Importantly, these tests should verify deployment and + initialization configuration + +# A NOTE ON MAKING PULL REQUESTS + +1. Follow the PR template checklist +2. Your PR will not be approved if the above criteria are not met diff --git a/packages/core/src/solc_0.8/faucet/FaucetsERC1155.md b/packages/faucets/contracts/FaucetsERC1155.md similarity index 100% rename from packages/core/src/solc_0.8/faucet/FaucetsERC1155.md rename to packages/faucets/contracts/FaucetsERC1155.md diff --git a/packages/core/src/solc_0.8/faucet/FaucetsERC1155.sol b/packages/faucets/contracts/FaucetsERC1155.sol similarity index 97% rename from packages/core/src/solc_0.8/faucet/FaucetsERC1155.sol rename to packages/faucets/contracts/FaucetsERC1155.sol index bdec6f1764..265e26eabb 100644 --- a/packages/core/src/solc_0.8/faucet/FaucetsERC1155.sol +++ b/packages/faucets/contracts/FaucetsERC1155.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.2; +pragma solidity ^0.8.21; -import "@openzeppelin/contracts-0.8/token/ERC1155/IERC1155.sol"; -import "@openzeppelin/contracts-0.8/access/Ownable.sol"; -import "@openzeppelin/contracts-0.8/security/ReentrancyGuard.sol"; -import "@openzeppelin/contracts-0.8/token/ERC1155/utils/ERC1155Holder.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; /** * @title FaucetsERC1155 diff --git a/packages/faucets/contracts/mock/FakeAsset.sol b/packages/faucets/contracts/mock/FakeAsset.sol new file mode 100644 index 0000000000..1eb47ac05c --- /dev/null +++ b/packages/faucets/contracts/mock/FakeAsset.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + +contract FakeAsset is ERC1155 { + constructor() ERC1155("https://test.sandbox.game/fake/url/item/{id}.json") {} + + function mint(uint256 id, uint256 amount) public { + _mint(msg.sender, id, amount, ""); + } +} \ No newline at end of file diff --git a/packages/faucets/docs.example b/packages/faucets/docs.example new file mode 100644 index 0000000000..e2d52c7138 --- /dev/null +++ b/packages/faucets/docs.example @@ -0,0 +1,21 @@ +Audience + +The intended audience for .md documentation is auditors, internal developers and external developer contributors. + +Features + +Include a summary of the PURPOSE of the smart contract, ensuring to describe any COMMERCIAL INTENT +List any ASSUMPTIONS that have been made about how the smart contract will be used +Who are the INTENDED USERS of the smart contract +What are the privileged / admin ROLES +What are the FEATURES of the smart contract + +Methods + +Describe as a minimum all the external and public methods used in the smart contract and their parameters +Be sure to describe the intended usage of the methods and any limitations + +Links + +Include link(s) here to test files that help to demonstrate the intended usage of the smart contract functions +Include link(s) here to image files that show the smart contract transaction flow diff --git a/packages/faucets/hardhat.config.ts b/packages/faucets/hardhat.config.ts new file mode 100644 index 0000000000..3dadf6b3b7 --- /dev/null +++ b/packages/faucets/hardhat.config.ts @@ -0,0 +1,21 @@ +import '@nomicfoundation/hardhat-toolbox'; +import {HardhatUserConfig} from 'hardhat/config'; + +const config: HardhatUserConfig = { + // solidity compiler version may be updated for new packages as required + // to ensure packages use up-to-date dependencies + solidity: { + compilers: [ + { + version: '0.8.21', + settings: { + optimizer: { + enabled: true, + runs: 2000, + }, + }, + }, + ], + }, +}; +export default config; diff --git a/packages/faucets/package.json b/packages/faucets/package.json new file mode 100644 index 0000000000..46dd8bafa5 --- /dev/null +++ b/packages/faucets/package.json @@ -0,0 +1,61 @@ +{ + "name": "@sandbox-smart-contracts/faucets", + "version": "0.0.1", + "description": "Faucets package", + "files": [ + "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": "hardhat test", + "coverage": "hardhat coverage --testfiles 'test/*.ts''test/*.js'", + "hardhat": "hardhat", + "compile": "hardhat compile" + }, + "mocha": { + "require": "hardhat/register", + "timeout": 40000, + "_": [ + "test/**/*.ts" + ] + }, + "devDependencies": { + "@ethersproject/abi": "^5.7.0", + "@ethersproject/providers": "^5.7.2", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.1", + "@nomicfoundation/hardhat-ethers": "^3.0.4", + "@nomicfoundation/hardhat-network-helpers": "^1.0.9", + "@nomicfoundation/hardhat-toolbox": "^3.0.0", + "@nomicfoundation/hardhat-verify": "^1.0.0", + "@nomiclabs/hardhat-ethers": "^2.2.3", + "@nomiclabs/hardhat-etherscan": "^3.1.7", + "@openzeppelin/contracts": "^4.9.3", + "@openzeppelin/contracts-upgradeable": "^4.9.2", + "@typechain/ethers-v6": "^0.4.0", + "@typechain/hardhat": "^8.0.0", + "@types/chai": "^4.3.6", + "@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.8", + "eslint": "^8.41.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-mocha": "^10.1.0", + "eslint-plugin-prettier": "^4.2.1", + "ethers": "^6.6.2", + "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.3", + "ts-node": "^10.9.1", + "typechain": "^8.2.0", + "typescript": "5.0.4" + } +} diff --git a/packages/faucets/test/faucetsERC1155.ts b/packages/faucets/test/faucetsERC1155.ts new file mode 100644 index 0000000000..5d250789e1 --- /dev/null +++ b/packages/faucets/test/faucetsERC1155.ts @@ -0,0 +1,318 @@ +import {expect} from 'chai'; +import {loadFixture} from '@nomicfoundation/hardhat-network-helpers'; +import {setupFaucetERC1155} from './fixtures'; +import {mine, time} from '@nomicfoundation/hardhat-network-helpers'; + +describe('FaucetsERC1155', function () { + it('Should allow a user to claim ERC1155 tokens', async function () { + const { + otherAccount, + mockAssetERC1155, + faucetsERC1155, + fakeAssets, + faucetLimit, + } = await loadFixture(setupFaucetERC1155); + + const otherAccountAddress = await otherAccount.getAddress(); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + await faucetsERC1155 + .connect(otherAccount) + .claim(mockAssetERC1155Address, fakeAssets[0].id, faucetLimit); + const otherAccountBalance = await mockAssetERC1155.balanceOf( + otherAccountAddress, + fakeAssets[0].id + ); + expect(otherAccountBalance).to.equal(faucetLimit); + }); + + it('Should not allow a user to claim more than the limit', async function () { + const { + otherAccount, + mockAssetERC1155, + faucetsERC1155, + faucetLimit, + fakeAssets, + } = await loadFixture(setupFaucetERC1155); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + + await expect( + faucetsERC1155 + .connect(otherAccount) + .claim(mockAssetERC1155Address, fakeAssets[0].id, faucetLimit + 1) + ).to.be.revertedWith('Faucets: AMOUNT_TOO_HIGH'); + }); + + it('Should not allow a user to claim before the faucet period expires', async function () { + const { + otherAccount, + mockAssetERC1155, + faucetsERC1155, + faucetLimit, + fakeAssets, + } = await loadFixture(setupFaucetERC1155); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + + await faucetsERC1155 + .connect(otherAccount) + .claim(mockAssetERC1155Address, fakeAssets[0].id, faucetLimit); + await expect( + faucetsERC1155 + .connect(otherAccount) + .claim(mockAssetERC1155Address, fakeAssets[0].id, faucetLimit) + ).to.be.revertedWith('Faucets: CLAIM_PERIOD_NOT_PASSED'); + }); + + it('Should allow a user to claim after the faucet period expires', async function () { + const { + otherAccount, + mockAssetERC1155, + faucetsERC1155, + faucetLimit, + fakeAssets, + faucetPeriod, + } = await loadFixture(setupFaucetERC1155); + const otherAccountAddress = await otherAccount.getAddress(); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + + await faucetsERC1155 + .connect(otherAccount) + .claim(mockAssetERC1155Address, fakeAssets[0].id, faucetLimit); + + await time.increase(faucetPeriod); + await mine(); + + await faucetsERC1155 + .connect(otherAccount) + .claim(mockAssetERC1155Address, fakeAssets[0].id, faucetLimit); + const otherAccountBalance = await mockAssetERC1155.balanceOf( + otherAccountAddress, + fakeAssets[0].id + ); + expect(otherAccountBalance).to.equal(faucetLimit * 2); + }); + + it("Should not allow a user to claim if the faucet doesn't have enough tokens", async function () { + const {otherAccount, mockAssetERC1155, faucetsERC1155, fakeAssets} = + await loadFixture(setupFaucetERC1155); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + + const highAmount = fakeAssets[1].supply + 1; + await expect( + faucetsERC1155 + .connect(otherAccount) + .claim(mockAssetERC1155Address, fakeAssets[1].id, highAmount) + ).to.be.revertedWith('Faucets: BALANCE_IS_NOT_ENOUGH'); + }); + + it("Should correctly get the faucet's period", async function () { + const {mockAssetERC1155, faucetsERC1155, faucetPeriod} = await loadFixture( + setupFaucetERC1155 + ); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + + const period = await faucetsERC1155.getPeriod(mockAssetERC1155Address); + expect(period).to.equal(faucetPeriod); + }); + + it('Should allow the owner to set a new faucet period', async function () { + const {owner, mockAssetERC1155, faucetsERC1155} = await loadFixture( + setupFaucetERC1155 + ); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + + const newPeriod = 7200; + await faucetsERC1155 + .connect(owner) + .setPeriod(mockAssetERC1155Address, newPeriod); + + const updatedPeriod = await faucetsERC1155.getPeriod( + mockAssetERC1155Address + ); + expect(updatedPeriod).to.equal(newPeriod); + }); + + it('Should not allow a non-owner to set a new faucet period', async function () { + const {otherAccount, mockAssetERC1155, faucetsERC1155} = await loadFixture( + setupFaucetERC1155 + ); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + + const newPeriod = 7200; + await expect( + faucetsERC1155 + .connect(otherAccount) + .setPeriod(mockAssetERC1155Address, newPeriod) + ).to.be.revertedWith('Ownable: caller is not the owner'); + }); + + it("Should allow the owner to withdraw all the faucet's tokens", async function () { + const {owner, mockAssetERC1155, faucetsERC1155, fakeAssets} = + await loadFixture(setupFaucetERC1155); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + const faucetsERC1155Address = await faucetsERC1155.getAddress(); + const ownerAddress = await owner.getAddress(); + const erc1155TokenIds = [fakeAssets[0].id, fakeAssets[1].id]; + + const faucetBalanceBefore = await mockAssetERC1155.balanceOf( + faucetsERC1155Address, + fakeAssets[0].id + ); + expect(faucetBalanceBefore).to.be.gt(0); + + await faucetsERC1155 + .connect(owner) + .withdraw(mockAssetERC1155Address, ownerAddress, erc1155TokenIds); + + const faucetBalanceAfter = await mockAssetERC1155.balanceOf( + faucetsERC1155Address, + fakeAssets[0].id + ); + const ownerBalanceAfter = await mockAssetERC1155.balanceOf( + ownerAddress, + fakeAssets[0].id + ); + + expect(faucetBalanceAfter).to.equal(0); + expect(ownerBalanceAfter).to.equal(faucetBalanceBefore); + }); + + it("Should not allow a non-owner to withdraw the faucet's tokens", async function () { + const {otherAccount, mockAssetERC1155, faucetsERC1155, fakeAssets} = + await loadFixture(setupFaucetERC1155); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + const otherAccountAddress = await otherAccount.getAddress(); + const erc1155TokenIds = [fakeAssets[0].id, fakeAssets[1].id]; + + await expect( + faucetsERC1155 + .connect(otherAccount) + .withdraw(mockAssetERC1155Address, otherAccountAddress, erc1155TokenIds) + ).to.be.revertedWith('Ownable: caller is not the owner'); + }); + + it('Should allow a user to batch claim multiple ERC1155 tokens from a single faucet', async function () { + const { + otherAccount, + mockAssetERC1155, + faucetsERC1155, + fakeAssets, + faucetLimit, + } = await loadFixture(setupFaucetERC1155); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + const otherAccountAddress = await otherAccount.getAddress(); + const erc1155TokenIds = [fakeAssets[0].id, fakeAssets[1].id]; + + const claimAmounts = [faucetLimit, fakeAssets[1].supply - 1]; + + await faucetsERC1155 + .connect(otherAccount) + .claimBatch(mockAssetERC1155Address, erc1155TokenIds, claimAmounts); + + const balanceTokenA = await mockAssetERC1155.balanceOf( + otherAccountAddress, + erc1155TokenIds[0] + ); + const balanceTokenB = await mockAssetERC1155.balanceOf( + otherAccountAddress, + erc1155TokenIds[1] + ); + + expect(balanceTokenA).to.equal(claimAmounts[0]); + expect(balanceTokenB).to.equal(claimAmounts[1]); + }); + + it('Should not allow a user to batch claim amounts greater than the set limits', async function () { + const { + otherAccount, + mockAssetERC1155, + faucetsERC1155, + fakeAssets, + faucetLimit, + } = await loadFixture(setupFaucetERC1155); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + const erc1155TokenIds = [fakeAssets[0].id, fakeAssets[1].id]; + const excessiveAmounts = [faucetLimit + 1, fakeAssets[1].supply + 1]; + + await expect( + faucetsERC1155 + .connect(otherAccount) + .claimBatch(mockAssetERC1155Address, erc1155TokenIds, excessiveAmounts) + ).to.be.revertedWith('Faucets: AMOUNT_TOO_HIGH'); + }); + + it('Should not allow a user to batch claim before the faucet period expires', async function () { + const { + otherAccount, + mockAssetERC1155, + faucetsERC1155, + fakeAssets, + faucetLimit, + } = await loadFixture(setupFaucetERC1155); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + const erc1155TokenIds = [fakeAssets[0].id, fakeAssets[1].id]; + + await faucetsERC1155 + .connect(otherAccount) + .claimBatch(mockAssetERC1155Address, erc1155TokenIds, [faucetLimit, 10]); + + await expect( + faucetsERC1155 + .connect(otherAccount) + .claimBatch(mockAssetERC1155Address, erc1155TokenIds, [faucetLimit, 10]) + ).to.be.revertedWith('Faucets: CLAIM_PERIOD_NOT_PASSED'); + }); + + it('Should allow a user to batch claim after the faucet period expires', async function () { + const { + otherAccount, + mockAssetERC1155, + faucetsERC1155, + fakeAssets, + faucetLimit, + faucetPeriod, + } = await loadFixture(setupFaucetERC1155); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + const otherAccountAddress = await otherAccount.getAddress(); + const erc1155TokenIds = [fakeAssets[0].id, fakeAssets[1].id]; + + await faucetsERC1155 + .connect(otherAccount) + .claimBatch(mockAssetERC1155Address, erc1155TokenIds, [faucetLimit, 10]); + + await time.increase(faucetPeriod + 1); + await mine(); + + await faucetsERC1155 + .connect(otherAccount) + .claimBatch(mockAssetERC1155Address, erc1155TokenIds, [faucetLimit, 10]); + + const balanceTokenA = await mockAssetERC1155.balanceOf( + otherAccountAddress, + erc1155TokenIds[0] + ); + const balanceTokenB = await mockAssetERC1155.balanceOf( + otherAccountAddress, + erc1155TokenIds[1] + ); + + expect(balanceTokenA).to.equal(faucetLimit * 2); + expect(balanceTokenB).to.equal(20); + }); + + it('Should revert if trying to batch claim for tokens not in the faucet', async function () { + const {otherAccount, mockAssetERC1155, faucetsERC1155, faucetLimit} = + await loadFixture(setupFaucetERC1155); + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + const notInFaucetTokenId = 9999; + + await expect( + faucetsERC1155 + .connect(otherAccount) + .claimBatch( + mockAssetERC1155Address, + [notInFaucetTokenId], + [faucetLimit] + ) + ).to.be.revertedWith('Faucets: TOKEN_DOES_NOT_EXIST'); + }); +}); diff --git a/packages/faucets/test/fixtures.ts b/packages/faucets/test/fixtures.ts new file mode 100644 index 0000000000..da0efb0b84 --- /dev/null +++ b/packages/faucets/test/fixtures.ts @@ -0,0 +1,71 @@ +import { ethers } from 'hardhat'; + +export const setupFaucetERC1155 = async function () { + const [deployer, owner, otherAccount] = await ethers.getSigners(); + + const Faucet = await ethers.getContractFactory('FaucetsERC1155'); + const faucetsERC1155 = await Faucet.deploy(owner.getAddress()); + await faucetsERC1155.waitForDeployment(); + + const MockAsset = await ethers.getContractFactory('FakeAsset'); + const mockAssetERC1155 = (await MockAsset.deploy()) + await mockAssetERC1155.waitForDeployment(); + + async function mintAssetERC1155({ id, supply }: { id: number; supply: number }) { + const receipt = await mockAssetERC1155.connect(owner).mint(id, supply); + return receipt; + } + + const fakeAssets = [ + { + id: 1, + supply: 100000 + }, + { + id: 2, + supply: 50 + } + ]; + const faucetPeriod = 3600; + const faucetLimit = 100; + + await mintAssetERC1155(fakeAssets[0]); + await mintAssetERC1155(fakeAssets[1]); + + const erc1155TokenIds = [fakeAssets[0].id, fakeAssets[1].id]; + const erc1155Amounts = [fakeAssets[0].supply, fakeAssets[1].supply]; + const mockAssetERC1155Address = await mockAssetERC1155.getAddress(); + const faucetsERC1155Address = await faucetsERC1155.getAddress(); + const ownerAddress = await owner.getAddress(); + + await faucetsERC1155 + .connect(owner) + .addFaucet( + mockAssetERC1155Address, + faucetPeriod, + faucetLimit, + erc1155TokenIds + ); + + await mockAssetERC1155 + .connect(owner) + .safeBatchTransferFrom( + ownerAddress, + faucetsERC1155Address, + erc1155TokenIds, + erc1155Amounts, + '0x' + ); + + return { + deployer, + owner, + otherAccount, + faucetsERC1155, + mockAssetERC1155, + mintAssetERC1155, + fakeAssets, + faucetPeriod, + faucetLimit + }; +}; diff --git a/packages/faucets/tsconfig.json b/packages/faucets/tsconfig.json new file mode 100644 index 0000000000..790a8309cf --- /dev/null +++ b/packages/faucets/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" + ] +} diff --git a/yarn.lock b/yarn.lock index 2c6be75160..4688ef040d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1084,7 +1084,7 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/hardhat-ethers@npm:^3.0.3": +"@nomicfoundation/hardhat-ethers@npm:^3.0.3, @nomicfoundation/hardhat-ethers@npm:^3.0.4": version: 3.0.4 resolution: "@nomicfoundation/hardhat-ethers@npm:3.0.4" dependencies: @@ -1108,6 +1108,17 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/hardhat-network-helpers@npm:^1.0.9": + version: 1.0.9 + resolution: "@nomicfoundation/hardhat-network-helpers@npm:1.0.9" + dependencies: + ethereumjs-util: ^7.1.4 + peerDependencies: + hardhat: ^2.9.5 + checksum: ff378795075af853aeaacb7bc0783928d947d7f9fb043c046fcaffdf1e1219c4af47b18ea7fa2c10fe0b25daef48f13ae8b103bc11ea494ecdfbe34a3dcdf936 + languageName: node + linkType: hard + "@nomicfoundation/hardhat-toolbox@npm:^2.0.2": version: 2.0.2 resolution: "@nomicfoundation/hardhat-toolbox@npm:2.0.2" @@ -1469,7 +1480,7 @@ __metadata: languageName: node linkType: hard -"@openzeppelin/contracts@npm:^4.2.0, @openzeppelin/contracts@npm:^4.7.3": +"@openzeppelin/contracts@npm:^4.2.0, @openzeppelin/contracts@npm:^4.7.3, @openzeppelin/contracts@npm:^4.9.3": version: 4.9.3 resolution: "@openzeppelin/contracts@npm:4.9.3" checksum: 4932063e733b35fa7669b9fe2053f69b062366c5c208b0c6cfa1ac451712100c78acff98120c3a4b88d94154c802be05d160d71f37e7d74cadbe150964458838 @@ -1678,6 +1689,47 @@ __metadata: languageName: unknown linkType: soft +"@sandbox-smart-contracts/faucets@workspace:packages/faucets": + version: 0.0.0-use.local + resolution: "@sandbox-smart-contracts/faucets@workspace:packages/faucets" + dependencies: + "@ethersproject/abi": ^5.7.0 + "@ethersproject/providers": ^5.7.2 + "@nomicfoundation/hardhat-chai-matchers": ^2.0.1 + "@nomicfoundation/hardhat-ethers": ^3.0.4 + "@nomicfoundation/hardhat-network-helpers": ^1.0.9 + "@nomicfoundation/hardhat-toolbox": ^3.0.0 + "@nomicfoundation/hardhat-verify": ^1.0.0 + "@nomiclabs/hardhat-ethers": ^2.2.3 + "@nomiclabs/hardhat-etherscan": ^3.1.7 + "@openzeppelin/contracts": ^4.9.3 + "@openzeppelin/contracts-upgradeable": ^4.9.2 + "@typechain/ethers-v6": ^0.4.0 + "@typechain/hardhat": ^8.0.0 + "@types/chai": ^4.3.6 + "@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.8 + eslint: ^8.41.0 + eslint-config-prettier: ^8.8.0 + eslint-plugin-mocha: ^10.1.0 + eslint-plugin-prettier: ^4.2.1 + ethers: ^6.6.2 + 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.3 + ts-node: ^10.9.1 + typechain: ^8.2.0 + typescript: 5.0.4 + languageName: unknown + linkType: soft + "@sandbox-smart-contracts/giveaway@npm:0.0.3": version: 0.0.3 resolution: "@sandbox-smart-contracts/giveaway@npm:0.0.3" @@ -2047,6 +2099,13 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^4.3.6": + version: 4.3.6 + resolution: "@types/chai@npm:4.3.6" + checksum: 32a6c18bf53fb3dbd89d1bfcadb1c6fd45cc0007c34e436393cc37a0a5a556f9e6a21d1e8dd71674c40cc36589d2f30bf4d9369d7787021e54d6e997b0d7300a + languageName: node + linkType: hard + "@types/concat-stream@npm:^1.6.0": version: 1.6.1 resolution: "@types/concat-stream@npm:1.6.1" @@ -3475,7 +3534,7 @@ __metadata: languageName: node linkType: hard -"chai@npm:^4.2.0, chai@npm:^4.3.7": +"chai@npm:^4.2.0, chai@npm:^4.3.7, chai@npm:^4.3.8": version: 4.3.8 resolution: "chai@npm:4.3.8" dependencies: