From ba1c74c7798996a7f285002ecb5a5e293c455257 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 26 Sep 2024 15:59:43 -0400 Subject: [PATCH] add support for lock-release pools --- .../tokenAdminRegistry/TokenPoolFactory.t.sol | 146 +++++++++++++++++- .../TokenPoolFactory/TokenPoolFactory.sol | 103 +++++++----- 2 files changed, 204 insertions(+), 45 deletions(-) diff --git a/contracts/src/v0.8/ccip/test/tokenAdminRegistry/TokenPoolFactory.t.sol b/contracts/src/v0.8/ccip/test/tokenAdminRegistry/TokenPoolFactory.t.sol index 2a6ba093e3..bf7e04dbe6 100644 --- a/contracts/src/v0.8/ccip/test/tokenAdminRegistry/TokenPoolFactory.t.sol +++ b/contracts/src/v0.8/ccip/test/tokenAdminRegistry/TokenPoolFactory.t.sol @@ -7,10 +7,12 @@ 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 {FactoryBurnMintERC20} from "../../tokenAdminRegistry/TokenPoolFactory/FactoryBurnMintERC20.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"; @@ -111,7 +113,8 @@ contract TokenPoolFactoryTests is TokenPoolFactorySetup { s_tokenInitCode, s_poolInitCode, poolCreationParams, - FAKE_SALT + FAKE_SALT, + TokenPoolFactory.PoolType.BURN_MINT ); assertNotEq(address(0), tokenAddress, "Token Address should not be 0"); @@ -164,6 +167,7 @@ contract TokenPoolFactoryTests is TokenPoolFactorySetup { "", // remotePoolAddress type(BurnMintTokenPool).creationCode, // remotePoolInitCode remoteChainConfig, // remoteChainConfig + TokenPoolFactory.PoolType.BURN_MINT, // poolType "", // remoteTokenAddress s_tokenInitCode, // remoteTokenInitCode RateLimiter.Config(false, 0, 0) @@ -182,7 +186,8 @@ contract TokenPoolFactoryTests is TokenPoolFactorySetup { s_tokenInitCode, s_poolInitCode, "", - FAKE_SALT + FAKE_SALT, + TokenPoolFactory.PoolType.BURN_MINT ); // Ensure that the remote Token was set to the one we predicted @@ -227,7 +232,8 @@ contract TokenPoolFactoryTests is TokenPoolFactorySetup { s_tokenInitCode, s_poolInitCode, "", - FAKE_SALT + FAKE_SALT, + TokenPoolFactory.PoolType.BURN_MINT ); assertEq( @@ -287,6 +293,7 @@ contract TokenPoolFactoryTests is TokenPoolFactorySetup { "", // 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 @@ -299,7 +306,8 @@ contract TokenPoolFactoryTests is TokenPoolFactorySetup { s_tokenInitCode, s_poolInitCode, "", - FAKE_SALT + FAKE_SALT, + TokenPoolFactory.PoolType.BURN_MINT ); assertEq(address(TokenPool(poolAddress).getToken()), tokenAddress, "Token Address should have been set locally"); @@ -344,7 +352,8 @@ contract TokenPoolFactoryTests is TokenPoolFactorySetup { new TokenPoolFactory.RemoteTokenPoolInfo[](0), s_poolInitCode, "", - FAKE_SALT + FAKE_SALT, + TokenPoolFactory.PoolType.BURN_MINT ); assertEq( @@ -381,6 +390,7 @@ contract TokenPoolFactoryTests is TokenPoolFactorySetup { 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 @@ -391,7 +401,8 @@ contract TokenPoolFactoryTests is TokenPoolFactorySetup { s_tokenInitCode, s_poolInitCode, poolCreationParams, - FAKE_SALT + FAKE_SALT, + TokenPoolFactory.PoolType.BURN_MINT ); assertNotEq(address(0), tokenAddress, "Token Address should not be 0"); @@ -419,4 +430,125 @@ contract TokenPoolFactoryTests is TokenPoolFactorySetup { 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), + s_tokenInitCode, + type(LockReleaseTokenPool).creationCode, + "", + FAKE_SALT, + TokenPoolFactory.PoolType.LOCK_RELEASE + ); + + 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/TokenPoolFactory.sol b/contracts/src/v0.8/ccip/tokenAdminRegistry/TokenPoolFactory/TokenPoolFactory.sol index baef42b20d..9fadb4ffc5 100644 --- a/contracts/src/v0.8/ccip/tokenAdminRegistry/TokenPoolFactory/TokenPoolFactory.sol +++ b/contracts/src/v0.8/ccip/tokenAdminRegistry/TokenPoolFactory/TokenPoolFactory.sol @@ -24,9 +24,9 @@ contract TokenPoolFactory is OwnerIsCreator, ITypeAndVersion { error InvalidZeroAddress(); enum PoolType { - BurnMint, - LockRelease - Custom + BURN_MINT, + LOCK_RELEASE, + CUSTOM } struct RemoteTokenPoolInfo { @@ -38,12 +38,9 @@ contract TokenPoolFactory is OwnerIsCreator, ITypeAndVersion { // If the empty parameter flag is provided, the address will be predicted bytes remotePoolInitCode; // The addresses of the remote RMNProxy and Router to use as immutable params and the factory to use in - // address calculations - - - bytes remotePoolInitArgs; - // RemoteChainConfig remoteChainConfig; RemoteChainConfig remoteChainConfig; + // The type of pool to deploy, as different pools require different constructor params + PoolType poolType; // the address of the remote token bytes remoteTokenAddress; // The init code for the remote token if it needs to be deployed @@ -61,11 +58,6 @@ contract TokenPoolFactory is OwnerIsCreator, ITypeAndVersion { address remoteRMNProxy; // The RMNProxy contract on the remote chain } - struct RemoteChainConfigUpdate { - uint64 remoteChainSelector; - RemoteChainConfig remoteChainConfig; - } - string public constant typeAndVersion = "TokenPoolFactory 1.0.0-dev"; ITokenAdminRegistry private immutable i_tokenAdminRegistry; @@ -104,7 +96,8 @@ contract TokenPoolFactory is OwnerIsCreator, ITypeAndVersion { bytes memory tokenInitCode, bytes calldata tokenPoolInitCode, bytes memory tokenPoolInitArgs, - bytes32 salt + 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 @@ -114,7 +107,7 @@ contract TokenPoolFactory is OwnerIsCreator, ITypeAndVersion { address token = Create2.deploy(0, salt, tokenInitCode); // Deploy the token pool - address pool = _createTokenPool(token, remoteTokenPools, tokenPoolInitCode, tokenPoolInitArgs, salt); + address pool = _createTokenPool(token, remoteTokenPools, tokenPoolInitCode, tokenPoolInitArgs, 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); @@ -141,14 +134,15 @@ contract TokenPoolFactory is OwnerIsCreator, ITypeAndVersion { RemoteTokenPoolInfo[] calldata remoteTokenPools, bytes calldata tokenPoolInitCode, bytes memory tokenPoolInitArgs, - bytes32 salt + 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, tokenPoolInitArgs, salt); + return _createTokenPool(token, remoteTokenPools, tokenPoolInitCode, tokenPoolInitArgs, salt, poolType); } // ================================================================ @@ -168,7 +162,8 @@ contract TokenPoolFactory is OwnerIsCreator, ITypeAndVersion { RemoteTokenPoolInfo[] calldata remoteTokenPools, bytes calldata tokenPoolInitCode, bytes memory tokenPoolInitArgs, - bytes32 salt + 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); @@ -194,26 +189,16 @@ contract TokenPoolFactory is OwnerIsCreator, ITypeAndVersion { // 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) { - // Generate the initCode that will be used on the remote chain. It is assumed that tokenInitCode - // will be the same on all chains, so it can be reused here. - - // Combine the initCode with the initArgs to create the full deployment code - bytes32 remotePoolInitcode = keccak256( - bytes.concat( - remoteTokenPool.remotePoolInitCode, - // Calculate the remote pool Args with an empty allowList, remote RMN, and Remote Router addresses. - abi.encode( - abi.decode(remoteTokenPool.remoteTokenAddress, (address)), - new address[](0), - remoteTokenPool.remoteChainConfig.remoteRMNProxy, - remoteTokenPool.remoteChainConfig.remoteRouter - ) - ) + 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(remotePoolInitcode, remoteTokenPool.remoteChainConfig.remotePoolFactory) + salt.computeAddress(remotePoolInitcodeHash, remoteTokenPool.remoteChainConfig.remotePoolFactory) ); } @@ -227,11 +212,12 @@ contract TokenPoolFactory is OwnerIsCreator, ITypeAndVersion { }); } - // If the user doesn't want to provide any special parameters which may be needed for a custom the token pool then - // use the standard burn/mint token pool params. Since the user can provide custom token pool init code, - // they must also provide custom constructor args. if (tokenPoolInitArgs.length == 0) { - tokenPoolInitArgs = abi.encode(token, new address[](0), i_rmnProxy, i_ccipRouter); + 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 constructor param that must be accounted for + 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 @@ -246,6 +232,47 @@ contract TokenPoolFactory is OwnerIsCreator, ITypeAndVersion { return poolAddress; } + function _generatePoolInitcodeHash( + bytes memory initCode, + RemoteChainConfig memory remoteChainConfig, + address remoteTokenAddress, + PoolType poolType + ) private pure 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 == PoolType.LOCK_RELEASE) { + return + keccak256( + abi.encodePacked( + initCode, + // constructor(address, address[], address, bool, address) + abi.encode( + remoteTokenAddress, + new address[](0), + remoteChainConfig.remoteRMNProxy, + true, + remoteChainConfig.remoteRouter + ) + ) + ); + } else { + // TODO Figure out Custom + return bytes32(0); + } + } + /// @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.