diff --git a/.gas-snapshot b/.gas-snapshot index 4fc933d..72502c7 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -4,9 +4,12 @@ AddEntryTest:test_RevertWhen_InvalidAddress() (gas: 25133) AddEntryTest:test_RevertWhen_SenderIsNotTokenDeployer() (gas: 14827) CommunityERC20Test:test_Deployment() (gas: 35198) CommunityTokenDeployerTest:test_Deployment() (gas: 14805) -CommunityVaultBaseERC20Test:test_Deployment() (gas: 10436) -CommunityVaultBaseERC721Test:test_Deployment() (gas: 10436) -CommunityVaultTest:test_Deployment() (gas: 10436) +CommunityVaultBaseERC20Test:test_Deployment() (gas: 10572) +CommunityVaultBaseERC721Test:test_Deployment() (gas: 10572) +CommunityVaultBaseTransferERC721Test:test_Deployment() (gas: 10572) +CommunityVaultDepositERC721Test:testSuccessfulDepositERC721() (gas: 184700) +CommunityVaultDepositERC721Test:test_Deployment() (gas: 10714) +CommunityVaultTest:test_Deployment() (gas: 10572) CreateTest:test_Create() (gas: 2374801) CreateTest:test_Create() (gas: 2661968) CreateTest:test_RevertWhen_InvalidOwnerTokenAddress() (gas: 15523) @@ -30,6 +33,9 @@ DeploymentTest:test_Deployment() (gas: 14671) DeploymentTest:test_Deployment() (gas: 14671) DeploymentTest:test_Deployment() (gas: 17295) DeploymentTest:test_Deployment() (gas: 36430) +DepositERC20Test:testDepositZeroTokens() (gas: 15199) +DepositERC20Test:testSuccessfulDepositERC20() (gas: 85584) +DepositERC20Test:test_Deployment() (gas: 10594) GetEntryTest:test_ReturnZeroAddressIfEntryDoesNotExist() (gas: 11906) MintToTest:test_Deployment() (gas: 35220) MintToTest:test_Deployment() (gas: 83308) @@ -82,16 +88,18 @@ SetTokenDeployerAddressTest:test_RevertWhen_SenderIsNotOwner() (gas: 12438) SetTokenDeployerAddressTest:test_RevertWhen_SenderIsNotOwner() (gas: 12438) SetTokenDeployerAddressTest:test_SetTokenDeployerAddress() (gas: 22768) SetTokenDeployerAddressTest:test_SetTokenDeployerAddress() (gas: 22768) -TransferERC20ByAdminTest:test_AdminCanTransferERC20() (gas: 84406) -TransferERC20ByAdminTest:test_Deployment() (gas: 10556) -TransferERC20ByAdminTest:test_LengthMismatch() (gas: 31872) -TransferERC20ByAdminTest:test_NoRecipients() (gas: 25198) -TransferERC20ByAdminTest:test_TransferAmountZero() (gas: 61686) -TransferERC20ByNonAdminTest:test_Deployment() (gas: 10458) -TransferERC20ByNonAdminTest:test_revertIfCalledByNonAdmin() (gas: 35570) -TransferERC721ByAdminTest:test_AdminCanTransferERC721() (gas: 107114) -TransferERC721ByAdminTest:test_Deployment() (gas: 10556) -TransferERC721ByAdminTest:test_LengthMismatch() (gas: 31875) -TransferERC721ByAdminTest:test_NoRecipients() (gas: 25213) -TransferERC721ByNonAdminTest:test_Deployment() (gas: 10458) -TransferERC721ByNonAdminTest:test_RevertIfCalledByNonAdmin() (gas: 35563) \ No newline at end of file +TransferERC20ByAdminTest:test_AdminCanTransferERC20() (gas: 97818) +TransferERC20ByAdminTest:test_Deployment() (gas: 10714) +TransferERC20ByAdminTest:test_LengthMismatch() (gas: 26146) +TransferERC20ByAdminTest:test_NoRecipients() (gas: 19516) +TransferERC20ByAdminTest:test_TransferAmountZero() (gas: 66057) +TransferERC20ByAdminTest:test_TransferERC20AmountTooBig() (gas: 59079) +TransferERC20ByNonAdminTest:test_Deployment() (gas: 10594) +TransferERC20ByNonAdminTest:test_revertIfCalledByNonAdmin() (gas: 29912) +TransferERC721ByAdminTest:test_AdminCanTransferERC721() (gas: 141776) +TransferERC721ByAdminTest:test_Deployment() (gas: 10670) +TransferERC721ByAdminTest:test_LengthMismatch() (gas: 26156) +TransferERC721ByAdminTest:test_NoRecipients() (gas: 19506) +TransferERC721ByAdminTest:test_RevertOnTransferERC721IfNotDeposited() (gas: 32736) +TransferERC721ByNonAdminTest:test_Deployment() (gas: 10714) +TransferERC721ByNonAdminTest:test_RevertIfCalledByNonAdmin() (gas: 29953) diff --git a/contracts/CommunityVault.sol b/contracts/CommunityVault.sol index b52739e..a4a51c6 100644 --- a/contracts/CommunityVault.sol +++ b/contracts/CommunityVault.sol @@ -5,6 +5,8 @@ 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 { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { CommunityOwnable } from "./CommunityOwnable.sol"; /** @@ -14,8 +16,9 @@ import { CommunityOwnable } from "./CommunityOwnable.sol"; * Only community owners, as defined in the CommunityOwnable contract, have * permissions to transfer these tokens out of the vault. */ -contract CommunityVault is CommunityOwnable { +contract CommunityVault is CommunityOwnable, IERC721Receiver { using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.UintSet; event ERC20Deposited(address indexed depositor, address indexed token, uint256 amount); event ERC721Deposited(address indexed depositor, address indexed token, uint256 tokenId); @@ -25,8 +28,12 @@ contract CommunityVault is CommunityOwnable { error CommunityVault_TransferAmountZero(); error CommunityVault_ERC20TransferAmountTooBig(); error CommunityVault_DepositAmountZero(); + error CommunityVault_IndexOutOfBounds(); + error CommunityVault_ERC721TokenAlreadyDeposited(); + error CommunityVault_ERC721TokenNotDeposited(); mapping(address => uint256) public erc20TokenBalances; + mapping(address => EnumerableSet.UintSet) private erc721TokenIds; constructor(address _ownerToken, address _masterToken) CommunityOwnable(_ownerToken, _masterToken) { } @@ -50,6 +57,50 @@ contract CommunityVault is CommunityOwnable { emit ERC20Deposited(msg.sender, token, amount); } + /** + * @dev Allows anyone to deposit multiple ERC721 tokens into the vault. + * @param token The address of the ERC721 token to deposit. + * @param tokenIds The IDs of the tokens to deposit. + */ + function depositERC721(address token, uint256[] memory tokenIds) public { + for (uint256 i = 0; i < tokenIds.length; i++) { + // Add the token ID to the EnumerableSet for the given token + bool added = erc721TokenIds[token].add(tokenIds[i]); + if (!added) { + revert CommunityVault_ERC721TokenAlreadyDeposited(); + } + + // Transfer the token from the sender to this contract + IERC721(token).safeTransferFrom(msg.sender, address(this), tokenIds[i]); + + // Emit an event for the deposit + emit ERC721Deposited(msg.sender, token, tokenIds[i]); + } + } + + /** + * @dev Gets the count of ERC721 tokens deposited for a given token address. + * @param token The address of the ERC721 token. + * @return The count of tokens deposited. + */ + function erc721TokenBalances(address token) public view returns (uint256) { + return erc721TokenIds[token].length(); + } + + /** + * @dev Retrieves a deposited ERC721 token ID by index. + * @param token The address of the ERC721 token. + * @param index The index of the token ID to retrieve. + * @return The token ID at the given index. + */ + function getERC721DepositedTokenByIndex(address token, uint256 index) public view returns (uint256) { + if (index >= erc721TokenIds[token].length()) { + revert CommunityVault_IndexOutOfBounds(); + } + + return erc721TokenIds[token].at(index); + } + /** * @dev Transfers ERC20 tokens to a list of addresses. * @param token The ERC20 token address. @@ -109,7 +160,21 @@ contract CommunityVault is CommunityOwnable { } for (uint256 i = 0; i < recipients.length; i++) { + bool removed = erc721TokenIds[token].remove(tokenIds[i]); + if (!removed) { + revert CommunityVault_ERC721TokenNotDeposited(); + } + IERC721(token).safeTransferFrom(address(this), recipients[i], tokenIds[i]); } } + + /** + * @dev Handles the receipt of an ERC721 token. + * @return bytes4 Returns `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` + * to indicate the contract implements `onERC721Received` as per ERC721. + */ + function onERC721Received(address, address, uint256, bytes calldata) public pure override returns (bytes4) { + return this.onERC721Received.selector; + } } diff --git a/test/CommunityVault.t.sol b/test/CommunityVault.t.sol index 3c0298f..433ea1d 100644 --- a/test/CommunityVault.t.sol +++ b/test/CommunityVault.t.sol @@ -168,26 +168,42 @@ contract CommunityVaultBaseERC721Test is CommunityVaultTest { function setUp() public virtual override { CommunityVaultTest.setUp(); - // mint 2 token to user + // mint 4 token to user address user = accounts[0]; erc721Token.mint(user); erc721Token.mint(user); + erc721Token.mint(user); + erc721Token.mint(user); + } +} + +contract CommunityVaultBaseTransferERC721Test is CommunityVaultBaseERC721Test { + function setUp() public virtual override { + CommunityVaultBaseERC721Test.setUp(); + + address user = accounts[0]; // user transfer 2 tokens to the vault + uint256[] memory ids = new uint256[](3); + ids[0] = 0; + ids[1] = 1; + ids[2] = 2; + vm.startPrank(user); - erc721Token.transferFrom(user, address(vault), 0); - erc721Token.transferFrom(user, address(vault), 1); + erc721Token.approve(address(vault), ids[0]); + erc721Token.approve(address(vault), ids[1]); + erc721Token.approve(address(vault), ids[2]); + vault.depositERC721(address(erc721Token), ids); vm.stopPrank(); } } -contract TransferERC721ByNonAdminTest is CommunityVaultBaseERC721Test { +contract TransferERC721ByNonAdminTest is CommunityVaultBaseTransferERC721Test { function setUp() public virtual override { - CommunityVaultBaseERC721Test.setUp(); + CommunityVaultBaseTransferERC721Test.setUp(); } function test_RevertIfCalledByNonAdmin() public { - assertEq(erc721Token.balanceOf(address(vault)), 2); uint256[] memory ids = new uint256[](2); ids[0] = 0; ids[1] = 1; @@ -199,14 +215,12 @@ contract TransferERC721ByNonAdminTest is CommunityVaultBaseERC721Test { } } -contract TransferERC721ByAdminTest is CommunityVaultBaseERC721Test { +contract TransferERC721ByAdminTest is CommunityVaultBaseTransferERC721Test { function setUp() public virtual override { - CommunityVaultBaseERC721Test.setUp(); + CommunityVaultBaseTransferERC721Test.setUp(); } function test_LengthMismatch() public { - assertEq(erc721Token.balanceOf(address(vault)), 2); - uint256[] memory ids = new uint256[](1); ids[0] = 0; @@ -216,8 +230,6 @@ contract TransferERC721ByAdminTest is CommunityVaultBaseERC721Test { } function test_NoRecipients() public { - assertEq(erc721Token.balanceOf(address(vault)), 2); - uint256[] memory ids = new uint256[](0); address[] memory tmpAccounts = new address[](0); @@ -227,10 +239,16 @@ contract TransferERC721ByAdminTest is CommunityVaultBaseERC721Test { } function test_AdminCanTransferERC721() public { - assertEq(erc721Token.balanceOf(address(vault)), 2); + assertEq(erc721Token.balanceOf(address(vault)), 3); + assertEq(vault.erc721TokenBalances(address(erc721Token)), 3); + + // accounts[0] has 1 token with id 3 + assertEq(erc721Token.balanceOf(accounts[0]), 1); + assertEq(erc721Token.balanceOf(accounts[1]), 0); assertEq(erc721Token.ownerOf(0), address(vault)); assertEq(erc721Token.ownerOf(1), address(vault)); + assertEq(erc721Token.ownerOf(2), address(vault)); uint256[] memory ids = new uint256[](2); ids[0] = 0; @@ -239,6 +257,49 @@ contract TransferERC721ByAdminTest is CommunityVaultBaseERC721Test { vm.prank(deployer); vault.transferERC721(address(erc721Token), accounts, ids); - assertEq(erc721Token.balanceOf(address(vault)), 0); + assertEq(erc721Token.balanceOf(address(vault)), 1); + assertEq(vault.erc721TokenBalances(address(erc721Token)), 1); + + assertEq(erc721Token.balanceOf(accounts[0]), 2); + assertEq(erc721Token.balanceOf(accounts[1]), 1); + } + + function test_RevertOnTransferERC721IfNotDeposited() public { + // id 3 is not deposited + assertEq(erc721Token.ownerOf(3), address(accounts[0])); + + uint256[] memory ids = new uint256[](1); + ids[0] = 3; + + address[] memory accountsList = new address[](1); + accountsList[0] = accounts[0]; + + vm.prank(deployer); + vm.expectRevert(CommunityVault.CommunityVault_ERC721TokenNotDeposited.selector); + vault.transferERC721(address(erc721Token), accountsList, ids); + } +} + +contract CommunityVaultDepositERC721Test is CommunityVaultBaseERC721Test { + function setUp() public virtual override { + CommunityVaultBaseERC721Test.setUp(); + } + + function testSuccessfulDepositERC721() public { + uint256[] memory ids = new uint256[](2); + ids[0] = 0; + ids[1] = 1; + + uint256 initialVaultBalance = erc721Token.balanceOf(address(vault)); + uint256 initialTokenBalanceValue = vault.erc721TokenBalances(address(erc721Token)); + + vm.startPrank(accounts[0]); + erc721Token.approve(address(vault), ids[0]); + erc721Token.approve(address(vault), ids[1]); + vault.depositERC721(address(erc721Token), ids); + vm.stopPrank(); + + assertEq(erc721Token.balanceOf(address(vault)), initialVaultBalance + 2); + assertEq(vault.erc721TokenBalances(address(erc721Token)), initialTokenBalanceValue + 2); } }