Robinhood MainnetChain ID: 4663
Contract detail

ForeignController

ForeignController is a verified Robinhood Chain smart contract at 0x2c10885d…36ed80cf with 0 indexed transactions.

Address type
Smart contract
Transactions
0
History coverage
No indexed activity found
Verification
Verified
Compiler
v0.8.25+commit.b61c2a91
Verification match
partial
Creation transaction
0xb7ffa44b…d39288e3
Created at block
#231786
Language
solidity
EVM version
cancun
License
none
Compiler optimization
Enabled · 1 runs
Verified contract

Contract source code

// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.21;

import { IAToken }            from "aave-v3-origin/src/core/contracts/interfaces/IAToken.sol";
import { IPool as IAavePool } from "aave-v3-origin/src/core/contracts/interfaces/IPool.sol";

import { IERC7540 } from "forge-std/interfaces/IERC7540.sol";

import { AccessControl } from "openzeppelin-contracts/contracts/access/AccessControl.sol";

import { IERC20 }   from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
import { IERC4626 } from "openzeppelin-contracts/contracts/interfaces/IERC4626.sol";

import { IPSM3 } from "spark-psm/src/interfaces/IPSM3.sol";

import { IALMProxy }     from "./interfaces/IALMProxy.sol";
import { ICCTPLike }     from "./interfaces/CCTPInterfaces.sol";
import { IRateLimits }   from "./interfaces/IRateLimits.sol";
import { IPendleMarket } from "./interfaces/PendleInterfaces.sol";

import { CurveLib }     from "./libraries/CurveLib.sol";
import { MerklLib }     from "./libraries/MerklLib.sol";
import { PendleLib }    from "./libraries/PendleLib.sol";
import { CCTPLib }      from "./libraries/CCTPLib.sol";
import { ERC20Lib }     from "./libraries/common/ERC20Lib.sol";
import { UniswapV3Lib } from "./libraries/UniswapV3Lib.sol";

import { ISwapRouter, INonfungiblePositionManager }                    from "./interfaces/UniswapV3Interfaces.sol";
import { ICentrifugeV3VaultLike, IAsyncRedeemManagerLike, ISpokeLike } from "./interfaces/CentrifugeInterfaces.sol";

import "./interfaces/ILayerZero.sol";

import { OptionsBuilder } from "layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";

import { RateLimitHelpers } from "./RateLimitHelpers.sol";

interface IATokenWithPool is IAToken {
    function POOL() external view returns(address);
}

contract ForeignController is AccessControl {

    using OptionsBuilder for bytes;

    /**********************************************************************************************/
    /*** Events                                                                                 ***/
    /**********************************************************************************************/

    // NOTE: This is used to track individual transfers for offchain processing of CCTP transactions
    event CCTPTransferInitiated(
        uint64  indexed nonce,
        uint32  indexed destinationDomain,
        bytes32 indexed mintRecipient,
        uint256 usdcAmount
    );
    event CentrifugeRecipientSet(uint16 indexed destinationCentrifugeId, bytes32 recipient);
    event LayerZeroRecipientSet(uint32 indexed destinationEndpointId, bytes32 layerZeroRecipient);
    event MaxExchangeRateSet(address indexed token, uint256 maxExchangeRate);
    event MaxSlippageSet(address indexed pool, uint256 maxSlippage);
    event MintRecipientSet(uint32 indexed destinationDomain, bytes32 mintRecipient);
    event RelayerRemoved(address indexed relayer);
    event MerklDistributorSet(address indexed merklDistributor);

    event UniswapV3PoolLowerTickUpdated(address indexed pool, int24 lowerTick);
    event UniswapV3PoolUpperTickUpdated(address indexed pool, int24 upperTick);
    event UniswapV3PoolMaxTickDeltaSet(address indexed pool, uint24 maxTickDelta);
    event UniswapV3PoolTwapSecondsAgoUpdated(address indexed pool, uint32 twapSecondsAgo);

    /**********************************************************************************************/
    /*** State variables                                                                        ***/
    /**********************************************************************************************/

    uint256 public constant EXCHANGE_RATE_PRECISION = 1e36;

    bytes32 public FREEZER = keccak256("FREEZER");
    bytes32 public RELAYER = keccak256("RELAYER");

    bytes32 public LIMIT_4626_DEPOSIT        = keccak256("LIMIT_4626_DEPOSIT");
    bytes32 public LIMIT_4626_WITHDRAW       = keccak256("LIMIT_4626_WITHDRAW");
    bytes32 public LIMIT_7540_DEPOSIT        = keccak256("LIMIT_7540_DEPOSIT");
    bytes32 public LIMIT_7540_REDEEM         = keccak256("LIMIT_7540_REDEEM");
    bytes32 public LIMIT_AAVE_DEPOSIT        = keccak256("LIMIT_AAVE_DEPOSIT");
    bytes32 public LIMIT_AAVE_WITHDRAW       = keccak256("LIMIT_AAVE_WITHDRAW");
    bytes32 public LIMIT_ASSET_TRANSFER      = keccak256("LIMIT_ASSET_TRANSFER");
    bytes32 public LIMIT_CENTRIFUGE_TRANSFER = keccak256("LIMIT_CENTRIFUGE_TRANSFER");
    bytes32 public LIMIT_CURVE_DEPOSIT       = keccak256("LIMIT_CURVE_DEPOSIT");
    bytes32 public LIMIT_CURVE_SWAP          = keccak256("LIMIT_CURVE_SWAP");
    bytes32 public LIMIT_CURVE_WITHDRAW      = keccak256("LIMIT_CURVE_WITHDRAW");
    bytes32 public LIMIT_LAYERZERO_TRANSFER  = keccak256("LIMIT_LAYERZERO_TRANSFER");
    bytes32 public LIMIT_PENDLE_PT_REDEEM    = keccak256("LIMIT_PENDLE_PT_REDEEM");
    bytes32 public LIMIT_PSM_DEPOSIT         = keccak256("LIMIT_PSM_DEPOSIT");
    bytes32 public LIMIT_PSM_WITHDRAW        = keccak256("LIMIT_PSM_WITHDRAW");
    bytes32 public LIMIT_USDC_TO_CCTP        = keccak256("LIMIT_USDC_TO_CCTP");
    bytes32 public LIMIT_USDC_TO_DOMAIN      = keccak256("LIMIT_USDC_TO_DOMAIN");
    bytes32 public LIMIT_UNISWAP_V3_DEPOSIT  = keccak256("LIMIT_UNISWAP_V3_DEPOSIT");
    bytes32 public LIMIT_UNISWAP_V3_SWAP     = keccak256("LIMIT_UNISWAP_V3_SWAP");
    bytes32 public LIMIT_UNISWAP_V3_WITHDRAW = keccak256("LIMIT_UNISWAP_V3_WITHDRAW");

    uint256 internal CENTRIFUGE_REQUEST_ID = 0;

    // @dev https://github.com/uniswap/v4-core/blob/80311e34080fee64b6fc6c916e9a51a437d0e482/src/libraries/TickMath.sol#L20-L23
    int24 internal MIN_TICK = -887_272;
    int24 internal MAX_TICK =  887_272;

    IALMProxy   public proxy;
    ICCTPLike   public cctp;
    IPSM3       public psm;
    IRateLimits public rateLimits;
    IERC20      public usdc;
    address     public pendleRouter;
    address     public merklDistributor;

    ISwapRouter                 public uniswapV3Router;
    INonfungiblePositionManager public uniswapV3PositionManager;

    mapping(address pool => uint256 maxSlippage)                     public maxSlippages;  // 1e18 precision
    mapping(address pool => UniswapV3Lib.UniswapV3PoolParams params) public uniswapV3PoolParams;

    mapping(uint32 destinationDomain       => bytes32 mintRecipient)      public mintRecipients;
    mapping(uint32 destinationEndpointId   => bytes32 layerZeroRecipient) public layerZeroRecipients;
    mapping(uint16 destinationCentrifugeId => bytes32 recipient)          public centrifugeRecipients;

    // ERC4626 exchange rate thresholds (1e36 precision)
    mapping(address token => uint256 maxExchangeRate) public maxExchangeRates;

    /**********************************************************************************************/
    /*** Initialization                                                                         ***/
    /**********************************************************************************************/

    constructor(
        address admin_,
        address proxy_,
        address rateLimits_,
        address psm_,
        address usdc_,
        address cctp_,
        address pendleRouter_,
        address uniswapV3Router_,
        address uniswapV3PositionManager_
    ) {
        _grantRole(DEFAULT_ADMIN_ROLE, admin_);

        proxy                    = IALMProxy(proxy_);
        rateLimits               = IRateLimits(rateLimits_);
        psm                      = IPSM3(psm_);
        usdc                     = IERC20(usdc_);
        cctp                     = ICCTPLike(cctp_);
        pendleRouter             = pendleRouter_;
        uniswapV3Router          = ISwapRouter(uniswapV3Router_);
        uniswapV3PositionManager = INonfungiblePositionManager(uniswapV3PositionManager_);
    }

    /**********************************************************************************************/
    /*** Modifiers                                                                              ***/
    /**********************************************************************************************/

    modifier rateLimited(bytes32 key, uint256 amount) {
        rateLimits.triggerRateLimitDecrease(key, amount);
        _;
    }

    modifier rateLimitedAsset(bytes32 key, address asset, uint256 amount) {
        rateLimits.triggerRateLimitDecrease(RateLimitHelpers.makeAssetKey(key, asset), amount);
        _;
    }

    modifier rateLimitExists(bytes32 key) {
        require(
            rateLimits.getRateLimitData(key).maxAmount > 0,
            "ForeignController/invalid-action"
        );
        _;
    }

    /**********************************************************************************************/
    /*** Admin functions                                                                        ***/
    /**********************************************************************************************/

    function setMintRecipient(uint32 destinationDomain, bytes32 mintRecipient)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
    {
        mintRecipients[destinationDomain] = mintRecipient;
        emit MintRecipientSet(destinationDomain, mintRecipient);
    }

    function setLayerZeroRecipient(uint32 destinationEndpointId, bytes32 layerZeroRecipient)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
    {
        layerZeroRecipients[destinationEndpointId] = layerZeroRecipient;
        emit LayerZeroRecipientSet(destinationEndpointId, layerZeroRecipient);
    }

    function setMaxSlippage(address pool, uint256 maxSlippage)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
    {
        require(maxSlippage <= 1e18, "ForeignController/max-slippage-out-of-bounds");
        maxSlippages[pool] = maxSlippage;
        emit MaxSlippageSet(pool, maxSlippage);
    }

    function setCentrifugeRecipient(uint16 destinationCentrifugeId, bytes32 recipient)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
    {
        centrifugeRecipients[destinationCentrifugeId] = recipient;
        emit CentrifugeRecipientSet(destinationCentrifugeId, recipient);
    }

    function setUniswapV3PoolMaxTickDelta(address pool, uint24 maxTickDelta) external {
        _checkRole(DEFAULT_ADMIN_ROLE);

        require(
            maxTickDelta > 0 &&
            maxTickDelta <= UniswapV3Lib.MAX_TICK_DELTA,
            "ForeignController/max-tick-delta-out-of-bounds"
        );

        UniswapV3Lib.UniswapV3PoolParams storage params = uniswapV3PoolParams[pool];
        params.swapMaxTickDelta = maxTickDelta;
        emit UniswapV3PoolMaxTickDeltaSet(pool, maxTickDelta);
    }

    function setUniswapV3AddLiquidityLowerTickBound(address pool, int24 lowerTickBound) external onlyRole(DEFAULT_ADMIN_ROLE) {
        UniswapV3Lib.UniswapV3PoolParams storage params = uniswapV3PoolParams[pool];
        require(lowerTickBound >= MIN_TICK && lowerTickBound < params.addLiquidityTickBounds.upper, "ForeignController/lower-tick-out-of-bounds");

        params.addLiquidityTickBounds.lower = lowerTickBound;
        emit UniswapV3PoolLowerTickUpdated(pool, lowerTickBound);
    }

    function setUniswapV3AddLiquidityUpperTickBound(address pool, int24 upperTickBound) external onlyRole(DEFAULT_ADMIN_ROLE) {
        UniswapV3Lib.UniswapV3PoolParams storage params = uniswapV3PoolParams[pool];
        require(upperTickBound > params.addLiquidityTickBounds.lower && upperTickBound <= MAX_TICK, "ForeignController/upper-tick-out-of-bounds");

        params.addLiquidityTickBounds.upper = upperTickBound;
        emit UniswapV3PoolUpperTickUpdated(pool, upperTickBound);
    }

    function setUniswapV3TwapSecondsAgo(address pool, uint32 twapSecondsAgo) external onlyRole(DEFAULT_ADMIN_ROLE) {
        UniswapV3Lib.UniswapV3PoolParams storage params = uniswapV3PoolParams[pool];
        // Required due to casting in UniswapV3OracleLibrary.consult
        // Limits twapSecondsAgo to approximately 68 years
        require(twapSecondsAgo < uint32(type(int32).max), "ForeignController/twap-seconds-ago-out-of-bounds");
        params.twapSecondsAgo = twapSecondsAgo;
        emit UniswapV3PoolTwapSecondsAgoUpdated(pool, twapSecondsAgo);
    }

    function setMerklDistributor(address merklDistributor_)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
    {
        merklDistributor = merklDistributor_;
        emit MerklDistributorSet(merklDistributor_);
    }

    function setMaxExchangeRate(address token, uint256 shares, uint256 maxExpectedAssets) external {
        _checkRole(DEFAULT_ADMIN_ROLE);

        require(token != address(0), "ForeignController/token-zero-address");

        emit MaxExchangeRateSet(
            token,
            maxExchangeRates[token] = _getExchangeRate(shares, maxExpectedAssets)
        );
    }

    /**********************************************************************************************/
    /*** Freezer functions                                                                      ***/
    /**********************************************************************************************/

    function removeRelayer(address relayer) external onlyRole(FREEZER) {
        _revokeRole(RELAYER, relayer);
        emit RelayerRemoved(relayer);
    }

    /**********************************************************************************************/
    /*** Relayer PSM functions                                                                  ***/
    /**********************************************************************************************/

    function depositPSM(address asset, uint256 amount)
        external
        onlyRole(RELAYER)
        rateLimitedAsset(LIMIT_PSM_DEPOSIT, asset, amount)
        returns (uint256 shares)
    {
        // Approve `asset` to PSM from the proxy (assumes the proxy has enough `asset`).
        ERC20Lib.approve(proxy, asset, address(psm), amount);

        // Deposit `amount` of `asset` in the PSM, decode the result to get `shares`.
        shares = abi.decode(
            proxy.doCall(
                address(psm),
                abi.encodeCall(
                    psm.deposit,
                    (asset, address(proxy), amount)
                )
            ),
            (uint256)
        );
    }

    // NOTE: !!! Rate limited at end of function !!!
    function withdrawPSM(address asset, uint256 maxAmount)
        external
        onlyRole(RELAYER)
        returns (uint256 assetsWithdrawn)
    {
        // Withdraw up to `maxAmount` of `asset` in the PSM, decode the result
        // to get `assetsWithdrawn` (assumes the proxy has enough PSM shares).
        assetsWithdrawn = abi.decode(
            proxy.doCall(
                address(psm),
                abi.encodeCall(
                    psm.withdraw,
                    (asset, address(proxy), maxAmount)
                )
            ),
            (uint256)
        );

        rateLimits.triggerRateLimitDecrease(
            RateLimitHelpers.makeAssetKey(LIMIT_PSM_WITHDRAW, asset),
            assetsWithdrawn
        );
    }

    /**********************************************************************************************/
    /*** Relayer bridging functions                                                             ***/
    /**********************************************************************************************/

    function transferUSDCToCCTP(uint256 usdcAmount, uint32 destinationDomain) external {
        _checkRole(RELAYER);

        CCTPLib.transferUSDCToCCTP(CCTPLib.TransferUSDCToCCTPParams({
            proxy             : proxy,
            rateLimits        : rateLimits,
            cctp              : cctp,
            usdc              : usdc,
            domainRateLimitId : LIMIT_USDC_TO_DOMAIN,
            cctpRateLimitId   : LIMIT_USDC_TO_CCTP,
            mintRecipient     : mintRecipients[destinationDomain],
            destinationDomain : destinationDomain,
            usdcAmount        : usdcAmount
        }));
    }

    // NOTE: !!! This function was deployed without integration testing !!!
    //       KEEP RATE LIMIT AT ZERO until LayerZero dependencies are live and
    //       all functionality has been thoroughly integration tested.
    function transferTokenLayerZero(
        address oftAddress,
        uint256 amount,
        uint32  destinationEndpointId
    )
        external payable
    {
        _checkRole(RELAYER);
        _rateLimited(
            keccak256(abi.encode(LIMIT_LAYERZERO_TRANSFER, oftAddress, destinationEndpointId)),
            amount
        );

        // NOTE: Full integration testing of this logic is not possible without OFTs with
        //       approvalRequired == true. Add integration testing for this case before
        //       using in production.
        if (ILayerZero(oftAddress).approvalRequired()) {
            ERC20Lib.approve(proxy, ILayerZero(oftAddress).token(), oftAddress, amount);
        }

        bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200_000, 0);

        SendParam memory sendParams = SendParam({
            dstEid       : destinationEndpointId,
            to           : layerZeroRecipients[destinationEndpointId],
            amountLD     : amount,
            minAmountLD  : 0,
            extraOptions : options,
            composeMsg   : "",
            oftCmd       : ""
        });

        // Query the min amount received on the destination chain and set it.
        ( ,, OFTReceipt memory receipt ) = ILayerZero(oftAddress).quoteOFT(sendParams);
        sendParams.minAmountLD = receipt.amountReceivedLD;

        MessagingFee memory fee = ILayerZero(oftAddress).quoteSend(sendParams, false);

        proxy.doCallWithValue{value: fee.nativeFee}(
            oftAddress,
            abi.encodeCall(ILayerZero.send, (sendParams, fee, address(proxy))),
            fee.nativeFee
        );
    }

    /**********************************************************************************************/
    /*** Relayer ERC20 functions                                                                ***/
    /**********************************************************************************************/

    function transferAsset(address asset, address destination, uint256 amount) external {
        _checkRole(RELAYER);
        _rateLimited(
            RateLimitHelpers.makeAssetDestinationKey(LIMIT_ASSET_TRANSFER, asset, destination),
            amount
        );

        ERC20Lib.transfer(proxy, asset, destination, amount);
    }

    /**********************************************************************************************/
    /*** Relayer ERC4626 functions                                                              ***/
    /**********************************************************************************************/

    function depositERC4626(address token, uint256 amount)
        external
        onlyRole(RELAYER)
        rateLimitedAsset(LIMIT_4626_DEPOSIT, token, amount)
        returns (uint256 shares)
    {
        // Note that whitelist is done by rate limits.
        IERC20 asset = IERC20(IERC4626(token).asset());

        // Approve asset to token from the proxy (assumes the proxy has enough of the asset).
        ERC20Lib.approve(proxy, address(asset), token, amount);

        // Deposit asset into the token, proxy receives token shares, decode the resulting shares.
        shares = abi.decode(
            proxy.doCall(
                token,
                abi.encodeCall(IERC4626(token).deposit, (amount, address(proxy)))
            ),
            (uint256)
        );

        require(
            _getExchangeRate(shares, amount) <= maxExchangeRates[token],
            "ForeignController/exchange-rate-too-high"
        );
    }

    function withdrawERC4626(address token, uint256 amount)
        external
        onlyRole(RELAYER)
        rateLimitedAsset(LIMIT_4626_WITHDRAW, token, amount)
        returns (uint256 shares)
    {
        // Withdraw asset from a token, decode resulting shares.
        // Assumes proxy has adequate token shares.
        shares = abi.decode(
            proxy.doCall(
                token,
                abi.encodeCall(IERC4626(token).withdraw, (amount, address(proxy), address(proxy)))
            ),
            (uint256)
        );
    }

    // NOTE: !!! Rate limited at end of function !!!
    function redeemERC4626(address token, uint256 shares)
        external
        onlyRole(RELAYER)
        returns (uint256 assets)
    {
        // Redeem shares for assets from the token, decode the resulting assets.
        // Assumes proxy has adequate token shares.
        assets = abi.decode(
            proxy.doCall(
                token,
                abi.encodeCall(IERC4626(token).redeem, (shares, address(proxy), address(proxy)))
            ),
            (uint256)
        );

        rateLimits.triggerRateLimitDecrease(
            RateLimitHelpers.makeAssetKey(LIMIT_4626_WITHDRAW, token),
            assets
        );
    }

    /**********************************************************************************************/
    /*** Relayer ERC7540 functions                                                              ***/
    /**********************************************************************************************/

    function requestDepositERC7540(address token, uint256 amount)
        external
        onlyRole(RELAYER)
        rateLimitedAsset(LIMIT_7540_DEPOSIT, token, amount)
    {

        // Note that whitelist is done by rate limits
        IERC20 asset = IERC20(IERC7540(token).asset());

        // Approve asset to vault from the proxy (assumes the proxy has enough of the asset).
        ERC20Lib.approve(proxy, address(asset), token, amount);

        // Submit deposit request by transferring assets
        proxy.doCall(
            token,
            abi.encodeCall(IERC7540(token).requestDeposit, (amount, address(proxy), address(proxy)))
        );
    }

    function claimDepositERC7540(address token)
        external
        onlyRole(RELAYER)
        rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_DEPOSIT, token))
    {

        uint256 shares = IERC7540(token).maxMint(address(proxy));

        // Claim shares from the vault to the proxy
        proxy.doCall(
            token,
            abi.encodeCall(IERC4626(token).mint, (shares, address(proxy)))
        );
    }

    function requestRedeemERC7540(address token, uint256 shares)
        external
        onlyRole(RELAYER)
        rateLimitedAsset(LIMIT_7540_REDEEM, token, IERC7540(token).convertToAssets(shares))
    {
        // Submit redeem request by transferring shares
        proxy.doCall(
            token,
            abi.encodeCall(IERC7540(token).requestRedeem, (shares, address(proxy), address(proxy)))
        );
    }

    function claimRedeemERC7540(address token)
        external
        onlyRole(RELAYER)
        rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_REDEEM, token))
    {
        uint256 assets = IERC7540(token).maxWithdraw(address(proxy));

        // Claim assets from the vault to the proxy
        proxy.doCall(
            token,
            abi.encodeCall(IERC7540(token).withdraw, (assets, address(proxy), address(proxy)))
        );
    }

    /**********************************************************************************************/
    /*** Relayer Centrifuge functions                                                           ***/
    /**********************************************************************************************/

    // NOTE: These cancelation methods are compatible with ERC-7887

    function cancelCentrifugeDepositRequest(address token)
        external
        onlyRole(RELAYER)
        rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_DEPOSIT, token))
    {
        // NOTE: While the cancelation is pending, no new deposit request can be submitted
        proxy.doCall(
            token,
            abi.encodeCall(
                ICentrifugeV3VaultLike(token).cancelDepositRequest,
                (CENTRIFUGE_REQUEST_ID, address(proxy))
            )
        );
    }

    function claimCentrifugeCancelDepositRequest(address token)
        external
        onlyRole(RELAYER)
        rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_DEPOSIT, token))
    {
        proxy.doCall(
            token,
            abi.encodeCall(
                ICentrifugeV3VaultLike(token).claimCancelDepositRequest,
                (CENTRIFUGE_REQUEST_ID, address(proxy), address(proxy))
            )
        );
    }

    function cancelCentrifugeRedeemRequest(address token)
        external
        onlyRole(RELAYER)
        rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_REDEEM, token))
    {
        // NOTE: While the cancelation is pending, no new redeem request can be submitted
        proxy.doCall(
            token,
            abi.encodeCall(
                ICentrifugeV3VaultLike(token).cancelRedeemRequest,
                (CENTRIFUGE_REQUEST_ID, address(proxy))
            )
        );
    }

    function claimCentrifugeCancelRedeemRequest(address token)
        external
        onlyRole(RELAYER)
        rateLimitExists(RateLimitHelpers.makeAssetKey(LIMIT_7540_REDEEM, token))
    {
        proxy.doCall(
            token,
            abi.encodeCall(
                ICentrifugeV3VaultLike(token).claimCancelRedeemRequest,
                (CENTRIFUGE_REQUEST_ID, address(proxy), address(proxy))
            )
        );
    }

    function transferSharesCentrifuge(
        address token,
        uint128 amount,
        uint16  destinationCentrifugeId
    )
        external payable
    {
        _checkRole(RELAYER);
        _rateLimited(
            keccak256(abi.encode(LIMIT_CENTRIFUGE_TRANSFER, token, destinationCentrifugeId)),
            amount
        );

        bytes32 recipient = centrifugeRecipients[destinationCentrifugeId];
        require(recipient != 0, "ForeignController/centrifuge-id-not-configured");

        ICentrifugeV3VaultLike centrifugeVault = ICentrifugeV3VaultLike(token);

        address spoke = IAsyncRedeemManagerLike(centrifugeVault.manager()).spoke();

        // Initiate cross-chain transfer via the specific spoke address
        proxy.doCallWithValue{value: msg.value}(
            spoke,
            abi.encodeCall(
                ISpokeLike(spoke).crosschainTransferShares,
                (
                    destinationCentrifugeId,
                    centrifugeVault.poolId(),
                    centrifugeVault.scId(),
                    recipient,
                    amount,
                    0
                )
            ),
            msg.value
        );
    }

    /**********************************************************************************************/
    /*** Relayer Aave functions                                                                 ***/
    /**********************************************************************************************/

    function depositAave(address aToken, uint256 amount)
        external
        onlyRole(RELAYER)
        rateLimitedAsset(LIMIT_AAVE_DEPOSIT, aToken, amount)
    {
        require(maxSlippages[aToken] != 0, "ForeignController/max-slippage-not-set");

        IERC20    underlying = IERC20(IATokenWithPool(aToken).UNDERLYING_ASSET_ADDRESS());
        IAavePool pool       = IAavePool(IATokenWithPool(aToken).POOL());

        uint256 aTokenBalance = IERC20(aToken).balanceOf(address(proxy));

        // Approve underlying to Aave pool from the proxy (assumes the proxy has enough underlying).
        ERC20Lib.approve(proxy, address(underlying), address(pool), amount);

        // Deposit underlying into Aave pool, proxy receives aTokens.
        proxy.doCall(
            address(pool),
            abi.encodeCall(pool.supply, (address(underlying), amount, address(proxy), 0))
        );

        uint256 newATokens = IERC20(aToken).balanceOf(address(proxy)) - aTokenBalance;

        require(
            newATokens >= amount * maxSlippages[aToken] / 1e18,
            "ForeignController/slippage-too-high"
        );
    }

    // NOTE: !!! Rate limited at end of function !!!
    function withdrawAave(address aToken, uint256 amount)
        external
        onlyRole(RELAYER)
        returns (uint256 amountWithdrawn)
    {
        IAavePool pool = IAavePool(IATokenWithPool(aToken).POOL());

        // Withdraw underlying from Aave pool, decode resulting amount withdrawn.
        // Assumes proxy has adequate aTokens.
        amountWithdrawn = abi.decode(
            proxy.doCall(
                address(pool),
                abi.encodeCall(
                    pool.withdraw,
                    (IATokenWithPool(aToken).UNDERLYING_ASSET_ADDRESS(), amount, address(proxy))
                )
            ),
            (uint256)
        );

        rateLimits.triggerRateLimitDecrease(
            RateLimitHelpers.makeAssetKey(LIMIT_AAVE_WITHDRAW, aToken),
            amountWithdrawn
        );
    }

    /**********************************************************************************************/
    /*** Relayer Curve StableSwap functions                                                     ***/
    /**********************************************************************************************/

    function swapCurve(
        address pool,
        uint256 inputIndex,
        uint256 outputIndex,
        uint256 amountIn,
        uint256 minAmountOut
    )
        external returns (uint256 amountOut)
    {
        _checkRole(RELAYER);

        amountOut = CurveLib.swap(CurveLib.SwapCurveParams({
            proxy        : proxy,
            rateLimits   : rateLimits,
            pool         : pool,
            rateLimitId  : LIMIT_CURVE_SWAP,
            inputIndex   : inputIndex,
            outputIndex  : outputIndex,
            amountIn     : amountIn,
            minAmountOut : minAmountOut,
            maxSlippage  : maxSlippages[pool]
        }));
    }

    function addLiquidityCurve(
        address   pool,
        uint256[] memory depositAmounts,
        uint256   minLpAmount
    )
        external returns (uint256 shares)
    {
        _checkRole(RELAYER);

        shares = CurveLib.addLiquidity(CurveLib.AddLiquidityParams({
            proxy                   : proxy,
            rateLimits              : rateLimits,
            pool                    : pool,
            addLiquidityRateLimitId : LIMIT_CURVE_DEPOSIT,
            swapRateLimitId         : LIMIT_CURVE_SWAP,
            minLpAmount             : minLpAmount,
            maxSlippage             : maxSlippages[pool],
            depositAmounts          : depositAmounts
        }));
    }

    function removeLiquidityCurve(
        address   pool,
        uint256   lpBurnAmount,
        uint256[] memory minWithdrawAmounts
    )
        external returns (uint256[] memory withdrawnTokens)
    {
        _checkRole(RELAYER);

        withdrawnTokens = CurveLib.removeLiquidity(CurveLib.RemoveLiquidityParams({
            proxy              : proxy,
            rateLimits         : rateLimits,
            pool               : pool,
            rateLimitId        : LIMIT_CURVE_WITHDRAW,
            lpBurnAmount       : lpBurnAmount,
            minWithdrawAmounts : minWithdrawAmounts,
            maxSlippage        : maxSlippages[pool]
        }));
    }

    /**********************************************************************************************/
    /*** Relayer Merkl functions                                                                 ***/
    /**********************************************************************************************/

    function toggleOperatorMerkl(address operator) external {
        _checkRole(RELAYER);
        require(address(merklDistributor) != address(0), "ForeignController/merkl-distributor-not-set");

        MerklLib.toggleOperator(MerklLib.MerklToggleOperatorParams({
            proxy        : proxy,
            distributor  : merklDistributor,
            operator     : operator
        }));
    }

    /**********************************************************************************************/
    /*** Relayer Pendle functions                                                               ***/
    /**********************************************************************************************/

    function redeemPendlePT(
        address pendleMarket,
        uint256 pyAmountIn,
        uint256 minAmountOut
    ) external {
        _checkRole(RELAYER);

        PendleLib.redeemPendlePT(PendleLib.RedeemPendlePTParams({
            proxy        : proxy,
            rateLimits   : rateLimits,
            rateLimitId  : LIMIT_PENDLE_PT_REDEEM,
            pendleMarket : IPendleMarket(pendleMarket),
            pendleRouter : pendleRouter,
            pyAmountIn   : pyAmountIn,
            minAmountOut : minAmountOut
        }));
    }

    /**********************************************************************************************/
    /*** Relayer UniswapV3 functions                                                            ***/
    /**********************************************************************************************/
    function swapUniswapV3(
        address pool,
        address tokenIn,
        uint256 amountIn,
        uint256 minAmountOut,
        uint24  swapMaxTickDelta
    )
        external returns (uint256 amountOut)
    {
        _checkRole(RELAYER);

        amountOut = UniswapV3Lib.swap(
            UniswapV3Lib.UniV3Context({
                proxy       : proxy,
                rateLimits  : rateLimits,
                rateLimitId : LIMIT_UNISWAP_V3_SWAP,
                pool        : pool
            }),
            UniswapV3Lib.SwapParams({
                router       : uniswapV3Router,
                tokenIn      : tokenIn,
                amountIn     : amountIn,
                minAmountOut : minAmountOut,
                tickDelta    : swapMaxTickDelta,
                poolParams   : uniswapV3PoolParams[pool]
            })
        );
    }


    function addLiquidityUniswapV3(
        address                   pool,
        uint256                   tokenId,
        UniswapV3Lib.Tick         calldata tick,
        UniswapV3Lib.TokenAmounts calldata target,
        UniswapV3Lib.TokenAmounts calldata min,
        uint256                   deadline
    )
        external
        returns (uint256 tokenId_, uint128 liquidity_, uint256 amount0_, uint256 amount1_)
    {
        _checkRole(RELAYER);

        UniswapV3Lib.UniswapV3PoolParams memory poolParams = uniswapV3PoolParams[pool];
        uint256 maxSlippage                                = maxSlippages[pool];

        (tokenId_, liquidity_, amount0_, amount1_) = UniswapV3Lib.addLiquidity(
            UniswapV3Lib.UniV3Context({
                proxy       : proxy,
                rateLimits  : rateLimits,
                rateLimitId : LIMIT_UNISWAP_V3_DEPOSIT,
                pool        : pool
            }),
            UniswapV3Lib.AddLiquidityParams({
                positionManager : uniswapV3PositionManager,
                tokenId         : tokenId,
                tick            : tick,
                target          : target,
                min             : min,
                tickBounds      : poolParams.addLiquidityTickBounds,
                maxSlippage     : maxSlippage,
                deadline        : deadline,
                twapSecondsAgo  : poolParams.twapSecondsAgo
            })
        );
    }

    function removeLiquidityUniswapV3(
        address                   pool,
        uint256                   tokenId,
        uint128                   liquidity,
        UniswapV3Lib.TokenAmounts calldata min,
        uint256                   deadline
    )
        external
        onlyRole(RELAYER)
        returns (uint256 amount0Collected, uint256 amount1Collected)
    {
        return UniswapV3Lib.removeLiquidity(
            UniswapV3Lib.UniV3Context({
                proxy       : proxy,
                rateLimits  : rateLimits,
                rateLimitId : LIMIT_UNISWAP_V3_WITHDRAW,
                pool        : pool
            }),
            UniswapV3Lib.RemoveLiquidityParams({
                positionManager : uniswapV3PositionManager,
                tokenId         : tokenId,
                liquidity       : liquidity,
                min             : min,
                maxSlippage     : maxSlippages[pool],
                deadline        : deadline
            })
        );
    }


    /**********************************************************************************************/
    /*** Internal helper functions                                                              ***/
    /**********************************************************************************************/

    function _rateLimited(bytes32 key, uint256 amount) internal {
        rateLimits.triggerRateLimitDecrease(key, amount);
    }

    /**********************************************************************************************/
    /*** Exchange rate helper functions                                                         ***/
    /**********************************************************************************************/

    function _getExchangeRate(uint256 shares, uint256 assets) internal pure returns (uint256) {
        // Return 0 for zero assets first, to handle the valid case of 0 shares and 0 assets.
        if (assets == 0) return 0;

        // Zero shares with non-zero assets is invalid (infinite exchange rate).
        if (shares == 0) revert("ForeignController/zero-shares");

        return (EXCHANGE_RATE_PRECISION * assets) / shares;
    }

}