From cd06a01d5e2b0564ef0f532efc3d0306d9b00c05 Mon Sep 17 00:00:00 2001 From: CheyenneAtapour Date: Wed, 17 Jul 2024 22:12:12 -0700 Subject: [PATCH] feat: remove ccip dependencies --- .gitmodules | 3 - remappings.txt | 1 - src/contracts/misc/ArbGhoSteward.sol | 6 +- src/contracts/misc/GhoStewardV2.sol | 4 +- .../misc/interfaces/IArbGhoSteward.sol | 2 +- .../misc/interfaces/IGhoStewardV2.sol | 2 +- src/contracts/misc/mocks/Client.sol | 38 ++ src/contracts/misc/mocks/ConfirmedOwner.sol | 10 + .../misc/mocks/ConfirmedOwnerWithProposal.sol | 65 ++++ src/contracts/misc/mocks/IARM.sol | 17 + src/contracts/misc/mocks/IBurnMintERC20.sol | 29 ++ .../misc/mocks/ILiquidityContainer.sol | 16 + src/contracts/misc/mocks/IOwnable.sol | 10 + src/contracts/misc/mocks/IPool.sol | 46 +++ src/contracts/misc/mocks/IRouter.sol | 38 ++ src/contracts/misc/mocks/ITypeAndVersion.sol | 6 + src/contracts/misc/mocks/OwnerIsCreator.sol | 10 + src/contracts/misc/mocks/RateLimiter.sol | 173 +++++++++ .../mocks/UpgradeableBurnMintTokenPool.sol | 64 ++++ .../UpgradeableBurnMintTokenPoolAbstract.sol | 55 +++ .../mocks/UpgradeableLockReleaseTokenPool.sol | 271 +++++++++++++++ .../misc/mocks/UpgradeableTokenPool.sol | 329 ++++++++++++++++++ src/test/TestArbGhoSteward.t.sol | 2 +- src/test/TestGhoBase.t.sol | 8 +- src/test/TestGhoStewardV2.t.sol | 2 +- 25 files changed, 1190 insertions(+), 17 deletions(-) create mode 100644 src/contracts/misc/mocks/Client.sol create mode 100644 src/contracts/misc/mocks/ConfirmedOwner.sol create mode 100644 src/contracts/misc/mocks/ConfirmedOwnerWithProposal.sol create mode 100644 src/contracts/misc/mocks/IARM.sol create mode 100644 src/contracts/misc/mocks/IBurnMintERC20.sol create mode 100644 src/contracts/misc/mocks/ILiquidityContainer.sol create mode 100644 src/contracts/misc/mocks/IOwnable.sol create mode 100644 src/contracts/misc/mocks/IPool.sol create mode 100644 src/contracts/misc/mocks/IRouter.sol create mode 100644 src/contracts/misc/mocks/ITypeAndVersion.sol create mode 100644 src/contracts/misc/mocks/OwnerIsCreator.sol create mode 100644 src/contracts/misc/mocks/RateLimiter.sol create mode 100644 src/contracts/misc/mocks/UpgradeableBurnMintTokenPool.sol create mode 100644 src/contracts/misc/mocks/UpgradeableBurnMintTokenPoolAbstract.sol create mode 100644 src/contracts/misc/mocks/UpgradeableLockReleaseTokenPool.sol create mode 100644 src/contracts/misc/mocks/UpgradeableTokenPool.sol diff --git a/.gitmodules b/.gitmodules index 58455407..a17a4543 100644 --- a/.gitmodules +++ b/.gitmodules @@ -26,6 +26,3 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "lib/ccip"] - path = lib/ccip - url = https://github.com/aave/ccip diff --git a/remappings.txt b/remappings.txt index f577579c..e7ac529f 100644 --- a/remappings.txt +++ b/remappings.txt @@ -15,4 +15,3 @@ aave-v3-periphery/=lib/aave-address-book/lib/aave-v3-periphery/ erc4626-tests/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/lib/erc4626-tests/ openzeppelin-contracts/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/ solidity-utils/=lib/solidity-utils/src/ -ccip/=lib/ccip/contracts/src/ diff --git a/src/contracts/misc/ArbGhoSteward.sol b/src/contracts/misc/ArbGhoSteward.sol index 04f7dbff..d221a69d 100644 --- a/src/contracts/misc/ArbGhoSteward.sol +++ b/src/contracts/misc/ArbGhoSteward.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.10; import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; -import {IOwnable} from 'ccip/v0.8/shared/interfaces/IOwnable.sol'; +import {IOwnable} from './mocks/IOwnable.sol'; import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; import {IPoolConfigurator} from '@aave/core-v3/contracts/interfaces/IPoolConfigurator.sol'; @@ -16,8 +16,8 @@ import {IGsm} from '../facilitators/gsm/interfaces/IGsm.sol'; import {IGsmFeeStrategy} from '../facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; import {IGhoToken} from '../gho/interfaces/IGhoToken.sol'; import {IArbGhoSteward} from './interfaces/IArbGhoSteward.sol'; -import {UpgradeableBurnMintTokenPool} from 'ccip/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol'; -import {RateLimiter} from 'ccip/v0.8/ccip/libraries/RateLimiter.sol'; +import {UpgradeableBurnMintTokenPool} from './mocks/UpgradeableBurnMintTokenPool.sol'; +import {RateLimiter} from './mocks/RateLimiter.sol'; /** * @title ArbGhoSteward diff --git a/src/contracts/misc/GhoStewardV2.sol b/src/contracts/misc/GhoStewardV2.sol index cde0cd80..131fceb5 100644 --- a/src/contracts/misc/GhoStewardV2.sol +++ b/src/contracts/misc/GhoStewardV2.sol @@ -15,8 +15,8 @@ import {IGsm} from '../facilitators/gsm/interfaces/IGsm.sol'; import {IGsmFeeStrategy} from '../facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; import {IGhoToken} from '../gho/interfaces/IGhoToken.sol'; import {IGhoStewardV2} from './interfaces/IGhoStewardV2.sol'; -import {UpgradeableLockReleaseTokenPool} from 'ccip/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol'; -import {RateLimiter} from 'ccip/v0.8/ccip/libraries/RateLimiter.sol'; +import {UpgradeableLockReleaseTokenPool} from './mocks/UpgradeableLockReleaseTokenPool.sol'; +import {RateLimiter} from './mocks/RateLimiter.sol'; /** * @title GhoStewardV2 diff --git a/src/contracts/misc/interfaces/IArbGhoSteward.sol b/src/contracts/misc/interfaces/IArbGhoSteward.sol index 5816b19d..a95df0ec 100644 --- a/src/contracts/misc/interfaces/IArbGhoSteward.sol +++ b/src/contracts/misc/interfaces/IArbGhoSteward.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; -import {RateLimiter} from 'ccip/v0.8/ccip/libraries/RateLimiter.sol'; +import {RateLimiter} from '../mocks/RateLimiter.sol'; /** * @title IGhoStewardV2 diff --git a/src/contracts/misc/interfaces/IGhoStewardV2.sol b/src/contracts/misc/interfaces/IGhoStewardV2.sol index bed0b822..58e7fedf 100644 --- a/src/contracts/misc/interfaces/IGhoStewardV2.sol +++ b/src/contracts/misc/interfaces/IGhoStewardV2.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; -import {RateLimiter} from 'ccip/v0.8/ccip/libraries/RateLimiter.sol'; +import {RateLimiter} from '../mocks/RateLimiter.sol'; /** * @title IGhoStewardV2 diff --git a/src/contracts/misc/mocks/Client.sol b/src/contracts/misc/mocks/Client.sol new file mode 100644 index 00000000..51bcbda5 --- /dev/null +++ b/src/contracts/misc/mocks/Client.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// End consumer library. +library Client { + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct EVMTokenAmount { + address token; // token address on the local chain. + uint256 amount; // Amount of tokens. + } + + struct Any2EVMMessage { + bytes32 messageId; // MessageId corresponding to ccipSend on source. + uint64 sourceChainSelector; // Source chain selector. + bytes sender; // abi.decode(sender) if coming from an EVM chain. + bytes data; // payload sent in original message. + EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation. + } + + // If extraArgs is empty bytes, the default is 200k gas limit. + struct EVM2AnyMessage { + bytes receiver; // abi.encode(receiver address) for dest EVM chains + bytes data; // Data payload + EVMTokenAmount[] tokenAmounts; // Token transfers + address feeToken; // Address of feeToken. address(0) means you will send msg.value. + bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV1) + } + + // bytes4(keccak256("CCIP EVMExtraArgsV1")); + bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9; + struct EVMExtraArgsV1 { + uint256 gasLimit; + } + + function _argsToBytes(EVMExtraArgsV1 memory extraArgs) internal pure returns (bytes memory bts) { + return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs); + } +} diff --git a/src/contracts/misc/mocks/ConfirmedOwner.sol b/src/contracts/misc/mocks/ConfirmedOwner.sol new file mode 100644 index 00000000..780d2ba6 --- /dev/null +++ b/src/contracts/misc/mocks/ConfirmedOwner.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ConfirmedOwnerWithProposal} from './ConfirmedOwnerWithProposal.sol'; + +/// @title The ConfirmedOwner contract +/// @notice A contract with helpers for basic contract ownership. +contract ConfirmedOwner is ConfirmedOwnerWithProposal { + constructor(address newOwner) ConfirmedOwnerWithProposal(newOwner, address(0)) {} +} diff --git a/src/contracts/misc/mocks/ConfirmedOwnerWithProposal.sol b/src/contracts/misc/mocks/ConfirmedOwnerWithProposal.sol new file mode 100644 index 00000000..57edc457 --- /dev/null +++ b/src/contracts/misc/mocks/ConfirmedOwnerWithProposal.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IOwnable} from './IOwnable.sol'; + +/// @title The ConfirmedOwner contract +/// @notice A contract with helpers for basic contract ownership. +contract ConfirmedOwnerWithProposal is IOwnable { + address private s_owner; + address private s_pendingOwner; + + event OwnershipTransferRequested(address indexed from, address indexed to); + event OwnershipTransferred(address indexed from, address indexed to); + + constructor(address newOwner, address pendingOwner) { + // solhint-disable-next-line custom-errors + require(newOwner != address(0), 'Cannot set owner to zero'); + + s_owner = newOwner; + if (pendingOwner != address(0)) { + _transferOwnership(pendingOwner); + } + } + + /// @notice Allows an owner to begin transferring ownership to a new address. + function transferOwnership(address to) public override onlyOwner { + _transferOwnership(to); + } + + /// @notice Allows an ownership transfer to be completed by the recipient. + function acceptOwnership() external override { + // solhint-disable-next-line custom-errors + require(msg.sender == s_pendingOwner, 'Must be proposed owner'); + + address oldOwner = s_owner; + s_owner = msg.sender; + s_pendingOwner = address(0); + + emit OwnershipTransferred(oldOwner, msg.sender); + } + + /// @notice Get the current owner + function owner() public view override returns (address) { + return s_owner; + } + + /// @notice validate, transfer ownership, and emit relevant events + function _transferOwnership(address to) internal { + s_pendingOwner = to; + + emit OwnershipTransferRequested(s_owner, to); + } + + /// @notice validate access + function _validateOwnership() internal view { + // solhint-disable-next-line custom-errors + require(msg.sender == s_owner, 'Only callable by owner'); + } + + /// @notice Reverts if called by anyone other than the contract owner. + modifier onlyOwner() { + _validateOwnership(); + _; + } +} diff --git a/src/contracts/misc/mocks/IARM.sol b/src/contracts/misc/mocks/IARM.sol new file mode 100644 index 00000000..f0d4d36a --- /dev/null +++ b/src/contracts/misc/mocks/IARM.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @notice This interface contains the only ARM-related functions that might be used on-chain by other CCIP contracts. +interface IARM { + /// @notice A Merkle root tagged with the address of the commit store contract it is destined for. + struct TaggedRoot { + address commitStore; + bytes32 root; + } + + /// @notice Callers MUST NOT cache the return value as a blessed tagged root could become unblessed. + function isBlessed(TaggedRoot calldata taggedRoot) external view returns (bool); + + /// @notice When the ARM is "cursed", CCIP pauses until the curse is lifted. + function isCursed() external view returns (bool); +} diff --git a/src/contracts/misc/mocks/IBurnMintERC20.sol b/src/contracts/misc/mocks/IBurnMintERC20.sol new file mode 100644 index 00000000..e67b1d2e --- /dev/null +++ b/src/contracts/misc/mocks/IBurnMintERC20.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +interface IBurnMintERC20 is IERC20 { + /// @notice Mints new tokens for a given address. + /// @param account The address to mint the new tokens to. + /// @param amount The number of tokens to be minted. + /// @dev this function increases the total supply. + function mint(address account, uint256 amount) external; + + /// @notice Burns tokens from the sender. + /// @param amount The number of tokens to be burned. + /// @dev this function decreases the total supply. + function burn(uint256 amount) external; + + /// @notice Burns tokens from a given address.. + /// @param account The address to burn tokens from. + /// @param amount The number of tokens to be burned. + /// @dev this function decreases the total supply. + function burn(address account, uint256 amount) external; + + /// @notice Burns tokens from a given address.. + /// @param account The address to burn tokens from. + /// @param amount The number of tokens to be burned. + /// @dev this function decreases the total supply. + function burnFrom(address account, uint256 amount) external; +} diff --git a/src/contracts/misc/mocks/ILiquidityContainer.sol b/src/contracts/misc/mocks/ILiquidityContainer.sol new file mode 100644 index 00000000..062325d9 --- /dev/null +++ b/src/contracts/misc/mocks/ILiquidityContainer.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/// @notice Interface for a liquidity container, this can be a CCIP token pool. +interface ILiquidityContainer { + event LiquidityAdded(address indexed provider, uint256 indexed amount); + event LiquidityRemoved(address indexed provider, uint256 indexed amount); + + /// @notice Provide additional liquidity to the container. + /// @dev Should emit LiquidityAdded + function provideLiquidity(uint256 amount) external; + + /// @notice Withdraws liquidity from the container to the msg sender + /// @dev Should emit LiquidityRemoved + function withdrawLiquidity(uint256 amount) external; +} diff --git a/src/contracts/misc/mocks/IOwnable.sol b/src/contracts/misc/mocks/IOwnable.sol new file mode 100644 index 00000000..3141fe9a --- /dev/null +++ b/src/contracts/misc/mocks/IOwnable.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IOwnable { + function owner() external returns (address); + + function transferOwnership(address recipient) external; + + function acceptOwnership() external; +} diff --git a/src/contracts/misc/mocks/IPool.sol b/src/contracts/misc/mocks/IPool.sol new file mode 100644 index 00000000..cd77d128 --- /dev/null +++ b/src/contracts/misc/mocks/IPool.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +// Shared public interface for multiple pool types. +// Each pool type handles a different child token model (lock/unlock, mint/burn.) +interface IPool { + /// @notice Lock tokens into the pool or burn the tokens. + /// @param originalSender Original sender of the tokens. + /// @param receiver Receiver of the tokens on destination chain. + /// @param amount Amount to lock or burn. + /// @param remoteChainSelector Destination chain Id. + /// @param extraArgs Additional data passed in by sender for lockOrBurn processing + /// in custom pools on source chain. + /// @return retData Optional field that contains bytes. Unused for now but already + /// implemented to allow future upgrades while preserving the interface. + function lockOrBurn( + address originalSender, + bytes calldata receiver, + uint256 amount, + uint64 remoteChainSelector, + bytes calldata extraArgs + ) external returns (bytes memory); + + /// @notice Releases or mints tokens to the receiver address. + /// @param originalSender Original sender of the tokens. + /// @param receiver Receiver of the tokens. + /// @param amount Amount to release or mint. + /// @param remoteChainSelector Source chain Id. + /// @param extraData Additional data supplied offchain for releaseOrMint processing in + /// custom pools on dest chain. This could be an attestation that was retrieved through a + /// third party API. + /// @dev offchainData can come from any untrusted source. + function releaseOrMint( + bytes memory originalSender, + address receiver, + uint256 amount, + uint64 remoteChainSelector, + bytes memory extraData + ) external; + + /// @notice Gets the IERC20 token that this pool can lock or burn. + /// @return token The IERC20 token representation. + function getToken() external view returns (IERC20 token); +} diff --git a/src/contracts/misc/mocks/IRouter.sol b/src/contracts/misc/mocks/IRouter.sol new file mode 100644 index 00000000..733a9422 --- /dev/null +++ b/src/contracts/misc/mocks/IRouter.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Client} from './Client.sol'; + +interface IRouter { + error OnlyOffRamp(); + + /// @notice Route the message to its intended receiver contract. + /// @param message Client.Any2EVMMessage struct. + /// @param gasForCallExactCheck of params for exec + /// @param gasLimit set of params for exec + /// @param receiver set of params for exec + /// @dev if the receiver is a contracts that signals support for CCIP execution through EIP-165. + /// the contract is called. If not, only tokens are transferred. + /// @return success A boolean value indicating whether the ccip message was received without errors. + /// @return retBytes A bytes array containing return data form CCIP receiver. + /// @return gasUsed the gas used by the external customer call. Does not include any overhead. + function routeMessage( + Client.Any2EVMMessage calldata message, + uint16 gasForCallExactCheck, + uint256 gasLimit, + address receiver + ) external returns (bool success, bytes memory retBytes, uint256 gasUsed); + + /// @notice Returns the configured onramp for a specific destination chain. + /// @param destChainSelector The destination chain Id to get the onRamp for. + /// @return onRampAddress The address of the onRamp. + function getOnRamp(uint64 destChainSelector) external view returns (address onRampAddress); + + /// @notice Return true if the given offRamp is a configured offRamp for the given source chain. + /// @param sourceChainSelector The source chain selector to check. + /// @param offRamp The address of the offRamp to check. + function isOffRamp( + uint64 sourceChainSelector, + address offRamp + ) external view returns (bool isOffRamp); +} diff --git a/src/contracts/misc/mocks/ITypeAndVersion.sol b/src/contracts/misc/mocks/ITypeAndVersion.sol new file mode 100644 index 00000000..135f6d0a --- /dev/null +++ b/src/contracts/misc/mocks/ITypeAndVersion.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ITypeAndVersion { + function typeAndVersion() external pure returns (string memory); +} diff --git a/src/contracts/misc/mocks/OwnerIsCreator.sol b/src/contracts/misc/mocks/OwnerIsCreator.sol new file mode 100644 index 00000000..e3cf0de2 --- /dev/null +++ b/src/contracts/misc/mocks/OwnerIsCreator.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ConfirmedOwner} from './ConfirmedOwner.sol'; + +/// @title The OwnerIsCreator contract +/// @notice A contract with helpers for basic contract ownership. +contract OwnerIsCreator is ConfirmedOwner { + constructor() ConfirmedOwner(msg.sender) {} +} diff --git a/src/contracts/misc/mocks/RateLimiter.sol b/src/contracts/misc/mocks/RateLimiter.sol new file mode 100644 index 00000000..b5f4f4b9 --- /dev/null +++ b/src/contracts/misc/mocks/RateLimiter.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/// @notice Implements Token Bucket rate limiting. +/// @dev uint128 is safe for rate limiter state. +/// For USD value rate limiting, it can adequately store USD value in 18 decimals. +/// For ERC20 token amount rate limiting, all tokens that will be listed will have at most +/// a supply of uint128.max tokens, and it will therefore not overflow the bucket. +/// In exceptional scenarios where tokens consumed may be larger than uint128, +/// e.g. compromised issuer, an enabled RateLimiter will check and revert. +library RateLimiter { + error BucketOverfilled(); + error OnlyCallableByAdminOrOwner(); + error TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress); + error TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress); + error AggregateValueMaxCapacityExceeded(uint256 capacity, uint256 requested); + error AggregateValueRateLimitReached(uint256 minWaitInSeconds, uint256 available); + error InvalidRatelimitRate(Config rateLimiterConfig); + error DisabledNonZeroRateLimit(Config config); + error RateLimitMustBeDisabled(); + + event TokensConsumed(uint256 tokens); + event ConfigChanged(Config config); + + struct TokenBucket { + uint128 tokens; // ──────╮ Current number of tokens that are in the bucket. + uint32 lastUpdated; // │ Timestamp in seconds of the last token refill, good for 100+ years. + bool isEnabled; // ──────╯ Indication whether the rate limiting is enabled or not + uint128 capacity; // ────╮ Maximum number of tokens that can be in the bucket. + uint128 rate; // ────────╯ Number of tokens per second that the bucket is refilled. + } + + struct Config { + bool isEnabled; // Indication whether the rate limiting should be enabled + uint128 capacity; // ────╮ Specifies the capacity of the rate limiter + uint128 rate; // ───────╯ Specifies the rate of the rate limiter + } + + /// @notice _consume removes the given tokens from the pool, lowering the + /// rate tokens allowed to be consumed for subsequent calls. + /// @param requestTokens The total tokens to be consumed from the bucket. + /// @param tokenAddress The token to consume capacity for, use 0x0 to indicate aggregate value capacity. + /// @dev Reverts when requestTokens exceeds bucket capacity or available tokens in the bucket + /// @dev emits removal of requestTokens if requestTokens is > 0 + function _consume( + TokenBucket storage s_bucket, + uint256 requestTokens, + address tokenAddress + ) internal { + // If there is no value to remove or rate limiting is turned off, skip this step to reduce gas usage + if (!s_bucket.isEnabled || requestTokens == 0) { + return; + } + + uint256 tokens = s_bucket.tokens; + uint256 capacity = s_bucket.capacity; + uint256 timeDiff = block.timestamp - s_bucket.lastUpdated; + + if (timeDiff != 0) { + if (tokens > capacity) revert BucketOverfilled(); + + // Refill tokens when arriving at a new block time + tokens = _calculateRefill(capacity, tokens, timeDiff, s_bucket.rate); + + s_bucket.lastUpdated = uint32(block.timestamp); + } + + if (capacity < requestTokens) { + // Token address 0 indicates consuming aggregate value rate limit capacity. + if (tokenAddress == address(0)) + revert AggregateValueMaxCapacityExceeded(capacity, requestTokens); + revert TokenMaxCapacityExceeded(capacity, requestTokens, tokenAddress); + } + if (tokens < requestTokens) { + uint256 rate = s_bucket.rate; + // Wait required until the bucket is refilled enough to accept this value, round up to next higher second + // Consume is not guaranteed to succeed after wait time passes if there is competing traffic. + // This acts as a lower bound of wait time. + uint256 minWaitInSeconds = ((requestTokens - tokens) + (rate - 1)) / rate; + + if (tokenAddress == address(0)) + revert AggregateValueRateLimitReached(minWaitInSeconds, tokens); + revert TokenRateLimitReached(minWaitInSeconds, tokens, tokenAddress); + } + tokens -= requestTokens; + + // Downcast is safe here, as tokens is not larger than capacity + s_bucket.tokens = uint128(tokens); + emit TokensConsumed(requestTokens); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function _currentTokenBucketState( + TokenBucket memory bucket + ) internal view returns (TokenBucket memory) { + // We update the bucket to reflect the status at the exact time of the + // call. This means we might need to refill a part of the bucket based + // on the time that has passed since the last update. + bucket.tokens = uint128( + _calculateRefill( + bucket.capacity, + bucket.tokens, + block.timestamp - bucket.lastUpdated, + bucket.rate + ) + ); + bucket.lastUpdated = uint32(block.timestamp); + return bucket; + } + + /// @notice Sets the rate limited config. + /// @param s_bucket The token bucket + /// @param config The new config + function _setTokenBucketConfig(TokenBucket storage s_bucket, Config memory config) internal { + // First update the bucket to make sure the proper rate is used for all the time + // up until the config change. + uint256 timeDiff = block.timestamp - s_bucket.lastUpdated; + if (timeDiff != 0) { + s_bucket.tokens = uint128( + _calculateRefill(s_bucket.capacity, s_bucket.tokens, timeDiff, s_bucket.rate) + ); + + s_bucket.lastUpdated = uint32(block.timestamp); + } + + s_bucket.tokens = uint128(_min(config.capacity, s_bucket.tokens)); + s_bucket.isEnabled = config.isEnabled; + s_bucket.capacity = config.capacity; + s_bucket.rate = config.rate; + + emit ConfigChanged(config); + } + + /// @notice Validates the token bucket config + function _validateTokenBucketConfig(Config memory config, bool mustBeDisabled) internal pure { + if (config.isEnabled) { + if (config.rate >= config.capacity || config.rate == 0) { + revert InvalidRatelimitRate(config); + } + if (mustBeDisabled) { + revert RateLimitMustBeDisabled(); + } + } else { + if (config.rate != 0 || config.capacity != 0) { + revert DisabledNonZeroRateLimit(config); + } + } + } + + /// @notice Calculate refilled tokens + /// @param capacity bucket capacity + /// @param tokens current bucket tokens + /// @param timeDiff block time difference since last refill + /// @param rate bucket refill rate + /// @return the value of tokens after refill + function _calculateRefill( + uint256 capacity, + uint256 tokens, + uint256 timeDiff, + uint256 rate + ) private pure returns (uint256) { + return _min(capacity, tokens + timeDiff * rate); + } + + /// @notice Return the smallest of two integers + /// @param a first int + /// @param b second int + /// @return smallest + function _min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } +} diff --git a/src/contracts/misc/mocks/UpgradeableBurnMintTokenPool.sol b/src/contracts/misc/mocks/UpgradeableBurnMintTokenPool.sol new file mode 100644 index 00000000..5bf69ce2 --- /dev/null +++ b/src/contracts/misc/mocks/UpgradeableBurnMintTokenPool.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; + +import {ITypeAndVersion} from './ITypeAndVersion.sol'; +import {IBurnMintERC20} from './IBurnMintERC20.sol'; + +import {UpgradeableTokenPool} from './UpgradeableTokenPool.sol'; +import {UpgradeableBurnMintTokenPoolAbstract} from './UpgradeableBurnMintTokenPoolAbstract.sol'; + +import {IRouter} from './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 +contract UpgradeableBurnMintTokenPool is + Initializable, + UpgradeableBurnMintTokenPoolAbstract, + ITypeAndVersion +{ + string public constant override typeAndVersion = 'BurnMintTokenPool 1.4.0'; + + /// @dev Constructor + /// @param token The bridgeable token that is managed by this pool. + /// @param armProxy The address of the arm proxy + /// @param allowlistEnabled True if pool is set to access-controlled mode, false otherwise + constructor( + address token, + address armProxy, + bool allowlistEnabled + ) UpgradeableTokenPool(IBurnMintERC20(token), armProxy, allowlistEnabled) {} + + /// @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)) revert ZeroAddressNotAllowed(); + if (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/src/contracts/misc/mocks/UpgradeableBurnMintTokenPoolAbstract.sol b/src/contracts/misc/mocks/UpgradeableBurnMintTokenPoolAbstract.sol new file mode 100644 index 00000000..681561b4 --- /dev/null +++ b/src/contracts/misc/mocks/UpgradeableBurnMintTokenPoolAbstract.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IBurnMintERC20} from './IBurnMintERC20.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 + /// @param amount Amount to burn + /// @dev The whenHealthy check is important to ensure that even if a ramp is compromised + /// we're able to stop token movement via ARM. + function lockOrBurn( + address originalSender, + bytes calldata, + uint256 amount, + uint64 remoteChainSelector, + bytes calldata + ) + external + virtual + override + onlyOnRamp(remoteChainSelector) + checkAllowList(originalSender) + whenHealthy + returns (bytes memory) + { + _consumeOutboundRateLimit(remoteChainSelector, amount); + _burn(amount); + emit Burned(msg.sender, amount); + return ''; + } + + /// @notice Mint tokens from the pool to the recipient + /// @param receiver Recipient address + /// @param amount Amount to mint + /// @dev The whenHealthy check is important to ensure that even if a ramp is compromised + /// we're able to stop token movement via ARM. + function releaseOrMint( + bytes memory, + address receiver, + uint256 amount, + uint64 remoteChainSelector, + bytes memory + ) external virtual override whenHealthy onlyOffRamp(remoteChainSelector) { + _consumeInboundRateLimit(remoteChainSelector, amount); + IBurnMintERC20(address(i_token)).mint(receiver, amount); + emit Minted(msg.sender, receiver, amount); + } +} diff --git a/src/contracts/misc/mocks/UpgradeableLockReleaseTokenPool.sol b/src/contracts/misc/mocks/UpgradeableLockReleaseTokenPool.sol new file mode 100644 index 00000000..032c6817 --- /dev/null +++ b/src/contracts/misc/mocks/UpgradeableLockReleaseTokenPool.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; + +import {ITypeAndVersion} from './ITypeAndVersion.sol'; +import {ILiquidityContainer} from './ILiquidityContainer.sol'; + +import {UpgradeableTokenPool} from './UpgradeableTokenPool.sol'; +import {RateLimiter} from './RateLimiter.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; + +import {IRouter} from './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 + Initializable, + UpgradeableTokenPool, + ILiquidityContainer, + ITypeAndVersion +{ + using SafeERC20 for IERC20; + + error InsufficientLiquidity(); + error LiquidityNotAccepted(); + error Unauthorized(address caller); + + error BridgeLimitExceeded(uint256 bridgeLimit); + error NotEnoughBridgedAmount(); + + event BridgeLimitUpdated(uint256 oldBridgeLimit, uint256 newBridgeLimit); + event BridgeLimitAdminUpdated(address indexed oldAdmin, address indexed newAdmin); + + string public constant override typeAndVersion = 'LockReleaseTokenPool 1.4.0'; + + /// @dev The unique lock release pool flag to signal through EIP 165. + bytes4 private constant LOCK_RELEASE_INTERFACE_ID = bytes4(keccak256('LockReleaseTokenPool')); + + /// @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 The address of the rate limiter admin. + /// @dev Can be address(0) if none is configured. + address internal s_rateLimitAdmin; + + /// @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 armProxy The address of the arm 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( + address token, + address armProxy, + bool allowlistEnabled, + bool acceptLiquidity + ) UpgradeableTokenPool(IERC20(token), armProxy, allowlistEnabled) { + i_acceptLiquidity = acceptLiquidity; + } + + /// @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 virtual initializer { + if (owner == address(0)) revert ZeroAddressNotAllowed(); + if (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); + } + s_bridgeLimit = bridgeLimit; + } + + /// @notice Locks the token in the pool + /// @param amount Amount to lock + /// @dev The whenHealthy check is important to ensure that even if a ramp is compromised + /// we're able to stop token movement via ARM. + function lockOrBurn( + address originalSender, + bytes calldata, + uint256 amount, + uint64 remoteChainSelector, + bytes calldata + ) + external + virtual + override + onlyOnRamp(remoteChainSelector) + checkAllowList(originalSender) + whenHealthy + returns (bytes memory) + { + // Increase bridged amount because tokens are leaving the source chain + if ((s_currentBridged += amount) > s_bridgeLimit) revert BridgeLimitExceeded(s_bridgeLimit); + + _consumeOutboundRateLimit(remoteChainSelector, amount); + emit Locked(msg.sender, amount); + return ''; + } + + /// @notice Release tokens from the pool to the recipient + /// @param receiver Recipient address + /// @param amount Amount to release + /// @dev The whenHealthy check is important to ensure that even if a ramp is compromised + /// we're able to stop token movement via ARM. + function releaseOrMint( + bytes memory, + address receiver, + uint256 amount, + uint64 remoteChainSelector, + bytes memory + ) external virtual override onlyOffRamp(remoteChainSelector) whenHealthy { + // This should never occur. Amount should never exceed the current bridged amount + if (amount > s_currentBridged) revert NotEnoughBridgedAmount(); + // Reduce bridged amount because tokens are back to source chain + s_currentBridged -= amount; + + _consumeInboundRateLimit(remoteChainSelector, amount); + getToken().safeTransfer(receiver, amount); + emit Released(msg.sender, receiver, amount); + } + + /// @notice returns the lock release interface flag used for EIP165 identification. + function getLockReleaseInterfaceId() public pure returns (bytes4) { + return LOCK_RELEASE_INTERFACE_ID; + } + + // @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return + interfaceId == LOCK_RELEASE_INTERFACE_ID || + interfaceId == type(ILiquidityContainer).interfaceId || + super.supportsInterface(interfaceId); + } + + /// @notice Gets Rebalancer, can be address(0) if none is configured. + /// @return The current liquidity manager. + function getRebalancer() external view returns (address) { + return s_rebalancer; + } + + /// @notice Sets the Rebalancer address. + /// @dev Only callable by the owner. + function setRebalancer(address rebalancer) external onlyOwner { + s_rebalancer = rebalancer; + } + + /// @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 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. + /// @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; + } + + /// @notice Gets the rate limiter admin address. + function getRateLimitAdmin() external view returns (address) { + return s_rateLimitAdmin; + } + + /// @notice Gets the bridge limiter admin address. + function getBridgeLimitAdmin() external view returns (address) { + return s_bridgeLimitAdmin; + } + + /// @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 Sets the rate limiter admin address. + /// @dev Only callable by the owner or the rate limiter admin. NOTE: overwrites the normal + /// onlyAdmin check in the base implementation to also allow the rate limiter admin. + /// @param remoteChainSelector The remote chain selector for which the rate limits apply. + /// @param outboundConfig The new outbound rate limiter config. + /// @param inboundConfig The new inbound rate limiter config. + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) external override { + if (msg.sender != s_rateLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); + + _setRateLimitConfig(remoteChainSelector, outboundConfig, inboundConfig); + } +} diff --git a/src/contracts/misc/mocks/UpgradeableTokenPool.sol b/src/contracts/misc/mocks/UpgradeableTokenPool.sol new file mode 100644 index 00000000..a54744cd --- /dev/null +++ b/src/contracts/misc/mocks/UpgradeableTokenPool.sol @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IPool} from './IPool.sol'; +import {IARM} from './IARM.sol'; +import {IRouter} from './IRouter.sol'; + +import {OwnerIsCreator} from './OwnerIsCreator.sol'; +import {RateLimiter} from './RateLimiter.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; +import {EnumerableSet} from '@openzeppelin/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 IPool, OwnerIsCreator, IERC165 { + 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 BadARMSignal(); + error ChainAlreadyExists(uint64 chainSelector); + + 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, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + event ChainConfigured( + uint64 remoteChainSelector, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + event ChainRemoved(uint64 remoteChainSelector); + 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 is allowed + 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 + } + + /// @dev The bridgeable token that is managed by this pool. + IERC20 internal immutable i_token; + /// @dev The address of the arm proxy + address internal immutable i_armProxy; + /// @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 uin256 format because of the EnumerableSet implementation. + EnumerableSet.UintSet internal s_remoteChainSelectors; + /// @dev Outbound rate limits. Corresponds to the inbound rate limit for the pool + /// on the remote chain. + mapping(uint64 => RateLimiter.TokenBucket) internal s_outboundRateLimits; + /// @dev Inbound rate limits. This allows per destination chain + /// token issuer specified rate limiting (e.g. issuers may trust chains to varying + /// degrees and prefer different limits) + mapping(uint64 => RateLimiter.TokenBucket) internal s_inboundRateLimits; + + constructor(IERC20 token, address armProxy, bool allowlistEnabled) { + if (address(token) == address(0)) revert ZeroAddressNotAllowed(); + i_token = token; + i_armProxy = armProxy; + i_allowlistEnabled = allowlistEnabled; + } + + /// @notice Get ARM proxy address + /// @return armProxy Address of arm proxy + function getArmProxy() public view returns (address armProxy) { + return i_armProxy; + } + + /// @inheritdoc IPool + function getToken() public view override 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); + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == type(IPool).interfaceId || interfaceId == type(IERC165).interfaceId; + } + + // ================================================================ + // │ Chain permissions │ + // ================================================================ + + /// @notice Checks whether a chain selector is permissioned on this contract. + /// @return true if the given chain selector is a permissioned remote chain. + 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); + } + + s_outboundRateLimits[update.remoteChainSelector] = RateLimiter.TokenBucket({ + rate: update.outboundRateLimiterConfig.rate, + capacity: update.outboundRateLimiterConfig.capacity, + tokens: update.outboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.outboundRateLimiterConfig.isEnabled + }); + + s_inboundRateLimits[update.remoteChainSelector] = RateLimiter.TokenBucket({ + rate: update.inboundRateLimiterConfig.rate, + capacity: update.inboundRateLimiterConfig.capacity, + tokens: update.inboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.inboundRateLimiterConfig.isEnabled + }); + emit ChainAdded( + update.remoteChainSelector, + update.outboundRateLimiterConfig, + update.inboundRateLimiterConfig + ); + } else { + // If the chain doesn't exist, revert + if (!s_remoteChainSelectors.remove(update.remoteChainSelector)) { + revert NonExistentChain(update.remoteChainSelector); + } + + delete s_inboundRateLimits[update.remoteChainSelector]; + delete s_outboundRateLimits[update.remoteChainSelector]; + emit ChainRemoved(update.remoteChainSelector); + } + } + } + + // ================================================================ + // │ Rate limiting │ + // ================================================================ + + /// @notice Consumes outbound rate limiting capacity in this pool + function _consumeOutboundRateLimit(uint64 remoteChainSelector, uint256 amount) internal { + s_outboundRateLimits[remoteChainSelector]._consume(amount, address(i_token)); + } + + /// @notice Consumes inbound rate limiting capacity in this pool + function _consumeInboundRateLimit(uint64 remoteChainSelector, uint256 amount) internal { + s_inboundRateLimits[remoteChainSelector]._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_outboundRateLimits[remoteChainSelector]._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_inboundRateLimits[remoteChainSelector]._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 virtual onlyOwner { + _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_outboundRateLimits[remoteChainSelector]._setTokenBucketConfig(outboundConfig); + RateLimiter._validateTokenBucketConfig(inboundConfig, false); + s_inboundRateLimits[remoteChainSelector]._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. + modifier onlyOnRamp(uint64 remoteChainSelector) { + 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. + modifier onlyOffRamp(uint64 remoteChainSelector) { + if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); + if (!s_router.isOffRamp(remoteChainSelector, msg.sender)) + revert CallerIsNotARampOnRouter(msg.sender); + _; + } + + // ================================================================ + // │ Allowlist │ + // ================================================================ + + modifier checkAllowList(address sender) { + if (i_allowlistEnabled && !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. + /// @dev allowListing will be removed before public launch + 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); + } + } + } + + /// @notice Ensure that there is no active curse. + modifier whenHealthy() { + if (IARM(i_armProxy).isCursed()) revert BadARMSignal(); + _; + } +} diff --git a/src/test/TestArbGhoSteward.t.sol b/src/test/TestArbGhoSteward.t.sol index 08e6dec6..d30f6374 100644 --- a/src/test/TestArbGhoSteward.t.sol +++ b/src/test/TestArbGhoSteward.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; -import {RateLimiter} from 'ccip/v0.8/ccip/libraries/RateLimiter.sol'; +import {RateLimiter} from 'src/contracts/misc/mocks/RateLimiter.sol'; contract TestArbGhoSteward is TestGhoBase { using ReserveConfiguration for DataTypes.ReserveConfigurationMap; diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index acd6e2f4..145ac033 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -82,10 +82,10 @@ import {SampleSwapFreezer} from '../contracts/facilitators/gsm/misc/SampleSwapFr import {GsmRegistry} from '../contracts/facilitators/gsm/misc/GsmRegistry.sol'; // CCIP contracts -import {UpgradeableTokenPool} from 'ccip/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol'; -import {UpgradeableLockReleaseTokenPool} from 'ccip/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol'; -import {UpgradeableBurnMintTokenPool} from 'ccip/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol'; -import {RateLimiter} from 'ccip/v0.8/ccip/libraries/RateLimiter.sol'; +import {UpgradeableTokenPool} from '../contracts/misc/mocks/UpgradeableTokenPool.sol'; +import {UpgradeableLockReleaseTokenPool} from '../contracts/misc/mocks/UpgradeableLockReleaseTokenPool.sol'; +import {UpgradeableBurnMintTokenPool} from '../contracts/misc/mocks/UpgradeableBurnMintTokenPool.sol'; +import {RateLimiter} from '../contracts/misc/mocks/RateLimiter.sol'; contract TestGhoBase is Test, Constants, Events { using WadRayMath for uint256; diff --git a/src/test/TestGhoStewardV2.t.sol b/src/test/TestGhoStewardV2.t.sol index 984b6d0e..d520ac7b 100644 --- a/src/test/TestGhoStewardV2.t.sol +++ b/src/test/TestGhoStewardV2.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; -import {RateLimiter} from 'ccip/v0.8/ccip/libraries/RateLimiter.sol'; +import {RateLimiter} from '../contracts/misc/mocks/RateLimiter.sol'; contract TestGhoStewardV2 is TestGhoBase { using ReserveConfiguration for DataTypes.ReserveConfigurationMap;