diff --git a/contracts/gas-snapshots/ccip.gas-snapshot b/contracts/gas-snapshots/ccip.gas-snapshot index 8bf5c45e55..38e3a72dfc 100644 --- a/contracts/gas-snapshots/ccip.gas-snapshot +++ b/contracts/gas-snapshots/ccip.gas-snapshot @@ -996,6 +996,12 @@ TokenPoolAndProxy:test_lockOrBurn_lockRelease_Success() (gas: 5793246) TokenPoolAndProxy:test_setPreviousPool_Success() (gas: 3070731) TokenPoolAndProxyMigration:test_tokenPoolMigration_Success_1_2() (gas: 6434801) TokenPoolAndProxyMigration:test_tokenPoolMigration_Success_1_4() (gas: 6634934) +TokenPoolFactoryTests:test_TokenPoolFactory_Constructor_Revert() (gas: 1188046) +TokenPoolFactoryTests:test_createTokenPoolLockRelease_NoExistingToken_predict_Success() (gas: 12420072) +TokenPoolFactoryTests:test_createTokenPool_ExistingRemoteToken_AndPredictPool_Success() (gas: 12434881) +TokenPoolFactoryTests:test_createTokenPool_WithNoExistingRemoteContracts_predict_Success() (gas: 12702418) +TokenPoolFactoryTests:test_createTokenPool_WithNoExistingTokenOnRemoteChain_Success() (gas: 5740593) +TokenPoolFactoryTests:test_createTokenPool_WithRemoteTokenAndRemotePool_Success() (gas: 5880940) TokenPoolWithAllowList_applyAllowListUpdates:test_AllowListNotEnabled_Revert() (gas: 1979943) TokenPoolWithAllowList_applyAllowListUpdates:test_OnlyOwner_Revert() (gas: 12113) TokenPoolWithAllowList_applyAllowListUpdates:test_SetAllowListSkipsZero_Success() (gas: 23476) diff --git a/contracts/gas-snapshots/operatorforwarder.gas-snapshot b/contracts/gas-snapshots/operatorforwarder.gas-snapshot index 66bb19f1f6..0414f500c2 100644 --- a/contracts/gas-snapshots/operatorforwarder.gas-snapshot +++ b/contracts/gas-snapshots/operatorforwarder.gas-snapshot @@ -4,12 +4,12 @@ FactoryTest:test_DeployNewOperatorAndForwarder_Success() (gas: 4069305) FactoryTest:test_DeployNewOperator_Success() (gas: 3020464) ForwarderTest:test_Forward_Success(uint256) (runs: 257, μ: 226979, ~: 227289) ForwarderTest:test_MultiForward_Success(uint256,uint256) (runs: 257, μ: 258577, ~: 259120) -ForwarderTest:test_OwnerForward_Success() (gas: 30118) +ForwarderTest:test_OwnerForward_Success() (gas: 30096) ForwarderTest:test_SetAuthorizedSenders_Success() (gas: 160524) ForwarderTest:test_TransferOwnershipWithMessage_Success() (gas: 35123) -OperatorTest:test_CancelOracleRequest_Success() (gas: 274436) -OperatorTest:test_FulfillOracleRequest_Success() (gas: 330603) -OperatorTest:test_NotAuthorizedSender_Revert() (gas: 246716) -OperatorTest:test_OracleRequest_Success() (gas: 250019) -OperatorTest:test_SendRequestAndCancelRequest_Success(uint96) (runs: 257, μ: 387121, ~: 387124) -OperatorTest:test_SendRequest_Success(uint96) (runs: 257, μ: 303612, ~: 303615) \ No newline at end of file +OperatorTest:test_CancelOracleRequest_Success() (gas: 274295) +OperatorTest:test_FulfillOracleRequest_Success() (gas: 330480) +OperatorTest:test_NotAuthorizedSender_Revert() (gas: 246628) +OperatorTest:test_OracleRequest_Success() (gas: 249843) +OperatorTest:test_SendRequestAndCancelRequest_Success(uint96) (runs: 257, μ: 386787, ~: 386790) +OperatorTest:test_SendRequest_Success(uint96) (runs: 257, μ: 303436, ~: 303439) \ No newline at end of file diff --git a/contracts/src/v0.8/ccip/interfaces/IRegistryModuleOwnerCustom.sol b/contracts/src/v0.8/ccip/interfaces/IRegistryModuleOwnerCustom.sol new file mode 100644 index 0000000000..f0412015d3 --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IRegistryModuleOwnerCustom.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.19; + +interface IRegistryModuleOwnerCustom { + /// @notice Registers the admin of the token using the `getCCIPAdmin` method. + /// @param token The token to register the admin for. + /// @dev The caller must be the admin returned by the `getCCIPAdmin` method. + function registerAdminViaGetCCIPAdmin(address token) external; + + /// @notice Registers the admin of the token using the `owner` method. + /// @param token The token to register the admin for. + /// @dev The caller must be the admin returned by the `owner` method. + function registerAdminViaOwner(address token) external; +} diff --git a/contracts/src/v0.8/ccip/interfaces/ITokenAdminRegistry.sol b/contracts/src/v0.8/ccip/interfaces/ITokenAdminRegistry.sol index 0e44122901..3cd620580e 100644 --- a/contracts/src/v0.8/ccip/interfaces/ITokenAdminRegistry.sol +++ b/contracts/src/v0.8/ccip/interfaces/ITokenAdminRegistry.sol @@ -9,4 +9,22 @@ interface ITokenAdminRegistry { /// @param localToken The token to register the administrator for. /// @param administrator The administrator to register. function proposeAdministrator(address localToken, address administrator) external; + + /// @notice Accepts the administrator role for a token. + /// @param localToken The token to accept the administrator role for. + /// @dev This function can only be called by the pending administrator. + function acceptAdminRole(address localToken) external; + + /// @notice Sets the pool for a token. Setting the pool to address(0) effectively delists the token + /// from CCIP. Setting the pool to any other address enables the token on CCIP. + /// @param localToken The token to set the pool for. + /// @param pool The pool to set for the token. + function setPool(address localToken, address pool) external; + + /// @notice Transfers the administrator role for a token to a new address with a 2-step process. + /// @param localToken The token to transfer the administrator role for. + /// @param newAdmin The address to transfer the administrator role to. Can be address(0) to cancel + /// a pending transfer. + /// @dev The new admin must call `acceptAdminRole` to accept the role. + function transferAdminRole(address localToken, address newAdmin) external; } diff --git a/contracts/src/v0.8/ccip/test/legacy/TokenPoolAndProxy.t.sol b/contracts/src/v0.8/ccip/test/legacy/TokenPoolAndProxy.t.sol index 9645d70b7a..a721d37c64 100644 --- a/contracts/src/v0.8/ccip/test/legacy/TokenPoolAndProxy.t.sol +++ b/contracts/src/v0.8/ccip/test/legacy/TokenPoolAndProxy.t.sol @@ -361,8 +361,12 @@ contract TokenPoolAndProxy is EVM2EVMOnRampSetup { } function test_lockOrBurn_burnWithFromMint_Success() public { - s_pool = - new BurnWithFromMintTokenPoolAndProxy(s_token, new address[](0), address(s_mockRMN), address(s_sourceRouter)); + s_pool = new BurnWithFromMintTokenPoolAndProxy( + s_token, + new address[](0), + address(s_mockRMN), + address(s_sourceRouter) + ); _configurePool(); _deployOldPool(); _assertLockOrBurnCorrect(); @@ -374,8 +378,13 @@ contract TokenPoolAndProxy is EVM2EVMOnRampSetup { } function test_lockOrBurn_lockRelease_Success() public { - s_pool = - new LockReleaseTokenPoolAndProxy(s_token, new address[](0), address(s_mockRMN), false, address(s_sourceRouter)); + s_pool = new LockReleaseTokenPoolAndProxy( + s_token, + new address[](0), + address(s_mockRMN), + false, + address(s_sourceRouter) + ); _configurePool(); _deployOldPool(); _assertLockOrBurnCorrect(); @@ -391,11 +400,17 @@ contract TokenPoolAndProxy is EVM2EVMOnRampSetup { s_token.grantMintAndBurnRoles(address(s_legacyPool)); TokenPool1_2.RampUpdate[] memory onRampUpdates = new TokenPool1_2.RampUpdate[](1); - onRampUpdates[0] = - TokenPool1_2.RampUpdate({ramp: address(s_pool), allowed: true, rateLimiterConfig: _getInboundRateLimiterConfig()}); + onRampUpdates[0] = TokenPool1_2.RampUpdate({ + ramp: address(s_pool), + allowed: true, + rateLimiterConfig: _getInboundRateLimiterConfig() + }); TokenPool1_2.RampUpdate[] memory offRampUpdates = new TokenPool1_2.RampUpdate[](1); - offRampUpdates[0] = - TokenPool1_2.RampUpdate({ramp: address(s_pool), allowed: true, rateLimiterConfig: _getInboundRateLimiterConfig()}); + offRampUpdates[0] = TokenPool1_2.RampUpdate({ + ramp: address(s_pool), + allowed: true, + rateLimiterConfig: _getInboundRateLimiterConfig() + }); BurnMintTokenPool1_2(address(s_legacyPool)).applyRampUpdates(onRampUpdates, offRampUpdates); } @@ -506,8 +521,13 @@ contract TokenPoolAndProxy is EVM2EVMOnRampSetup { } function test_setPreviousPool_Success() public { - LockReleaseTokenPoolAndProxy pool = - new LockReleaseTokenPoolAndProxy(s_token, new address[](0), address(s_mockRMN), true, address(s_sourceRouter)); + LockReleaseTokenPoolAndProxy pool = new LockReleaseTokenPoolAndProxy( + s_token, + new address[](0), + address(s_mockRMN), + true, + address(s_sourceRouter) + ); assertEq(pool.getPreviousPool(), address(0)); @@ -539,13 +559,23 @@ contract LockReleaseTokenPoolAndProxySetup is RouterSetup { RouterSetup.setUp(); s_token = new BurnMintERC677("LINK", "LNK", 18, 0); deal(address(s_token), OWNER, type(uint256).max); - s_lockReleaseTokenPoolAndProxy = - new LockReleaseTokenPoolAndProxy(s_token, new address[](0), address(s_mockRMN), true, address(s_sourceRouter)); + s_lockReleaseTokenPoolAndProxy = new LockReleaseTokenPoolAndProxy( + s_token, + new address[](0), + address(s_mockRMN), + true, + address(s_sourceRouter) + ); s_allowedList.push(USER_1); s_allowedList.push(DUMMY_CONTRACT_ADDRESS); - s_lockReleaseTokenPoolAndProxyWithAllowList = - new LockReleaseTokenPoolAndProxy(s_token, s_allowedList, address(s_mockRMN), true, address(s_sourceRouter)); + s_lockReleaseTokenPoolAndProxyWithAllowList = new LockReleaseTokenPoolAndProxy( + s_token, + s_allowedList, + address(s_mockRMN), + true, + address(s_sourceRouter) + ); TokenPool.ChainUpdate[] memory chainUpdate = new TokenPool.ChainUpdate[](1); chainUpdate[0] = TokenPool.ChainUpdate({ @@ -588,8 +618,13 @@ contract LockReleaseTokenPoolPoolAndProxy_canAcceptLiquidity is LockReleaseToken function test_CanAcceptLiquidity_Success() public { assertEq(true, s_lockReleaseTokenPoolAndProxy.canAcceptLiquidity()); - s_lockReleaseTokenPoolAndProxy = - new LockReleaseTokenPoolAndProxy(s_token, new address[](0), address(s_mockRMN), false, address(s_sourceRouter)); + s_lockReleaseTokenPoolAndProxy = new LockReleaseTokenPoolAndProxy( + s_token, + new address[](0), + address(s_mockRMN), + false, + address(s_sourceRouter) + ); assertEq(false, s_lockReleaseTokenPoolAndProxy.canAcceptLiquidity()); } } @@ -621,8 +656,13 @@ contract LockReleaseTokenPoolPoolAndProxy_provideLiquidity is LockReleaseTokenPo } function test_LiquidityNotAccepted_Revert() public { - s_lockReleaseTokenPoolAndProxy = - new LockReleaseTokenPoolAndProxy(s_token, new address[](0), address(s_mockRMN), false, address(s_sourceRouter)); + s_lockReleaseTokenPoolAndProxy = new LockReleaseTokenPoolAndProxy( + s_token, + new address[](0), + address(s_mockRMN), + false, + address(s_sourceRouter) + ); vm.expectRevert(LockReleaseTokenPoolAndProxy.LiquidityNotAccepted.selector); s_lockReleaseTokenPoolAndProxy.provideLiquidity(1); diff --git a/contracts/src/v0.8/ccip/test/tokenAdminRegistry/TokenPoolFactory.t.sol b/contracts/src/v0.8/ccip/test/tokenAdminRegistry/TokenPoolFactory.t.sol new file mode 100644 index 0000000000..0a1f2a66f3 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/tokenAdminRegistry/TokenPoolFactory.t.sol @@ -0,0 +1,461 @@ +pragma solidity ^0.8.24; + +import {IOwner} from "../../interfaces/IOwner.sol"; +import {ITokenAdminRegistry} from "../../interfaces/ITokenAdminRegistry.sol"; + +import {OwnerIsCreator} from "../../../shared/access/OwnerIsCreator.sol"; + +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {BurnMintTokenPool} from "../../pools/BurnMintTokenPool.sol"; +import {LockReleaseTokenPool} from "../../pools/LockReleaseTokenPool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; + +import {RegistryModuleOwnerCustom} from "../../tokenAdminRegistry/RegistryModuleOwnerCustom.sol"; +import {TokenAdminRegistry} from "../../tokenAdminRegistry/TokenAdminRegistry.sol"; +import {FactoryBurnMintERC20} from "../../tokenAdminRegistry/TokenPoolFactory/FactoryBurnMintERC20.sol"; +import {TokenPoolFactory} from "../../tokenAdminRegistry/TokenPoolFactory/TokenPoolFactory.sol"; +import {TokenAdminRegistrySetup} from "./TokenAdminRegistry.t.sol"; + +import {Create2} from "../../../vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Create2.sol"; + +contract TokenPoolFactorySetup is TokenAdminRegistrySetup { + using Create2 for bytes32; + + TokenPoolFactory internal s_tokenPoolFactory; + RegistryModuleOwnerCustom internal s_registryModuleOwnerCustom; + + bytes internal s_poolInitCode; + bytes internal s_poolInitArgs; + + bytes32 internal constant FAKE_SALT = keccak256(abi.encode("FAKE_SALT")); + + address internal s_rmnProxy = address(0x1234); + + bytes internal s_tokenCreationParams; + bytes internal s_tokenInitCode; + + uint256 public constant PREMINT_AMOUNT = 1e20; // 100 tokens in 18 decimals + + function setUp() public virtual override { + TokenAdminRegistrySetup.setUp(); + + s_registryModuleOwnerCustom = new RegistryModuleOwnerCustom(address(s_tokenAdminRegistry)); + s_tokenAdminRegistry.addRegistryModule(address(s_registryModuleOwnerCustom)); + + s_tokenPoolFactory = + new TokenPoolFactory(s_tokenAdminRegistry, s_registryModuleOwnerCustom, s_rmnProxy, address(s_sourceRouter)); + + // Create Init Code for BurnMintERC20 TestToken with 18 decimals and supply cap of max uint256 value + s_tokenCreationParams = abi.encode("TestToken", "TT", 18, type(uint256).max, PREMINT_AMOUNT, OWNER); + + s_tokenInitCode = abi.encodePacked(type(FactoryBurnMintERC20).creationCode, s_tokenCreationParams); + + s_poolInitCode = type(BurnMintTokenPool).creationCode; + + // Create Init Args for BurnMintTokenPool with no allowlist minus the token address + address[] memory allowlist = new address[](1); + allowlist[0] = OWNER; + s_poolInitArgs = abi.encode(allowlist, address(0x1234), s_sourceRouter); + } +} + +contract TokenPoolFactoryTests is TokenPoolFactorySetup { + using Create2 for bytes32; + + function test_TokenPoolFactory_Constructor_Revert() public { + // Revert cause the tokenAdminRegistry is address(0) + vm.expectRevert(TokenPoolFactory.InvalidZeroAddress.selector); + new TokenPoolFactory(ITokenAdminRegistry(address(0)), RegistryModuleOwnerCustom(address(0)), address(0), address(0)); + + new TokenPoolFactory( + ITokenAdminRegistry(address(0xdeadbeef)), + RegistryModuleOwnerCustom(address(0xdeadbeef)), + address(0xdeadbeef), + address(0xdeadbeef) + ); + } + + function test_createTokenPool_WithNoExistingTokenOnRemoteChain_Success() public { + vm.startPrank(OWNER); + + bytes32 dynamicSalt = keccak256(abi.encodePacked(FAKE_SALT, OWNER)); + + address predictedTokenAddress = + Create2.computeAddress(dynamicSalt, keccak256(s_tokenInitCode), address(s_tokenPoolFactory)); + + // Create the constructor params for the predicted pool + bytes memory poolCreationParams = abi.encode(predictedTokenAddress, new address[](0), s_rmnProxy, s_sourceRouter); + + // Predict the address of the pool before we make the tx by using the init code and the params + bytes memory predictedPoolInitCode = abi.encodePacked(s_poolInitCode, poolCreationParams); + + address predictedPoolAddress = + dynamicSalt.computeAddress(keccak256(predictedPoolInitCode), address(s_tokenPoolFactory)); + + (address tokenAddress, address poolAddress) = s_tokenPoolFactory.deployTokenAndTokenPool( + new TokenPoolFactory.RemoteTokenPoolInfo[](0), + s_tokenInitCode, + s_poolInitCode, + FAKE_SALT, + TokenPoolFactory.PoolType.BURN_MINT + ); + + assertNotEq(address(0), tokenAddress, "Token Address should not be 0"); + assertNotEq(address(0), poolAddress, "Pool Address should not be 0"); + + assertEq(predictedTokenAddress, tokenAddress, "Token Address should have been predicted"); + assertEq(predictedPoolAddress, poolAddress, "Pool Address should have been predicted"); + + s_tokenAdminRegistry.acceptAdminRole(tokenAddress); + OwnerIsCreator(tokenAddress).acceptOwnership(); + OwnerIsCreator(poolAddress).acceptOwnership(); + + assertEq(poolAddress, s_tokenAdminRegistry.getPool(tokenAddress), "Token Pool should be set"); + assertEq(IOwner(tokenAddress).owner(), OWNER, "Token should be owned by the owner"); + assertEq(IOwner(poolAddress).owner(), OWNER, "Token should be owned by the owner"); + } + + function test_createTokenPool_WithNoExistingRemoteContracts_predict_Success() public { + vm.startPrank(OWNER); + bytes32 dynamicSalt = keccak256(abi.encodePacked(FAKE_SALT, OWNER)); + + // We have to create a new factory, registry module, and token admin registry to simulate the other chain + TokenAdminRegistry newTokenAdminRegistry = new TokenAdminRegistry(); + RegistryModuleOwnerCustom newRegistryModule = new RegistryModuleOwnerCustom(address(newTokenAdminRegistry)); + + // We want to deploy a new factory and Owner Module. + TokenPoolFactory newTokenPoolFactory = + new TokenPoolFactory(newTokenAdminRegistry, newRegistryModule, s_rmnProxy, address(s_destRouter)); + + newTokenAdminRegistry.addRegistryModule(address(newRegistryModule)); + + TokenPoolFactory.RemoteChainConfig memory remoteChainConfig = + TokenPoolFactory.RemoteChainConfig(address(newTokenPoolFactory), address(s_destRouter), address(s_rmnProxy)); + + // Create an array of remote pools where nothing exists yet, but we want to predict the address for + // the new pool and token on DEST_CHAIN_SELECTOR + TokenPoolFactory.RemoteTokenPoolInfo[] memory remoteTokenPools = new TokenPoolFactory.RemoteTokenPoolInfo[](1); + + // The only field that matters is DEST_CHAIN_SELECTOR because we dont want any existing token pool or token + // on the remote chain + remoteTokenPools[0] = TokenPoolFactory.RemoteTokenPoolInfo( + DEST_CHAIN_SELECTOR, // remoteChainSelector + "", // remotePoolAddress + type(BurnMintTokenPool).creationCode, // remotePoolInitCode + remoteChainConfig, // remoteChainConfig + TokenPoolFactory.PoolType.BURN_MINT, // poolType + "", // remoteTokenAddress + s_tokenInitCode, // remoteTokenInitCode + RateLimiter.Config(false, 0, 0) + ); + + // Predict the address of the token and pool on the DESTINATION chain + address predictedTokenAddress = dynamicSalt.computeAddress(keccak256(s_tokenInitCode), address(newTokenPoolFactory)); + + // Since the remote chain information was provided, we should be able to get the information from the newly + // deployed token pool using the available getter functions + (, address poolAddress) = s_tokenPoolFactory.deployTokenAndTokenPool( + remoteTokenPools, // No existing remote pools + s_tokenInitCode, // Token Init Code + s_poolInitCode, // Pool Init Code + FAKE_SALT, // Salt + TokenPoolFactory.PoolType.BURN_MINT // Pool Type + ); + + // Ensure that the remote Token was set to the one we predicted + assertEq( + abi.encode(predictedTokenAddress), + TokenPool(poolAddress).getRemoteToken(DEST_CHAIN_SELECTOR), + "Token Address should have been predicted" + ); + + { + // Create the constructor params for the predicted pool + // The predictedTokenAddress is NOT abi-encoded since the raw evm-address + // is used in the constructor params + bytes memory predictedPoolCreationParams = + abi.encode(predictedTokenAddress, new address[](0), s_rmnProxy, address(s_destRouter)); + + // Take the init code and concat the destination params to it, the initCode shouldn't change + bytes memory predictedPoolInitCode = abi.encodePacked(s_poolInitCode, predictedPoolCreationParams); + + // Predict the address of the pool on the DESTINATION chain + address predictedPoolAddress = + dynamicSalt.computeAddress(keccak256(predictedPoolInitCode), address(newTokenPoolFactory)); + + // Assert that the address set for the remote pool is the same as the predicted address + assertEq( + abi.encode(predictedPoolAddress), + TokenPool(poolAddress).getRemotePool(DEST_CHAIN_SELECTOR), + "Pool Address should have been predicted" + ); + } + + // On the new token pool factory, representing a destination chain, + // deploy a new token and a new pool + (address newTokenAddress, address newPoolAddress) = newTokenPoolFactory.deployTokenAndTokenPool( + new TokenPoolFactory.RemoteTokenPoolInfo[](0), + s_tokenInitCode, + s_poolInitCode, + FAKE_SALT, + TokenPoolFactory.PoolType.BURN_MINT + ); + + assertEq( + TokenPool(poolAddress).getRemotePool(DEST_CHAIN_SELECTOR), + abi.encode(newPoolAddress), + "New Pool Address should have been deployed correctly" + ); + + assertEq( + TokenPool(poolAddress).getRemoteToken(DEST_CHAIN_SELECTOR), + abi.encode(newTokenAddress), + "New Token Address should have been deployed correctly" + ); + } + + function test_createTokenPool_ExistingRemoteToken_AndPredictPool_Success() public { + vm.startPrank(OWNER); + bytes32 dynamicSalt = keccak256(abi.encodePacked(FAKE_SALT, OWNER)); + + FactoryBurnMintERC20 newRemoteToken = + new FactoryBurnMintERC20("TestToken", "TT", 18, type(uint256).max, PREMINT_AMOUNT, OWNER); + + // We have to create a new factory, registry module, and token admin registry to simulate the other chain + TokenAdminRegistry newTokenAdminRegistry = new TokenAdminRegistry(); + RegistryModuleOwnerCustom newRegistryModule = new RegistryModuleOwnerCustom(address(newTokenAdminRegistry)); + + // We want to deploy a new factory and Owner Module. + TokenPoolFactory newTokenPoolFactory = + new TokenPoolFactory(newTokenAdminRegistry, newRegistryModule, s_rmnProxy, address(s_destRouter)); + + newTokenAdminRegistry.addRegistryModule(address(newRegistryModule)); + + TokenPoolFactory.RemoteChainConfig memory remoteChainConfig = + TokenPoolFactory.RemoteChainConfig(address(newTokenPoolFactory), address(s_destRouter), address(s_rmnProxy)); + + // Create an array of remote pools where nothing exists yet, but we want to predict the address for + // the new pool and token on DEST_CHAIN_SELECTOR + TokenPoolFactory.RemoteTokenPoolInfo[] memory remoteTokenPools = new TokenPoolFactory.RemoteTokenPoolInfo[](1); + + // The only field that matters is DEST_CHAIN_SELECTOR because we dont want any existing token pool or token + // on the remote chain + remoteTokenPools[0] = TokenPoolFactory.RemoteTokenPoolInfo( + DEST_CHAIN_SELECTOR, // remoteChainSelector + "", // remotePoolAddress + type(BurnMintTokenPool).creationCode, // remotePoolInitCode + remoteChainConfig, // remoteChainConfig + TokenPoolFactory.PoolType.BURN_MINT, // poolType + abi.encode(address(newRemoteToken)), // remoteTokenAddress + s_tokenInitCode, // remoteTokenInitCode + RateLimiter.Config(false, 0, 0) // rateLimiterConfig + ); + + // Since the remote chain information was provided, we should be able to get the information from the newly + // deployed token pool using the available getter functions + (address tokenAddress, address poolAddress) = s_tokenPoolFactory.deployTokenAndTokenPool( + remoteTokenPools, s_tokenInitCode, s_poolInitCode, FAKE_SALT, TokenPoolFactory.PoolType.BURN_MINT + ); + + assertEq(address(TokenPool(poolAddress).getToken()), tokenAddress, "Token Address should have been set locally"); + + // Ensure that the remote Token was set to the one we predicted + assertEq( + abi.encode(address(newRemoteToken)), + TokenPool(poolAddress).getRemoteToken(DEST_CHAIN_SELECTOR), + "Token Address should have been predicted" + ); + + // Create the constructor params for the predicted pool + // The predictedTokenAddress is NOT abi-encoded since the raw evm-address + // is used in the constructor params + bytes memory predictedPoolCreationParams = + abi.encode(address(newRemoteToken), new address[](0), s_rmnProxy, address(s_destRouter)); + + // Take the init code and concat the destination params to it, the initCode shouldn't change + bytes memory predictedPoolInitCode = abi.encodePacked(s_poolInitCode, predictedPoolCreationParams); + + // Predict the address of the pool on the DESTINATION chain + address predictedPoolAddress = + dynamicSalt.computeAddress(keccak256(predictedPoolInitCode), address(newTokenPoolFactory)); + + // Assert that the address set for the remote pool is the same as the predicted address + assertEq( + abi.encode(predictedPoolAddress), + TokenPool(poolAddress).getRemotePool(DEST_CHAIN_SELECTOR), + "Pool Address should have been predicted" + ); + + // On the new token pool factory, representing a destination chain, + // deploy a new token and a new pool + address newPoolAddress = newTokenPoolFactory.deployTokenPoolWithExistingToken( + address(newRemoteToken), + new TokenPoolFactory.RemoteTokenPoolInfo[](0), + s_poolInitCode, + FAKE_SALT, + TokenPoolFactory.PoolType.BURN_MINT + ); + + assertEq( + abi.encode(newRemoteToken), + TokenPool(poolAddress).getRemoteToken(DEST_CHAIN_SELECTOR), + "Remote Token Address should have been set correctly" + ); + + assertEq( + TokenPool(poolAddress).getRemotePool(DEST_CHAIN_SELECTOR), + abi.encode(newPoolAddress), + "New Pool Address should have been deployed correctly" + ); + } + + function test_createTokenPool_WithRemoteTokenAndRemotePool_Success() public { + vm.startPrank(OWNER); + + bytes memory RANDOM_TOKEN_ADDRESS = abi.encode(makeAddr("RANDOM_TOKEN")); + bytes memory RANDOM_POOL_ADDRESS = abi.encode(makeAddr("RANDOM_POOL")); + + // Create an array of remote pools with some fake addresses + TokenPoolFactory.RemoteTokenPoolInfo[] memory remoteTokenPools = new TokenPoolFactory.RemoteTokenPoolInfo[](1); + + remoteTokenPools[0] = TokenPoolFactory.RemoteTokenPoolInfo( + DEST_CHAIN_SELECTOR, // remoteChainSelector + RANDOM_POOL_ADDRESS, // remotePoolAddress + type(BurnMintTokenPool).creationCode, // remotePoolInitCode + TokenPoolFactory.RemoteChainConfig(address(0), address(0), address(0)), // remoteChainConfig + TokenPoolFactory.PoolType.BURN_MINT, // poolType + RANDOM_TOKEN_ADDRESS, // remoteTokenAddress + "", // remoteTokenInitCode + RateLimiter.Config(false, 0, 0) // rateLimiterConfig + ); + + (address tokenAddress, address poolAddress) = s_tokenPoolFactory.deployTokenAndTokenPool( + remoteTokenPools, s_tokenInitCode, s_poolInitCode, FAKE_SALT, TokenPoolFactory.PoolType.BURN_MINT + ); + + assertNotEq(address(0), tokenAddress, "Token Address should not be 0"); + assertNotEq(address(0), poolAddress, "Pool Address should not be 0"); + + s_tokenAdminRegistry.acceptAdminRole(tokenAddress); + OwnerIsCreator(tokenAddress).acceptOwnership(); + OwnerIsCreator(poolAddress).acceptOwnership(); + + assertEq( + TokenPool(poolAddress).getRemoteToken(DEST_CHAIN_SELECTOR), + RANDOM_TOKEN_ADDRESS, + "Remote Token Address should have been set" + ); + + assertEq( + TokenPool(poolAddress).getRemotePool(DEST_CHAIN_SELECTOR), + RANDOM_POOL_ADDRESS, + "Remote Pool Address should have been set" + ); + + assertEq(poolAddress, s_tokenAdminRegistry.getPool(tokenAddress), "Token Pool should be set"); + + assertEq(IOwner(tokenAddress).owner(), OWNER, "Token should be owned by the owner"); + + assertEq(IOwner(poolAddress).owner(), OWNER, "Token should be owned by the owner"); + } + + function test_createTokenPoolLockRelease_NoExistingToken_predict_Success() public { + vm.startPrank(OWNER); + bytes32 dynamicSalt = keccak256(abi.encodePacked(FAKE_SALT, OWNER)); + + // We have to create a new factory, registry module, and token admin registry to simulate the other chain + TokenAdminRegistry newTokenAdminRegistry = new TokenAdminRegistry(); + RegistryModuleOwnerCustom newRegistryModule = new RegistryModuleOwnerCustom(address(newTokenAdminRegistry)); + + // We want to deploy a new factory and Owner Module. + TokenPoolFactory newTokenPoolFactory = + new TokenPoolFactory(newTokenAdminRegistry, newRegistryModule, s_rmnProxy, address(s_destRouter)); + + newTokenAdminRegistry.addRegistryModule(address(newRegistryModule)); + + TokenPoolFactory.RemoteChainConfig memory remoteChainConfig = + TokenPoolFactory.RemoteChainConfig(address(newTokenPoolFactory), address(s_destRouter), address(s_rmnProxy)); + + // Create an array of remote pools where nothing exists yet, but we want to predict the address for + // the new pool and token on DEST_CHAIN_SELECTOR + TokenPoolFactory.RemoteTokenPoolInfo[] memory remoteTokenPools = new TokenPoolFactory.RemoteTokenPoolInfo[](1); + + // The only field that matters is DEST_CHAIN_SELECTOR because we dont want any existing token pool or token + // on the remote chain + remoteTokenPools[0] = TokenPoolFactory.RemoteTokenPoolInfo( + DEST_CHAIN_SELECTOR, // remoteChainSelector + "", // remotePoolAddress + type(LockReleaseTokenPool).creationCode, // remotePoolInitCode + remoteChainConfig, // remoteChainConfig + TokenPoolFactory.PoolType.LOCK_RELEASE, // poolType + "", // remoteTokenAddress + s_tokenInitCode, // remoteTokenInitCode + RateLimiter.Config(false, 0, 0) + ); + + // Predict the address of the token and pool on the DESTINATION chain + address predictedTokenAddress = dynamicSalt.computeAddress(keccak256(s_tokenInitCode), address(newTokenPoolFactory)); + + // Since the remote chain information was provided, we should be able to get the information from the newly + // deployed token pool using the available getter functions + (, address poolAddress) = s_tokenPoolFactory.deployTokenAndTokenPool( + remoteTokenPools, + s_tokenInitCode, + type(LockReleaseTokenPool).creationCode, + FAKE_SALT, + TokenPoolFactory.PoolType.LOCK_RELEASE + ); + + // Ensure that the remote Token was set to the one we predicted + assertEq( + abi.encode(predictedTokenAddress), + TokenPool(poolAddress).getRemoteToken(DEST_CHAIN_SELECTOR), + "Token Address should have been predicted" + ); + + { + // Create the constructor params for the predicted pool + // The predictedTokenAddress is NOT abi-encoded since the raw evm-address + // is used in the constructor params + bytes memory predictedPoolCreationParams = + abi.encode(predictedTokenAddress, new address[](0), s_rmnProxy, true, address(s_destRouter)); + + // Take the init code and concat the destination params to it, the initCode shouldn't change + bytes memory predictedPoolInitCode = + abi.encodePacked(type(LockReleaseTokenPool).creationCode, predictedPoolCreationParams); + + // Predict the address of the pool on the DESTINATION chain + address predictedPoolAddress = + dynamicSalt.computeAddress(keccak256(predictedPoolInitCode), address(newTokenPoolFactory)); + + // Assert that the address set for the remote pool is the same as the predicted address + assertEq( + abi.encode(predictedPoolAddress), + TokenPool(poolAddress).getRemotePool(DEST_CHAIN_SELECTOR), + "Pool Address should have been predicted" + ); + } + + // On the new token pool factory, representing a destination chain, + // deploy a new token and a new pool + (address newTokenAddress, address newPoolAddress) = newTokenPoolFactory.deployTokenAndTokenPool( + new TokenPoolFactory.RemoteTokenPoolInfo[](0), // No existing remote pools + s_tokenInitCode, // Token Init Code + type(LockReleaseTokenPool).creationCode, // Pool Init Code + FAKE_SALT, // Salt + TokenPoolFactory.PoolType.LOCK_RELEASE // Pool Type + ); + + assertEq( + TokenPool(poolAddress).getRemotePool(DEST_CHAIN_SELECTOR), + abi.encode(newPoolAddress), + "New Pool Address should have been deployed correctly" + ); + + assertEq( + TokenPool(poolAddress).getRemoteToken(DEST_CHAIN_SELECTOR), + abi.encode(newTokenAddress), + "New Token Address should have been deployed correctly" + ); + } +} diff --git a/contracts/src/v0.8/ccip/tokenAdminRegistry/TokenPoolFactory/FactoryBurnMintERC20.sol b/contracts/src/v0.8/ccip/tokenAdminRegistry/TokenPoolFactory/FactoryBurnMintERC20.sol new file mode 100644 index 0000000000..c59be5775d --- /dev/null +++ b/contracts/src/v0.8/ccip/tokenAdminRegistry/TokenPoolFactory/FactoryBurnMintERC20.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IGetCCIPAdmin} from "../../../ccip/interfaces/IGetCCIPAdmin.sol"; +import {IOwnable} from "../../../shared/interfaces/IOwnable.sol"; +import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; + +import {OwnerIsCreator} from "../../../shared/access/OwnerIsCreator.sol"; + +import {ERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/ERC20.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {ERC20Burnable} from + "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol"; +import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice A basic ERC20 compatible token contract with burn and minting roles. +/// @dev The constructor has been modified to support the deployment pattern used by a factory contract. +/// @dev The total supply can be limited during deployment. +contract FactoryBurnMintERC20 is IBurnMintERC20, IGetCCIPAdmin, IERC165, ERC20Burnable, OwnerIsCreator { + using EnumerableSet for EnumerableSet.AddressSet; + + error SenderNotMinter(address sender); + error SenderNotBurner(address sender); + error MaxSupplyExceeded(uint256 supplyAfterMint); + + event MintAccessGranted(address indexed minter); + event BurnAccessGranted(address indexed burner); + event MintAccessRevoked(address indexed minter); + event BurnAccessRevoked(address indexed burner); + + event CCIPAdminTransferred(address indexed previousAdmin, address indexed newAdmin); + + /// @dev The number of decimals for the token + uint8 internal immutable i_decimals; + + /// @dev The maximum supply of the token, 0 if unlimited + uint256 internal immutable i_maxSupply; + + /// @dev the CCIPAdmin can be used to register with the CCIP token admin registry, but has no other special powers, + /// and can only be transferred by the owner. + address internal s_ccipAdmin; + + /// @dev the allowed minter addresses + EnumerableSet.AddressSet internal s_minters; + /// @dev the allowed burner addresses + EnumerableSet.AddressSet internal s_burners; + + constructor( + string memory name, + string memory symbol, + uint8 decimals_, + uint256 maxSupply_, + uint256 preMint_, + address newOwner_ + ) ERC20(name, symbol) { + i_decimals = decimals_; + i_maxSupply = maxSupply_; + + s_ccipAdmin = newOwner_; + + // Mint the initial supply to the new Owner, saving gas by not calling if the mint amount is zero + if (preMint_ != 0) _mint(newOwner_, preMint_); + + // Grant the deployer the minter and burner roles. This contract is expected to be deployed by a factory + // contract that will transfer ownership to the correct address after deployment, so granting minting and burning + // privileges here saves gas by not requiring two transactions. + grantMintRole(newOwner_); + grantBurnRole(newOwner_); + } + + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == type(IERC20).interfaceId || interfaceId == type(IBurnMintERC20).interfaceId + || interfaceId == type(IERC165).interfaceId || interfaceId == type(IOwnable).interfaceId; + } + + // ================================================================ + // | ERC20 | + // ================================================================ + + /// @dev Returns the number of decimals used in its user representation. + function decimals() public view virtual override returns (uint8) { + return i_decimals; + } + + /// @dev Returns the max supply of the token, 0 if unlimited. + function maxSupply() public view virtual returns (uint256) { + return i_maxSupply; + } + + /// @dev Uses OZ ERC20 _transfer to disallow sending to address(0). + /// @dev Disallows sending to address(this) + function _transfer(address from, address to, uint256 amount) internal virtual override validAddress(to) { + super._transfer(from, to, amount); + } + + /// @dev Uses OZ ERC20 _approve to disallow approving for address(0). + /// @dev Disallows approving for address(this) + function _approve(address owner, address spender, uint256 amount) internal virtual override validAddress(spender) { + super._approve(owner, spender, amount); + } + + /// @dev Exists to be backwards compatible with the older naming convention. + /// @param spender the account being approved to spend on the users' behalf. + /// @param subtractedValue the amount being removed from the approval. + /// @return success Bool to return if the approval was successfully decreased. + function decreaseApproval(address spender, uint256 subtractedValue) external returns (bool success) { + return decreaseAllowance(spender, subtractedValue); + } + + /// @dev Exists to be backwards compatible with the older naming convention. + /// @param spender the account being approved to spend on the users' behalf. + /// @param addedValue the amount being added to the approval. + function increaseApproval(address spender, uint256 addedValue) external { + increaseAllowance(spender, addedValue); + } + + /// @notice Check if recipient is valid (not this contract address). + /// @param recipient the account we transfer/approve to. + /// @dev Reverts with an empty revert to be compatible with the existing link token when + /// the recipient is this contract address. + modifier validAddress(address recipient) virtual { + // solhint-disable-next-line reason-string, gas-custom-errors + if (recipient == address(this)) revert(); + _; + } + + // ================================================================ + // | Burning & minting | + // ================================================================ + + /// @inheritdoc ERC20Burnable + /// @dev Uses OZ ERC20 _burn to disallow burning from address(0). + /// @dev Decreases the total supply. + function burn(uint256 amount) public override(IBurnMintERC20, ERC20Burnable) onlyBurner { + super.burn(amount); + } + + /// @inheritdoc IBurnMintERC20 + /// @dev Alias for BurnFrom for compatibility with the older naming convention. + /// @dev Uses burnFrom for all validation & logic. + + function burn(address account, uint256 amount) public virtual override { + burnFrom(account, amount); + } + + /// @inheritdoc ERC20Burnable + /// @dev Uses OZ ERC20 _burn to disallow burning from address(0). + /// @dev Decreases the total supply. + function burnFrom(address account, uint256 amount) public override(IBurnMintERC20, ERC20Burnable) onlyBurner { + super.burnFrom(account, amount); + } + + /// @inheritdoc IBurnMintERC20 + /// @dev Uses OZ ERC20 _mint to disallow minting to address(0). + /// @dev Disallows minting to address(this) + /// @dev Increases the total supply. + function mint(address account, uint256 amount) external override onlyMinter validAddress(account) { + if (i_maxSupply != 0 && totalSupply() + amount > i_maxSupply) revert MaxSupplyExceeded(totalSupply() + amount); + + _mint(account, amount); + } + + // ================================================================ + // | Roles | + // ================================================================ + + /// @notice grants both mint and burn roles to `burnAndMinter`. + /// @dev calls public functions so this function does not require + /// access controls. This is handled in the inner functions. + function grantMintAndBurnRoles(address burnAndMinter) external { + grantMintRole(burnAndMinter); + grantBurnRole(burnAndMinter); + } + + /// @notice Grants mint role to the given address. + /// @dev only the owner can call this function. + function grantMintRole(address minter) public onlyOwner { + if (s_minters.add(minter)) { + emit MintAccessGranted(minter); + } + } + + /// @notice Grants burn role to the given address. + /// @dev only the owner can call this function. + /// @param burner the address to grant the burner role to + function grantBurnRole(address burner) public onlyOwner { + if (s_burners.add(burner)) { + emit BurnAccessGranted(burner); + } + } + + /// @notice Revokes mint role for the given address. + /// @dev only the owner can call this function. + /// @param minter the address to revoke the mint role from. + function revokeMintRole(address minter) public onlyOwner { + if (s_minters.remove(minter)) { + emit MintAccessRevoked(minter); + } + } + + /// @notice Revokes burn role from the given address. + /// @dev only the owner can call this function + /// @param burner the address to revoke the burner role from + function revokeBurnRole(address burner) public onlyOwner { + if (s_burners.remove(burner)) { + emit BurnAccessRevoked(burner); + } + } + + /// @notice Returns all permissioned minters + function getMinters() public view returns (address[] memory) { + return s_minters.values(); + } + + /// @notice Returns all permissioned burners + function getBurners() public view returns (address[] memory) { + return s_burners.values(); + } + + /// @notice Returns the current CCIPAdmin + function getCCIPAdmin() public view returns (address) { + return s_ccipAdmin; + } + + /// @notice Transfers the CCIPAdmin role to a new address + /// @dev only the owner can call this function, NOT the current ccipAdmin, and 1-step ownership transfer is used. + /// @param newAdmin The address to transfer the CCIPAdmin role to. Setting to address(0) is a valid way to revoke + /// the role + function setCCIPAdmin(address newAdmin) public onlyOwner { + address currentAdmin = s_ccipAdmin; + + s_ccipAdmin = newAdmin; + + emit CCIPAdminTransferred(currentAdmin, newAdmin); + } + + // ================================================================ + // | Access | + // ================================================================ + + /// @notice Checks whether a given address is a minter for this token. + /// @return true if the address is allowed to mint. + function isMinter(address minter) public view returns (bool) { + return s_minters.contains(minter); + } + + /// @notice Checks whether a given address is a burner for this token. + /// @return true if the address is allowed to burn. + function isBurner(address burner) public view returns (bool) { + return s_burners.contains(burner); + } + + /// @notice Checks whether the msg.sender is a permissioned minter for this token + /// @dev Reverts with a SenderNotMinter if the check fails + modifier onlyMinter() { + if (!isMinter(msg.sender)) revert SenderNotMinter(msg.sender); + _; + } + + /// @notice Checks whether the msg.sender is a permissioned burner for this token + /// @dev Reverts with a SenderNotBurner if the check fails + modifier onlyBurner() { + if (!isBurner(msg.sender)) revert SenderNotBurner(msg.sender); + _; + } +} diff --git a/contracts/src/v0.8/ccip/tokenAdminRegistry/TokenPoolFactory/TokenPoolFactory.sol b/contracts/src/v0.8/ccip/tokenAdminRegistry/TokenPoolFactory/TokenPoolFactory.sol new file mode 100644 index 0000000000..f76a0f63eb --- /dev/null +++ b/contracts/src/v0.8/ccip/tokenAdminRegistry/TokenPoolFactory/TokenPoolFactory.sol @@ -0,0 +1,286 @@ +pragma solidity 0.8.24; + +import {IOwnable} from "../../../shared/interfaces/IOwnable.sol"; +import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; +import {ITokenAdminRegistry} from "../../interfaces/ITokenAdminRegistry.sol"; + +import {OwnerIsCreator} from "../../../shared/access/OwnerIsCreator.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {RegistryModuleOwnerCustom} from "../RegistryModuleOwnerCustom.sol"; + +import {Create2} from "../../../vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Create2.sol"; + +/// @notice A contract for deploying new tokens and token pools, and configuring them with the token admin registry +/// @dev At the end of the transaction, the ownership transfer process will begin, but the user must accept the +/// ownership transfer in a separate transaction. +/// @dev The address prediction mechanism is only capable of deploying and predicting addresses for EVM based chains. +/// adding compatibility for other chains will require additional offchain computation. +contract TokenPoolFactory is OwnerIsCreator, ITypeAndVersion { + using Create2 for bytes32; + + event RemoteChainConfigUpdated(uint64 indexed remoteChainSelector, RemoteChainConfig remoteChainConfig); + + error InvalidZeroAddress(); + + /// @notice The type of pool to deploy. Types may be expanded in future versions + enum PoolType { + BURN_MINT, + LOCK_RELEASE + } + + struct RemoteTokenPoolInfo { + uint64 remoteChainSelector; // The CCIP specific selector for the remote chain + bytes remotePoolAddress; // The address of the remote pool to either deploy or use as is. If empty, address + // will be predicted + bytes remotePoolInitCode; // Remote pool creation code if it needs to be deployed, without constructor params + // appended to the end. + RemoteChainConfig remoteChainConfig; // The addresses of the remote RMNProxy, Router, and factory for determining + // the remote address + PoolType poolType; // The type of pool to deploy, either Burn/Mint or Lock/Release + bytes remoteTokenAddress; // EVM address for remote token. If empty, the address will be predicted + bytes remoteTokenInitCode; // The init code to be deployed on the remote chain and includes constructor params + RateLimiter.Config rateLimiterConfig; // Token Pool rate limit. Values will be applied on incoming an outgoing messages + } + + // solhint-disable-next-line gas-struct-packing + struct RemoteChainConfig { + address remotePoolFactory; // The factory contract on the remote chain which will make the deployment + address remoteRouter; // The router on the remote chain + address remoteRMNProxy; // The RMNProxy contract on the remote chain + } + + string public constant typeAndVersion = "TokenPoolFactory 1.7.0-dev"; + + ITokenAdminRegistry private immutable i_tokenAdminRegistry; + RegistryModuleOwnerCustom private immutable i_registryModuleOwnerCustom; + + address private immutable i_rmnProxy; + address private immutable i_ccipRouter; + + /// @notice Construct the TokenPoolFactory + /// @param tokenAdminRegistry The address of the token admin registry + /// @param tokenAdminModule The address of the token admin module which can register the token via ownership module + /// @param rmnProxy The address of the RMNProxy contract token pools will be deployed with + /// @param ccipRouter The address of the CCIPRouter contract token pools will be deployed with + constructor( + ITokenAdminRegistry tokenAdminRegistry, + RegistryModuleOwnerCustom tokenAdminModule, + address rmnProxy, + address ccipRouter + ) { + if ( + address(tokenAdminRegistry) == address(0) || address(tokenAdminModule) == address(0) || rmnProxy == address(0) + || ccipRouter == address(0) + ) revert InvalidZeroAddress(); + + i_tokenAdminRegistry = ITokenAdminRegistry(tokenAdminRegistry); + i_registryModuleOwnerCustom = RegistryModuleOwnerCustom(tokenAdminModule); + i_rmnProxy = rmnProxy; + i_ccipRouter = ccipRouter; + } + + // ================================================================ + // | Top-Level Deployment | + // ================================================================ + + /// @notice Deploys a token and token pool with the given token information and configures it with remote token pools + /// @dev The token and token pool are deployed in the same transaction, and the token pool is configured with the + /// remote token pools. The token pool is then set in the token admin registry. Ownership of the everything is transferred + /// to the msg.sender, but must be accepted in a separate transaction due to 2-step ownership transfer. + /// @param remoteTokenPools An array of remote token pools info to be used in the pool's applyChainUpdates function + /// or to be predicted if the pool has not been deployed yet on the remote chain + /// @param tokenInitCode The creation code for the token, which includes the constructor parameters already appended + /// @param tokenPoolInitCode The creation code for the token pool, without the constructor parameters appended + /// @param salt The salt to be used in the create2 deployment of the token and token pool to ensure a unique address + /// @param poolType The type of pool to deploy, either Burn/Mint or Lock/Release + /// @return token The address of the token that was deployed + /// @return pool The address of the token pool that was deployed + function deployTokenAndTokenPool( + RemoteTokenPoolInfo[] calldata remoteTokenPools, + bytes memory tokenInitCode, + bytes calldata tokenPoolInitCode, + bytes32 salt, + PoolType poolType + ) external returns (address, address) { + // Ensure a unique deployment between senders even if the same input parameter is used to prevent + // DOS/Frontrunning attacks + salt = keccak256(abi.encodePacked(salt, msg.sender)); + + // Deploy the token. The constructor parameters are already provided in the tokenInitCode + address token = Create2.deploy(0, salt, tokenInitCode); + + // Deploy the token pool + address pool = _createTokenPool(token, remoteTokenPools, tokenPoolInitCode, salt, poolType); + + // Set the token pool for token in the token admin registry since this contract is the token and pool owner + _setTokenPoolInTokenAdminRegistry(token, pool); + + // Begin the 2 step ownership transfer of the newly deployed token to the msg.sender + IOwnable(token).transferOwnership(msg.sender); + + return (token, pool); + } + + /// @notice Deploys a token pool with an existing ERC20 token + /// @dev Since the token already exists, this contract is not the owner and therefore cannot configure the + /// token pool in the token admin registry in the same transaction. The user must invoke the calls to the + /// tokenAdminRegistry manually + /// @param token The address of the existing token to be used in the token pool + /// @param remoteTokenPools An array of remote token pools info to be used in the pool's applyChainUpdates function + /// @param tokenPoolInitCode The creation code for the token pool + /// @param salt The salt to be used in the create2 deployment of the token pool + /// @return poolAddress The address of the token pool that was deployed + function deployTokenPoolWithExistingToken( + address token, + RemoteTokenPoolInfo[] calldata remoteTokenPools, + bytes calldata tokenPoolInitCode, + bytes32 salt, + PoolType poolType + ) external returns (address poolAddress) { + // Ensure a unique deployment between senders even if the same input parameter is used to prevent + // DOS/Frontrunning attacks + salt = keccak256(abi.encodePacked(salt, msg.sender)); + + // create the token pool and return the address + return _createTokenPool(token, remoteTokenPools, tokenPoolInitCode, salt, poolType); + } + + // ================================================================ + // | Pool Deployment/Configuration | + // ================================================================ + + /// @notice Deploys a token pool with the given token information and remote token pools + /// @param token The token to be used in the token pool + /// @param remoteTokenPools An array of remote token pools info to be used in the pool's applyChainUpdates function + /// @param tokenPoolInitCode The creation code for the token pool + /// @param salt The salt to be used in the create2 deployment of the token pool + /// @return poolAddress The address of the token pool that was deployed + function _createTokenPool( + address token, + RemoteTokenPoolInfo[] calldata remoteTokenPools, + bytes calldata tokenPoolInitCode, + bytes32 salt, + PoolType poolType + ) private returns (address) { + // Create an array of chain updates to apply to the token pool + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](remoteTokenPools.length); + + RemoteTokenPoolInfo memory remoteTokenPool; + for (uint256 i = 0; i < remoteTokenPools.length; ++i) { + remoteTokenPool = remoteTokenPools[i]; + + // If the user provides an empty byte string, indicated no token has already been deployed, + // then the address of the token needs to be predicted. Otherwise the address provided will be used. + if (remoteTokenPool.remoteTokenAddress.length == 0) { + // The user must provide the initCode for the remote token, so its address can be predicted correctly. It's + // provided in the remoteTokenInitCode field for the remoteTokenPool + remoteTokenPool.remoteTokenAddress = abi.encode( + salt.computeAddress( + keccak256(remoteTokenPool.remoteTokenInitCode), remoteTokenPool.remoteChainConfig.remotePoolFactory + ) + ); + } + + // If the user provides an empty byte string parameter, indicating the pool has not been deployed yet, + // the address of the pool should be predicted. Otherwise use the provided address. + if (remoteTokenPool.remotePoolAddress.length == 0) { + // Address is predicted based on the init code hash and the deployer, so the hash must first be computed + // using the initCode and a concatenated set of constructor parameters. + bytes32 remotePoolInitcodeHash = _generatePoolInitcodeHash( + remoteTokenPool.remotePoolInitCode, + remoteTokenPool.remoteChainConfig, + abi.decode(remoteTokenPool.remoteTokenAddress, (address)), + remoteTokenPool.poolType + ); + + // Abi encode the computed remote address so it can be used as bytes in the chain update + remoteTokenPool.remotePoolAddress = + abi.encode(salt.computeAddress(remotePoolInitcodeHash, remoteTokenPool.remoteChainConfig.remotePoolFactory)); + } + + chainUpdates[i] = TokenPool.ChainUpdate({ + remoteChainSelector: remoteTokenPool.remoteChainSelector, + allowed: true, + remotePoolAddress: remoteTokenPool.remotePoolAddress, + remoteTokenAddress: remoteTokenPool.remoteTokenAddress, + outboundRateLimiterConfig: remoteTokenPool.rateLimiterConfig, + inboundRateLimiterConfig: remoteTokenPool.rateLimiterConfig + }); + } + + // Construct the initArgs for the token pool using the immutable contracts for CCIP on the local chain + bytes memory tokenPoolInitArgs; + if (poolType == PoolType.BURN_MINT) { + tokenPoolInitArgs = abi.encode(token, new address[](0), i_rmnProxy, i_ccipRouter); + } else if (poolType == PoolType.LOCK_RELEASE) { + // Lock/Release pools have an additional boolean constructor parameter that must be accounted for, acceptLiquidity, + // which is set to true by default in this case. Users wishing to set it to false must deploy the pool manually. + tokenPoolInitArgs = abi.encode(token, new address[](0), i_rmnProxy, true, i_ccipRouter); + } + + // Construct the deployment code from the initCode and the initArgs and then deploy + address poolAddress = Create2.deploy(0, salt, abi.encodePacked(tokenPoolInitCode, tokenPoolInitArgs)); + + // Apply the chain updates to the token pool + TokenPool(poolAddress).applyChainUpdates(chainUpdates); + + // Begin the 2 step ownership transfer of the token pool to the msg.sender. + IOwnable(poolAddress).transferOwnership(address(msg.sender)); // 2 step ownership transfer + + return poolAddress; + } + + /// @notice Generates the hash of the init code the pool will be deployed with + /// @dev The init code hash is used with Create2 to predict the address of the pool on the remote chain + /// @dev ABI-encoding limitations prevent arbitrary constructor parameters from being used, so pool type must be + /// restricted to those with known types in the constructor. This function should be updated if new pool types are needed. + /// @param initCode The init code of the pool + /// @param remoteChainConfig The remote chain config for the pool + /// @param remoteTokenAddress The address of the remote token + /// @param poolType The type of pool to deploy + /// @return bytes32 hash of the init code to be used in the deterministic address calculation + function _generatePoolInitcodeHash( + bytes memory initCode, + RemoteChainConfig memory remoteChainConfig, + address remoteTokenAddress, + PoolType poolType + ) internal pure virtual returns (bytes32) { + if (poolType == PoolType.BURN_MINT) { + return keccak256( + abi.encodePacked( + initCode, + // constructor(address, address[], address, address) + abi.encode( + remoteTokenAddress, new address[](0), remoteChainConfig.remoteRMNProxy, remoteChainConfig.remoteRouter + ) + ) + ); + } else { + // if poolType is PoolType.LOCK_RELEASE, but may be expanded in future versions + return keccak256( + abi.encodePacked( + initCode, + // constructor(address, address[], address, bool, address) + abi.encode( + remoteTokenAddress, new address[](0), remoteChainConfig.remoteRMNProxy, true, remoteChainConfig.remoteRouter + ) + ) + ); + } + } + + /// @notice Sets the token pool address in the token admin registry for a newly deployed token pool. + /// @dev this function should only be called when the token is deployed by this contract as well, otherwise + /// the token pool will not be able to be set in the token admin registry, and this function will revert. + /// @param token The address of the token to set the pool for + /// @param pool The address of the pool to set in the token admin registry + function _setTokenPoolInTokenAdminRegistry(address token, address pool) private { + i_registryModuleOwnerCustom.registerAdminViaOwner(token); + i_tokenAdminRegistry.acceptAdminRole(token); + i_tokenAdminRegistry.setPool(token, pool); + + // Begin the 2 admin transfer process which must be accepted in a separate tx. + i_tokenAdminRegistry.transferAdminRole(token, msg.sender); + } +} diff --git a/contracts/src/v0.8/shared/test/token/ERC677/OpStackBurnMintERC677.t.sol b/contracts/src/v0.8/shared/test/token/ERC677/OpStackBurnMintERC677.t.sol index 614b3bea15..b084ebd38a 100644 --- a/contracts/src/v0.8/shared/test/token/ERC677/OpStackBurnMintERC677.t.sol +++ b/contracts/src/v0.8/shared/test/token/ERC677/OpStackBurnMintERC677.t.sol @@ -6,6 +6,7 @@ import {IOptimismMintableERC20Minimal, IOptimismMintableERC20} from "../../../to import {IERC677} from "../../../token/ERC677/IERC677.sol"; import {BurnMintERC677} from "../../../token/ERC677/BurnMintERC677.sol"; + import {BaseTest} from "../../BaseTest.t.sol"; import {OpStackBurnMintERC677} from "../../../token/ERC677/OpStackBurnMintERC677.sol"; diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Create2.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Create2.sol new file mode 100644 index 0000000000..fe0a6edd56 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Create2.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/Create2.sol) + +pragma solidity ^0.8.20; + +import {Errors} from "./Errors.sol"; + +/** + * @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer. + * `CREATE2` can be used to compute in advance the address where a smart + * contract will be deployed, which allows for interesting new mechanisms known + * as 'counterfactual interactions'. + * + * See the https://eips.ethereum.org/EIPS/eip-1014#motivation[EIP] for more + * information. + */ +library Create2 { + /** + * @dev There's no code to deploy. + */ + error Create2EmptyBytecode(); + + /** + * @dev Deploys a contract using `CREATE2`. The address where the contract + * will be deployed can be known in advance via {computeAddress}. + * + * The bytecode for a contract can be obtained from Solidity with + * `type(contractName).creationCode`. + * + * Requirements: + * + * - `bytecode` must not be empty. + * - `salt` must have not been used for `bytecode` already. + * - the factory must have a balance of at least `amount`. + * - if `amount` is non-zero, `bytecode` must have a `payable` constructor. + */ + function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address addr) { + if (address(this).balance < amount) { + revert Errors.InsufficientBalance(address(this).balance, amount); + } + if (bytecode.length == 0) { + revert Create2EmptyBytecode(); + } + assembly ("memory-safe") { + addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt) + // if no address was created, and returndata is not empty, bubble revert + if and(iszero(addr), not(iszero(returndatasize()))) { + let p := mload(0x40) + returndatacopy(p, 0, returndatasize()) + revert(p, returndatasize()) + } + } + if (addr == address(0)) { + revert Errors.FailedDeployment(); + } + } + + /** + * @dev Returns the address where a contract will be stored if deployed via {deploy}. Any change in the + * `bytecodeHash` or `salt` will result in a new destination address. + */ + function computeAddress(bytes32 salt, bytes32 bytecodeHash) internal view returns (address) { + return computeAddress(salt, bytecodeHash, address(this)); + } + + /** + * @dev Returns the address where a contract will be stored if deployed via {deploy} from a contract located at + * `deployer`. If `deployer` is this contract's address, returns the same value as {computeAddress}. + */ + function computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer) internal pure returns (address addr) { + assembly ("memory-safe") { + let ptr := mload(0x40) // Get free memory pointer + + // | | ↓ ptr ... ↓ ptr + 0x0B (start) ... ↓ ptr + 0x20 ... ↓ ptr + 0x40 ... | + // |-------------------|---------------------------------------------------------------------------| + // | bytecodeHash | CCCCCCCCCCCCC...CC | + // | salt | BBBBBBBBBBBBB...BB | + // | deployer | 000000...0000AAAAAAAAAAAAAAAAAAA...AA | + // | 0xFF | FF | + // |-------------------|---------------------------------------------------------------------------| + // | memory | 000000...00FFAAAAAAAAAAAAAAAAAAA...AABBBBBBBBBBBBB...BBCCCCCCCCCCCCC...CC | + // | keccak(start, 85) | ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ | + + mstore(add(ptr, 0x40), bytecodeHash) + mstore(add(ptr, 0x20), salt) + mstore(ptr, deployer) // Right-aligned with 12 preceding garbage bytes + let start := add(ptr, 0x0b) // The hashed data starts at the final garbage byte which we will set to 0xff + mstore8(start, 0xff) + addr := and(keccak256(start, 85), 0xffffffffffffffffffffffffffffffffffffffff) + } + } +} \ No newline at end of file diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Errors.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Errors.sol new file mode 100644 index 0000000000..cb8833b46e --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v5.0.2/contracts/utils/Errors.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @dev Collection of common custom errors used in multiple contracts + * + * IMPORTANT: Backwards compatibility is not guaranteed in future versions of the library. + * It is recommended to avoid relying on the error API for critical functionality. + */ +library Errors { + /** + * @dev The ETH balance of the account is not enough to perform the operation. + */ + error InsufficientBalance(uint256 balance, uint256 needed); + + /** + * @dev A call to an address target failed. The target may have reverted. + */ + error FailedCall(); + + /** + * @dev The deployment failed. + */ + error FailedDeployment(); + + /** + * @dev A necessary precompile is missing. + */ + error MissingPrecompile(address); +} \ No newline at end of file