diff --git a/test/Base.t.sol b/test/Base.t.sol index 12bc7ce6..7be67285 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -13,6 +13,7 @@ import { ModuleKeeper } from "./../src/ModuleKeeper.sol"; import { DockRegistry } from "./../src/DockRegistry.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { MockERC721Collection } from "./mocks/MockERC721Collection.sol"; +import { MockERC1155Collection } from "./mocks/MockERC1155Collection.sol"; abstract contract Base_Test is Test, Events { /*////////////////////////////////////////////////////////////////////////// @@ -33,6 +34,7 @@ abstract contract Base_Test is Test, Events { MockNonCompliantContainer internal mockNonCompliantContainer; MockBadReceiver internal mockBadReceiver; MockERC721Collection internal mockERC721; + MockERC1155Collection internal mockERC1155; /*////////////////////////////////////////////////////////////////////////// TEST STORAGE @@ -62,6 +64,7 @@ abstract contract Base_Test is Test, Events { mockNonCompliantContainer = new MockNonCompliantContainer({ _owner: users.admin }); mockBadReceiver = new MockBadReceiver(); mockERC721 = new MockERC721Collection("MockERC721Collection", "MC"); + mockERC1155 = new MockERC1155Collection("https://nft.com/0x1.json"); // Create a mock modules array mockModules.push(address(mockModule)); diff --git a/test/mocks/MockERC1155Collection.sol b/test/mocks/MockERC1155Collection.sol new file mode 100644 index 00000000..02962efa --- /dev/null +++ b/test/mocks/MockERC1155Collection.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { ERC1155 } from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + +/// @notice A mock implementation of ERC-1155 that implements the {IERC1155} interface +contract MockERC1155Collection is ERC1155 { + uint256 private _tokenTypeIdCounter; + + constructor(string memory uri) ERC1155(uri) { + // Start the token type ID counter at 1 + _tokenTypeIdCounter = 1; + } + + function mint(address to, uint256 amount) public returns (uint256) { + // Generate a new token ID + uint256 tokenId = _tokenTypeIdCounter; + + // Mint the token to the specified address + _mint(to, tokenId, amount, ""); + + // Increment the token ID counter + unchecked { + _tokenTypeIdCounter++; + } + + // Return the token ID + return tokenId; + } + + function mintBatch(address to, uint256[] memory amounts) public returns (uint256[] memory) { + // Create a new array to store the token IDs + uint256 cachedAmount = amounts.length; + uint256[] memory tokenIds = new uint256[](cachedAmount); + + for (uint256 i; i < cachedAmount; ++i) { + // Generate a new token ID for each amount + tokenIds[i] = _tokenTypeIdCounter; + + // Increment the token ID counter + unchecked { + ++_tokenTypeIdCounter; + } + } + + // Mint the tokens to the specified address + _mintBatch(to, tokenIds, amounts, ""); + + // Return the token IDs + return tokenIds; + } + + function burn(uint256 tokenId, uint256 amount) public { + _burn(msg.sender, tokenId, amount); + } + + function burnBatch(uint256[] memory tokenIds, uint256[] memory amounts) public { + _burnBatch(msg.sender, tokenIds, amounts); + } +} diff --git a/test/unit/concrete/container/withdraw-erc1155/withdrawERC1155.t.sol b/test/unit/concrete/container/withdraw-erc1155/withdrawERC1155.t.sol new file mode 100644 index 00000000..bd555baf --- /dev/null +++ b/test/unit/concrete/container/withdraw-erc1155/withdrawERC1155.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { Container_Unit_Concrete_Test } from "../Container.t.sol"; +import { Errors } from "../../../../utils/Errors.sol"; +import { Events } from "../../../../utils/Events.sol"; +import { IERC1155 } from "@openzeppelin/contracts/interfaces/IERC1155.sol"; + +contract WithdrawERC1155_Unit_Concrete_Test is Container_Unit_Concrete_Test { + uint256[] ids; + uint256[] amounts; + + function setUp() public virtual override { + Container_Unit_Concrete_Test.setUp(); + + ids = new uint256[](2); + amounts = new uint256[](2); + + ids[0] = 1; + ids[1] = 2; + amounts[0] = 100; + amounts[1] = 200; + } + + function test_RevertWhen_CallerNotOwner() external { + // Make Bob the caller for this test suite who is not the owner of the container + vm.startPrank({ msgSender: users.bob }); + + // Expect the next call to revert with the {CallerNotContainerOwner} error + vm.expectRevert(Errors.CallerNotContainerOwner.selector); + + // Run the test + container.withdrawERC1155({ collection: IERC1155(address(0x0)), ids: ids, amounts: amounts }); + } + + modifier whenCallerOwner() { + // Make Eve the caller for the next test suite as she's the owner of the container + vm.startPrank({ msgSender: users.eve }); + _; + } + + function test_RevertWhen_InsufficientERC1155Balance() external whenCallerOwner { + // Expect the next call to revert with the {ERC1155InsufficientBalance} error + vm.expectRevert( + abi.encodeWithSelector( + Errors.ERC1155InsufficientBalance.selector, address(container), 0, amounts[0], ids[0] + ) + ); + + // Run the test by attempting to withdraw a nonexistent ERC1155 token + container.withdrawERC1155({ collection: mockERC1155, ids: ids, amounts: amounts }); + } + + modifier whenExistingERC1155Token() { + // Mint 100 ERC1155 tokens to the container contract + mockERC1155.mintBatch({ to: address(container), amounts: amounts }); + _; + } + + function test_WithdrawERC1155() external whenCallerOwner whenExistingERC1155Token { + uint256[] memory idsToWithdraw = new uint256[](1); + uint256[] memory amountsToWithdraw = new uint256[](1); + idsToWithdraw[0] = 1; + amountsToWithdraw[0] = 100; + + // Expect the {ERC721Withdrawn} event to be emitted + vm.expectEmit(); + emit Events.ERC1155Withdrawn({ + to: users.eve, + collection: address(mockERC1155), + ids: idsToWithdraw, + amounts: amountsToWithdraw + }); + + // Run the test + container.withdrawERC1155({ collection: mockERC1155, ids: idsToWithdraw, amounts: amountsToWithdraw }); + + // Assert the actual and expected token type 1 ERC1155 balance of Eve + uint256 actualBalanceOfEve = mockERC1155.balanceOf(users.eve, idsToWithdraw[0]); + assertEq(actualBalanceOfEve, amountsToWithdraw[0]); + } + + function test_WithdrawERC1155_Batch() external whenCallerOwner whenExistingERC1155Token { + // Expect the {ERC721Withdrawn} event to be emitted + vm.expectEmit(); + emit Events.ERC1155Withdrawn({ to: users.eve, collection: address(mockERC1155), ids: ids, amounts: amounts }); + + // Run the test + container.withdrawERC1155({ collection: mockERC1155, ids: ids, amounts: amounts }); + + // Assert the actual and expected balance of any ERC1155 tokens + uint256 numberOfTokens = ids.length; + for (uint256 i; i < numberOfTokens; ++i) { + uint256 actualBalanceOfEve = mockERC1155.balanceOf(users.eve, ids[i]); + assertEq(actualBalanceOfEve, amounts[i]); + } + } +} diff --git a/test/unit/concrete/container/withdraw-erc1155/withdrawERC1155.tree b/test/unit/concrete/container/withdraw-erc1155/withdrawERC1155.tree new file mode 100644 index 00000000..cb85f99b --- /dev/null +++ b/test/unit/concrete/container/withdraw-erc1155/withdrawERC1155.tree @@ -0,0 +1,9 @@ +withdrawERC1155.t.sol +├── when the caller IS NOT the container owner +│ └── it should revert with the {CallerNotContainerOwner} error +└── when the caller IS the container owner + ├── when there the ERC-1155 balance IS NOT sufficient + │ └── it should revert with the {ERC1155InsufficientBalance} error + └── when there the ERC-1155 balance IS sufficient + ├── it should transfer the token(s) to the caller + └── it should emit an {ERC1155Withdrawn} event diff --git a/test/utils/Errors.sol b/test/utils/Errors.sol index f3506dc9..6068dea8 100644 --- a/test/utils/Errors.sol +++ b/test/utils/Errors.sol @@ -36,6 +36,9 @@ library Errors { /// @notice Thrown when the ERC-721 token ID does not exist error ERC721NonexistentToken(uint256 tokenId); + /// @notice Thrown when the balance of the sender is insufficient to perform an ERC-1155 transfer + error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); + /*////////////////////////////////////////////////////////////////////////// MODULE-MANAGER //////////////////////////////////////////////////////////////////////////*/ diff --git a/test/utils/Events.sol b/test/utils/Events.sol index 68269c78..b397c30e 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -77,9 +77,9 @@ abstract contract Events { /// @notice Emitted when an ERC-1155 token is withdrawn from the container /// @param to The address to which the tokens were transferred - /// @param id The ID of the token - /// @param value The amount of the tokens withdrawn - event ERC1155Withdrawn(address indexed to, address indexed collection, uint256 id, uint256 value); + /// @param ids The IDs of the tokens + /// @param amounts The amounts of the tokens + event ERC1155Withdrawn(address indexed to, address indexed collection, uint256[] ids, uint256[] amounts); /// @notice Emitted when a module execution is successful /// @param module The address of the module