diff --git a/.gas-snapshot b/.gas-snapshot index 4f68341..3027a2d 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,6 +1,6 @@ -AddToAllowlist_Unit_Concrete_Test:test_AddToAllowlist() (gas: 241256) -AddToAllowlist_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 13752) -AddToAllowlist_Unit_Concrete_Test:test_RevertWhen_InvalidZeroCodeModule() (gas: 13925) +AddToAllowlist_Unit_Concrete_Test:test_AddToAllowlist() (gas: 178178) +AddToAllowlist_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 12954) +AddToAllowlist_Unit_Concrete_Test:test_RevertWhen_InvalidZeroCodeModule() (gas: 13122) CancelInvoice_Integration_Concret_Test:test_CancelInvoice_PaymentMethodLinearStream_StatusOngoing() (gas: 446811) CancelInvoice_Integration_Concret_Test:test_CancelInvoice_PaymentMethodLinearStream_StatusPending() (gas: 30578) CancelInvoice_Integration_Concret_Test:test_CancelInvoice_PaymentMethodTranchedStream_StatusOngoing() (gas: 446790) @@ -13,38 +13,39 @@ CancelInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodLinearStream CancelInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_StatusOngoing_SenderNoInitialtStreamSender() (gas: 404628) CancelInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_StatusPending_SenderNotInvoiceRecipient() (gas: 20611) CancelInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTransfer_SenderNotInvoiceRecipient() (gas: 20553) -ComputeNumberOfPayments_Helpers_Test:test_ComputeNumberOfPayments_Monthly() (gas: 4156) -ComputeNumberOfPayments_Helpers_Test:test_ComputeNumberOfPayments_Weekly() (gas: 4082) -ComputeNumberOfPayments_Helpers_Test:test_ComputeNumberOfPayments_Yearly() (gas: 4266) -Constructor_DockRegistry_Test:test_Constructor() (gas: 3353665) -Constructor_ModuleKeeper_Test:test_Constructor() (gas: 416041) +ComputeNumberOfPayments_Helpers_Test:test_ComputeNumberOfPayments_Monthly() (gas: 3530) +ComputeNumberOfPayments_Helpers_Test:test_ComputeNumberOfPayments_Weekly() (gas: 3483) +ComputeNumberOfPayments_Helpers_Test:test_ComputeNumberOfPayments_Yearly() (gas: 3613) +Constructor_DockRegistry_Test:test_Constructor() (gas: 2577196) +Constructor_ModuleKeeper_Test:test_Constructor() (gas: 241794) Constructor_StreamManager_Integration_Concret_Test:test_Constructor() (gas: 1615852) -CreateContainer_Unit_Concrete_Test:test_CreateContainer_DockIdNonZero() (gas: 3030474) -CreateContainer_Unit_Concrete_Test:test_CreateContainer_DockIdZero() (gas: 1548403) -CreateContainer_Unit_Concrete_Test:test_RevertWhen_CallerNotDockOwner() (gas: 1561938) -CreateInvoice_Integration_Concret_Test:test_CreateInvoice_LinearStream() (gas: 258963) -CreateInvoice_Integration_Concret_Test:test_CreateInvoice_PaymentMethodOneOffTransfer() (gas: 259167) -CreateInvoice_Integration_Concret_Test:test_CreateInvoice_RecurringTransfer() (gas: 260318) -CreateInvoice_Integration_Concret_Test:test_CreateInvoice_Tranched() (gas: 260591) +CreateContainer_Unit_Concrete_Test:test_CreateContainer_DockIdNonZero() (gas: 3153626) +CreateContainer_Unit_Concrete_Test:test_CreateContainer_DockIdZero() (gas: 1607335) +CreateContainer_Unit_Concrete_Test:test_RevertWhen_CallerNotDockOwner() (gas: 1624571) +CreateInvoice_Integration_Concret_Test:test_CreateInvoice_LinearStream() (gas: 259080) +CreateInvoice_Integration_Concret_Test:test_CreateInvoice_PaymentMethodOneOffTransfer() (gas: 259284) +CreateInvoice_Integration_Concret_Test:test_CreateInvoice_RecurringTransfer() (gas: 260435) +CreateInvoice_Integration_Concret_Test:test_CreateInvoice_Tranched() (gas: 260708) CreateInvoice_Integration_Concret_Test:test_RevertWhen_CallerNotContract() (gas: 89299) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_EndTimeInThePast() (gas: 107235) +CreateInvoice_Integration_Concret_Test:test_RevertWhen_EndTimeInThePast() (gas: 107352) CreateInvoice_Integration_Concret_Test:test_RevertWhen_NonCompliantContainer() (gas: 92554) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodLinearStream_PaymentAssetNativeToken() (gas: 107353) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodRecurringTransfer_PaymentIntervalTooShortForSelectedRecurrence() (gas: 108043) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_PaymentAssetNativeToken() (gas: 108523) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_PaymentIntervalTooShortForSelectedRecurrence() (gas: 108087) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_RecurrenceSetToOneOff() (gas: 106731) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_StartTimeGreaterThanEndTime() (gas: 106559) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_ZeroPaymentAmount() (gas: 85918) -CreateInvoice_Integration_Fuzz_Test:testFuzz_CreateInvoice(uint8,uint8,address,uint40,uint40,uint128) (runs: 10001, μ: 202392, ~: 259073) -DisableModule_Unit_Concrete_Test:test_DisableModule() (gas: 249454) -DisableModule_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 22806) -EnableModule_Unit_Concrete_Test:test_EnableModule() (gas: 42771) -EnableModule_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 22785) -EnableModule_Unit_Concrete_Test:test_RevertWhen_ModuleNotAllowlisted() (gas: 32658) -Execute_Unit_Concrete_Test:test_Execute() (gas: 96131) -Execute_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 26420) -Execute_Unit_Concrete_Test:test_RevertWhen_ModuleNotEnabled() (gas: 26485) +CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodLinearStream_PaymentAssetNativeToken() (gas: 107470) +CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodRecurringTransfer_PaymentIntervalTooShortForSelectedRecurrence() (gas: 108160) +CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_PaymentAssetNativeToken() (gas: 108640) +CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_PaymentIntervalTooShortForSelectedRecurrence() (gas: 108204) +CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_RecurrenceSetToOneOff() (gas: 106848) +CreateInvoice_Integration_Concret_Test:test_RevertWhen_StartTimeGreaterThanEndTime() (gas: 106676) +CreateInvoice_Integration_Concret_Test:test_RevertWhen_ZeroPaymentAmount() (gas: 86035) +CreateInvoice_Integration_Fuzz_Test:testFuzz_CreateInvoice(uint8,uint8,address,uint40,uint40,uint128) (runs: 10001, μ: 202482, ~: 259190) +DisableModule_Unit_Concrete_Test:test_DisableModule() (gas: 183277) +DisableModule_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 21350) +EnableModule_Unit_Concrete_Test:test_EnableModule() (gas: 39197) +EnableModule_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 21302) +EnableModule_Unit_Concrete_Test:test_RevertWhen_ModuleNotAllowlisted() (gas: 29881) +Execute_Unit_Concrete_Test:test_Execute() (gas: 89623) +Execute_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 23959) +Execute_Unit_Concrete_Test:test_RevertWhen_ModuleNotEnabled() (gas: 24032) +Fallback_Unit_Concrete_Test:test_Fallback() (gas: 20887) PayInvoice_Integration_Concret_Test:test_PayInvoice_PaymentMethodLinearStream() (gas: 309509) PayInvoice_Integration_Concret_Test:test_PayInvoice_PaymentMethodTranchedStream() (gas: 436650) PayInvoice_Integration_Concret_Test:test_PayInvoice_PaymentMethodTransfer_ERC20Token_Recurring() (gas: 87178) @@ -52,34 +53,41 @@ PayInvoice_Integration_Concret_Test:test_PayInvoice_PaymentMethodTransfer_Native PayInvoice_Integration_Concret_Test:test_RevertWhen_InvoiceAlreadyPaid() (gas: 62436) PayInvoice_Integration_Concret_Test:test_RevertWhen_InvoiceCanceled() (gas: 29609) PayInvoice_Integration_Concret_Test:test_RevertWhen_InvoiceNull() (gas: 17829) -PayInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTransfer_NativeTokenTransferFails() (gas: 170827) +PayInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTransfer_NativeTokenTransferFails() (gas: 170944) PayInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTransfer_PaymentAmountLessThanInvoiceValue() (gas: 31912) -PayInvoice_Integration_Fuzz_Test:testFuzz_PayInvoice(uint8,uint8,uint40,uint40,uint128) (runs: 10002, μ: 369458, ~: 342362) -Receive_Unit_Concrete_Test:test_Receive() (gas: 21868) -RemoveFromAllowlist_Unit_Concrete_Test:test_AddToAllowlist() (gas: 23859) -RemoveFromAllowlist_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 13752) -TransferContainerOwnership_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 1561103) -TransferContainerOwnership_Unit_Concrete_Test:test_RevertWhen_InvalidOwnerZeroAddress() (gas: 1559040) -TransferContainerOwnership_Unit_Concrete_Test:test_TransferContainerOwnership() (gas: 1569104) -TransferDockOwnership_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 1560963) -TransferDockOwnership_Unit_Concrete_Test:test_TransferDockOwnership() (gas: 1568645) -TransferOwnership_Unit_Concrete_Test:test_RevertWhen_CallerNotCurrentOwner() (gas: 15849) -TransferOwnership_Unit_Concrete_Test:test_RevertWhen_NewOwnerZeroAddress() (gas: 13765) -TransferOwnership_Unit_Concrete_Test:test_TransferOwnership() (gas: 24347) -TransferOwnership_Unit_Fuzz_Test:testFuzz_RevertWhen_CallerNotCurrentOwner(address) (runs: 10002, μ: 14624, ~: 14624) -TransferOwnership_Unit_Fuzz_Test:testFuzz_TransferOwnership(address) (runs: 10002, μ: 22748, ~: 22748) -UpdateModuleKeeper_Unit_Concrete_Test:test_RevertWhen_CallerNotRegistryOwner() (gas: 20219) -UpdateModuleKeeper_Unit_Concrete_Test:test_UpdateModuleKeeper() (gas: 29457) +PayInvoice_Integration_Fuzz_Test:testFuzz_PayInvoice(uint8,uint8,uint40,uint40,uint128) (runs: 10002, μ: 370886, ~: 342456) +Receive_Unit_Concrete_Test:test_Receive() (gas: 20733) +RemoveFromAllowlist_Unit_Concrete_Test:test_AddToAllowlist() (gas: 22211) +RemoveFromAllowlist_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 12981) +TransferContainerOwnership_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 1624113) +TransferContainerOwnership_Unit_Concrete_Test:test_RevertWhen_InvalidOwnerZeroAddress() (gas: 1622072) +TransferContainerOwnership_Unit_Concrete_Test:test_TransferContainerOwnership() (gas: 1630377) +TransferDockOwnership_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 1623891) +TransferDockOwnership_Unit_Concrete_Test:test_TransferDockOwnership() (gas: 1629839) +TransferOwnership_Unit_Concrete_Test:test_RevertWhen_CallerNotCurrentOwner() (gas: 15031) +TransferOwnership_Unit_Concrete_Test:test_RevertWhen_NewOwnerZeroAddress() (gas: 12972) +TransferOwnership_Unit_Concrete_Test:test_TransferOwnership() (gas: 22589) +TransferOwnership_Unit_Fuzz_Test:testFuzz_RevertWhen_CallerNotCurrentOwner(address) (runs: 10002, μ: 13560, ~: 13560) +TransferOwnership_Unit_Fuzz_Test:testFuzz_TransferOwnership(address) (runs: 10002, μ: 20802, ~: 20803) +UpdateModuleKeeper_Unit_Concrete_Test:test_RevertWhen_CallerNotRegistryOwner() (gas: 18570) +UpdateModuleKeeper_Unit_Concrete_Test:test_UpdateModuleKeeper() (gas: 26803) UpdateStreamBrokerFee_Integration_Concret_Test:test_RevertWhen_CallerNotOwner() (gas: 12864) UpdateStreamBrokerFee_Integration_Concret_Test:test_UpdateStreamBrokerFee() (gas: 38936) -UpgradeToAndCall_DockRegistry_Test:test_RevertWhen_CallerNotRegistryOwner() (gas: 23388) -UpgradeToAndCall_DockRegistry_Test:test_UpgradeToAndCall() (gas: 35430) -WithdrawERC20_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 23342) -WithdrawERC20_Unit_Concrete_Test:test_RevertWhen_InsufficientERC20ToWithdraw() (gas: 31627) -WithdrawERC20_Unit_Concrete_Test:test_WithdrawERC20() (gas: 104376) +UpgradeToAndCall_DockRegistry_Test:test_RevertWhen_CallerNotRegistryOwner() (gas: 21343) +UpgradeToAndCall_DockRegistry_Test:test_UpgradeToAndCall() (gas: 32559) +WithdrawERC1155_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 35766) +WithdrawERC1155_Unit_Concrete_Test:test_RevertWhen_InsufficientERC1155Balance() (gas: 47878) +WithdrawERC1155_Unit_Concrete_Test:test_WithdrawERC1155() (gas: 119637) +WithdrawERC1155_Unit_Concrete_Test:test_WithdrawERC1155_Batch() (gas: 138047) +WithdrawERC20_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 21352) +WithdrawERC20_Unit_Concrete_Test:test_RevertWhen_InsufficientERC20ToWithdraw() (gas: 28992) +WithdrawERC20_Unit_Concrete_Test:test_WithdrawERC20() (gas: 95442) +WithdrawERC721_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 21367) +WithdrawERC721_Unit_Concrete_Test:test_RevertWhen_NonexistentERC721Token() (gas: 34534) +WithdrawERC721_Unit_Concrete_Test:test_WithdrawERC721() (gas: 99513) WithdrawLinearStream_Integration_Concret_Test:test_WithdrawStream_LinearStream() (gas: 295994) WithdrawLinearStream_Integration_Concret_Test:test_WithdrawStream_TranchedStream() (gas: 424090) -WithdrawNative_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 22882) -WithdrawNative_Unit_Concrete_Test:test_RevertWhen_InsufficientNativeToWithdraw() (gas: 22871) -WithdrawNative_Unit_Concrete_Test:test_RevertWhen_NativeWithdrawFailed() (gas: 38894) -WithdrawNative_Unit_Concrete_Test:test_WithdrawNative() (gas: 45302) \ No newline at end of file +WithdrawNative_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 21281) +WithdrawNative_Unit_Concrete_Test:test_RevertWhen_InsufficientNativeToWithdraw() (gas: 21271) +WithdrawNative_Unit_Concrete_Test:test_RevertWhen_NativeWithdrawFailed() (gas: 36766) +WithdrawNative_Unit_Concrete_Test:test_WithdrawNative() (gas: 42337) \ No newline at end of file diff --git a/src/Container.sol b/src/Container.sol index 92e0123..b21d03e 100644 --- a/src/Container.sol +++ b/src/Container.sol @@ -2,8 +2,12 @@ pragma solidity ^0.8.26; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import { ExcessivelySafeCall } from "@nomad-xyz/excessively-safe-call/src/ExcessivelySafeCall.sol"; import { IContainer } from "./interfaces/IContainer.sol"; @@ -31,6 +35,21 @@ contract Container is IContainer, ModuleManager { dockRegistry = _dockRegistry; } + /*////////////////////////////////////////////////////////////////////////// + RECEIVE & FALLBACK + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Allow container to receive native token (ETH) + receive() external payable { + // Log the successful native token deposit + emit NativeReceived({ from: msg.sender, amount: msg.value }); + } + + /// @dev Fallback function to handle incoming calls with data + fallback() external payable { + emit NativeReceived({ from: msg.sender, amount: msg.value }); + } + /*////////////////////////////////////////////////////////////////////////// MODIFIERS //////////////////////////////////////////////////////////////////////////*/ @@ -80,7 +99,36 @@ contract Container is IContainer, ModuleManager { asset.safeTransfer({ to: msg.sender, value: amount }); // Log the successful ERC-20 token withdrawal - emit AssetWithdrawn({ sender: msg.sender, asset: address(asset), amount: amount }); + emit AssetWithdrawn({ to: msg.sender, asset: address(asset), amount: amount }); + } + + /// @inheritdoc IContainer + function withdrawERC721(IERC721 collection, uint256 tokenId) public onlyOwner { + // Checks, Effects, Interactions: withdraw by transferring the token to the container owner + // Notes: + // - we're using `safeTransferFrom` as the owner can be an ERC-4337 smart account + // therefore the `onERC721Received` hook must be implemented + collection.safeTransferFrom(address(this), msg.sender, tokenId); + + // Log the successful ERC-721 token withdrawal + emit ERC721Withdrawn({ to: msg.sender, collection: address(collection), tokenId: tokenId }); + } + + /// @inheritdoc IContainer + function withdrawERC1155(IERC1155 collection, uint256[] memory ids, uint256[] memory amounts) public onlyOwner { + // Checks, Effects, Interactions: withdraw by transferring the tokens to the container owner + // Notes: + // - we're using `safeTransferFrom` as the owner can be an ERC-4337 smart account + // therefore the `onERC1155Received` hook must be implemented + // - depending on the length of the `ids` array, we're using `safeBatchTransferFrom` or `safeTransferFrom` + if (ids.length > 1) { + collection.safeBatchTransferFrom({ from: address(this), to: msg.sender, ids: ids, values: amounts, data: "" }); + } else { + collection.safeTransferFrom({ from: address(this), to: msg.sender, id: ids[0], value: amounts[0], data: "" }); + } + + // Log the successful ERC-1155 token withdrawal + emit ERC1155Withdrawn(msg.sender, address(collection), ids, amounts); } /// @inheritdoc IContainer @@ -89,12 +137,12 @@ contract Container is IContainer, ModuleManager { if (amount > address(this).balance) revert Errors.InsufficientNativeToWithdraw(); // Interactions: withdraw by transferring the amount to the sender - (bool success,) = payable(msg.sender).call{ value: amount }(""); + (bool success,) = msg.sender.call{ value: amount }(""); // Revert if the call failed if (!success) revert Errors.NativeWithdrawFailed(); // Log the successful native token withdrawal - emit AssetWithdrawn({ sender: msg.sender, asset: address(0), amount: amount }); + emit AssetWithdrawn({ to: msg.sender, asset: address(0), amount: amount }); } /// @inheritdoc IModuleManager @@ -107,12 +155,6 @@ contract Container is IContainer, ModuleManager { super.disableModule(module); } - /// @dev Allow container to receive native token (ETH) - receive() external payable { - // Log the successful native token deposit - emit NativeDeposited({ sender: msg.sender, amount: msg.value }); - } - /*////////////////////////////////////////////////////////////////////////// CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ @@ -121,4 +163,47 @@ contract Container is IContainer, ModuleManager { function supportsInterface(bytes4 interfaceId) public pure override returns (bool) { return interfaceId == type(IContainer).interfaceId || interfaceId == type(IERC165).interfaceId; } + + /// @inheritdoc IERC721Receiver + function onERC721Received( + address, + address from, + uint256 tokenId, + bytes calldata + ) external override returns (bytes4) { + // Log the successful ERC-721 token receipt + emit ERC721Received(from, tokenId); + + return this.onERC721Received.selector; + } + + /// @inheritdoc IERC1155Receiver + function onERC1155Received( + address, + address from, + uint256 id, + uint256 value, + bytes calldata + ) external override returns (bytes4) { + // Log the successful ERC-1155 token receipt + emit ERC1155Received(from, id, value); + + return this.onERC1155Received.selector; + } + + /// @inheritdoc IERC1155Receiver + function onERC1155BatchReceived( + address, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata + ) external override returns (bytes4) { + for (uint256 i; i < ids.length; ++i) { + // Log the successful ERC-1155 token receipt + emit ERC1155Received(from, ids[i], values[i]); + } + + return this.onERC1155BatchReceived.selector; + } } diff --git a/src/interfaces/IContainer.sol b/src/interfaces/IContainer.sol index 861059b..e53473b 100644 --- a/src/interfaces/IContainer.sol +++ b/src/interfaces/IContainer.sol @@ -2,30 +2,58 @@ pragma solidity ^0.8.26; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; /// @title IContainer /// @notice Contract that provides functionalities to store native token (ETH) value and any ERC-20 tokens, allowing /// external modules to be executed by extending its core functionalities -interface IContainer is IERC165 { +interface IContainer is IERC165, IERC721Receiver, IERC1155Receiver { /*////////////////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////////////////*/ /// @notice Emitted when an `amount` amount of `asset` native tokens (ETH) is deposited on the container - /// @param sender The address of the depositor + /// @param from The address of the depositor /// @param amount The amount of the deposited ERC-20 token - event NativeDeposited(address indexed sender, uint256 amount); + event NativeReceived(address indexed from, uint256 amount); - /// @notice Emitted when an `amount` amount of `asset` ERC-20 asset is withdrawn from the container - /// @param sender The address to which the tokens were transferred - /// @param asset The address of the withdrawn ERC-20 token - /// @param amount The amount of the withdrawn ERC-20 token - event AssetWithdrawn(address indexed sender, address indexed asset, uint256 amount); + /// @notice Emitted when an ERC-721 token is received by the container + /// @param from The address of the depositor + /// @param tokenId The ID of the received token + event ERC721Received(address indexed from, uint256 indexed tokenId); + + /// @notice Emitted when an ERC-1155 token is received by the container + /// @param from The address of the depositor + /// @param id The ID of the received token + /// @param value The amount of tokens received + event ERC1155Received(address indexed from, uint256 indexed id, uint256 value); + + /// @notice Emitted when an `amount` amount of `asset` ERC-20 asset or native ETH is withdrawn from the container + /// @param to The address to which the tokens were transferred + /// @param asset The address of the ERC-20 token or zero-address for native ETH + /// @param amount The withdrawn amount + event AssetWithdrawn(address indexed to, address indexed asset, uint256 amount); + + /// @notice Emitted when an ERC-721 token is withdrawn from the container + /// @param to The address to which the token was transferred + /// @param collection The address of the ERC-721 collection + /// @param tokenId The ID of the token + event ERC721Withdrawn(address indexed to, address indexed collection, uint256 tokenId); + + /// @notice Emitted when a `value` amount of ERC-1155 `id` tokens are withdrawn from the container + /// @param to The address to which the tokens were transferred + /// @param collection The address of the ERC-1155 collection + /// @param ids The IDs of the tokens + /// @param values The amounts of the token types withdrawn + event ERC1155Withdrawn(address indexed to, address indexed collection, uint256[] ids, uint256[] values); /// @notice Emitted when a module execution is successful - /// @param module The address of the module that was executed - /// @param value The value sent to the module address required for the call + /// @param module The address of the module + /// @param value The value sent to the module required for the call /// @param data The ABI-encoded method called on the module event ModuleExecutionSucceded(address indexed module, uint256 value, bytes data); @@ -39,7 +67,7 @@ interface IContainer is IERC165 { /// @param data The ABI-encode definition of the method (+inputs) to call function execute(address module, uint256 value, bytes memory data) external returns (bool success); - /// @notice Withdraws an `amount` amount of `asset` ERC-20 token from the container + /// @notice Withdraws an `amount` amount of `asset` ERC-20 token /// /// Requirements: /// - `msg.sender` must be the owner of the container @@ -48,7 +76,26 @@ interface IContainer is IERC165 { /// @param amount The amount of the ERC-20 token to withdraw function withdrawERC20(IERC20 asset, uint256 amount) external; - /// @notice Withdraws an `amount` amount of native token (ETH) from the container + /// @notice Withdraws the `tokenId` token of the ERC-721 `collection` collection + /// + /// Requirements: + /// - `msg.sender` must be the owner of the container + /// + /// @param collection The address of the ERC-721 collection + /// @param tokenId The ID of the token to withdraw + function withdrawERC721(IERC721 collection, uint256 tokenId) external; + + /// @notice Withdraws an `amount` amount of the ERC-1155 `id` token + /// + /// Requirements: + /// - `msg.sender` must be the owner of the container + /// + /// @param collection The address of the ERC-1155 collection + /// @param ids The IDs of tokens to withdraw + /// @param amounts The amounts of tokens to withdraw + function withdrawERC1155(IERC1155 collection, uint256[] memory ids, uint256[] memory amounts) external; + + /// @notice Withdraws an `amount` amount of native token (ETH) /// /// Requirements: /// - `msg.sender` must be the owner of the container diff --git a/test/Base.t.sol b/test/Base.t.sol index 26da9fb..7be6728 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -12,6 +12,8 @@ import { Container } from "./../src/Container.sol"; 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 { /*////////////////////////////////////////////////////////////////////////// @@ -31,6 +33,8 @@ abstract contract Base_Test is Test, Events { MockModule internal mockModule; MockNonCompliantContainer internal mockNonCompliantContainer; MockBadReceiver internal mockBadReceiver; + MockERC721Collection internal mockERC721; + MockERC1155Collection internal mockERC1155; /*////////////////////////////////////////////////////////////////////////// TEST STORAGE @@ -59,6 +63,8 @@ abstract contract Base_Test is Test, Events { mockModule = new MockModule(); 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 0000000..02962ef --- /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/mocks/MockERC721Collection.sol b/test/mocks/MockERC721Collection.sol new file mode 100644 index 0000000..71949f8 --- /dev/null +++ b/test/mocks/MockERC721Collection.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +/// @notice A mock implementation of ERC-721 that implements the {IERC721} interface +contract MockERC721Collection is ERC721 { + uint256 private _tokenIdCounter; + + constructor(string memory name, string memory symbol) ERC721(name, symbol) { + // Start the token ID counter at 1 + _tokenIdCounter = 1; + } + + function mint(address to) public returns (uint256) { + // Generate a new token ID + uint256 tokenId = _tokenIdCounter; + + // Mint the token to the specified address + _safeMint(to, tokenId); + + // Increment the token ID counter + unchecked { + _tokenIdCounter++; + } + + // Return the token ID + return tokenId; + } + + function burn(uint256 tokenId) public { + _burn(tokenId); + } +} diff --git a/test/unit/concrete/container/fallback/fallback.t.sol b/test/unit/concrete/container/fallback/fallback.t.sol new file mode 100644 index 0000000..07705e3 --- /dev/null +++ b/test/unit/concrete/container/fallback/fallback.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { Container_Unit_Concrete_Test } from "../Container.t.sol"; +import { Events } from "../../../../utils/Events.sol"; + +contract Fallback_Unit_Concrete_Test is Container_Unit_Concrete_Test { + function setUp() public virtual override { + Container_Unit_Concrete_Test.setUp(); + } + + function test_Fallback() external { + // Make Bob the caller for this test suite + vm.startPrank({ msgSender: users.bob }); + + // Expect the {NativeReceived} event to be emitted upon ETH deposit with data + vm.expectEmit(); + emit Events.NativeReceived({ from: users.bob, amount: 1 ether }); + + // Run the test + (bool success,) = address(container).call{ value: 1 ether }("test"); + if (!success) revert(); + + // Assert the {Container} contract balance + assertEq(address(container).balance, 1 ether); + } +} diff --git a/test/unit/concrete/container/fallback/fallback.tree b/test/unit/concrete/container/fallback/fallback.tree new file mode 100644 index 0000000..46d45ea --- /dev/null +++ b/test/unit/concrete/container/fallback/fallback.tree @@ -0,0 +1,2 @@ +fallback.t.sol +└── it should emit an {NativeReceived} event \ No newline at end of file diff --git a/test/unit/concrete/container/receive/receive.t.sol b/test/unit/concrete/container/receive/receive.t.sol index f281a51..f1525ba 100644 --- a/test/unit/concrete/container/receive/receive.t.sol +++ b/test/unit/concrete/container/receive/receive.t.sol @@ -13,12 +13,12 @@ contract Receive_Unit_Concrete_Test is Container_Unit_Concrete_Test { // Make Bob the caller for this test suite vm.startPrank({ msgSender: users.bob }); - // Expect the {NativeDeposited} event to be emitted upon ETH deposit + // Expect the {NativeReceived} event to be emitted upon ETH deposit vm.expectEmit(); - emit Events.NativeDeposited({ sender: users.bob, amount: 1 ether }); + emit Events.NativeReceived({ from: users.bob, amount: 1 ether }); // Run the test - (bool success, ) = address(container).call{ value: 1 ether }(""); + (bool success,) = address(container).call{ value: 1 ether }(""); if (!success) revert(); // Assert the {Container} contract balance diff --git a/test/unit/concrete/container/receive/receive.tree b/test/unit/concrete/container/receive/receive.tree index 8f16c0d..e762629 100644 --- a/test/unit/concrete/container/receive/receive.tree +++ b/test/unit/concrete/container/receive/receive.tree @@ -1,2 +1,2 @@ receive.t.sol -└── it should emit an {NativeDeposited} event \ No newline at end of file +└── it should emit an {NativeReceived} event \ No newline at end of file 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 0000000..bd555ba --- /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 0000000..cb85f99 --- /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/unit/concrete/container/withdraw-erc20/withdrawERC20.t.sol b/test/unit/concrete/container/withdraw-erc20/withdrawERC20.t.sol index 81558b0..ae1a425 100644 --- a/test/unit/concrete/container/withdraw-erc20/withdrawERC20.t.sol +++ b/test/unit/concrete/container/withdraw-erc20/withdrawERC20.t.sol @@ -51,7 +51,7 @@ contract WithdrawERC20_Unit_Concrete_Test is Container_Unit_Concrete_Test { // Expect the {AssetWithdrawn} event to be emitted vm.expectEmit(); - emit Events.AssetWithdrawn({ sender: users.eve, asset: address(usdt), amount: 10e6 }); + emit Events.AssetWithdrawn({ to: users.eve, asset: address(usdt), amount: 10e6 }); // Run the test container.withdrawERC20({ asset: IERC20(address(usdt)), amount: 10e6 }); diff --git a/test/unit/concrete/container/withdraw-erc721/withdrawERC721.t.sol b/test/unit/concrete/container/withdraw-erc721/withdrawERC721.t.sol new file mode 100644 index 0000000..3346d84 --- /dev/null +++ b/test/unit/concrete/container/withdraw-erc721/withdrawERC721.t.sol @@ -0,0 +1,57 @@ +// 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 { IERC721 } from "@openzeppelin/contracts/interfaces/IERC721.sol"; + +contract WithdrawERC721_Unit_Concrete_Test is Container_Unit_Concrete_Test { + function setUp() public virtual override { + Container_Unit_Concrete_Test.setUp(); + } + + 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.withdrawERC721({ collection: IERC721(address(0x0)), tokenId: 1 }); + } + + 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_NonexistentERC721Token() external whenCallerOwner { + // Expect the next call to revert with the {ERC721NonexistentToken} error + vm.expectRevert(abi.encodeWithSelector(Errors.ERC721NonexistentToken.selector, 1)); + + // Run the test by attempting to withdraw a nonexistent ERC721 token + container.withdrawERC721({ collection: mockERC721, tokenId: 1 }); + } + + modifier whenExistingERC721Token() { + // Mint an ERC721 token to the container contract + mockERC721.mint({ to: address(container) }); + _; + } + + function test_WithdrawERC721() external whenCallerOwner whenExistingERC721Token { + // Expect the {ERC721Withdrawn} event to be emitted + vm.expectEmit(); + emit Events.ERC721Withdrawn({ to: users.eve, collection: address(mockERC721), tokenId: 1 }); + + // Run the test + container.withdrawERC721({ collection: mockERC721, tokenId: 1 }); + + // Assert the actual and expected owner of the ERC721 token + address actualOwner = mockERC721.ownerOf(1); + assertEq(actualOwner, users.eve); + } +} diff --git a/test/unit/concrete/container/withdraw-erc721/withdrawERC721.tree b/test/unit/concrete/container/withdraw-erc721/withdrawERC721.tree new file mode 100644 index 0000000..91ac7f6 --- /dev/null +++ b/test/unit/concrete/container/withdraw-erc721/withdrawERC721.tree @@ -0,0 +1,9 @@ +withdrawERC721.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 is no existing ERC-721 token to be transferred + │ └── it should revert with the {ERC721WithdrawalFailed} error + └── when there is an existing ERC-721 token to be transferred + ├── it should transfer the token to the caller + └── it should emit an {ERC721Withdrawn} event diff --git a/test/unit/concrete/container/withdraw-native/withdrawNative.t.sol b/test/unit/concrete/container/withdraw-native/withdrawNative.t.sol index 9847077..523259d 100644 --- a/test/unit/concrete/container/withdraw-native/withdrawNative.t.sol +++ b/test/unit/concrete/container/withdraw-native/withdrawNative.t.sol @@ -85,7 +85,7 @@ contract WithdrawNative_Unit_Concrete_Test is Container_Unit_Concrete_Test { // Expect the {AssetWithdrawn} event to be emitted vm.expectEmit(); - emit Events.AssetWithdrawn({ sender: users.eve, asset: address(0x0), amount: ethToWithdraw }); + emit Events.AssetWithdrawn({ to: users.eve, asset: address(0x0), amount: ethToWithdraw }); // Run the test container.withdrawNative({ amount: ethToWithdraw }); diff --git a/test/utils/Errors.sol b/test/utils/Errors.sol index 033075a..6068dea 100644 --- a/test/utils/Errors.sol +++ b/test/utils/Errors.sol @@ -33,6 +33,12 @@ library Errors { /// @notice Thrown when the deposited ERC-20 token amount is zero error InvalidAssetZeroAmount(); + /// @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 10836e6..b397c30 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -48,19 +48,42 @@ abstract contract Events { //////////////////////////////////////////////////////////////////////////*/ /// @notice Emitted when an `amount` amount of `asset` native tokens (ETH) is deposited on the container - /// @param sender The address of the depositor + /// @param from The address of the depositor /// @param amount The amount of the deposited ERC-20 token - event NativeDeposited(address indexed sender, uint256 amount); - - /// @notice Emitted when an `amount` amount of `asset` ERC-20 asset is withdrawn from the container - /// @param sender The address to which the tokens were transferred - /// @param asset The address of the withdrawn ERC-20 token - /// @param amount The amount of the withdrawn ERC-20 token - event AssetWithdrawn(address indexed sender, address indexed asset, uint256 amount); + event NativeReceived(address indexed from, uint256 amount); + + /// @notice Emitted when an ERC-721 token is received by the container + /// @param from The address of the depositor + /// @param tokenId The ID of the received token + event ERC721Received(address indexed from, uint256 indexed tokenId); + + /// @notice Emitted when an ERC-1155 token is received by the container + /// @param from The address of the depositor + /// @param id The ID of the received token + /// @param value The amount of tokens received + event ERC1155Received(address indexed from, uint256 indexed id, uint256 value); + + /// @notice Emitted when an `amount` amount of `asset` ERC-20 asset or native ETH is withdrawn from the container + /// @param to The address to which the tokens were transferred + /// @param asset The address of the ERC-20 token or zero-address for native ETH + /// @param amount The withdrawn amount + event AssetWithdrawn(address indexed to, address indexed asset, uint256 amount); + + /// @notice Emitted when an ERC-721 token is withdrawn from the container + /// @param to The address to which the token was transferred + /// @param collection The address of the ERC-721 collection + /// @param tokenId The ID of the token + event ERC721Withdrawn(address indexed to, address indexed collection, uint256 tokenId); + + /// @notice Emitted when an ERC-1155 token is withdrawn from the container + /// @param to The address to which the tokens were transferred + /// @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 that was executed - /// @param value The value sent to the module address required for the call + /// @param module The address of the module + /// @param value The value sent to the module required for the call /// @param data The ABI-encoded method called on the module event ModuleExecutionSucceded(address indexed module, uint256 value, bytes data);