diff --git a/contracts/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol new file mode 100644 index 0000000000..df35f45684 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Initializable} from "solidity-utils/contracts/transparent-proxy/Initializable.sol"; +import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; +import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; + +import {UpgradeableBurnMintTokenPoolAbstract} from "./UpgradeableBurnMintTokenPoolAbstract.sol"; +import {UpgradeableTokenPool} from "./UpgradeableTokenPool.sol"; + +import {IRouter} from "../../interfaces/IRouter.sol"; + +/// @title UpgradeableBurnMintTokenPool +/// @author Aave Labs +/// @notice Upgradeable version of Chainlink's CCIP BurnMintTokenPool +/// @dev Contract adaptations: +/// - Implementation of Initializable to allow upgrades +/// - Move of allowlist and router definition to initialization stage +/// - Inclusion of rate limit admin who may configure rate limits in addition to owner +contract UpgradeableBurnMintTokenPool is UpgradeableBurnMintTokenPoolAbstract, ITypeAndVersion, Initializable { + string public constant override typeAndVersion = "BurnMintTokenPool 1.5.0"; + + /// @dev Constructor + /// @param token The bridgeable token that is managed by this pool. + /// @param rmnProxy The address of the arm proxy + /// @param allowlistEnabled True if pool is set to access-controlled mode, false otherwise + constructor( + IBurnMintERC20 token, + address rmnProxy, + bool allowlistEnabled + ) UpgradeableTokenPool(token, rmnProxy, allowlistEnabled) { + _disableInitializers(); + } + + /// @dev Initializer + /// @dev The address passed as `owner_` must accept ownership after initialization. + /// @dev The `allowlist` is only effective if pool is set to access-controlled mode + /// @param owner_ The address of the owner + /// @param allowlist A set of addresses allowed to trigger lockOrBurn as original senders + /// @param router The address of the router + function initialize(address owner_, address[] memory allowlist, address router) public virtual initializer { + if (owner_ == address(0) || router == address(0)) revert ZeroAddressNotAllowed(); + _transferOwnership(owner_); + + s_router = IRouter(router); + + // Pool can be set as permissioned or permissionless at deployment time only to save hot-path gas. + if (i_allowlistEnabled) { + _applyAllowListUpdates(new address[](0), allowlist); + } + } + + /// @inheritdoc UpgradeableBurnMintTokenPoolAbstract + function _burn(uint256 amount) internal virtual override { + IBurnMintERC20(address(i_token)).burn(amount); + } +} diff --git a/contracts/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPoolAbstract.sol b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPoolAbstract.sol new file mode 100644 index 0000000000..0e73ccc1d6 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPoolAbstract.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; + +import {Pool} from "../../libraries/Pool.sol"; +import {UpgradeableTokenPool} from "./UpgradeableTokenPool.sol"; + +abstract contract UpgradeableBurnMintTokenPoolAbstract is UpgradeableTokenPool { + /// @notice Contains the specific burn call for a pool. + /// @dev overriding this method allows us to create pools with different burn signatures + /// without duplicating the underlying logic. + function _burn(uint256 amount) internal virtual; + + /// @notice Burn the token in the pool + /// @dev The _validateLockOrBurn check is an essential security check + function lockOrBurn( + Pool.LockOrBurnInV1 calldata lockOrBurnIn + ) external virtual override returns (Pool.LockOrBurnOutV1 memory) { + _validateLockOrBurn(lockOrBurnIn); + + _burn(lockOrBurnIn.amount); + + emit Burned(msg.sender, lockOrBurnIn.amount); + + return Pool.LockOrBurnOutV1({destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: ""}); + } + + /// @notice Mint tokens from the pool to the recipient + /// @dev The _validateReleaseOrMint check is an essential security check + function releaseOrMint( + Pool.ReleaseOrMintInV1 calldata releaseOrMintIn + ) external virtual override returns (Pool.ReleaseOrMintOutV1 memory) { + _validateReleaseOrMint(releaseOrMintIn); + + // Mint to the receiver + IBurnMintERC20(address(i_token)).mint(releaseOrMintIn.receiver, releaseOrMintIn.amount); + + emit Minted(msg.sender, releaseOrMintIn.receiver, releaseOrMintIn.amount); + + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.amount}); + } +} diff --git a/contracts/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol new file mode 100644 index 0000000000..70adc38030 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Initializable} from "solidity-utils/contracts/transparent-proxy/Initializable.sol"; + +import {ILiquidityContainer} from "../../../liquiditymanager/interfaces/ILiquidityContainer.sol"; +import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; + +import {Pool} from "../../libraries/Pool.sol"; +import {UpgradeableTokenPool} from "./UpgradeableTokenPool.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IRouter} from "../../interfaces/IRouter.sol"; + +/// @title UpgradeableLockReleaseTokenPool +/// @author Aave Labs +/// @notice Upgradeable version of Chainlink's CCIP LockReleaseTokenPool +/// @dev Contract adaptations: +/// - Implementation of Initializable to allow upgrades +/// - Move of allowlist and router definition to initialization stage +/// - Addition of a bridge limit to regulate the maximum amount of tokens that can be transferred out (burned/locked) +contract UpgradeableLockReleaseTokenPool is UpgradeableTokenPool, ILiquidityContainer, ITypeAndVersion, Initializable { + using SafeERC20 for IERC20; + + error InsufficientLiquidity(); + error LiquidityNotAccepted(); + error BridgeLimitExceeded(uint256 bridgeLimit); + error NotEnoughBridgedAmount(); + + event BridgeLimitUpdated(uint256 oldBridgeLimit, uint256 newBridgeLimit); + event BridgeLimitAdminUpdated(address indexed oldAdmin, address indexed newAdmin); + + event LiquidityTransferred(address indexed from, uint256 amount); + + string public constant override typeAndVersion = "LockReleaseTokenPool 1.5.0"; + + /// @dev Whether or not the pool accepts liquidity. + /// External liquidity is not required when there is one canonical token deployed to a chain, + /// and CCIP is facilitating mint/burn on all the other chains, in which case the invariant + /// balanceOf(pool) on home chain >= sum(totalSupply(mint/burn "wrapped" token) on all remote chains) should always hold + bool internal immutable i_acceptLiquidity; + /// @notice The address of the rebalancer. + address internal s_rebalancer; + + /// @notice Maximum amount of tokens that can be bridged to other chains + uint256 private s_bridgeLimit; + /// @notice Amount of tokens bridged (transferred out) + /// @dev Must always be equal to or below the bridge limit + uint256 private s_currentBridged; + /// @notice The address of the bridge limit admin. + /// @dev Can be address(0) if none is configured. + address internal s_bridgeLimitAdmin; + + // / @dev Constructor + // / @param token The bridgeable token that is managed by this pool. + // / @param rmnProxy The address of the rmn proxy + // / @param allowlistEnabled True if pool is set to access-controlled mode, false otherwise + // / @param acceptLiquidity True if the pool accepts liquidity, false otherwise + constructor( + IERC20 token, + address rmnProxy, + bool allowListEnabled, + bool acceptLiquidity + ) UpgradeableTokenPool(token, rmnProxy, allowListEnabled) { + i_acceptLiquidity = acceptLiquidity; + + _disableInitializers(); + } + + /// @dev Initializer + /// @dev The address passed as `owner_` must accept ownership after initialization. + /// @dev The `allowlist` is only effective if pool is set to access-controlled mode + /// @param owner_ The address of the owner + /// @param allowlist A set of addresses allowed to trigger lockOrBurn as original senders + /// @param router The address of the router + /// @param bridgeLimit The maximum amount of tokens that can be bridged to other chains + function initialize( + address owner_, + address[] memory allowlist, + address router, + uint256 bridgeLimit + ) public initializer { + if (router == address(0) || owner_ == address(0)) revert ZeroAddressNotAllowed(); + + _transferOwnership(owner_); + s_router = IRouter(router); + if (i_allowlistEnabled) _applyAllowListUpdates(new address[](0), allowlist); + s_bridgeLimit = bridgeLimit; + } + + /// @notice Locks the token in the pool + /// @dev The _validateLockOrBurn check is an essential security check + function lockOrBurn( + Pool.LockOrBurnInV1 calldata lockOrBurnIn + ) external virtual override returns (Pool.LockOrBurnOutV1 memory) { + // Increase bridged amount because tokens are leaving the source chain + if ((s_currentBridged += lockOrBurnIn.amount) > s_bridgeLimit) revert BridgeLimitExceeded(s_bridgeLimit); + + _validateLockOrBurn(lockOrBurnIn); + + emit Locked(msg.sender, lockOrBurnIn.amount); + + return Pool.LockOrBurnOutV1({destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: ""}); + } + + /// @notice Release tokens from the pool to the recipient + /// @dev The _validateReleaseOrMint check is an essential security check + function releaseOrMint( + Pool.ReleaseOrMintInV1 calldata releaseOrMintIn + ) external virtual override returns (Pool.ReleaseOrMintOutV1 memory) { + // This should never occur. Amount should never exceed the current bridged amount + if (releaseOrMintIn.amount > s_currentBridged) revert NotEnoughBridgedAmount(); + // Reduce bridged amount because tokens are back to source chain + s_currentBridged -= releaseOrMintIn.amount; + + _validateReleaseOrMint(releaseOrMintIn); + + // Release to the recipient + getToken().safeTransfer(releaseOrMintIn.receiver, releaseOrMintIn.amount); + + emit Released(msg.sender, releaseOrMintIn.receiver, releaseOrMintIn.amount); + + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.amount}); + } + + /// @notice Sets the bridge limit, the maximum amount of tokens that can be bridged out + /// @dev Only callable by the owner or the bridge limit admin or owner. + /// @dev Bridge limit changes should be carefully managed, specially when reducing below the current bridged amount + /// @param newBridgeLimit The new bridge limit + function setBridgeLimit(uint256 newBridgeLimit) external { + if (msg.sender != s_bridgeLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); + uint256 oldBridgeLimit = s_bridgeLimit; + s_bridgeLimit = newBridgeLimit; + emit BridgeLimitUpdated(oldBridgeLimit, newBridgeLimit); + } + + /// @notice Sets the bridge limit admin address. + /// @dev Only callable by the owner. + /// @param bridgeLimitAdmin The new bridge limit admin address. + function setBridgeLimitAdmin(address bridgeLimitAdmin) external onlyOwner { + address oldAdmin = s_bridgeLimitAdmin; + s_bridgeLimitAdmin = bridgeLimitAdmin; + emit BridgeLimitAdminUpdated(oldAdmin, bridgeLimitAdmin); + } + + /// @notice Gets the bridge limit + /// @return The maximum amount of tokens that can be transferred out to other chains + function getBridgeLimit() external view virtual returns (uint256) { + return s_bridgeLimit; + } + + /// @notice Gets the current bridged amount to other chains + /// @return The amount of tokens transferred out to other chains + function getCurrentBridgedAmount() external view virtual returns (uint256) { + return s_currentBridged; + } + + // @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == type(ILiquidityContainer).interfaceId || super.supportsInterface(interfaceId); + } + + /// @notice Gets LiquidityManager, can be address(0) if none is configured. + /// @return The current liquidity manager. + function getRebalancer() external view returns (address) { + return s_rebalancer; + } + + /// @notice Gets the bridge limiter admin address. + function getBridgeLimitAdmin() external view returns (address) { + return s_bridgeLimitAdmin; + } + + /// @notice Sets the LiquidityManager address. + /// @dev Only callable by the owner. + function setRebalancer(address rebalancer) external onlyOwner { + s_rebalancer = rebalancer; + } + + /// @notice Checks if the pool can accept liquidity. + /// @return true if the pool can accept liquidity, false otherwise. + function canAcceptLiquidity() external view returns (bool) { + return i_acceptLiquidity; + } + + /// @notice Adds liquidity to the pool. The tokens should be approved first. + /// @param amount The amount of liquidity to provide. + function provideLiquidity(uint256 amount) external { + if (!i_acceptLiquidity) revert LiquidityNotAccepted(); + if (s_rebalancer != msg.sender) revert Unauthorized(msg.sender); + + i_token.safeTransferFrom(msg.sender, address(this), amount); + emit LiquidityAdded(msg.sender, amount); + } + + /// @notice Removed liquidity to the pool. The tokens will be sent to msg.sender. + /// @param amount The amount of liquidity to remove. + function withdrawLiquidity(uint256 amount) external { + if (s_rebalancer != msg.sender) revert Unauthorized(msg.sender); + + if (i_token.balanceOf(address(this)) < amount) revert InsufficientLiquidity(); + i_token.safeTransfer(msg.sender, amount); + emit LiquidityRemoved(msg.sender, amount); + } + + /// @notice This function can be used to transfer liquidity from an older version of the pool to this pool. To do so + /// this pool will have to be set as the rebalancer in the older version of the pool. This allows it to transfer the + /// funds in the old pool to the new pool. + /// @dev When upgrading a LockRelease pool, this function can be called at the same time as the pool is changed in the + /// TokenAdminRegistry. This allows for a smooth transition of both liquidity and transactions to the new pool. + /// Alternatively, when no multicall is available, a portion of the funds can be transferred to the new pool before + /// changing which pool CCIP uses, to ensure both pools can operate. Then the pool should be changed in the + /// TokenAdminRegistry, which will activate the new pool. All new transactions will use the new pool and its + /// liquidity. Finally, the remaining liquidity can be transferred to the new pool using this function one more time. + /// @param from The address of the old pool. + /// @param amount The amount of liquidity to transfer. + function transferLiquidity(address from, uint256 amount) external onlyOwner { + UpgradeableLockReleaseTokenPool(from).withdrawLiquidity(amount); + + emit LiquidityTransferred(from, amount); + } +} diff --git a/contracts/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol new file mode 100644 index 0000000000..58331aa41f --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol @@ -0,0 +1,436 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IPoolV1} from "../../interfaces/IPool.sol"; +import {IRMN} from "../../interfaces/IRMN.sol"; +import {IRouter} from "../../interfaces/IRouter.sol"; + +import {OwnerIsCreator} from "../../../shared/access/OwnerIsCreator.sol"; +import {Pool} from "../../libraries/Pool.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {IERC165} from "../../../vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol"; +import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice Base abstract class with common functions for all token pools. +/// A token pool serves as isolated place for holding tokens and token specific logic +/// that may execute as tokens move across the bridge. +abstract contract UpgradeableTokenPool is IPoolV1, OwnerIsCreator { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.UintSet; + using RateLimiter for RateLimiter.TokenBucket; + + error CallerIsNotARampOnRouter(address caller); + error ZeroAddressNotAllowed(); + error SenderNotAllowed(address sender); + error AllowListNotEnabled(); + error NonExistentChain(uint64 remoteChainSelector); + error ChainNotAllowed(uint64 remoteChainSelector); + error CursedByRMN(); + error ChainAlreadyExists(uint64 chainSelector); + error InvalidSourcePoolAddress(bytes sourcePoolAddress); + error InvalidToken(address token); + error Unauthorized(address caller); + + event Locked(address indexed sender, uint256 amount); + event Burned(address indexed sender, uint256 amount); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event ChainAdded( + uint64 remoteChainSelector, + bytes remoteToken, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + event ChainConfigured( + uint64 remoteChainSelector, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + event ChainRemoved(uint64 remoteChainSelector); + event RemotePoolSet(uint64 indexed remoteChainSelector, bytes previousPoolAddress, bytes remotePoolAddress); + event AllowListAdd(address sender); + event AllowListRemove(address sender); + event RouterUpdated(address oldRouter, address newRouter); + + struct ChainUpdate { + uint64 remoteChainSelector; // ──╮ Remote chain selector + bool allowed; // ────────────────╯ Whether the chain should be enabled + bytes remotePoolAddress; // Address of the remote pool, ABI encoded in the case of a remote EVM chain. + bytes remoteTokenAddress; // Address of the remote token, ABI encoded in the case of a remote EVM chain. + RateLimiter.Config outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain + RateLimiter.Config inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain + } + + struct RemoteChainConfig { + RateLimiter.TokenBucket outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain + RateLimiter.TokenBucket inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain + bytes remotePoolAddress; // Address of the remote pool, ABI encoded in the case of a remote EVM chain. + bytes remoteTokenAddress; // Address of the remote token, ABI encoded in the case of a remote EVM chain. + } + + /// @dev The bridgeable token that is managed by this pool. + IERC20 internal immutable i_token; + /// @dev The address of the RMN proxy + address internal immutable i_rmnProxy; + /// @dev The immutable flag that indicates if the pool is access-controlled. + bool internal immutable i_allowlistEnabled; + /// @dev A set of addresses allowed to trigger lockOrBurn as original senders. + /// Only takes effect if i_allowlistEnabled is true. + /// This can be used to ensure only token-issuer specified addresses can + /// move tokens. + EnumerableSet.AddressSet internal s_allowList; + /// @dev The address of the router + IRouter internal s_router; + /// @dev A set of allowed chain selectors. We want the allowlist to be enumerable to + /// be able to quickly determine (without parsing logs) who can access the pool. + /// @dev The chain selectors are in uint256 format because of the EnumerableSet implementation. + EnumerableSet.UintSet internal s_remoteChainSelectors; + mapping(uint64 remoteChainSelector => RemoteChainConfig) internal s_remoteChainConfigs; + /// @notice The address of the rate limiter admin. + /// @dev Can be address(0) if none is configured. + address internal s_rateLimitAdmin; + + constructor(IERC20 token, address rmnProxy, bool allowListEnabled) { + if (address(token) == address(0) || rmnProxy == address(0)) revert ZeroAddressNotAllowed(); + i_token = token; + i_rmnProxy = rmnProxy; + + // Pool can be set as permissioned or permissionless at deployment time only to save hot-path gas. + i_allowlistEnabled = allowListEnabled; + } + + /// @notice Get RMN proxy address + /// @return rmnProxy Address of RMN proxy + function getRmnProxy() public view returns (address rmnProxy) { + return i_rmnProxy; + } + + /// @inheritdoc IPoolV1 + function isSupportedToken(address token) public view virtual returns (bool) { + return token == address(i_token); + } + + /// @notice Gets the IERC20 token that this pool can lock or burn. + /// @return token The IERC20 token representation. + function getToken() public view returns (IERC20 token) { + return i_token; + } + + /// @notice Gets the pool's Router + /// @return router The pool's Router + function getRouter() public view returns (address router) { + return address(s_router); + } + + /// @notice Sets the pool's Router + /// @param newRouter The new Router + function setRouter(address newRouter) public onlyOwner { + if (newRouter == address(0)) revert ZeroAddressNotAllowed(); + address oldRouter = address(s_router); + s_router = IRouter(newRouter); + + emit RouterUpdated(oldRouter, newRouter); + } + + /// @notice Signals which version of the pool interface is supported + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return + interfaceId == Pool.CCIP_POOL_V1 || + interfaceId == type(IPoolV1).interfaceId || + interfaceId == type(IERC165).interfaceId; + } + + // ================================================================ + // │ Validation │ + // ================================================================ + + /// @notice Validates the lock or burn input for correctness on + /// - token to be locked or burned + /// - RMN curse status + /// - allowlist status + /// - if the sender is a valid onRamp + /// - rate limit status + /// @param lockOrBurnIn The input to validate. + /// @dev This function should always be called before executing a lock or burn. Not doing so would allow + /// for various exploits. + function _validateLockOrBurn(Pool.LockOrBurnInV1 memory lockOrBurnIn) internal { + if (!isSupportedToken(lockOrBurnIn.localToken)) revert InvalidToken(lockOrBurnIn.localToken); + if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(lockOrBurnIn.remoteChainSelector)))) revert CursedByRMN(); + _checkAllowList(lockOrBurnIn.originalSender); + + _onlyOnRamp(lockOrBurnIn.remoteChainSelector); + _consumeOutboundRateLimit(lockOrBurnIn.remoteChainSelector, lockOrBurnIn.amount); + } + + /// @notice Validates the release or mint input for correctness on + /// - token to be released or minted + /// - RMN curse status + /// - if the sender is a valid offRamp + /// - if the source pool is valid + /// - rate limit status + /// @param releaseOrMintIn The input to validate. + /// @dev This function should always be called before executing a release or mint. Not doing so would allow + /// for various exploits. + function _validateReleaseOrMint(Pool.ReleaseOrMintInV1 memory releaseOrMintIn) internal { + if (!isSupportedToken(releaseOrMintIn.localToken)) revert InvalidToken(releaseOrMintIn.localToken); + if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(releaseOrMintIn.remoteChainSelector)))) revert CursedByRMN(); + _onlyOffRamp(releaseOrMintIn.remoteChainSelector); + + // Validates that the source pool address is configured on this pool. + bytes memory configuredRemotePool = getRemotePool(releaseOrMintIn.remoteChainSelector); + if ( + configuredRemotePool.length == 0 || + keccak256(releaseOrMintIn.sourcePoolAddress) != keccak256(configuredRemotePool) + ) { + revert InvalidSourcePoolAddress(releaseOrMintIn.sourcePoolAddress); + } + _consumeInboundRateLimit(releaseOrMintIn.remoteChainSelector, releaseOrMintIn.amount); + } + + // ================================================================ + // │ Chain permissions │ + // ================================================================ + + /// @notice Gets the pool address on the remote chain. + /// @param remoteChainSelector Remote chain selector. + /// @dev To support non-evm chains, this value is encoded into bytes + function getRemotePool(uint64 remoteChainSelector) public view returns (bytes memory) { + return s_remoteChainConfigs[remoteChainSelector].remotePoolAddress; + } + + /// @notice Gets the token address on the remote chain. + /// @param remoteChainSelector Remote chain selector. + /// @dev To support non-evm chains, this value is encoded into bytes + function getRemoteToken(uint64 remoteChainSelector) public view returns (bytes memory) { + return s_remoteChainConfigs[remoteChainSelector].remoteTokenAddress; + } + + /// @notice Sets the remote pool address for a given chain selector. + /// @param remoteChainSelector The remote chain selector for which the remote pool address is being set. + /// @param remotePoolAddress The address of the remote pool. + function setRemotePool(uint64 remoteChainSelector, bytes calldata remotePoolAddress) external onlyOwner { + if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector); + + bytes memory prevAddress = s_remoteChainConfigs[remoteChainSelector].remotePoolAddress; + s_remoteChainConfigs[remoteChainSelector].remotePoolAddress = remotePoolAddress; + + emit RemotePoolSet(remoteChainSelector, prevAddress, remotePoolAddress); + } + + /// @inheritdoc IPoolV1 + function isSupportedChain(uint64 remoteChainSelector) public view returns (bool) { + return s_remoteChainSelectors.contains(remoteChainSelector); + } + + /// @notice Get list of allowed chains + /// @return list of chains. + function getSupportedChains() public view returns (uint64[] memory) { + uint256[] memory uint256ChainSelectors = s_remoteChainSelectors.values(); + uint64[] memory chainSelectors = new uint64[](uint256ChainSelectors.length); + for (uint256 i = 0; i < uint256ChainSelectors.length; ++i) { + chainSelectors[i] = uint64(uint256ChainSelectors[i]); + } + + return chainSelectors; + } + + /// @notice Sets the permissions for a list of chains selectors. Actual senders for these chains + /// need to be allowed on the Router to interact with this pool. + /// @dev Only callable by the owner + /// @param chains A list of chains and their new permission status & rate limits. Rate limits + /// are only used when the chain is being added through `allowed` being true. + function applyChainUpdates(ChainUpdate[] calldata chains) external virtual onlyOwner { + for (uint256 i = 0; i < chains.length; ++i) { + ChainUpdate memory update = chains[i]; + RateLimiter._validateTokenBucketConfig(update.outboundRateLimiterConfig, !update.allowed); + RateLimiter._validateTokenBucketConfig(update.inboundRateLimiterConfig, !update.allowed); + + if (update.allowed) { + // If the chain already exists, revert + if (!s_remoteChainSelectors.add(update.remoteChainSelector)) { + revert ChainAlreadyExists(update.remoteChainSelector); + } + + if (update.remotePoolAddress.length == 0 || update.remoteTokenAddress.length == 0) { + revert ZeroAddressNotAllowed(); + } + + s_remoteChainConfigs[update.remoteChainSelector] = RemoteChainConfig({ + outboundRateLimiterConfig: RateLimiter.TokenBucket({ + rate: update.outboundRateLimiterConfig.rate, + capacity: update.outboundRateLimiterConfig.capacity, + tokens: update.outboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.outboundRateLimiterConfig.isEnabled + }), + inboundRateLimiterConfig: RateLimiter.TokenBucket({ + rate: update.inboundRateLimiterConfig.rate, + capacity: update.inboundRateLimiterConfig.capacity, + tokens: update.inboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.inboundRateLimiterConfig.isEnabled + }), + remotePoolAddress: update.remotePoolAddress, + remoteTokenAddress: update.remoteTokenAddress + }); + + emit ChainAdded( + update.remoteChainSelector, + update.remoteTokenAddress, + update.outboundRateLimiterConfig, + update.inboundRateLimiterConfig + ); + } else { + // If the chain doesn't exist, revert + if (!s_remoteChainSelectors.remove(update.remoteChainSelector)) { + revert NonExistentChain(update.remoteChainSelector); + } + + delete s_remoteChainConfigs[update.remoteChainSelector]; + + emit ChainRemoved(update.remoteChainSelector); + } + } + } + + // ================================================================ + // │ Rate limiting │ + // ================================================================ + + /// @notice Sets the rate limiter admin address. + /// @dev Only callable by the owner. + /// @param rateLimitAdmin The new rate limiter admin address. + function setRateLimitAdmin(address rateLimitAdmin) external onlyOwner { + s_rateLimitAdmin = rateLimitAdmin; + } + + /// @notice Gets the rate limiter admin address. + function getRateLimitAdmin() external view returns (address) { + return s_rateLimitAdmin; + } + + /// @notice Consumes outbound rate limiting capacity in this pool + function _consumeOutboundRateLimit(uint64 remoteChainSelector, uint256 amount) internal { + s_remoteChainConfigs[remoteChainSelector].outboundRateLimiterConfig._consume(amount, address(i_token)); + } + + /// @notice Consumes inbound rate limiting capacity in this pool + function _consumeInboundRateLimit(uint64 remoteChainSelector, uint256 amount) internal { + s_remoteChainConfigs[remoteChainSelector].inboundRateLimiterConfig._consume(amount, address(i_token)); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function getCurrentOutboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (RateLimiter.TokenBucket memory) { + return s_remoteChainConfigs[remoteChainSelector].outboundRateLimiterConfig._currentTokenBucketState(); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function getCurrentInboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (RateLimiter.TokenBucket memory) { + return s_remoteChainConfigs[remoteChainSelector].inboundRateLimiterConfig._currentTokenBucketState(); + } + + /// @notice Sets the chain rate limiter config. + /// @param remoteChainSelector The remote chain selector for which the rate limits apply. + /// @param outboundConfig The new outbound rate limiter config, meaning the onRamp rate limits for the given chain. + /// @param inboundConfig The new inbound rate limiter config, meaning the offRamp rate limits for the given chain. + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) external { + if (msg.sender != s_rateLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); + + _setRateLimitConfig(remoteChainSelector, outboundConfig, inboundConfig); + } + + function _setRateLimitConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) internal { + if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector); + RateLimiter._validateTokenBucketConfig(outboundConfig, false); + s_remoteChainConfigs[remoteChainSelector].outboundRateLimiterConfig._setTokenBucketConfig(outboundConfig); + RateLimiter._validateTokenBucketConfig(inboundConfig, false); + s_remoteChainConfigs[remoteChainSelector].inboundRateLimiterConfig._setTokenBucketConfig(inboundConfig); + emit ChainConfigured(remoteChainSelector, outboundConfig, inboundConfig); + } + + // ================================================================ + // │ Access │ + // ================================================================ + + /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender + /// is a permissioned onRamp for the given chain on the Router. + function _onlyOnRamp(uint64 remoteChainSelector) internal view { + if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); + if (!(msg.sender == s_router.getOnRamp(remoteChainSelector))) revert CallerIsNotARampOnRouter(msg.sender); + } + + /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender + /// is a permissioned offRamp for the given chain on the Router. + function _onlyOffRamp(uint64 remoteChainSelector) internal view { + if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); + if (!s_router.isOffRamp(remoteChainSelector, msg.sender)) revert CallerIsNotARampOnRouter(msg.sender); + } + + // ================================================================ + // │ Allowlist │ + // ================================================================ + + function _checkAllowList(address sender) internal view { + if (i_allowlistEnabled) { + if (!s_allowList.contains(sender)) { + revert SenderNotAllowed(sender); + } + } + } + + /// @notice Gets whether the allowList functionality is enabled. + /// @return true is enabled, false if not. + function getAllowListEnabled() external view returns (bool) { + return i_allowlistEnabled; + } + + /// @notice Gets the allowed addresses. + /// @return The allowed addresses. + function getAllowList() external view returns (address[] memory) { + return s_allowList.values(); + } + + /// @notice Apply updates to the allow list. + /// @param removes The addresses to be removed. + /// @param adds The addresses to be added. + function applyAllowListUpdates(address[] calldata removes, address[] calldata adds) external onlyOwner { + _applyAllowListUpdates(removes, adds); + } + + /// @notice Internal version of applyAllowListUpdates to allow for reuse in the constructor. + function _applyAllowListUpdates(address[] memory removes, address[] memory adds) internal { + if (!i_allowlistEnabled) revert AllowListNotEnabled(); + + for (uint256 i = 0; i < removes.length; ++i) { + address toRemove = removes[i]; + if (s_allowList.remove(toRemove)) { + emit AllowListRemove(toRemove); + } + } + for (uint256 i = 0; i < adds.length; ++i) { + address toAdd = adds[i]; + if (toAdd == address(0)) { + continue; + } + if (s_allowList.add(toAdd)) { + emit AllowListAdd(toAdd); + } + } + } +} diff --git a/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableBurnMintTokenPoolAbstract_diff.md b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableBurnMintTokenPoolAbstract_diff.md new file mode 100644 index 0000000000..d29e8514b6 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableBurnMintTokenPoolAbstract_diff.md @@ -0,0 +1,24 @@ +```diff +diff --git a/src/v0.8/ccip/pools/BurnMintTokenPoolAbstract.sol b/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPoolAbstract.sol +index 99908c91d0..0e73ccc1d6 100644 +--- a/src/v0.8/ccip/pools/BurnMintTokenPoolAbstract.sol ++++ b/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPoolAbstract.sol +@@ -1,12 +1,12 @@ + // SPDX-License-Identifier: BUSL-1.1 +-pragma solidity 0.8.24; ++pragma solidity ^0.8.0; + +-import {IBurnMintERC20} from "../../shared/token/ERC20/IBurnMintERC20.sol"; ++import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; + +-import {Pool} from "../libraries/Pool.sol"; +-import {TokenPool} from "./TokenPool.sol"; ++import {Pool} from "../../libraries/Pool.sol"; ++import {UpgradeableTokenPool} from "./UpgradeableTokenPool.sol"; + +-abstract contract BurnMintTokenPoolAbstract is TokenPool { ++abstract contract UpgradeableBurnMintTokenPoolAbstract is UpgradeableTokenPool { + /// @notice Contains the specific burn call for a pool. + /// @dev overriding this method allows us to create pools with different burn signatures + /// without duplicating the underlying logic. +``` diff --git a/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableBurnMintTokenPool_diff.md b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableBurnMintTokenPool_diff.md new file mode 100644 index 0000000000..b3259bf512 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableBurnMintTokenPool_diff.md @@ -0,0 +1,79 @@ +```diff +diff --git a/src/v0.8/ccip/pools/BurnMintTokenPool.sol b/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol +index c48c8e51fb..df35f45684 100644 +--- a/src/v0.8/ccip/pools/BurnMintTokenPool.sol ++++ b/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol +@@ -1,29 +1,56 @@ + // SPDX-License-Identifier: BUSL-1.1 +-pragma solidity 0.8.24; ++pragma solidity ^0.8.0; + +-import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +-import {IBurnMintERC20} from "../../shared/token/ERC20/IBurnMintERC20.sol"; ++import {Initializable} from "solidity-utils/contracts/transparent-proxy/Initializable.sol"; ++import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; ++import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; + +-import {BurnMintTokenPoolAbstract} from "./BurnMintTokenPoolAbstract.sol"; +-import {TokenPool} from "./TokenPool.sol"; ++import {UpgradeableBurnMintTokenPoolAbstract} from "./UpgradeableBurnMintTokenPoolAbstract.sol"; ++import {UpgradeableTokenPool} from "./UpgradeableTokenPool.sol"; + +-/// @notice This pool mints and burns a 3rd-party token. +-/// @dev Pool whitelisting mode is set in the constructor and cannot be modified later. +-/// It either accepts any address as originalSender, or only accepts whitelisted originalSender. +-/// The only way to change whitelisting mode is to deploy a new pool. +-/// If that is expected, please make sure the token's burner/minter roles are adjustable. +-/// @dev This contract is a variant of BurnMintTokenPool that uses `burn(amount)`. +-contract BurnMintTokenPool is BurnMintTokenPoolAbstract, ITypeAndVersion { ++import {IRouter} from "../../interfaces/IRouter.sol"; ++ ++/// @title UpgradeableBurnMintTokenPool ++/// @author Aave Labs ++/// @notice Upgradeable version of Chainlink's CCIP BurnMintTokenPool ++/// @dev Contract adaptations: ++/// - Implementation of Initializable to allow upgrades ++/// - Move of allowlist and router definition to initialization stage ++/// - Inclusion of rate limit admin who may configure rate limits in addition to owner ++contract UpgradeableBurnMintTokenPool is UpgradeableBurnMintTokenPoolAbstract, ITypeAndVersion, Initializable { + string public constant override typeAndVersion = "BurnMintTokenPool 1.5.0"; + ++ /// @dev Constructor ++ /// @param token The bridgeable token that is managed by this pool. ++ /// @param rmnProxy The address of the arm proxy ++ /// @param allowlistEnabled True if pool is set to access-controlled mode, false otherwise + constructor( + IBurnMintERC20 token, +- address[] memory allowlist, + address rmnProxy, +- address router +- ) TokenPool(token, allowlist, rmnProxy, router) {} ++ bool allowlistEnabled ++ ) UpgradeableTokenPool(token, rmnProxy, allowlistEnabled) { ++ _disableInitializers(); ++ } ++ ++ /// @dev Initializer ++ /// @dev The address passed as `owner_` must accept ownership after initialization. ++ /// @dev The `allowlist` is only effective if pool is set to access-controlled mode ++ /// @param owner_ The address of the owner ++ /// @param allowlist A set of addresses allowed to trigger lockOrBurn as original senders ++ /// @param router The address of the router ++ function initialize(address owner_, address[] memory allowlist, address router) public virtual initializer { ++ if (owner_ == address(0) || router == address(0)) revert ZeroAddressNotAllowed(); ++ _transferOwnership(owner_); ++ ++ s_router = IRouter(router); ++ ++ // Pool can be set as permissioned or permissionless at deployment time only to save hot-path gas. ++ if (i_allowlistEnabled) { ++ _applyAllowListUpdates(new address[](0), allowlist); ++ } ++ } + +- /// @inheritdoc BurnMintTokenPoolAbstract ++ /// @inheritdoc UpgradeableBurnMintTokenPoolAbstract + function _burn(uint256 amount) internal virtual override { + IBurnMintERC20(address(i_token)).burn(amount); + } +``` diff --git a/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableLockReleaseTokenPool_diff.md b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableLockReleaseTokenPool_diff.md new file mode 100644 index 0000000000..f189249781 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableLockReleaseTokenPool_diff.md @@ -0,0 +1,191 @@ +```diff +diff --git a/src/v0.8/ccip/pools/LockReleaseTokenPool.sol b/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol +index 3a4a4aef6d..70adc38030 100644 +--- a/src/v0.8/ccip/pools/LockReleaseTokenPool.sol ++++ b/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol +@@ -1,24 +1,35 @@ + // SPDX-License-Identifier: BUSL-1.1 +-pragma solidity 0.8.24; ++pragma solidity ^0.8.0; + +-import {ILiquidityContainer} from "../../liquiditymanager/interfaces/ILiquidityContainer.sol"; +-import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; ++import {Initializable} from "solidity-utils/contracts/transparent-proxy/Initializable.sol"; + +-import {Pool} from "../libraries/Pool.sol"; +-import {TokenPool} from "./TokenPool.sol"; ++import {ILiquidityContainer} from "../../../liquiditymanager/interfaces/ILiquidityContainer.sol"; ++import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; + +-import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +-import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; ++import {Pool} from "../../libraries/Pool.sol"; ++import {UpgradeableTokenPool} from "./UpgradeableTokenPool.sol"; + +-/// @notice Token pool used for tokens on their native chain. This uses a lock and release mechanism. +-/// Because of lock/unlock requiring liquidity, this pool contract also has function to add and remove +-/// liquidity. This allows for proper bookkeeping for both user and liquidity provider balances. +-/// @dev One token per LockReleaseTokenPool. +-contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion { ++import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; ++import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; ++import {IRouter} from "../../interfaces/IRouter.sol"; ++ ++/// @title UpgradeableLockReleaseTokenPool ++/// @author Aave Labs ++/// @notice Upgradeable version of Chainlink's CCIP LockReleaseTokenPool ++/// @dev Contract adaptations: ++/// - Implementation of Initializable to allow upgrades ++/// - Move of allowlist and router definition to initialization stage ++/// - Addition of a bridge limit to regulate the maximum amount of tokens that can be transferred out (burned/locked) ++contract UpgradeableLockReleaseTokenPool is UpgradeableTokenPool, ILiquidityContainer, ITypeAndVersion, Initializable { + using SafeERC20 for IERC20; + + error InsufficientLiquidity(); + error LiquidityNotAccepted(); ++ error BridgeLimitExceeded(uint256 bridgeLimit); ++ error NotEnoughBridgedAmount(); ++ ++ event BridgeLimitUpdated(uint256 oldBridgeLimit, uint256 newBridgeLimit); ++ event BridgeLimitAdminUpdated(address indexed oldAdmin, address indexed newAdmin); + + event LiquidityTransferred(address indexed from, uint256 amount); + +@@ -32,14 +43,50 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion + /// @notice The address of the rebalancer. + address internal s_rebalancer; + ++ /// @notice Maximum amount of tokens that can be bridged to other chains ++ uint256 private s_bridgeLimit; ++ /// @notice Amount of tokens bridged (transferred out) ++ /// @dev Must always be equal to or below the bridge limit ++ uint256 private s_currentBridged; ++ /// @notice The address of the bridge limit admin. ++ /// @dev Can be address(0) if none is configured. ++ address internal s_bridgeLimitAdmin; ++ ++ // / @dev Constructor ++ // / @param token The bridgeable token that is managed by this pool. ++ // / @param rmnProxy The address of the rmn proxy ++ // / @param allowlistEnabled True if pool is set to access-controlled mode, false otherwise ++ // / @param acceptLiquidity True if the pool accepts liquidity, false otherwise + constructor( + IERC20 token, +- address[] memory allowlist, + address rmnProxy, +- bool acceptLiquidity, +- address router +- ) TokenPool(token, allowlist, rmnProxy, router) { ++ bool allowListEnabled, ++ bool acceptLiquidity ++ ) UpgradeableTokenPool(token, rmnProxy, allowListEnabled) { + i_acceptLiquidity = acceptLiquidity; ++ ++ _disableInitializers(); ++ } ++ ++ /// @dev Initializer ++ /// @dev The address passed as `owner_` must accept ownership after initialization. ++ /// @dev The `allowlist` is only effective if pool is set to access-controlled mode ++ /// @param owner_ The address of the owner ++ /// @param allowlist A set of addresses allowed to trigger lockOrBurn as original senders ++ /// @param router The address of the router ++ /// @param bridgeLimit The maximum amount of tokens that can be bridged to other chains ++ function initialize( ++ address owner_, ++ address[] memory allowlist, ++ address router, ++ uint256 bridgeLimit ++ ) public initializer { ++ if (router == address(0) || owner_ == address(0)) revert ZeroAddressNotAllowed(); ++ ++ _transferOwnership(owner_); ++ s_router = IRouter(router); ++ if (i_allowlistEnabled) _applyAllowListUpdates(new address[](0), allowlist); ++ s_bridgeLimit = bridgeLimit; + } + + /// @notice Locks the token in the pool +@@ -47,6 +94,9 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion + function lockOrBurn( + Pool.LockOrBurnInV1 calldata lockOrBurnIn + ) external virtual override returns (Pool.LockOrBurnOutV1 memory) { ++ // Increase bridged amount because tokens are leaving the source chain ++ if ((s_currentBridged += lockOrBurnIn.amount) > s_bridgeLimit) revert BridgeLimitExceeded(s_bridgeLimit); ++ + _validateLockOrBurn(lockOrBurnIn); + + emit Locked(msg.sender, lockOrBurnIn.amount); +@@ -59,6 +109,11 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion + function releaseOrMint( + Pool.ReleaseOrMintInV1 calldata releaseOrMintIn + ) external virtual override returns (Pool.ReleaseOrMintOutV1 memory) { ++ // This should never occur. Amount should never exceed the current bridged amount ++ if (releaseOrMintIn.amount > s_currentBridged) revert NotEnoughBridgedAmount(); ++ // Reduce bridged amount because tokens are back to source chain ++ s_currentBridged -= releaseOrMintIn.amount; ++ + _validateReleaseOrMint(releaseOrMintIn); + + // Release to the recipient +@@ -69,6 +124,38 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.amount}); + } + ++ /// @notice Sets the bridge limit, the maximum amount of tokens that can be bridged out ++ /// @dev Only callable by the owner or the bridge limit admin or owner. ++ /// @dev Bridge limit changes should be carefully managed, specially when reducing below the current bridged amount ++ /// @param newBridgeLimit The new bridge limit ++ function setBridgeLimit(uint256 newBridgeLimit) external { ++ if (msg.sender != s_bridgeLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); ++ uint256 oldBridgeLimit = s_bridgeLimit; ++ s_bridgeLimit = newBridgeLimit; ++ emit BridgeLimitUpdated(oldBridgeLimit, newBridgeLimit); ++ } ++ ++ /// @notice Sets the bridge limit admin address. ++ /// @dev Only callable by the owner. ++ /// @param bridgeLimitAdmin The new bridge limit admin address. ++ function setBridgeLimitAdmin(address bridgeLimitAdmin) external onlyOwner { ++ address oldAdmin = s_bridgeLimitAdmin; ++ s_bridgeLimitAdmin = bridgeLimitAdmin; ++ emit BridgeLimitAdminUpdated(oldAdmin, bridgeLimitAdmin); ++ } ++ ++ /// @notice Gets the bridge limit ++ /// @return The maximum amount of tokens that can be transferred out to other chains ++ function getBridgeLimit() external view virtual returns (uint256) { ++ return s_bridgeLimit; ++ } ++ ++ /// @notice Gets the current bridged amount to other chains ++ /// @return The amount of tokens transferred out to other chains ++ function getCurrentBridgedAmount() external view virtual returns (uint256) { ++ return s_currentBridged; ++ } ++ + // @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == type(ILiquidityContainer).interfaceId || super.supportsInterface(interfaceId); +@@ -80,6 +167,11 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion + return s_rebalancer; + } + ++ /// @notice Gets the bridge limiter admin address. ++ function getBridgeLimitAdmin() external view returns (address) { ++ return s_bridgeLimitAdmin; ++ } ++ + /// @notice Sets the LiquidityManager address. + /// @dev Only callable by the owner. + function setRebalancer(address rebalancer) external onlyOwner { +@@ -124,7 +216,7 @@ contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion + /// @param from The address of the old pool. + /// @param amount The amount of liquidity to transfer. + function transferLiquidity(address from, uint256 amount) external onlyOwner { +- LockReleaseTokenPool(from).withdrawLiquidity(amount); ++ UpgradeableLockReleaseTokenPool(from).withdrawLiquidity(amount); + + emit LiquidityTransferred(from, amount); + } +``` diff --git a/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableTokenPool_diff.md b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableTokenPool_diff.md new file mode 100644 index 0000000000..b1c6dfee6d --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/GHO/diffs/UpgradeableTokenPool_diff.md @@ -0,0 +1,85 @@ +```diff +diff --git a/src/v0.8/ccip/pools/TokenPool.sol b/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol +index ebd613134a..58331aa41f 100644 +--- a/src/v0.8/ccip/pools/TokenPool.sol ++++ b/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol +@@ -1,22 +1,22 @@ + // SPDX-License-Identifier: BUSL-1.1 +-pragma solidity 0.8.24; ++pragma solidity ^0.8.0; + +-import {IPoolV1} from "../interfaces/IPool.sol"; +-import {IRMN} from "../interfaces/IRMN.sol"; +-import {IRouter} from "../interfaces/IRouter.sol"; ++import {IPoolV1} from "../../interfaces/IPool.sol"; ++import {IRMN} from "../../interfaces/IRMN.sol"; ++import {IRouter} from "../../interfaces/IRouter.sol"; + +-import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; +-import {Pool} from "../libraries/Pool.sol"; +-import {RateLimiter} from "../libraries/RateLimiter.sol"; ++import {OwnerIsCreator} from "../../../shared/access/OwnerIsCreator.sol"; ++import {Pool} from "../../libraries/Pool.sol"; ++import {RateLimiter} from "../../libraries/RateLimiter.sol"; + +-import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +-import {IERC165} from "../../vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol"; +-import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol"; ++import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; ++import {IERC165} from "../../../vendor/openzeppelin-solidity/v5.0.2/contracts/utils/introspection/IERC165.sol"; ++import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol"; + + /// @notice Base abstract class with common functions for all token pools. + /// A token pool serves as isolated place for holding tokens and token specific logic + /// that may execute as tokens move across the bridge. +-abstract contract TokenPool is IPoolV1, OwnerIsCreator { ++abstract contract UpgradeableTokenPool is IPoolV1, OwnerIsCreator { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.UintSet; + using RateLimiter for RateLimiter.TokenBucket; +@@ -92,17 +92,13 @@ abstract contract TokenPool is IPoolV1, OwnerIsCreator { + /// @dev Can be address(0) if none is configured. + address internal s_rateLimitAdmin; + +- constructor(IERC20 token, address[] memory allowlist, address rmnProxy, address router) { +- if (address(token) == address(0) || router == address(0) || rmnProxy == address(0)) revert ZeroAddressNotAllowed(); ++ constructor(IERC20 token, address rmnProxy, bool allowListEnabled) { ++ if (address(token) == address(0) || rmnProxy == address(0)) revert ZeroAddressNotAllowed(); + i_token = token; + i_rmnProxy = rmnProxy; +- s_router = IRouter(router); + + // Pool can be set as permissioned or permissionless at deployment time only to save hot-path gas. +- i_allowlistEnabled = allowlist.length > 0; +- if (i_allowlistEnabled) { +- _applyAllowListUpdates(new address[](0), allowlist); +- } ++ i_allowlistEnabled = allowListEnabled; + } + + /// @notice Get RMN proxy address +@@ -140,8 +136,10 @@ abstract contract TokenPool is IPoolV1, OwnerIsCreator { + + /// @notice Signals which version of the pool interface is supported + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { +- return interfaceId == Pool.CCIP_POOL_V1 || interfaceId == type(IPoolV1).interfaceId +- || interfaceId == type(IERC165).interfaceId; ++ return ++ interfaceId == Pool.CCIP_POOL_V1 || ++ interfaceId == type(IPoolV1).interfaceId || ++ interfaceId == type(IERC165).interfaceId; + } + + // ================================================================ +@@ -183,8 +181,8 @@ abstract contract TokenPool is IPoolV1, OwnerIsCreator { + // Validates that the source pool address is configured on this pool. + bytes memory configuredRemotePool = getRemotePool(releaseOrMintIn.remoteChainSelector); + if ( +- configuredRemotePool.length == 0 +- || keccak256(releaseOrMintIn.sourcePoolAddress) != keccak256(configuredRemotePool) ++ configuredRemotePool.length == 0 || ++ keccak256(releaseOrMintIn.sourcePoolAddress) != keccak256(configuredRemotePool) + ) { + revert InvalidSourcePoolAddress(releaseOrMintIn.sourcePoolAddress); + } +``` diff --git a/contracts/src/v0.8/shared/access/ConfirmedOwnerWithProposal.sol b/contracts/src/v0.8/shared/access/ConfirmedOwnerWithProposal.sol index 2a6dd94e10..3b691ad7ca 100644 --- a/contracts/src/v0.8/shared/access/ConfirmedOwnerWithProposal.sol +++ b/contracts/src/v0.8/shared/access/ConfirmedOwnerWithProposal.sol @@ -45,10 +45,7 @@ contract ConfirmedOwnerWithProposal is IOwnable { } /// @notice validate, transfer ownership, and emit relevant events - function _transferOwnership(address to) private { - // solhint-disable-next-line gas-custom-errors - require(to != msg.sender, "Cannot transfer to self"); - + function _transferOwnership(address to) internal { s_pendingOwner = to; emit OwnershipTransferRequested(s_owner, to);