From 6f83e7346c4d47e0ca1b81d0263a8702a9c47321 Mon Sep 17 00:00:00 2001 From: 0xb337r007 <120017422+0xb337r007@users.noreply.github.com> Date: Tue, 12 Dec 2023 23:10:27 +0100 Subject: [PATCH] add CommunityVault contract (#22) * add CommunityVault contract * use named import * test vault transfer functions for erc20 and erc721 * fix camel case test name * update format with latest version of forge --- .gas-snapshot | 1 + contracts/CommunityVault.sol | 83 +++++++ .../factories/CommunityMasterTokenFactory.sol | 7 +- .../factories/CommunityOwnerTokenFactory.sol | 8 +- contracts/mocks/TestERC20Token.sol | 12 + contracts/mocks/TestERC721Token.sol | 15 ++ script/DeployOwnerAndMasterToken.s.sol | 5 +- test/CollectibleV1.t.sol | 9 +- test/CommunityERC20.t.sol | 11 +- test/CommunityVault.t.sol | 215 ++++++++++++++++++ 10 files changed, 332 insertions(+), 34 deletions(-) create mode 100644 contracts/CommunityVault.sol create mode 100644 contracts/mocks/TestERC20Token.sol create mode 100644 contracts/mocks/TestERC721Token.sol create mode 100644 test/CommunityVault.t.sol diff --git a/.gas-snapshot b/.gas-snapshot index 5f54e93..9a7105f 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -5,6 +5,7 @@ AddEntryTest:test_RevertWhen_SenderIsNotTokenDeployer() (gas: 14827) CollectibleV1Test:test_Deployment() (gas: 36386) CommunityERC20Test:test_Deployment() (gas: 35198) CommunityTokenDeployerTest:test_Deployment() (gas: 14805) +CommunityVaultTest:test_Deployment() (gas: 10436) CreateTest:test_Create() (gas: 2269916) CreateTest:test_Create() (gas: 2568994) CreateTest:test_RevertWhen_InvalidOwnerTokenAddress() (gas: 15523) diff --git a/contracts/CommunityVault.sol b/contracts/CommunityVault.sol new file mode 100644 index 0000000..4f97978 --- /dev/null +++ b/contracts/CommunityVault.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Mozilla Public License 2.0 + +pragma solidity ^0.8.17; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { CommunityOwnable } from "./CommunityOwnable.sol"; + +/** + * @title CommunityVault + * @dev This contract acts as a Vault for storing ERC20 and ERC721 tokens. + * It allows any user to deposit tokens into the vault. + * Only community owners, as defined in the CommunityOwnable contract, have + * permissions to transfer these tokens out of the vault. + */ +contract CommunityVault is CommunityOwnable { + using SafeERC20 for IERC20; + + error CommunityVault_LengthMismatch(); + error CommunityVault_NoRecipients(); + error CommunityVault_TransferAmountZero(); + + constructor(address _ownerToken, address _masterToken) CommunityOwnable(_ownerToken, _masterToken) { } + + /** + * @dev Transfers ERC20 tokens to a list of addresses. + * @param token The ERC20 token address. + * @param recipients The list of recipient addresses. + * @param amounts The list of amounts to transfer to each recipient. + */ + function transferERC20( + address token, + address[] calldata recipients, + uint256[] calldata amounts + ) + external + onlyCommunityOwnerOrTokenMaster + { + if (recipients.length != amounts.length) { + revert CommunityVault_LengthMismatch(); + } + + if (recipients.length == 0) { + revert CommunityVault_NoRecipients(); + } + + for (uint256 i = 0; i < recipients.length; i++) { + if (amounts[i] == 0) { + revert CommunityVault_TransferAmountZero(); + } + + IERC20(token).safeTransfer(recipients[i], amounts[i]); + } + } + + /** + * @dev Transfers ERC721 tokens to a list of addresses. + * @param token The ERC721 token address. + * @param recipients The list of recipient addresses. + * @param tokenIds The list of token IDs to transfer to each recipient. + */ + function transferERC721( + address token, + address[] calldata recipients, + uint256[] calldata tokenIds + ) + external + onlyCommunityOwnerOrTokenMaster + { + if (recipients.length != tokenIds.length) { + revert CommunityVault_LengthMismatch(); + } + + if (recipients.length == 0) { + revert CommunityVault_NoRecipients(); + } + + for (uint256 i = 0; i < recipients.length; i++) { + IERC721(token).safeTransferFrom(address(this), recipients[i], tokenIds[i]); + } + } +} diff --git a/contracts/factories/CommunityMasterTokenFactory.sol b/contracts/factories/CommunityMasterTokenFactory.sol index beb8298..04d6f5c 100644 --- a/contracts/factories/CommunityMasterTokenFactory.sol +++ b/contracts/factories/CommunityMasterTokenFactory.sol @@ -43,12 +43,7 @@ contract CommunityMasterTokenFactory is BaseTokenFactory { revert CommunityMasterTokenFactory_InvalidOwnerTokenAddress(); } - MasterToken masterToken = new MasterToken( - _name, - _symbol, - _baseURI, - _ownerToken - ); + MasterToken masterToken = new MasterToken(_name, _symbol, _baseURI, _ownerToken); emit CreateToken(address(masterToken)); return address(masterToken); } diff --git a/contracts/factories/CommunityOwnerTokenFactory.sol b/contracts/factories/CommunityOwnerTokenFactory.sol index 1929073..a6d64af 100644 --- a/contracts/factories/CommunityOwnerTokenFactory.sol +++ b/contracts/factories/CommunityOwnerTokenFactory.sol @@ -50,13 +50,7 @@ contract CommunityOwnerTokenFactory is BaseTokenFactory { revert CommunityOwnerTokenFactory_InvalidSignerPublicKey(); } - OwnerToken ownerToken = new OwnerToken( - _name, - _symbol, - _baseURI, - _receiver, - _signerPublicKey - ); + OwnerToken ownerToken = new OwnerToken(_name, _symbol, _baseURI, _receiver, _signerPublicKey); emit CreateToken(address(ownerToken)); return address(ownerToken); } diff --git a/contracts/mocks/TestERC20Token.sol b/contracts/mocks/TestERC20Token.sol new file mode 100644 index 0000000..7f8f559 --- /dev/null +++ b/contracts/mocks/TestERC20Token.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Mozilla Public License 2.0 +pragma solidity ^0.8.17; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract TestERC20Token is ERC20 { + constructor() ERC20("Test Token", "TEST") { } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} diff --git a/contracts/mocks/TestERC721Token.sol b/contracts/mocks/TestERC721Token.sol new file mode 100644 index 0000000..918bb51 --- /dev/null +++ b/contracts/mocks/TestERC721Token.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Mozilla Public License 2.0 +pragma solidity ^0.8.17; + +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract TestERC721Token is ERC721 { + uint256 private currentTokenId; + + constructor() ERC721("Test NFT", "TNFT") { } + + function mint(address to) external { + _mint(to, currentTokenId); + currentTokenId++; + } +} diff --git a/script/DeployOwnerAndMasterToken.s.sol b/script/DeployOwnerAndMasterToken.s.sol index 6450250..7c395bb 100644 --- a/script/DeployOwnerAndMasterToken.s.sol +++ b/script/DeployOwnerAndMasterToken.s.sol @@ -23,10 +23,7 @@ contract DeployOwnerAndMasterToken is BaseScript { ); MasterToken masterToken = new MasterToken( - masterTokenConfig.name, - masterTokenConfig.symbol, - masterTokenConfig.baseURI, - address(ownerToken) + masterTokenConfig.name, masterTokenConfig.symbol, masterTokenConfig.baseURI, address(ownerToken) ); vm.stopBroadcast(); diff --git a/test/CollectibleV1.t.sol b/test/CollectibleV1.t.sol index dab5974..a50d19b 100644 --- a/test/CollectibleV1.t.sol +++ b/test/CollectibleV1.t.sol @@ -29,14 +29,7 @@ contract CollectibleV1Test is Test { deployer = deploymentConfig.deployer(); collectibleV1 = new CollectibleV1( - name, - symbol, - maxSupply, - remoteBurnable, - transferable, - baseURI, - address(ownerToken), - address(masterToken) + name, symbol, maxSupply, remoteBurnable, transferable, baseURI, address(ownerToken), address(masterToken) ); accounts[0] = makeAddr("one"); diff --git a/test/CommunityERC20.t.sol b/test/CommunityERC20.t.sol index f1cb7df..414d404 100644 --- a/test/CommunityERC20.t.sol +++ b/test/CommunityERC20.t.sol @@ -27,15 +27,8 @@ contract CommunityERC20Test is Test { deployer = deploymentConfig.deployer(); - communityToken = new CommunityERC20( - name, - symbol, - decimals, - maxSupply, - baseURI, - address(ownerToken), - address(masterToken) - ); + communityToken = + new CommunityERC20(name, symbol, decimals, maxSupply, baseURI, address(ownerToken), address(masterToken)); accounts[0] = makeAddr("one"); accounts[1] = makeAddr("two"); diff --git a/test/CommunityVault.t.sol b/test/CommunityVault.t.sol new file mode 100644 index 0000000..e9826a2 --- /dev/null +++ b/test/CommunityVault.t.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.17; + +import { Test } from "forge-std/Test.sol"; +import { TestERC20Token } from "../contracts/mocks/TestERC20Token.sol"; +import { TestERC721Token } from "../contracts/mocks/TestERC721Token.sol"; +import { CommunityVault } from "../contracts/CommunityVault.sol"; +import { CommunityOwnable } from "../contracts/CommunityOwnable.sol"; +import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; +import { DeployOwnerAndMasterToken } from "../script/DeployOwnerAndMasterToken.s.sol"; +import { CommunityERC20 } from "../contracts/tokens/CommunityERC20.sol"; +import { OwnerToken } from "../contracts/tokens/OwnerToken.sol"; +import { MasterToken } from "../contracts/tokens/MasterToken.sol"; + +contract CommunityVaultTest is Test { + CommunityVault internal vault; + + address[] internal accounts = new address[](2); + address internal deployer; + + TestERC20Token internal erc20Token; + TestERC721Token internal erc721Token; + CommunityERC20 internal communityERC20Token; + OwnerToken internal ownerToken; + MasterToken internal masterToken; + + function setUp() public virtual { + DeploymentConfig deploymentConfig; + DeployOwnerAndMasterToken deployment = new DeployOwnerAndMasterToken(); + (ownerToken, masterToken, deploymentConfig) = deployment.run(); + + deployer = deploymentConfig.deployer(); + + erc20Token = new TestERC20Token(); + erc721Token = new TestERC721Token(); + + communityERC20Token = new CommunityERC20("Test", "TST", 18, 100, "", address(ownerToken), address(masterToken)); + + vault = new CommunityVault(address(ownerToken), address(masterToken)); + + accounts[0] = makeAddr("one"); + accounts[1] = makeAddr("two"); + } + + function test_Deployment() public { + assertEq(vault.ownerToken(), address(ownerToken)); + assertEq(vault.masterToken(), address(masterToken)); + } +} + +contract CommunityVaultBaseERC20Test is CommunityVaultTest { + function setUp() public virtual override { + CommunityVaultTest.setUp(); + + // mint 10 tokens to user + address user = accounts[0]; + erc20Token.mint(user, 10e18); + + // user transfer 10 tokens to the vault + vm.prank(user); + erc20Token.transfer(address(vault), 10e18); + } +} + +contract TransferERC20ByNonAdminTest is CommunityVaultBaseERC20Test { + function setUp() public virtual override { + CommunityVaultBaseERC20Test.setUp(); + } + + function test_revertIfCalledByNonAdmin() public { + assertEq(erc20Token.balanceOf(address(vault)), 10e18); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1; + amounts[1] = 1; + + vm.prank(accounts[0]); + + vm.expectRevert(CommunityOwnable.CommunityOwnable_NotAuthorized.selector); + vault.transferERC20(address(erc20Token), accounts, amounts); + } +} + +contract TransferERC20ByAdminTest is CommunityVaultBaseERC20Test { + function setUp() public virtual override { + CommunityVaultBaseERC20Test.setUp(); + } + + function test_LengthMismatch() public { + assertEq(erc20Token.balanceOf(address(vault)), 10e18); + + uint256[] memory amounts = new uint256[](1); + amounts[0] = 5e18; + + vm.prank(deployer); + vm.expectRevert(CommunityVault.CommunityVault_LengthMismatch.selector); + vault.transferERC20(address(erc20Token), accounts, amounts); + } + + function test_TransferAmountZero() public { + assertEq(erc20Token.balanceOf(address(vault)), 10e18); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 5e18; + amounts[1] = 0; + + vm.prank(deployer); + vm.expectRevert(CommunityVault.CommunityVault_TransferAmountZero.selector); + vault.transferERC20(address(erc20Token), accounts, amounts); + } + + function test_NoRecipients() public { + assertEq(erc20Token.balanceOf(address(vault)), 10e18); + + uint256[] memory amounts = new uint256[](0); + address[] memory tmpAccounts = new address[](0); + + vm.prank(deployer); + vm.expectRevert(CommunityVault.CommunityVault_NoRecipients.selector); + vault.transferERC20(address(erc20Token), tmpAccounts, amounts); + } + + function test_AdminCanTransferERC20() public { + assertEq(erc20Token.balanceOf(address(vault)), 10e18); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 5e18; + amounts[1] = 5e18; + + vm.prank(deployer); + vault.transferERC20(address(erc20Token), accounts, amounts); + + assertEq(erc20Token.balanceOf(address(vault)), 0); + } +} + +contract CommunityVaultBaseERC721Test is CommunityVaultTest { + function setUp() public virtual override { + CommunityVaultTest.setUp(); + + // mint 2 token to user + address user = accounts[0]; + erc721Token.mint(user); + erc721Token.mint(user); + + // user transfer 2 tokens to the vault + vm.startPrank(user); + erc721Token.transferFrom(user, address(vault), 0); + erc721Token.transferFrom(user, address(vault), 1); + vm.stopPrank(); + } +} + +contract TransferERC721ByNonAdminTest is CommunityVaultBaseERC721Test { + function setUp() public virtual override { + CommunityVaultBaseERC721Test.setUp(); + } + + function test_RevertIfCalledByNonAdmin() public { + assertEq(erc721Token.balanceOf(address(vault)), 2); + uint256[] memory ids = new uint256[](2); + ids[0] = 0; + ids[1] = 1; + + vm.prank(accounts[0]); + vm.expectRevert(CommunityOwnable.CommunityOwnable_NotAuthorized.selector); + + vault.transferERC721(address(erc721Token), accounts, ids); + } +} + +contract TransferERC721ByAdminTest is CommunityVaultBaseERC721Test { + function setUp() public virtual override { + CommunityVaultBaseERC721Test.setUp(); + } + + function test_LengthMismatch() public { + assertEq(erc721Token.balanceOf(address(vault)), 2); + + uint256[] memory ids = new uint256[](1); + ids[0] = 0; + + vm.prank(deployer); + vm.expectRevert(CommunityVault.CommunityVault_LengthMismatch.selector); + vault.transferERC721(address(erc721Token), accounts, ids); + } + + function test_NoRecipients() public { + assertEq(erc721Token.balanceOf(address(vault)), 2); + + uint256[] memory ids = new uint256[](0); + address[] memory tmpAccounts = new address[](0); + + vm.prank(deployer); + vm.expectRevert(CommunityVault.CommunityVault_NoRecipients.selector); + vault.transferERC721(address(erc721Token), tmpAccounts, ids); + } + + function test_AdminCanTransferERC721() public { + assertEq(erc721Token.balanceOf(address(vault)), 2); + + assertEq(erc721Token.ownerOf(0), address(vault)); + assertEq(erc721Token.ownerOf(1), address(vault)); + + uint256[] memory ids = new uint256[](2); + ids[0] = 0; + ids[1] = 1; + + vm.prank(deployer); + vault.transferERC721(address(erc721Token), accounts, ids); + + assertEq(erc721Token.balanceOf(address(vault)), 0); + } +}