diff --git a/contracts/allowlist/IWalletProxy.sol b/contracts/allowlist/IWalletProxy.sol new file mode 100644 index 00000000..087d906b --- /dev/null +++ b/contracts/allowlist/IWalletProxy.sol @@ -0,0 +1,11 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +// Interface to retrieve the implemention stored inside the Proxy contract +/// Interface for Passport Wallet's proxy contract. +interface IWalletProxy { + // Returns the current implementation address used by the proxy contract + // solhint-disable-next-line var-name-mixedcase + function PROXY_getImplementation() external view returns (address); +} diff --git a/contracts/allowlist/OperatorAllowlistUpgradeable.sol b/contracts/allowlist/OperatorAllowlistUpgradeable.sol new file mode 100644 index 00000000..d1425d0b --- /dev/null +++ b/contracts/allowlist/OperatorAllowlistUpgradeable.sol @@ -0,0 +1,172 @@ +// Copyright Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; + +// Introspection +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +// Interfaces +import {IOperatorAllowlist} from "./IOperatorAllowlist.sol"; +import {IWalletProxy} from "./IWalletProxy.sol"; + +/* + 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 (e.g. ERC721/ERC1155). + The registry will be a deployed contract that tokens may interface with and point to. +*/ + +contract OperatorAllowlistUpgradeable is + ERC165, + AccessControlEnumerableUpgradeable, + UUPSUpgradeable, + IOperatorAllowlist +{ + /// ===== 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); + /// ===== State Variables ===== + + /// @notice Only REGISTRAR_ROLE can invoke white listing registration and removal + bytes32 public constant REGISTRAR_ROLE = bytes32("REGISTRAR_ROLE"); + + /// @notice Only UPGRADE_ROLE can upgrade the contract + bytes32 public constant UPGRADE_ROLE = bytes32("UPGRADE_ROLE"); + + /// @notice Mapping of Allowlisted addresses + mapping(address => bool) private addressAllowlist; + + /// @notice Mapping of Allowlisted implementation addresses + mapping(address => bool) private addressImplementationAllowlist; + + /// @notice Mapping of Allowlisted bytecodes + mapping(bytes32 => bool) private bytecodeAllowlist; + + /// ===== Initializer ===== + + /** + * @notice Grants `DEFAULT_ADMIN_ROLE` to the supplied `admin` address + * @param _roleAdmin the address to grant `DEFAULT_ADMIN_ROLE` to + * @param _upgradeAdmin the address to grant `UPGRADE_ROLE` to + */ + function initialize(address _roleAdmin, address _upgradeAdmin, address _registerarAdmin) public initializer { + __UUPSUpgradeable_init(); + __AccessControl_init(); + _grantRole(DEFAULT_ADMIN_ROLE, _roleAdmin); + _grantRole(UPGRADE_ROLE, _upgradeAdmin); + _grantRole(REGISTRAR_ROLE, _registerarAdmin); + } + + /// ===== External functions ===== + + /** + * @notice Adds a list of multiple addresses to Allowlist + * @param addressTargets the addresses to be added to the allowlist + */ + function addAddressesToAllowlist(address[] calldata addressTargets) external onlyRole(REGISTRAR_ROLE) { + for (uint256 i; i < addressTargets.length; i++) { + addressAllowlist[addressTargets[i]] = true; + emit AddressAllowlistChanged(addressTargets[i], true); + } + } + + /** + * @notice Removes a list target address from Allowlist + * @param addressTargets the addresses to be removed from the allowlist + */ + function removeAddressesFromAllowlist(address[] calldata addressTargets) external onlyRole(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 onlyRole(REGISTRAR_ROLE) { + // get bytecode of wallet + bytes32 codeHash; + assembly { + codeHash := extcodehash(walletAddr) + } + bytecodeAllowlist[codeHash] = true; + // get address of wallet module + address impl = IWalletProxy(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 onlyRole(REGISTRAR_ROLE) { + // get bytecode of wallet + bytes32 codeHash; + assembly { + codeHash := extcodehash(walletAddr) + } + delete bytecodeAllowlist[codeHash]; + // get address of wallet module + address impl = IWalletProxy(walletAddr).PROXY_getImplementation(); + delete addressImplementationAllowlist[impl]; + + emit WalletAllowlistChanged(codeHash, walletAddr, false); + } + + /// ===== 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; + assembly { + codeHash := extcodehash(target) + } + if (bytecodeAllowlist[codeHash]) { + // If wallet proxy bytecode is approved, check addr of implementation contract + address impl = IWalletProxy(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, AccessControlEnumerableUpgradeable) returns (bool) { + return interfaceId == type(IOperatorAllowlist).interfaceId || super.supportsInterface(interfaceId); + } + + // Override the _authorizeUpgrade function + function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADE_ROLE) {} + + /// @notice storage gap for additional variables for upgrades + uint256[20] __OperatorAllowlistUpgradeableGap; +} diff --git a/contracts/allowlist/OperatorAllowlist.sol b/contracts/test/allowlist/OperatorAllowlist.sol similarity index 98% rename from contracts/allowlist/OperatorAllowlist.sol rename to contracts/test/allowlist/OperatorAllowlist.sol index dd527a77..b1d62caa 100644 --- a/contracts/allowlist/OperatorAllowlist.sol +++ b/contracts/test/allowlist/OperatorAllowlist.sol @@ -9,7 +9,7 @@ import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; // Interfaces -import {IOperatorAllowlist} from "./IOperatorAllowlist.sol"; +import {IOperatorAllowlist} from "../../allowlist/IOperatorAllowlist.sol"; // Interface to retrieve the implemention stored inside the Proxy contract interface IProxy { diff --git a/package.json b/package.json index bd2b2879..7766bb1e 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ }, "dependencies": { "@openzeppelin/contracts": "^4.9.3", + "@openzeppelin/contracts-upgradeable": "^4.9.3", "@rari-capital/solmate": "^6.4.0", "seaport": "https://github.com/immutable/seaport.git#1.5.0+im.1.3", "solidity-bits": "^0.4.0", diff --git a/test/allowlist/MockOAL.sol b/test/allowlist/MockOAL.sol new file mode 100644 index 00000000..0e7b20fc --- /dev/null +++ b/test/allowlist/MockOAL.sol @@ -0,0 +1,20 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {OperatorAllowlistUpgradeable} from "../../contracts/allowlist/OperatorAllowlistUpgradeable.sol"; + +/* + 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 (e.g. ERC721/ERC1155). + 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 MockOperatorAllowlistUpgradeable is OperatorAllowlistUpgradeable { + uint256 public mockInt; + + function setMockValue(uint256 val) public { + mockInt = val; + } +} diff --git a/test/allowlist/OperatorAllowlistUpgradeable.t.sol b/test/allowlist/OperatorAllowlistUpgradeable.t.sol new file mode 100644 index 00000000..63418be6 --- /dev/null +++ b/test/allowlist/OperatorAllowlistUpgradeable.t.sol @@ -0,0 +1,73 @@ +// Copyright Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {OperatorAllowlistUpgradeable} from "../../contracts/allowlist/OperatorAllowlistUpgradeable.sol"; +import {MockOperatorAllowlistUpgradeable} from "./MockOAL.sol"; +import {ImmutableERC721} from "../../contracts/token/erc721/preset/ImmutableERC721.sol"; +import {DeployOperatorAllowlist} from "../utils/DeployAllowlistProxy.sol"; + + +contract OperatorAllowlistTest is Test { + OperatorAllowlistUpgradeable public allowlist; + ImmutableERC721 public immutableERC721; + MockOperatorAllowlistUpgradeable public oalV2; + + uint256 feeReceiverKey = 1; + + address public admin = makeAddr("roleAdmin"); + address public upgrader = makeAddr("roleUpgrader"); + address public registerar = makeAddr("roleRegisterar"); + address feeReceiver = vm.addr(feeReceiverKey); + address proxyAddr; + address nonAuthorizedWallet; + + + function setUp() public { + DeployOperatorAllowlist deployScript = new DeployOperatorAllowlist(); + proxyAddr = deployScript.run(admin, upgrader, registerar); + + allowlist = OperatorAllowlistUpgradeable(proxyAddr); + + immutableERC721 = new ImmutableERC721( + admin, + "test", + "USDC", + "test-base-uri", + "test-contract-uri", + address(allowlist), + feeReceiver, + 0 + ); + + nonAuthorizedWallet = address(0x2); + } + + function testDeployment() public { + assertTrue(allowlist.hasRole(allowlist.DEFAULT_ADMIN_ROLE(), admin)); + assertTrue(allowlist.hasRole(allowlist.REGISTRAR_ROLE(), registerar)); + assertTrue(allowlist.hasRole(allowlist.UPGRADE_ROLE(), upgrader)); + assertEq(address(immutableERC721.operatorAllowlist()), proxyAddr); + } + + function testUpgradeToV2() public { + MockOperatorAllowlistUpgradeable oalImplV2 = new MockOperatorAllowlistUpgradeable(); + + vm.prank(upgrader); + allowlist.upgradeToAndCall(address(oalImplV2), abi.encodeWithSelector(oalImplV2.setMockValue.selector, 50)); + + oalV2 = MockOperatorAllowlistUpgradeable(proxyAddr); + + uint256 mockVal = oalV2.mockInt(); + assertEq(mockVal, 50); + } + + function testFailedUpgradeNoPerms() public { + MockOperatorAllowlistUpgradeable oalImplV2 = new MockOperatorAllowlistUpgradeable(); + vm.prank(nonAuthorizedWallet); + vm.expectRevert("Must have upgrade role to upgrade"); + allowlist.upgradeTo(address(oalImplV2)); + } +} \ No newline at end of file diff --git a/test/token/erc1155/ImmutableERC1155.t.sol b/test/token/erc1155/ImmutableERC1155.t.sol index c85e1b57..4bb1eb8e 100644 --- a/test/token/erc1155/ImmutableERC1155.t.sol +++ b/test/token/erc1155/ImmutableERC1155.t.sol @@ -1,19 +1,21 @@ -// SPDX-License-Identifier: UNLICENSED +// Copyright Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache 2.0 pragma solidity 0.8.19; import "forge-std/Test.sol"; import {ImmutableERC1155} from "../../../contracts/token/erc1155/preset/draft-ImmutableERC1155.sol"; import {IImmutableERC1155Errors} from "../../../contracts/errors/Errors.sol"; import {OperatorAllowlistEnforcementErrors} from "../../../contracts/errors/Errors.sol"; -import {OperatorAllowlist} from "../../../contracts/allowlist/OperatorAllowlist.sol"; +import {OperatorAllowlistUpgradeable} from "../../../contracts/allowlist/OperatorAllowlistUpgradeable.sol"; import {Sign} from "../../utils/Sign.sol"; +import {DeployOperatorAllowlist} from "../../utils/DeployAllowlistProxy.sol"; import {MockWallet} from "../../../contracts/mocks/MockWallet.sol"; import {MockWalletFactory} from "../../../contracts/mocks/MockWalletFactory.sol"; contract ImmutableERC1155Test is Test { ImmutableERC1155 public immutableERC1155; Sign public sign; - OperatorAllowlist public operatorAllowlist; + OperatorAllowlistUpgradeable public operatorAllowlist; MockWalletFactory public scmf; MockWallet public mockWalletModule; MockWallet public scw; @@ -41,11 +43,13 @@ contract ImmutableERC1155Test is Test { address public scwAddress; address public anotherScwAddress; + address public proxyAddr; function setUp() public { - operatorAllowlist = new OperatorAllowlist( - owner - ); + DeployOperatorAllowlist deployScript = new DeployOperatorAllowlist(); + proxyAddr = deployScript.run(owner, owner, owner); + operatorAllowlist = OperatorAllowlistUpgradeable(proxyAddr); + immutableERC1155 = new ImmutableERC1155( owner, "test", @@ -57,11 +61,7 @@ contract ImmutableERC1155Test is Test { ); operatorAddrs.push(minter); - vm.startPrank(owner); - bytes32 regiRole = operatorAllowlist.REGISTRAR_ROLE(); - operatorAllowlist.grantRegistrarRole(owner); - assertTrue(operatorAllowlist.hasRole(regiRole, owner)); - vm.stopPrank(); + assertTrue(operatorAllowlist.hasRole(operatorAllowlist.REGISTRAR_ROLE(), owner)); sign = new Sign(immutableERC1155.DOMAIN_SEPARATOR()); vm.prank(owner); @@ -98,7 +98,7 @@ contract ImmutableERC1155Test is Test { function _addAddrToAllowListAndApprove() private { vm.startPrank(owner); - operatorAllowlist.addAddressToAllowlist(operatorAddrs); + operatorAllowlist.addAddressesToAllowlist(operatorAddrs); immutableERC1155.setApprovalForAll(minter, true); vm.stopPrank(); } @@ -141,6 +141,10 @@ contract ImmutableERC1155Test is Test { assertTrue(operatorAllowlist.hasRole(adminRole, owner)); } + function test_DeploymentShouldSetAllowlistToProxy() public { + assertEq(address(immutableERC1155.operatorAllowlist()), proxyAddr); + } + /* * Metadata */ diff --git a/test/utils/DeployAllowlistProxy.sol b/test/utils/DeployAllowlistProxy.sol new file mode 100644 index 00000000..18552f23 --- /dev/null +++ b/test/utils/DeployAllowlistProxy.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {OperatorAllowlistUpgradeable} from "../../contracts/allowlist/OperatorAllowlistUpgradeable.sol"; + +/// Deploys the OperatorAllowlistUpgradeable contract behind an ERC1967 Proxy and returns the address of the proxy +contract DeployOperatorAllowlist { + function run(address admin, address upgradeAdmin, address registerarAdmin) external returns (address) { + OperatorAllowlistUpgradeable impl = new OperatorAllowlistUpgradeable(); + + bytes memory initData = abi.encodeWithSelector( + OperatorAllowlistUpgradeable.initialize.selector, + admin, + upgradeAdmin, + registerarAdmin + ); + + ERC1967Proxy proxy = new ERC1967Proxy( + address(impl), + initData + ); + + return address(proxy); + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index ef534997..b538430d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1161,6 +1161,11 @@ find-up "^4.1.0" fs-extra "^8.1.0" +"@openzeppelin/contracts-upgradeable@^4.9.3": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.5.tgz#572b5da102fc9be1d73f34968e0ca56765969812" + integrity sha512-f7L1//4sLlflAN7fVzJLoRedrf5Na3Oal5PZfIq55NFcVZ90EpV1q5xOvL4lFvg3MNICSDr2hH0JUBxwlxcoPg== + "@openzeppelin/contracts@^4.9.2": version "4.9.5" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.5.tgz#1eed23d4844c861a1835b5d33507c1017fa98de8"