diff --git a/packages/core/src/solc_0.8/faucet/FaucetsERC1155.md b/packages/core/src/solc_0.8/faucet/FaucetsERC1155.md deleted file mode 100644 index a942c33a28..0000000000 --- a/packages/core/src/solc_0.8/faucet/FaucetsERC1155.md +++ /dev/null @@ -1,54 +0,0 @@ -# FaucetsERC1155 Contract Documentation - -This contract is designed to allow the distribution of ERC1155 tokens from multiple faucets on testnet. Each faucet can have its own distribution settings, and only the owner can manage these faucets. - -## Prerequisites: - -- This contract makes use of the OpenZeppelin library for standard ERC and utility contracts. -- Solidity version: 0.8.2. - -## Features: - -1. Ability to add, enable, disable, and remove faucets. -2. Customize each faucet with a distribution limit and waiting period. -3. Withdraw tokens from the faucet. -4. Claim tokens from the faucet. - -## Events: - -- `FaucetAdded`: Emitted when a new faucet is added. -- `TokenAdded`: Emitted when a new token ID is added to a faucet. -- `FaucetStatusChanged`: Emitted when a faucet is enabled or disabled. -- `PeriodUpdated`: Emitted when the claim period for a faucet is updated. -- `LimitUpdated`: Emitted when the claim limit for a faucet is updated. -- `Claimed`: Emitted when tokens are claimed from a faucet. -- `Withdrawn`: Emitted when tokens are withdrawn from the contract. - -## Structs: - -- `FaucetInfo`: Contains information about each faucet, such as its enabled status, claim period, claim limit, and associated token IDs. - -## Modifiers: - -- `exists`: Checks if the specified faucet exists. - -## Functions: - -1. `getPeriod`: Returns the claim period for the specified faucet. -2. `setPeriod`: Sets the claim period for the specified faucet. -3. `getLimit`: Returns the claim limit for the specified faucet. -4. `setLimit`: Sets the claim limit for the specified faucet. -5. `addFaucet`: Adds a new faucet with the specified settings. -6. `removeFaucet`: Removes the specified faucet and transfers any remaining tokens to the owner. -7. `enableFaucet`: Enables the specified faucet. -8. `disableFaucet`: Disables the specified faucet. -9. `removeTokens`: Removes specific token IDs from the specified faucet and transfers the associated tokens to the owner. -10. `claim`: Claims a specified amount of a specified token from the specified faucet. -11. `withdraw`: Withdraws tokens from the contract to a specified address. -12. `_withdraw`: Internal function to handle withdrawal logic. -13. `claimBatch`: Claims multiple tokens from a specified faucet in a single transaction. - -## Notes: - -- This contract is designed to work with ERC1155 tokens, which are multi-token standard contracts. -- The owner has the ability to manage faucets, but individual users can claim tokens from the faucets based on the configured settings. 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/core/tsconfig.json b/packages/core/tsconfig.json index ff825839a2..17f1845b9a 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -18,5 +18,5 @@ "./test", "./utils", "./data" - ] +, "../deploy/deploy/300_faucets" ] } diff --git a/packages/core/deploy_polygon/300_faucets/300_deploy_faucetsERC1155.ts b/packages/deploy/deploy/300_faucets/300_deploy_faucetsERC1155.ts similarity index 70% rename from packages/core/deploy_polygon/300_faucets/300_deploy_faucetsERC1155.ts rename to packages/deploy/deploy/300_faucets/300_deploy_faucetsERC1155.ts index 72563484ca..df1bfa7d74 100644 --- a/packages/core/deploy_polygon/300_faucets/300_deploy_faucetsERC1155.ts +++ b/packages/deploy/deploy/300_faucets/300_deploy_faucetsERC1155.ts @@ -1,20 +1,20 @@ import {DeployFunction} from 'hardhat-deploy/types'; import {HardhatRuntimeEnvironment} from 'hardhat/types'; -import {skipUnlessTestnet} from '../../utils/network'; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const {deployments, getNamedAccounts} = hre; const {deploy} = deployments; - const {deployer, sandAdmin} = await getNamedAccounts(); + const {deployer, catalystMinter} = await getNamedAccounts(); await deploy('FaucetsERC1155', { from: deployer, + contract: + '@sandbox-smart-contracts/faucets/contracts/FaucetsERC1155.sol:FaucetsERC1155', log: true, skipIfAlreadyDeployed: true, - args: [sandAdmin], + args: [catalystMinter], }); }; func.tags = ['FaucetsERC1155', 'FaucetsERC1155_deploy']; -func.skip = skipUnlessTestnet; export default func; diff --git a/packages/deploy/hardhat.config.ts b/packages/deploy/hardhat.config.ts index 6bae2bd548..df5ac1fce0 100644 --- a/packages/deploy/hardhat.config.ts +++ b/packages/deploy/hardhat.config.ts @@ -9,6 +9,7 @@ import './tasks/importedPackages'; // Package name : solidity source code path const importedPackages = { '@sandbox-smart-contracts/giveaway': 'contracts/SignedMultiGiveaway.sol', + '@sandbox-smart-contracts/faucets': 'contracts/FaucetsERC1155.sol', }; const namedAccounts = { diff --git a/packages/deploy/package.json b/packages/deploy/package.json index 05cb15f5d1..e6ae6d438d 100644 --- a/packages/deploy/package.json +++ b/packages/deploy/package.json @@ -15,6 +15,7 @@ "homepage": "https://github.com/thesandboxgame/sandbox-smart-contracts#readme", "private": true, "dependencies": { + "@sandbox-smart-contracts/faucets": "0.0.1", "@sandbox-smart-contracts/giveaway": "0.0.3" }, "files": [ diff --git a/packages/deploy/test/faucets/faucets.test.ts b/packages/deploy/test/faucets/faucets.test.ts new file mode 100644 index 0000000000..e3a767a805 --- /dev/null +++ b/packages/deploy/test/faucets/faucets.test.ts @@ -0,0 +1,22 @@ +import {getContract, withSnapshot} from '../../utils/testUtils'; +import {expect} from 'chai'; + +const setupTest = withSnapshot(['FaucetsERC1155'], async (hre) => { + const namedAccount = await hre.getNamedAccounts(); + const contract = await getContract(hre, 'FaucetsERC1155'); + return { + contract, + namedAccount, + }; +}); + +describe('FaucetsERC1155', function () { + describe('check owner', function () { + it('owner', async function () { + const fixtures = await setupTest(); + expect(await fixtures.contract.owner()).to.be.equal( + fixtures.namedAccount.catalystMinter + ); + }); + }); +}); 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..33f0e5671c --- /dev/null +++ b/packages/faucets/.gitignore @@ -0,0 +1,16 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +# Hardhat files +cache +artifacts +./artifacts +./cache +./typechain + +deployments +generated-markups \ 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..c388994eb2 --- /dev/null +++ b/packages/faucets/README.md @@ -0,0 +1,66 @@ +# FaucetsERC1155 + +This contract is designed to allow the distribution of ERC1155 tokens from multiple faucets. Each faucet can have its own distribution settings, and only the owner can manage these faucets. + +## Running the project locally + +Install dependencies with `yarn` + +Testing: Use `yarn test` inside `packages/faucets` to run tests locally inside this package + +For testing from root (with workspace feature) use: `yarn workspace @sandbox-smart-contracts/faucets 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/faucets/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/faucets/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/faucets`: 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/faucets/contracts/FaucetsERC1155.md b/packages/faucets/contracts/FaucetsERC1155.md new file mode 100644 index 0000000000..373becbe90 --- /dev/null +++ b/packages/faucets/contracts/FaucetsERC1155.md @@ -0,0 +1,336 @@ +# Audience + +The intended audience for .md documentation is auditors, internal developers and external developer contributors. + +## Features: + +This contract is designed to allow the distribution of ERC1155 tokens from multiple faucets on testnet. Each faucet can have its own distribution settings, and only the owner can manage these faucets. + +- Ability to add, enable, disable, and remove faucets. +- Customize each faucet with a distribution limit and waiting period. +- Withdraw tokens from the faucet. +- Claim tokens from the faucet. + +## Structs info + +### FaucetInfo + +```solidity +struct FaucetInfo { + bool isFaucet; + bool isEnabled; + uint256 period; + uint256 limit; + uint256[] tokenIds; + mapping(uint256 => bool) tokenIdExists; + mapping(uint256 => mapping(address => uint256)) lastTimestamps; +} +``` + + +## Events info + +### FaucetAdded + +```solidity +event FaucetAdded(address indexed faucet, uint256 period, uint256 limit, uint256[] tokenIds) +``` + + +### TokenAdded + +```solidity +event TokenAdded(address indexed faucet, uint256 tokenId) +``` + + +### FaucetStatusChanged + +```solidity +event FaucetStatusChanged(address indexed faucet, bool enabled) +``` + + +### PeriodUpdated + +```solidity +event PeriodUpdated(address indexed faucet, uint256 period) +``` + + +### LimitUpdated + +```solidity +event LimitUpdated(address indexed faucet, uint256 limit) +``` + + +### Claimed + +```solidity +event Claimed(address indexed faucet, address indexed receiver, uint256 tokenId, uint256 amount) +``` + + +### Withdrawn + +```solidity +event Withdrawn(address indexed faucet, address indexed receiver, uint256[] tokenIds, uint256[] amounts) +``` + + +## Modifiers info + +### exists + +```solidity +modifier exists(address faucet) +``` + + +## Functions info + +### constructor + +```solidity +constructor(address owner) Ownable() +``` + + +### getPeriod (0x6da2147b) + +```solidity +function getPeriod( + address faucet +) external view exists(faucet) returns (uint256) +``` + +Gets the period of a given faucet. + + +Parameters: + +| Name | Type | Description | +| :----- | :------ | :-------------------------- | +| faucet | address | The address of the faucet. | + + +Return values: + +| Name | Type | Description | +| :--- | :------ | :------------------------------------------- | +| [0] | uint256 | The waiting period between claims for users. | + +### setPeriod (0x72540261) + +```solidity +function setPeriod( + address faucet, + uint256 newPeriod +) external onlyOwner exists(faucet) +``` + +Sets the period of a given faucet. + + +Parameters: + +| Name | Type | Description | +| :-------- | :------ | :----------------------------------------------- | +| faucet | address | The address of the faucet. | +| newPeriod | uint256 | The new waiting period between claims for users. | + +### getLimit (0x1ce28e72) + +```solidity +function getLimit( + address faucet +) external view exists(faucet) returns (uint256) +``` + +Gets the limit of a given faucet. + + +Parameters: + +| Name | Type | Description | +| :----- | :------ | :-------------------------- | +| faucet | address | The address of the faucet. | + + +Return values: + +| Name | Type | Description | +| :--- | :------ | :----------------------------------------------------- | +| [0] | uint256 | The maximum amount of tokens a user can claim at once. | + +### setLimit (0x36db43b5) + +```solidity +function setLimit( + address faucet, + uint256 newLimit +) external onlyOwner exists(faucet) +``` + +Sets the limit of a given faucet. + + +Parameters: + +| Name | Type | Description | +| :------- | :------ | :--------------------------------------------------------- | +| faucet | address | The address of the faucet. | +| newLimit | uint256 | The new maximum amount of tokens a user can claim at once. | + +### addFaucet (0xe2337fa8) + +```solidity +function addFaucet( + address faucet, + uint256 period, + uint256 limit, + uint256[] memory tokenIds +) public onlyOwner +``` + +Add a new faucet to the system. + + +Parameters: + +| Name | Type | Description | +| :------- | :-------- | :--------------------------------------------------------------- | +| faucet | address | The address of the ERC1155 token contract to be used as faucet. | +| period | uint256 | The waiting period between claims for users. | +| limit | uint256 | The maximum amount of tokens a user can claim at once. | +| tokenIds | uint256[] | List of token IDs that this faucet will distribute. | + +### removeFaucet (0x07229f14) + +```solidity +function removeFaucet( + address faucet +) external onlyOwner exists(faucet) nonReentrant +``` + +Removes a faucet and transfers any remaining tokens back to the owner. + + +Parameters: + +| Name | Type | Description | +| :----- | :------ | :----------------------------------- | +| faucet | address | Address of the faucet to be removed. | + +### enableFaucet (0xe4596dc4) + +```solidity +function enableFaucet(address faucet) external onlyOwner exists(faucet) +``` + +Enable a faucet, allowing users to make claims. + + +Parameters: + +| Name | Type | Description | +| :----- | :------ | :----------------------------------- | +| faucet | address | Address of the faucet to be enabled. | + +### disableFaucet (0x87a8af4e) + +```solidity +function disableFaucet(address faucet) external onlyOwner exists(faucet) +``` + +Disable a faucet, stopping users from making claims. + + +Parameters: + +| Name | Type | Description | +| :----- | :------ | :------------------------------------ | +| faucet | address | Address of the faucet to be disabled. | + +### removeTokens (0xecae5383) + +```solidity +function removeTokens( + address faucet, + uint256[] memory tokenIds +) external onlyOwner exists(faucet) nonReentrant +``` + +Remove specific tokens from a faucet. + + +Parameters: + +| Name | Type | Description | +| :------- | :-------- | :--------------------------- | +| faucet | address | Address of the faucet. | +| tokenIds | uint256[] | List of token IDs to remove. | + +### claim (0x2bc43fd9) + +```solidity +function claim( + address faucet, + uint256 tokenId, + uint256 amount +) external exists(faucet) nonReentrant +``` + +Claim tokens from a faucet. + + +Parameters: + +| Name | Type | Description | +| :------ | :------ | :------------------------------------ | +| faucet | address | Address of the faucet to claim from. | +| tokenId | uint256 | ID of the token to be claimed. | +| amount | uint256 | Amount of tokens to be claimed. | + +### withdraw (0x893bd7c8) + +```solidity +function withdraw( + address faucet, + address receiver, + uint256[] memory tokenIds +) external onlyOwner exists(faucet) nonReentrant +``` + +Function to withdraw the total balance of tokens from the contract to a specified address. + + +Parameters: + +| Name | Type | Description | +| :------- | :-------- | :----------------------------------------------------------------------------------------------------------------------------- | +| faucet | address | - The address of the ERC1155 contract (faucet) containing the tokens to be withdrawn. | +| receiver | address | - The address to which the tokens will be sent. | +| tokenIds | uint256[] | - An array of token IDs to be withdrawn. Emits a {Withdrawn} event. Requirements: - The `tokenIds` must exist in the faucet. | + +### claimBatch (0xe59e53c2) + +```solidity +function claimBatch( + address faucet, + uint256[] memory tokenIds, + uint256[] memory amounts +) external nonReentrant +``` + +Function to claim multiple tokens from a single faucet. + + +Parameters: + +| Name | Type | Description | +| :------- | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| faucet | address | - The address of the ERC1155 contract (faucet) to claim from. | +| tokenIds | uint256[] | - An array of token IDs to be claimed from the faucet. | +| amounts | uint256[] | - An array of amounts of tokens to be claimed for respective token IDs. Emits multiple {Claimed} events for each claim. Requirements: - The lengths of `tokenIds` and `amounts` arrays should be the same. - Each tokenId must exist in the faucet. | diff --git a/packages/core/src/solc_0.8/faucet/FaucetsERC1155.sol b/packages/faucets/contracts/FaucetsERC1155.sol similarity index 93% rename from packages/core/src/solc_0.8/faucet/FaucetsERC1155.sol rename to packages/faucets/contracts/FaucetsERC1155.sol index bdec6f1764..2c9881508f 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.18; -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 {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; /** * @title FaucetsERC1155 @@ -101,12 +101,7 @@ contract FaucetsERC1155 is Ownable, ERC1155Holder, ReentrancyGuard { * @param limit The maximum amount of tokens a user can claim at once. * @param tokenIds List of token IDs that this faucet will distribute. */ - function addFaucet( - address faucet, - uint256 period, - uint256 limit, - uint256[] memory tokenIds - ) public onlyOwner { + function addFaucet(address faucet, uint256 period, uint256 limit, uint256[] memory tokenIds) public onlyOwner { require(!faucets[faucet].isFaucet, "Faucets: FAUCET_ALREADY_EXISTS"); require(limit > 0, "Faucets: LIMIT_ZERO"); require(tokenIds.length > 0, "Faucets: TOKENS_CANNOT_BE_EMPTY"); @@ -197,11 +192,7 @@ contract FaucetsERC1155 is Ownable, ERC1155Holder, ReentrancyGuard { * @param tokenId ID of the token to be claimed. * @param amount Amount of tokens to be claimed. */ - function claim( - address faucet, - uint256 tokenId, - uint256 amount - ) external exists(faucet) nonReentrant { + function claim(address faucet, uint256 tokenId, uint256 amount) external exists(faucet) nonReentrant { FaucetInfo storage faucetInfo = faucets[faucet]; require(faucetInfo.isEnabled, "Faucets: FAUCET_DISABLED"); require(faucetInfo.tokenIdExists[tokenId], "Faucets: TOKEN_DOES_NOT_EXIST"); @@ -249,11 +240,7 @@ contract FaucetsERC1155 is Ownable, ERC1155Holder, ReentrancyGuard { * Requirements: * - The `tokenIds` must exist in the faucet. */ - function _withdraw( - address faucet, - address receiver, - uint256[] memory tokenIds - ) internal { + function _withdraw(address faucet, address receiver, uint256[] memory tokenIds) internal { FaucetInfo storage faucetInfo = faucets[faucet]; uint256[] memory balances = new uint256[](tokenIds.length); @@ -279,11 +266,7 @@ contract FaucetsERC1155 is Ownable, ERC1155Holder, ReentrancyGuard { * - The lengths of `tokenIds` and `amounts` arrays should be the same. * - Each tokenId must exist in the faucet. */ - function claimBatch( - address faucet, - uint256[] memory tokenIds, - uint256[] memory amounts - ) external nonReentrant { + function claimBatch(address faucet, uint256[] memory tokenIds, uint256[] memory amounts) external nonReentrant { require(tokenIds.length == amounts.length, "Faucets: ARRAY_LENGTH_MISMATCH"); for (uint256 i = 0; i < tokenIds.length; i++) { diff --git a/packages/faucets/contracts/mock/FakeAsset.sol b/packages/faucets/contracts/mock/FakeAsset.sol new file mode 100644 index 0000000000..c7a8c9364e --- /dev/null +++ b/packages/faucets/contracts/mock/FakeAsset.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import {ERC1155} from "@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, ""); + } +} 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..89c078525e --- /dev/null +++ b/packages/faucets/hardhat.config.ts @@ -0,0 +1,22 @@ +import '@nomicfoundation/hardhat-toolbox'; +import {HardhatUserConfig} from 'hardhat/config'; +import '@dlsl/hardhat-markup'; + +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.18', + 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..94715bbf9d --- /dev/null +++ b/packages/faucets/package.json @@ -0,0 +1,62 @@ +{ + "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": { + "@dlsl/hardhat-markup": "^1.0.0-rc.14", + "@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..f2814b0086 --- /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..e583986741 100644 --- a/yarn.lock +++ b/yarn.lock @@ -189,7 +189,7 @@ __metadata: languageName: node linkType: hard -"@dlsl/hardhat-markup@npm:^1.0.0-rc.11": +"@dlsl/hardhat-markup@npm:^1.0.0-rc.11, @dlsl/hardhat-markup@npm:^1.0.0-rc.14": version: 1.0.0-rc.14 resolution: "@dlsl/hardhat-markup@npm:1.0.0-rc.14" dependencies: @@ -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 @@ -1611,6 +1622,7 @@ __metadata: "@nomicfoundation/hardhat-chai-matchers": 1 "@nomicfoundation/hardhat-network-helpers": ^1.0.8 "@nomiclabs/hardhat-ethers": ^2.2.3 + "@sandbox-smart-contracts/faucets": 0.0.1 "@sandbox-smart-contracts/giveaway": 0.0.3 "@typechain/ethers-v5": ^11.0.0 "@typechain/hardhat": ^8.0.0 @@ -1678,6 +1690,48 @@ __metadata: languageName: unknown linkType: soft +"@sandbox-smart-contracts/faucets@0.0.1, @sandbox-smart-contracts/faucets@workspace:packages/faucets": + version: 0.0.0-use.local + resolution: "@sandbox-smart-contracts/faucets@workspace:packages/faucets" + dependencies: + "@dlsl/hardhat-markup": ^1.0.0-rc.14 + "@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 +2101,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 +3536,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: