diff --git a/dependecies/immutable/allowlist/IOperatorAllowlist.sol b/dependecies/immutable/allowlist/IOperatorAllowlist.sol new file mode 100644 index 00000000..8d0e9007 --- /dev/null +++ b/dependecies/immutable/allowlist/IOperatorAllowlist.sol @@ -0,0 +1,14 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.24; + +/** + * @notice Required interface of an OperatorAllowlist compliant contract + */ +interface IOperatorAllowlist { + /** + * @notice Returns true if an address is Allowlisted false otherwise + * @param target the address to be checked against the Allowlist + */ + function isAllowlisted(address target) external view returns (bool); +} diff --git a/dependecies/immutable/allowlist/OperatorAllowlistEnforced.sol b/dependecies/immutable/allowlist/OperatorAllowlistEnforced.sol new file mode 100644 index 00000000..463accff --- /dev/null +++ b/dependecies/immutable/allowlist/OperatorAllowlistEnforced.sol @@ -0,0 +1,136 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache 2.0 +// slither-disable-start calls-loop +pragma solidity ^0.8.24; + +// Allowlist Registry +import {IOperatorAllowlist} from "./IOperatorAllowlist.sol"; + +// Errors +import {OperatorAllowlistEnforcementErrors} from "../errors/Errors.sol"; + +library OperatorAllowlistEnforcedStorage { + + /// @custom:storage-location erc7201:operator.allowlist.enforced + bytes32 public constant OPERATOR_ALLOWLIST_ENFORCED_STORAGE_POSITION = + keccak256(abi.encode(uint256(keccak256("operator.allowlist.enforced")) - 1)) & ~bytes32(uint256(0xff)); + + struct Data { + address operatorAllowlist; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = OPERATOR_ALLOWLIST_ENFORCED_STORAGE_POSITION; + assembly { + data_.slot := position + } + } + +} + +interface IERC165 { + + function supportsInterface(bytes4 interfaceId) external view returns (bool); + +} + +/* + OperatorAllowlistEnforced is an abstract contract that token contracts can inherit in order to set the + address of the OperatorAllowlist registry that it will interface with, so that the token contract may + enable the restriction of approvals and transfers to allowlisted users. + OperatorAllowlistEnforced is not designed to be upgradeable or extended. +*/ + +abstract contract OperatorAllowlistEnforced is OperatorAllowlistEnforcementErrors { + + /// ===== Events ===== + + /// @notice Emitted whenever the transfer Allowlist registry is updated + event OperatorAllowlistRegistryUpdated(address oldRegistry, address newRegistry); + + /// ===== Modifiers ===== + + /** + * @notice Internal function to validate an approval, according to whether the target is an EOA or Allowlisted + * @param targetApproval the address of the approval target to be validated + */ + modifier validateApproval(address targetApproval) { + IOperatorAllowlist _operatorAllowlist = IOperatorAllowlist(_operatorAllowlistStorage().operatorAllowlist); + + // Check for: + // 1. approver is an EOA. Contract constructor is handled as transfers 'from' are blocked + // 2. approver is address or bytecode is allowlisted + if (msg.sender.code.length != 0 && !_operatorAllowlist.isAllowlisted(msg.sender)) { + revert ApproverNotInAllowlist(msg.sender); + } + + // Check for: + // 1. approval target is an EOA + // 2. approval target address is Allowlisted or target address bytecode is Allowlisted + if (targetApproval.code.length != 0 && !_operatorAllowlist.isAllowlisted(targetApproval)) { + revert ApproveTargetNotInAllowlist(targetApproval); + } + _; + } + + /** + * @notice Internal function to validate a transfer, according to whether the calling address, + * from address and to address is an EOA or Allowlisted + * @param from the address of the from target to be validated + * @param to the address of the to target to be validated + */ + modifier validateTransfer(address from, address to) { + IOperatorAllowlist _operatorAllowlist = IOperatorAllowlist(_operatorAllowlistStorage().operatorAllowlist); + + // Check for: + // 1. caller is an EOA + // 2. caller is Allowlisted or is the calling address bytecode is Allowlisted + if ( + msg.sender != tx.origin // solhint-disable-line avoid-tx-origin + && !_operatorAllowlist.isAllowlisted(msg.sender) + ) { + revert CallerNotInAllowlist(msg.sender); + } + + // Check for: + // 1. from is an EOA + // 2. from is Allowlisted or from address bytecode is Allowlisted + if (from.code.length != 0 && !_operatorAllowlist.isAllowlisted(from)) { + revert TransferFromNotInAllowlist(from); + } + + // Check for: + // 1. to is an EOA + // 2. to is Allowlisted or to address bytecode is Allowlisted + if (to.code.length != 0 && !_operatorAllowlist.isAllowlisted(to)) { + revert TransferToNotInAllowlist(to); + } + _; + } + + /// ===== External functions ===== + + /** + * @notice Internal function to set the operator allowlist the calling contract will interface with + * @param _operatorAllowlist the address of the Allowlist registry + */ + function _setOperatorAllowlistRegistry(address _operatorAllowlist) internal { + if (!IERC165(_operatorAllowlist).supportsInterface(type(IOperatorAllowlist).interfaceId)) { + revert AllowlistDoesNotImplementIOperatorAllowlist(); + } + + emit OperatorAllowlistRegistryUpdated(_operatorAllowlistStorage().operatorAllowlist, _operatorAllowlist); + _operatorAllowlistStorage().operatorAllowlist = _operatorAllowlist; + } + + /// @notice Get the current operator allowlist registry address + function operatorAllowlist() external view returns (address) { + return _operatorAllowlistStorage().operatorAllowlist; + } + + function _operatorAllowlistStorage() internal pure returns (OperatorAllowlistEnforcedStorage.Data storage) { + return OperatorAllowlistEnforcedStorage.data(); + } + +} +// slither-disable-end calls-loop diff --git a/dependecies/immutable/errors/Errors.sol b/dependecies/immutable/errors/Errors.sol new file mode 100644 index 00000000..17e48ee8 --- /dev/null +++ b/dependecies/immutable/errors/Errors.sol @@ -0,0 +1,59 @@ +//SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.24; + +interface IImmutableERC721Errors { + /// @dev Caller tried to mint an already burned token + error IImmutableERC721TokenAlreadyBurned(uint256 tokenId); + + /// @dev Caller tried to mint an already burned token + error IImmutableERC721SendingToZerothAddress(); + + /// @dev Caller tried to mint an already burned token + error IImmutableERC721MismatchedTransferLengths(); + + /// @dev Caller tried to mint a tokenid that is above the hybrid threshold + error IImmutableERC721IDAboveThreshold(uint256 tokenId); + + /// @dev Caller is not approved or owner + error IImmutableERC721NotOwnerOrOperator(uint256 tokenId); + + /// @dev Current token owner is not what was expected + error IImmutableERC721MismatchedTokenOwner(uint256 tokenId, address currentOwner); + + /// @dev Signer is zeroth address + error SignerCannotBeZerothAddress(); + + /// @dev Deadline exceeded for permit + error PermitExpired(); + + /// @dev Derived signature is invalid (EIP721 and EIP1271) + error InvalidSignature(); +} + +interface OperatorAllowlistEnforcementErrors { + /// @dev Error thrown when the operatorAllowlist address does not implement the IOperatorAllowlist interface + error AllowlistDoesNotImplementIOperatorAllowlist(); + + /// @dev Error thrown when calling address is not OperatorAllowlist + error CallerNotInAllowlist(address caller); + + /// @dev Error thrown when 'from' address is not OperatorAllowlist + error TransferFromNotInAllowlist(address from); + + /// @dev Error thrown when 'to' address is not OperatorAllowlist + error TransferToNotInAllowlist(address to); + + /// @dev Error thrown when approve target is not OperatorAllowlist + error ApproveTargetNotInAllowlist(address target); + + /// @dev Error thrown when approve target is not OperatorAllowlist + error ApproverNotInAllowlist(address approver); +} + +interface IImmutableERC1155Errors { + /// @dev Deadline exceeded for permit + error PermitExpired(); + + /// @dev Derived signature is invalid (EIP721 and EIP1271) + error InvalidSignature(); +} diff --git a/dependecies/immutable/test/allowlist/OperatorAllowlist.sol b/dependecies/immutable/test/allowlist/OperatorAllowlist.sol new file mode 100644 index 00000000..3ecaa0d0 --- /dev/null +++ b/dependecies/immutable/test/allowlist/OperatorAllowlist.sol @@ -0,0 +1,182 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.24; + +// Access Control +import {Role} from "../../../../src/Role.sol"; +import {OwnableRoles} from "@solady/auth/OwnableRoles.sol"; + +// Interfaces +import {IOperatorAllowlist} from "../../allowlist/IOperatorAllowlist.sol"; + +// Interface to retrieve the implemention stored inside the Proxy contract +interface IProxy { + + // Returns the current implementation address used by the proxy contract + // solhint-disable-next-line func-name-mixedcase + function PROXY_getImplementation() external view returns (address); + +} + +interface ERC165 { + + function supportsInterface(bytes4 interfaceId) external view returns (bool); + +} + +/* + OperatorAllowlist is an implementation of a Allowlist registry, storing addresses and bytecode + which are allowed to be approved operators and execute transfers of interfacing token contracts (admin, ). + The registry will be a deployed contract that tokens may interface with and point to. + OperatorAllowlist is not designed to be upgradeable or extended. +*/ + +contract OperatorAllowlist is ERC165, OwnableRoles, IOperatorAllowlist { + + /// @notice Mapping of Allowlisted addresses + mapping(address aContract => bool allowed) private addressAllowlist; + + /// @notice Mapping of Allowlisted implementation addresses + mapping(address impl => bool allowed) private addressImplementationAllowlist; + + /// @notice Mapping of Allowlisted bytecodes + mapping(bytes32 bytecodeHash => bool allowed) private bytecodeAllowlist; + + /// ===== Events ===== + + /// @notice Emitted when a target address is added or removed from the Allowlist + event AddressAllowlistChanged(address indexed target, bool added); + + /// @notice Emitted when a target smart contract wallet is added or removed from the Allowlist + event WalletAllowlistChanged(bytes32 indexed targetBytes, address indexed targetAddress, bool added); + + /// ===== Constructor ===== + + /** + * @notice Grants `Role._MANAGER_ROLE` to the supplied `admin` address + * @param admin the address to grant `Role._MANAGER_ROLE` to + */ + constructor(address admin) { + _initializeOwner(admin); + _grantRoles(admin, Role._MANAGER_ROLE); + } + + /// ===== External functions ===== + + /** + * @notice Add a target address to Allowlist + * @param addressTargets the addresses to be added to the allowlist + */ + function addAddressToAllowlist(address[] calldata addressTargets) external onlyRoles(Role._REGISTRAR_ROLE) { + for (uint256 i; i < addressTargets.length; i++) { + addressAllowlist[addressTargets[i]] = true; + emit AddressAllowlistChanged(addressTargets[i], true); + } + } + + /** + * @notice Remove a target address from Allowlist + * @param addressTargets the addresses to be removed from the allowlist + */ + function removeAddressFromAllowlist(address[] calldata addressTargets) external onlyRoles(Role._REGISTRAR_ROLE) { + for (uint256 i; i < addressTargets.length; i++) { + delete addressAllowlist[addressTargets[i]]; + emit AddressAllowlistChanged(addressTargets[i], false); + } + } + + /** + * @notice Add a smart contract wallet to the Allowlist. + * This will allowlist the proxy and implementation contract pair. + * First, the bytecode of the proxy is added to the bytecode allowlist. + * Second, the implementation address stored in the proxy is stored in the + * implementation address allowlist. + * @param walletAddr the wallet address to be added to the allowlist + */ + function addWalletToAllowlist(address walletAddr) external onlyRoles(Role._REGISTRAR_ROLE) { + // get bytecode of wallet + bytes32 codeHash; + // solhint-disable-next-line no-inline-assembly + assembly { + codeHash := extcodehash(walletAddr) + } + bytecodeAllowlist[codeHash] = true; + // get address of wallet module + address impl = IProxy(walletAddr).PROXY_getImplementation(); + addressImplementationAllowlist[impl] = true; + + emit WalletAllowlistChanged(codeHash, walletAddr, true); + } + + /** + * @notice Remove a smart contract wallet from the Allowlist + * This will remove the proxy bytecode hash and implementation contract address pair from the allowlist + * @param walletAddr the wallet address to be removed from the allowlist + */ + function removeWalletFromAllowlist(address walletAddr) external onlyRoles(Role._REGISTRAR_ROLE) { + // get bytecode of wallet + bytes32 codeHash; + // solhint-disable-next-line no-inline-assembly + assembly { + codeHash := extcodehash(walletAddr) + } + delete bytecodeAllowlist[codeHash]; + // get address of wallet module + address impl = IProxy(walletAddr).PROXY_getImplementation(); + delete addressImplementationAllowlist[impl]; + + emit WalletAllowlistChanged(codeHash, walletAddr, false); + } + + /** + * @notice Allows admin to grant `user` `Role._REGISTRAR_ROLE` role + * @param user the address that `Role._REGISTRAR_ROLE` will be granted to + */ + function grantRegistrarRole(address user) external onlyRoles(Role._MANAGER_ROLE) { + grantRoles(user, Role._REGISTRAR_ROLE); + } + + /** + * @notice Allows admin to revoke `Role._REGISTRAR_ROLE` role from `user` + * @param user the address that `Role._REGISTRAR_ROLE` will be revoked from + */ + function revokeRegistrarRole(address user) external onlyRoles(Role._MANAGER_ROLE) { + revokeRoles(user, Role._REGISTRAR_ROLE); + } + + /// ===== View functions ===== + + /** + * @notice Returns true if an address is Allowlisted, false otherwise + * @param target the address that will be checked for presence in the allowlist + */ + function isAllowlisted(address target) external view override returns (bool) { + if (addressAllowlist[target]) { + return true; + } + + // Check if caller is a Allowlisted smart contract wallet + bytes32 codeHash; + // solhint-disable-next-line no-inline-assembly + assembly { + codeHash := extcodehash(target) + } + if (bytecodeAllowlist[codeHash]) { + // If wallet proxy bytecode is approved, check addr of implementation contract + address impl = IProxy(target).PROXY_getImplementation(); + + return addressImplementationAllowlist[impl]; + } + + return false; + } + + /** + * @notice ERC-165 interface support + * @param interfaceId The interface identifier, which is a 4-byte selector. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165) returns (bool) { + return interfaceId == type(IOperatorAllowlist).interfaceId; + } + +} diff --git a/src/Role.sol b/src/Role.sol index 4026a657..45248029 100644 --- a/src/Role.sol +++ b/src/Role.sol @@ -9,6 +9,7 @@ library Role { uint256 internal constant _MINTER_ROLE = 1 << 0; uint256 internal constant _MANAGER_ROLE = 1 << 1; + uint256 internal constant _REGISTRAR_ROLE = 1 << 2; uint256 internal constant _INSTALLER_ROLE = 1 << 255; diff --git a/src/interface/ICrosschain.sol b/src/interface/ICrosschain.sol deleted file mode 100644 index b8de6212..00000000 --- a/src/interface/ICrosschain.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.20; - -interface ICrosschain { - - /** - * @notice Sends a cross-chain transaction. - * @param _destinationChain The destination chain ID. - * @param _callAddress The address of the contract on the destination chain. - * @param _payload The payload to send to the destination chain. - * @param _extraArgs The extra arguments to pass - * @dev extraArgs may contain items such as token, amount, feeTokenAddress, receipient, gasLimit, etc - */ - function sendCrossChainTransaction( - uint64 _destinationChain, - address _callAddress, - bytes calldata _payload, - bytes calldata _extraArgs - ) external payable; - - /** - * @notice callback function for when a cross-chain transaction is sent. - * @param _destinationChain The destination chain ID. - * @param _callAddress The address of the contract on the destination chain. - * @param _payload The payload sent to the destination chain. - * @param _extraArgs The extra arguments sent to the callAddress on the destination chain. - */ - function onCrossChainTransactionSent( - uint64 _destinationChain, - address _callAddress, - bytes calldata _payload, - bytes calldata _extraArgs - ) internal; - - /** - * @notice callback function for when a cross-chain transaction is received. - * @param _sourceChain The source chain ID. - * @param _sourceAddress The address of the contract on the source chain. - * @param _payload The payload sent to the destination chain. - * @param _extraArgs The extra arguments sent to the callAddress on the destination chain. - */ - function onCrossChainTransactionReceived( - uint64 _sourceChain, - address _sourceAddress, - bytes calldata _payload, - bytes calldata _extraArgs - ) internal; - - function setRouter(address _router) external; - function getRouter() external view returns (address); - -} diff --git a/src/module/token/immutable/ImmutableAllowlistERC1155.sol b/src/module/token/immutable/ImmutableAllowlistERC1155.sol new file mode 100644 index 00000000..1fcffaa4 --- /dev/null +++ b/src/module/token/immutable/ImmutableAllowlistERC1155.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Module} from "../../../Module.sol"; +import {Role} from "../../../Role.sol"; + +import { + OperatorAllowlistEnforced, + OperatorAllowlistEnforcedStorage +} from "../../../../dependecies/immutable/allowlist/OperatorAllowlistEnforced.sol"; +import {BeforeApproveForAllCallback} from "../../../callback/BeforeApproveForAllCallback.sol"; +import {BeforeBatchTransferCallbackERC1155} from "../../../callback/BeforeBatchTransferCallbackERC1155.sol"; +import {BeforeTransferCallbackERC1155} from "../../../callback/BeforeTransferCallbackERC1155.sol"; + +contract ImmutableAllowlistERC1155 is + Module, + BeforeApproveForAllCallback, + BeforeTransferCallbackERC1155, + BeforeBatchTransferCallbackERC1155, + OperatorAllowlistEnforced +{ + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when an unauthorized approval is attempted. + error OperatorAllowlistUnauthorizedApproval(address operator); + + /// @notice Emitted when an unauthorized transfer is attempted. + error OperatorAllowlistUnauthorizedTransfer(address from, address to, address operator); + + /// @notice Emitted when the operator allowlist is not set. + error OperatorAllowlistNotSet(); + + /*////////////////////////////////////////////////////////////// + MODULE CONFIG + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns all implemented callback and module functions. + function getModuleConfig() external pure override returns (ModuleConfig memory config) { + config.callbackFunctions = new CallbackFunction[](3); + config.fallbackFunctions = new FallbackFunction[](2); + + config.callbackFunctions[0] = CallbackFunction(this.beforeApproveForAll.selector); + config.callbackFunctions[1] = CallbackFunction(this.beforeTransferERC1155.selector); + config.callbackFunctions[2] = CallbackFunction(this.beforeBatchTransferERC1155.selector); + + config.fallbackFunctions[0] = + FallbackFunction({selector: this.setOperatorAllowlistRegistry.selector, permissionBits: Role._MANAGER_ROLE}); + config.fallbackFunctions[1] = FallbackFunction({selector: this.operatorAllowlist.selector, permissionBits: 0}); + + config.requiredInterfaces = new bytes4[](1); + config.requiredInterfaces[0] = 0xd9b67a26; // ERC1155 + + config.registerInstallationCallback = true; + } + + /*////////////////////////////////////////////////////////////// + CALLBACK FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Callback function for ERC1155.setApprovalForAll + function beforeApproveForAll(address _from, address _to, bool _approved) + external + override + validateApproval(_to) + returns (bytes memory) + {} + + /// @notice Callback function for ERC1155.transferFrom/safeTransferFrom + function beforeTransferERC1155(address _from, address _to, uint256 _id, uint256 _value) + external + override + validateTransfer(_from, _to) + returns (bytes memory) + {} + + /// @notice Callback function for ERC1155.transferFrom/safeTransferFrom + function beforeBatchTransferERC1155(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values) + external + override + validateTransfer(_from, _to) + returns (bytes memory) + {} + + /// @dev Called by a Core into an Module during the installation of the Module. + function onInstall(bytes calldata data) external { + address registry = abi.decode(data, (address)); + _setOperatorAllowlistRegistry(registry); + } + + /// @dev Called by a Core into an Module during the uninstallation of the Module. + function onUninstall(bytes calldata data) external {} + + /*////////////////////////////////////////////////////////////// + Encode install / uninstall data + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns bytes encoded install params, to be sent to `onInstall` function + function encodeBytesOnInstall(address operatorAllowlistRegistry) external pure returns (bytes memory) { + return abi.encode(operatorAllowlistRegistry); + } + + /// @dev Returns bytes encoded uninstall params, to be sent to `onUninstall` function + function encodeBytesOnUninstall() external pure returns (bytes memory) { + return ""; + } + + /*////////////////////////////////////////////////////////////// + FALLBACK FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Set the operator allowlist registry address + function setOperatorAllowlistRegistry(address newRegistry) external { + _setOperatorAllowlistRegistry(newRegistry); + } + +} diff --git a/src/module/token/immutable/ImmutableAllowlistERC721.sol b/src/module/token/immutable/ImmutableAllowlistERC721.sol new file mode 100644 index 00000000..d47f90c9 --- /dev/null +++ b/src/module/token/immutable/ImmutableAllowlistERC721.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Module} from "../../../Module.sol"; +import {Role} from "../../../Role.sol"; + +import { + OperatorAllowlistEnforced, + OperatorAllowlistEnforcedStorage +} from "../../../../dependecies/immutable/allowlist/OperatorAllowlistEnforced.sol"; +import {BeforeApproveCallbackERC721} from "../../../callback/BeforeApproveCallbackERC721.sol"; +import {BeforeApproveForAllCallback} from "../../../callback/BeforeApproveForAllCallback.sol"; +import {BeforeTransferCallbackERC721} from "../../../callback/BeforeTransferCallbackERC721.sol"; + +contract ImmutableAllowlistERC721 is + Module, + BeforeApproveCallbackERC721, + BeforeApproveForAllCallback, + BeforeTransferCallbackERC721, + OperatorAllowlistEnforced +{ + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when an unauthorized approval is attempted. + error OperatorAllowlistUnauthorizedApproval(address operator); + + /// @notice Emitted when an unauthorized transfer is attempted. + error OperatorAllowlistUnauthorizedTransfer(address from, address to, address operator); + + /// @notice Emitted when the operator allowlist is not set. + error OperatorAllowlistNotSet(); + + /*////////////////////////////////////////////////////////////// + MODULE CONFIG + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns all implemented callback and module functions. + function getModuleConfig() external pure override returns (ModuleConfig memory config) { + config.callbackFunctions = new CallbackFunction[](3); + config.fallbackFunctions = new FallbackFunction[](2); + + config.callbackFunctions[0] = CallbackFunction(this.beforeApproveERC721.selector); + config.callbackFunctions[1] = CallbackFunction(this.beforeApproveForAll.selector); + config.callbackFunctions[2] = CallbackFunction(this.beforeTransferERC721.selector); + + config.fallbackFunctions[0] = + FallbackFunction({selector: this.setOperatorAllowlistRegistry.selector, permissionBits: Role._MANAGER_ROLE}); + config.fallbackFunctions[1] = FallbackFunction({selector: this.operatorAllowlist.selector, permissionBits: 0}); + + config.requiredInterfaces = new bytes4[](1); + config.requiredInterfaces[0] = 0x80ac58cd; // ERC721 + + config.registerInstallationCallback = true; + } + + /*////////////////////////////////////////////////////////////// + CALLBACK FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Callback function for ERC721.approve + function beforeApproveERC721(address _from, address _to, uint256 _tokenId, bool _approve) + external + override + validateApproval(_to) + returns (bytes memory) + {} + + /// @notice Callback function for ERC721.setApprovalForAll + function beforeApproveForAll(address _from, address _to, bool _approved) + external + override + validateApproval(_to) + returns (bytes memory) + {} + + /// @notice Callback function for ERC721.transferFrom/safeTransferFrom + function beforeTransferERC721(address _from, address _to, uint256 _tokenId) + external + override + validateTransfer(_from, _to) + returns (bytes memory) + {} + + /// @dev Called by a Core into an Module during the installation of the Module. + function onInstall(bytes calldata data) external { + address registry = abi.decode(data, (address)); + _setOperatorAllowlistRegistry(registry); + } + + /// @dev Called by a Core into an Module during the uninstallation of the Module. + function onUninstall(bytes calldata data) external {} + + /*////////////////////////////////////////////////////////////// + Encode install / uninstall data + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns bytes encoded install params, to be sent to `onInstall` function + function encodeBytesOnInstall(address operatorAllowlistRegistry) external pure returns (bytes memory) { + return abi.encode(operatorAllowlistRegistry); + } + + /// @dev Returns bytes encoded uninstall params, to be sent to `onUninstall` function + function encodeBytesOnUninstall() external pure returns (bytes memory) { + return ""; + } + + /*////////////////////////////////////////////////////////////// + FALLBACK FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Set the operator allowlist registry address + function setOperatorAllowlistRegistry(address newRegistry) external { + _setOperatorAllowlistRegistry(newRegistry); + } + +} diff --git a/test/module/immutable/ImmutableAllowlistERC1155.t.sol b/test/module/immutable/ImmutableAllowlistERC1155.t.sol new file mode 100644 index 00000000..b229d1d3 --- /dev/null +++ b/test/module/immutable/ImmutableAllowlistERC1155.t.sol @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import "lib/forge-std/src/console.sol"; + +import {Test} from "forge-std/Test.sol"; +import {Role} from "src/Role.sol"; + +// Target contract + +import {Module} from "src/Module.sol"; +import {ERC1155Core} from "src/core/token/ERC1155Core.sol"; + +import {ICore} from "src/interface/ICore.sol"; +import {IModuleConfig} from "src/interface/IModuleConfig.sol"; +import {ImmutableAllowlistERC1155} from "src/module/token/immutable/ImmutableAllowlistERC1155.sol"; + +import {OperatorAllowlistEnforced} from "dependecies/immutable/allowlist/OperatorAllowlistEnforced.sol"; +import {OperatorAllowlistEnforcementErrors} from "dependecies/immutable/errors/Errors.sol"; +import {OperatorAllowlist} from "dependecies/immutable/test/allowlist/OperatorAllowlist.sol"; + +contract Core is ERC1155Core { + + constructor( + string memory name, + string memory symbol, + string memory contractURI, + address owner, + address[] memory modules, + bytes[] memory moduleInstallData + ) ERC1155Core(name, symbol, contractURI, owner, modules, moduleInstallData) {} + + // disable mint, approve and tokenId callbacks for these tests + function _beforeMint(address to, uint256 tokenId, uint256 value, bytes memory data) internal override {} + + function _updateTokenId(uint256 tokenId) internal override returns (uint256) { + return tokenId; + } + +} + +contract DummyContract { + + ERC1155Core public immutable erc1155Core; + + constructor(address payable _erc1155Core) { + erc1155Core = ERC1155Core(_erc1155Core); + } + + // Implement the IERC1155Receiver functions to accept ERC1155 tokens + + function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes calldata data) + external + returns (bytes4) + { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } + + // Required to declare support for the ERC1155Receiver interface + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return interfaceId == 0x4e2312e0; + } + + // Set approval for the operator to manage tokens + function setApprovalForAll(address _operator) external { + erc1155Core.setApprovalForAll(_operator, true); + } + + // Transfer a single token + function transfer(address _to, uint256 _tokenId) external { + erc1155Core.safeTransferFrom(address(this), _to, _tokenId, 1, ""); + } + + // Batch transfer multiple tokens + function batchTransfer(address _to, uint256[] calldata tokenIds, uint256[] calldata amounts) external { + erc1155Core.safeBatchTransferFrom(address(this), _to, tokenIds, amounts, ""); + } + +} + +contract ImmutableAllowlistERC1155Test is Test { + + Core public core; + + ImmutableAllowlistERC1155 public immutableAllowlistModule; + OperatorAllowlist public operatorAllowlist; + DummyContract public dummyContract1; + DummyContract public dummyContract2; + + address public owner = address(0x1); + address public actorOne = address(0x2); + address public actorTwo = address(0x3); + address public actorThree = address(0x4); + + event OperatorAllowlistRegistryUpdated(address oldRegistry, address newRegistry); + + function setUp() public { + address[] memory modules; + bytes[] memory moduleData; + + core = new Core("test", "TEST", "", owner, modules, moduleData); + immutableAllowlistModule = new ImmutableAllowlistERC1155(); + + vm.prank(owner); + operatorAllowlist = new OperatorAllowlist(owner); + + // install module + vm.startPrank(owner); + bytes memory encodedOperatorAllowlist = + immutableAllowlistModule.encodeBytesOnInstall(address(operatorAllowlist)); + core.installModule(address(immutableAllowlistModule), encodedOperatorAllowlist); + vm.stopPrank(); + + // set registrar role for owner + vm.prank(owner); + operatorAllowlist.grantRegistrarRole(owner); + + // deploy dummy contract + dummyContract1 = new DummyContract(payable(address(core))); + dummyContract2 = new DummyContract(payable(address(core))); + + // mint tokens + core.mint(actorOne, 0, 1, string(""), ""); // tokenId 0 + core.mint(actorTwo, 1, 1, string(""), ""); // tokenId 1 + core.mint(actorThree, 3, 1, string(""), ""); // tokenId 2 + + vm.prank(owner); + core.grantRoles(owner, Role._MANAGER_ROLE); + } + + function allowlist(address _target) internal { + address[] memory allowlist = new address[](1); + allowlist[0] = _target; + vm.prank(owner); + operatorAllowlist.addAddressToAllowlist(allowlist); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `setOperatorAllowlistRegistry` + //////////////////////////////////////////////////////////////*/ + + function test_state_setOperatorAllowlistRegistry() public { + OperatorAllowlist operatorAllowlist2 = new OperatorAllowlist(owner); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit OperatorAllowlistRegistryUpdated(address(operatorAllowlist), address(operatorAllowlist2)); + ImmutableAllowlistERC1155(address(core)).setOperatorAllowlistRegistry(address(operatorAllowlist2)); + + assertEq(ImmutableAllowlistERC1155(address(core)).operatorAllowlist(), address(operatorAllowlist2)); + } + + function test_revert_setOperatorAllowlistRegistry() public { + vm.prank(owner); + // should revert since the allowlist does not implement the IOperatorAllowlist interface + // and that it doesn't implement supportsInterface + vm.expectRevert(); + ImmutableAllowlistERC1155(address(core)).setOperatorAllowlistRegistry(address(0x123)); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `beforeApproveForAll` + //////////////////////////////////////////////////////////////*/ + + function test_state_beforeApproveForAllERC1155() public { + // passes when msg.sender is an EOA and targetApproval is an EOA + vm.prank(actorOne); + core.setApprovalForAll(actorTwo, true); + + // set allowlist for dummy contract + address[] memory allowlist = new address[](3); + allowlist[0] = address(dummyContract1); + allowlist[1] = address(dummyContract2); + allowlist[2] = address(actorThree); + vm.prank(owner); + operatorAllowlist.addAddressToAllowlist(allowlist); + + vm.startPrank(actorThree); + core.mint(actorThree, 3, 1, string(""), ""); // tokenId 3 + core.safeTransferFrom(actorThree, address(dummyContract1), 3, 1, ""); + vm.stopPrank(); + + // passes when msg.sender is a contract and is allowlisted + // and when targetApproval is a contract and is allowlisted + dummyContract1.setApprovalForAll(address(dummyContract2)); + } + + function test_revert_beforeApproveForAllERC1155() public { + vm.prank(actorOne); + vm.expectRevert( + abi.encodeWithSelector( + OperatorAllowlistEnforcementErrors.ApproveTargetNotInAllowlist.selector, address(dummyContract1) + ) + ); + core.setApprovalForAll(address(dummyContract1), true); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `beforeTransferERC1155` + //////////////////////////////////////////////////////////////*/ + + function test_state_beforeTransferERC1155() public { + // set allowlist + address[] memory allowlist = new address[](5); + allowlist[0] = address(dummyContract1); + allowlist[1] = address(dummyContract2); + allowlist[2] = address(actorOne); + allowlist[3] = address(actorTwo); + allowlist[4] = address(actorThree); + vm.prank(owner); + operatorAllowlist.addAddressToAllowlist(allowlist); + + vm.prank(actorOne); + core.safeTransferFrom(actorOne, actorTwo, 0, 1, ""); + + // passes when msg.sender is an EOA and targetApproval is a contract and is allowlisted + core.mint(actorThree, 3, 1, string(""), ""); // tokenId 3 + vm.startPrank(actorThree); + core.safeTransferFrom(actorThree, address(dummyContract1), 3, 1, ""); + vm.stopPrank(); + + // passes when msg.sender is a contract and is allowlisted + // and when targetApproval is a contract and is allowlisted + dummyContract1.transfer(address(dummyContract2), 3); + } + + function test_revert_beforeTransferERC1155() public { + // fails when msg.sender is not allowlisted + vm.prank(actorOne); + vm.expectRevert( + abi.encodeWithSelector(OperatorAllowlistEnforcementErrors.CallerNotInAllowlist.selector, actorOne) + ); + core.safeTransferFrom(actorOne, actorTwo, 0, 1, ""); + + // fails when target is not allowlisted + allowlist(actorOne); + vm.prank(actorOne); + vm.expectRevert( + abi.encodeWithSelector( + OperatorAllowlistEnforcementErrors.TransferToNotInAllowlist.selector, address(dummyContract1) + ) + ); + core.safeTransferFrom(actorOne, address(dummyContract1), 0, 1, ""); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `beforeTransferERC1155` + //////////////////////////////////////////////////////////////*/ + + function test_state_beforeBatchTransferERC1155() public { + // set allowlist + address[] memory allowlist = new address[](5); + allowlist[0] = address(dummyContract1); + allowlist[1] = address(dummyContract2); + allowlist[2] = address(actorOne); + allowlist[3] = address(actorTwo); + allowlist[4] = address(actorThree); + vm.prank(owner); + operatorAllowlist.addAddressToAllowlist(allowlist); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1; + vm.prank(actorOne); + core.safeBatchTransferFrom(actorOne, actorTwo, tokenIds, amounts, ""); + + // passes when msg.sender is an EOA and targetApproval is a contract and is allowlisted + core.mint(actorThree, 3, 1, string(""), ""); // tokenId 3 + vm.startPrank(actorThree); + core.safeTransferFrom(actorThree, address(dummyContract1), 3, 1, ""); + vm.stopPrank(); + + // passes when msg.sender is a contract and is allowlisted + // and when targetApproval is a contract and is allowlisted + uint256[] memory _tokenIds = new uint256[](1); + tokenIds[0] = 3; + uint256[] memory _amounts = new uint256[](1); + amounts[0] = 1; + dummyContract1.batchTransfer(address(dummyContract2), _tokenIds, _amounts); + } + + function test_revert_beforeBatchTransferERC1155() public { + // fails when msg.sender is not allowlisted + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1; + vm.prank(actorOne); + vm.expectRevert( + abi.encodeWithSelector(OperatorAllowlistEnforcementErrors.CallerNotInAllowlist.selector, actorOne) + ); + core.safeBatchTransferFrom(actorOne, actorTwo, tokenIds, amounts, ""); + + // fails when target is not allowlisted + uint256[] memory _tokenIds = new uint256[](1); + tokenIds[0] = 3; + uint256[] memory _amounts = new uint256[](1); + amounts[0] = 1; + allowlist(actorOne); + vm.prank(actorOne); + vm.expectRevert( + abi.encodeWithSelector( + OperatorAllowlistEnforcementErrors.TransferToNotInAllowlist.selector, address(dummyContract1) + ) + ); + core.safeBatchTransferFrom(actorOne, address(dummyContract1), _tokenIds, _amounts, ""); + } + +} diff --git a/test/module/immutable/ImmutableAllowlistERC721.t.sol b/test/module/immutable/ImmutableAllowlistERC721.t.sol new file mode 100644 index 00000000..1750299c --- /dev/null +++ b/test/module/immutable/ImmutableAllowlistERC721.t.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import "lib/forge-std/src/console.sol"; + +import {Test} from "forge-std/Test.sol"; +import {Role} from "src/Role.sol"; + +// Target contract + +import {Module} from "src/Module.sol"; +import {ERC721Core} from "src/core/token/ERC721Core.sol"; + +import {ICore} from "src/interface/ICore.sol"; +import {IModuleConfig} from "src/interface/IModuleConfig.sol"; +import {ImmutableAllowlistERC721} from "src/module/token/immutable/ImmutableAllowlistERC721.sol"; + +import {OperatorAllowlistEnforced} from "dependecies/immutable/allowlist/OperatorAllowlistEnforced.sol"; +import {OperatorAllowlistEnforcementErrors} from "dependecies/immutable/errors/Errors.sol"; +import {OperatorAllowlist} from "dependecies/immutable/test/allowlist/OperatorAllowlist.sol"; + +contract Core is ERC721Core { + + constructor( + string memory name, + string memory symbol, + string memory contractURI, + address owner, + address[] memory modules, + bytes[] memory moduleInstallData + ) ERC721Core(name, symbol, contractURI, owner, modules, moduleInstallData) {} + + // disable mint and approve callbacks for these tests + function _beforeMint(address to, uint256 startTokenId, uint256 quantity, bytes calldata data) internal override {} + +} + +contract DummyContract { + + ERC721Core public immutable erc721Core; + + constructor(address payable _erc721Core) { + erc721Core = ERC721Core(_erc721Core); + } + + function approve(address _to, uint256 _tokenId) external { + erc721Core.approve(_to, _tokenId); + } + + function setApprovalForAll(address _operator) external { + erc721Core.setApprovalForAll(_operator, true); + } + + function transfer(address _to, uint256 _tokenId) external { + erc721Core.transferFrom(address(this), _to, _tokenId); + } + +} + +contract ImmutableAllowlistERC721Test is Test { + + Core public core; + + ImmutableAllowlistERC721 public immutableAllowlistModule; + OperatorAllowlist public operatorAllowlist; + DummyContract public dummyContract1; + DummyContract public dummyContract2; + + address public owner = address(0x1); + address public actorOne = address(0x2); + address public actorTwo = address(0x3); + address public actorThree = address(0x4); + + event OperatorAllowlistRegistryUpdated(address oldRegistry, address newRegistry); + + function setUp() public { + address[] memory modules; + bytes[] memory moduleData; + + core = new Core("test", "TEST", "", owner, modules, moduleData); + immutableAllowlistModule = new ImmutableAllowlistERC721(); + + vm.prank(owner); + operatorAllowlist = new OperatorAllowlist(owner); + + // install module + vm.startPrank(owner); + bytes memory encodedOperatorAllowlist = + immutableAllowlistModule.encodeBytesOnInstall(address(operatorAllowlist)); + core.installModule(address(immutableAllowlistModule), encodedOperatorAllowlist); + vm.stopPrank(); + + // set registrar role for owner + vm.prank(owner); + operatorAllowlist.grantRegistrarRole(owner); + + // deploy dummy contract + dummyContract1 = new DummyContract(payable(address(core))); + dummyContract2 = new DummyContract(payable(address(core))); + + // mint tokens + core.mint(actorOne, 1, string(""), ""); // tokenId 0 + core.mint(actorTwo, 1, string(""), ""); // tokenId 1 + core.mint(actorThree, 1, string(""), ""); // tokenId 2 + + vm.prank(owner); + core.grantRoles(owner, Role._MANAGER_ROLE); + } + + function allowlist(address _target) internal { + address[] memory allowlist = new address[](1); + allowlist[0] = _target; + vm.prank(owner); + operatorAllowlist.addAddressToAllowlist(allowlist); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `setOperatorAllowlistRegistry` + //////////////////////////////////////////////////////////////*/ + + function test_state_setOperatorAllowlistRegistry() public { + OperatorAllowlist operatorAllowlist2 = new OperatorAllowlist(owner); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit OperatorAllowlistRegistryUpdated(address(operatorAllowlist), address(operatorAllowlist2)); + ImmutableAllowlistERC721(address(core)).setOperatorAllowlistRegistry(address(operatorAllowlist2)); + + assertEq(ImmutableAllowlistERC721(address(core)).operatorAllowlist(), address(operatorAllowlist2)); + } + + function test_revert_setOperatorAllowlistRegistry() public { + vm.prank(owner); + // should revert since the allowlist does not implement the IOperatorAllowlist interface + // and that it doesn't implement supportsInterface + vm.expectRevert(); + ImmutableAllowlistERC721(address(core)).setOperatorAllowlistRegistry(address(0x123)); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `beforeApproveERC721` + //////////////////////////////////////////////////////////////*/ + + function test_state_beforeApproveERC721() public { + // passes when msg.sender is an EOA and targetApproval is an EOA + vm.prank(actorOne); + core.approve(actorTwo, 0); + + // set allowlist for dummy contract + address[] memory allowlist = new address[](3); + allowlist[0] = address(dummyContract1); + allowlist[1] = address(dummyContract2); + allowlist[2] = address(actorThree); + vm.prank(owner); + operatorAllowlist.addAddressToAllowlist(allowlist); + + vm.startPrank(actorThree); + core.mint(actorThree, 2, string(""), ""); // tokenId 3 + core.transferFrom(actorThree, address(dummyContract1), 3); + vm.stopPrank(); + + // passes when msg.sender is a contract and is allowlisted + // and when targetApproval is a contract and is allowlisted + dummyContract1.approve(address(dummyContract2), 3); + } + + function test_revert_beforeApproveERC721() public { + vm.prank(actorOne); + vm.expectRevert( + abi.encodeWithSelector( + OperatorAllowlistEnforcementErrors.ApproveTargetNotInAllowlist.selector, address(dummyContract1) + ) + ); + core.approve(address(dummyContract1), 0); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `beforeApproveForAll` + //////////////////////////////////////////////////////////////*/ + + function test_state_beforeApproveForAllERC721() public { + // passes when msg.sender is an EOA and targetApproval is an EOA + vm.prank(actorOne); + core.setApprovalForAll(actorTwo, true); + + // set allowlist for dummy contract + address[] memory allowlist = new address[](3); + allowlist[0] = address(dummyContract1); + allowlist[1] = address(dummyContract2); + allowlist[2] = address(actorThree); + vm.prank(owner); + operatorAllowlist.addAddressToAllowlist(allowlist); + + vm.startPrank(actorThree); + core.mint(actorThree, 1, string(""), ""); // tokenId 3 + core.transferFrom(actorThree, address(dummyContract1), 3); + vm.stopPrank(); + + // passes when msg.sender is a contract and is allowlisted + // and when targetApproval is a contract and is allowlisted + dummyContract1.setApprovalForAll(address(dummyContract2)); + } + + function test_revert_beforeApproveForAllERC721() public { + vm.prank(actorOne); + vm.expectRevert( + abi.encodeWithSelector( + OperatorAllowlistEnforcementErrors.ApproveTargetNotInAllowlist.selector, address(dummyContract1) + ) + ); + core.setApprovalForAll(address(dummyContract1), true); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `beforeTransferERC721` + //////////////////////////////////////////////////////////////*/ + + function test_state_beforeTransferERC721() public { + // set allowlist + address[] memory allowlist = new address[](5); + allowlist[0] = address(dummyContract1); + allowlist[1] = address(dummyContract2); + allowlist[2] = address(actorOne); + allowlist[3] = address(actorTwo); + allowlist[4] = address(actorThree); + vm.prank(owner); + operatorAllowlist.addAddressToAllowlist(allowlist); + + vm.prank(actorOne); + core.transferFrom(actorOne, actorTwo, 0); + + // passes when msg.sender is an EOA and targetApproval is a contract and is allowlisted + core.mint(actorThree, 1, string(""), ""); // tokenId 3 + vm.startPrank(actorThree); + core.transferFrom(actorThree, address(dummyContract1), 3); + vm.stopPrank(); + + // passes when msg.sender is a contract and is allowlisted + // and when targetApproval is a contract and is allowlisted + dummyContract1.transfer(address(dummyContract2), 3); + } + + function test_revert_beforeTransferERC721() public { + // fails when msg.sender is not allowlisted + vm.prank(actorOne); + vm.expectRevert( + abi.encodeWithSelector(OperatorAllowlistEnforcementErrors.CallerNotInAllowlist.selector, actorOne) + ); + core.transferFrom(actorOne, actorTwo, 0); + + // fails when target is not allowlisted + allowlist(actorOne); + vm.prank(actorOne); + vm.expectRevert( + abi.encodeWithSelector( + OperatorAllowlistEnforcementErrors.TransferToNotInAllowlist.selector, address(dummyContract1) + ) + ); + core.transferFrom(actorOne, address(dummyContract1), 0); + } + +} diff --git a/test/module/minting/ClaimableERC1155.t.sol b/test/module/minting/ClaimableERC1155.t.sol index c8107c0f..620cdfd9 100644 --- a/test/module/minting/ClaimableERC1155.t.sol +++ b/test/module/minting/ClaimableERC1155.t.sol @@ -293,8 +293,6 @@ contract ClaimableERC1155Test is Test { uid: bytes32("1") }); bytes memory sig = signMintRequest(claimRequest, permissionedActorPrivateKey); - console.log("permissoned actor address"); - console.logAddress(permissionedActor); uint256 balBefore = tokenRecipient.balance; assertEq(balBefore, 100 ether);