diff --git a/contracts/FraxOFTAdapterUpgradeable.sol b/contracts/FraxOFTAdapterUpgradeable.sol index 0b9e3b2f..005e550f 100644 --- a/contracts/FraxOFTAdapterUpgradeable.sol +++ b/contracts/FraxOFTAdapterUpgradeable.sol @@ -2,8 +2,14 @@ pragma solidity ^0.8.22; import { OFTAdapterUpgradeable } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/OFTAdapterUpgradeable.sol"; +import { SendParam, OFTLimit, OFTFeeDetail, OFTReceipt } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; +import { RateLimiterModule } from "contracts/modules/RateLimiterModule.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract FraxOFTAdapterUpgradeable is OFTAdapterUpgradeable, RateLimiterModule { + using SafeERC20 for IERC20; -contract FraxOFTAdapterUpgradeable is OFTAdapterUpgradeable { constructor( address _token, address _lzEndpoint @@ -22,4 +28,46 @@ contract FraxOFTAdapterUpgradeable is OFTAdapterUpgradeable { __Ownable_init(); _transferOwnership(_delegate); } + + function quoteOFT( + SendParam calldata _sendParam + ) + external + view + override + returns (OFTLimit memory oftLimit, OFTFeeDetail[] memory oftFeeDetails, OFTReceipt memory oftReceipt) + { + uint256 minAmountLD = 0; + uint256 maxAmountLD = _removeDust(_rateLimitedMaxAmountLD(_sendParam.dstEid)); + oftLimit = OFTLimit(minAmountLD, maxAmountLD); + + oftFeeDetails = new OFTFeeDetail[](0); + + (uint256 amountSentLD, uint256 amountReceivedLD) = _debitView( + _sendParam.amountLD, + _sendParam.minAmountLD, + _sendParam.dstEid + ); + oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD); + } + + function _debit( + uint256 _amountLD, + uint256 _minAmountLD, + uint32 _dstEid + ) internal override returns (uint256 amountSentLD, uint256 amountReceivedLD) { + (amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid); + _consumeOutboundRateLimit(_dstEid, amountSentLD); + innerToken.safeTransferFrom(msg.sender, address(this), amountSentLD); + } + + function _credit( + address _to, + uint256 _amountLD, + uint32 _srcEid + ) internal override returns (uint256 amountReceivedLD) { + _consumeInboundRateLimit(_srcEid, _amountLD); + innerToken.safeTransfer(_to, _amountLD); + return _amountLD; + } } diff --git a/contracts/FraxOFTMintableAdapterUpgradeable.sol b/contracts/FraxOFTMintableAdapterUpgradeable.sol index 5d338f34..32c915b1 100644 --- a/contracts/FraxOFTMintableAdapterUpgradeable.sol +++ b/contracts/FraxOFTMintableAdapterUpgradeable.sol @@ -2,14 +2,16 @@ pragma solidity ^0.8.22; import { OFTAdapterUpgradeable } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/OFTAdapterUpgradeable.sol"; +import { SendParam, OFTLimit, OFTFeeDetail, OFTReceipt } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; import { SupplyTrackingModule } from "./modules/SupplyTrackingModule.sol"; +import { RateLimiterModule } from "./modules/RateLimiterModule.sol"; interface IERC20PermitPermissionedOptiMintable { function minter_burn_from(address, uint256) external; function minter_mint(address, uint256) external; } -contract FraxOFTMintableAdapterUpgradeable is OFTAdapterUpgradeable, SupplyTrackingModule { +contract FraxOFTMintableAdapterUpgradeable is OFTAdapterUpgradeable, SupplyTrackingModule, RateLimiterModule { constructor( address _token, address _lzEndpoint @@ -36,6 +38,28 @@ contract FraxOFTMintableAdapterUpgradeable is OFTAdapterUpgradeable, SupplyTrack _setInitialTotalSupply(_eid, _amount); } + function quoteOFT( + SendParam calldata _sendParam + ) + external + view + override + returns (OFTLimit memory oftLimit, OFTFeeDetail[] memory oftFeeDetails, OFTReceipt memory oftReceipt) + { + uint256 minAmountLD = 0; + uint256 maxAmountLD = _removeDust(_rateLimitedMaxAmountLD(_sendParam.dstEid)); + oftLimit = OFTLimit(minAmountLD, maxAmountLD); + + oftFeeDetails = new OFTFeeDetail[](0); + + (uint256 amountSentLD, uint256 amountReceivedLD) = _debitView( + _sendParam.amountLD, + _sendParam.minAmountLD, + _sendParam.dstEid + ); + oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD); + } + /// @dev overrides OFTAdapterUpgradeable.sol to burn the tokens from the sender/track supply /// @dev added in v1.1.0 function _debit( @@ -44,6 +68,7 @@ contract FraxOFTMintableAdapterUpgradeable is OFTAdapterUpgradeable, SupplyTrack uint32 _dstEid ) internal override returns (uint256 amountSentLD, uint256 amountReceivedLD) { (amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid); + _consumeOutboundRateLimit(_dstEid, amountSentLD); _addToTotalTransferTo(_dstEid, amountSentLD); @@ -57,6 +82,7 @@ contract FraxOFTMintableAdapterUpgradeable is OFTAdapterUpgradeable, SupplyTrack uint256 _amountLD, uint32 _srcEid ) internal override returns (uint256 amountReceivedLD) { + _consumeInboundRateLimit(_srcEid, _amountLD); _addToTotalTransferFrom(_srcEid, _amountLD); diff --git a/contracts/FraxOFTMintableAdapterUpgradeableTIP20.sol b/contracts/FraxOFTMintableAdapterUpgradeableTIP20.sol index 02cbf8ec..86616f02 100644 --- a/contracts/FraxOFTMintableAdapterUpgradeableTIP20.sol +++ b/contracts/FraxOFTMintableAdapterUpgradeableTIP20.sol @@ -3,14 +3,15 @@ pragma solidity ^0.8.22; import { OFTAdapterUpgradeable } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/OFTAdapterUpgradeable.sol"; import { SupplyTrackingModule } from "contracts/modules/SupplyTrackingModule.sol"; +import { RateLimiterModule } from "contracts/modules/RateLimiterModule.sol"; import { TempoAltTokenBase } from "contracts/base/TempoAltTokenBase.sol"; import { ITIP20 } from "@tempo/interfaces/ITIP20.sol"; import { MessagingFee, MessagingReceipt } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; -import { IOFT, SendParam, OFTReceipt } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; +import { IOFT, SendParam, OFTLimit, OFTFeeDetail, OFTReceipt } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -contract FraxOFTMintableAdapterUpgradeableTIP20 is OFTAdapterUpgradeable, SupplyTrackingModule, TempoAltTokenBase { +contract FraxOFTMintableAdapterUpgradeableTIP20 is OFTAdapterUpgradeable, SupplyTrackingModule, TempoAltTokenBase, RateLimiterModule { /// @notice Emitted when ERC20 tokens are recovered event RecoveredERC20(address indexed token, uint256 amount); @@ -47,6 +48,28 @@ contract FraxOFTMintableAdapterUpgradeableTIP20 is OFTAdapterUpgradeable, Supply _setInitialTotalSupply(_eid, _amount); } + function quoteOFT( + SendParam calldata _sendParam + ) + external + view + override + returns (OFTLimit memory oftLimit, OFTFeeDetail[] memory oftFeeDetails, OFTReceipt memory oftReceipt) + { + uint256 minAmountLD = 0; + uint256 maxAmountLD = _removeDust(_rateLimitedMaxAmountLD(_sendParam.dstEid)); + oftLimit = OFTLimit(minAmountLD, maxAmountLD); + + oftFeeDetails = new OFTFeeDetail[](0); + + (uint256 amountSentLD, uint256 amountReceivedLD) = _debitView( + _sendParam.amountLD, + _sendParam.minAmountLD, + _sendParam.dstEid + ); + oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD); + } + /// @inheritdoc IOFT /// @dev Overrides send to prevent msg.value being sent (EndpointV2Alt uses ERC20 for gas) function send( @@ -81,6 +104,7 @@ contract FraxOFTMintableAdapterUpgradeableTIP20 is OFTAdapterUpgradeable, Supply uint32 _dstEid ) internal override returns (uint256 amountSentLD, uint256 amountReceivedLD) { (amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid); + _consumeOutboundRateLimit(_dstEid, amountSentLD); _addToTotalTransferTo(_dstEid, amountSentLD); @@ -96,6 +120,7 @@ contract FraxOFTMintableAdapterUpgradeableTIP20 is OFTAdapterUpgradeable, Supply uint256 _amountLD, uint32 _srcEid ) internal override returns (uint256 amountReceivedLD) { + _consumeInboundRateLimit(_srcEid, _amountLD); _addToTotalTransferFrom(_srcEid, _amountLD); ITIP20(address(innerToken)).mint(_to, _amountLD); diff --git a/contracts/FraxOFTUpgradeable.sol b/contracts/FraxOFTUpgradeable.sol index 43edaf08..d8fd133a 100644 --- a/contracts/FraxOFTUpgradeable.sol +++ b/contracts/FraxOFTUpgradeable.sol @@ -2,14 +2,15 @@ pragma solidity ^0.8.22; import { OFTUpgradeable } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/OFTUpgradeable.sol"; -import { SendParam } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; +import { SendParam, OFTLimit, OFTFeeDetail, OFTReceipt } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; import {EIP3009Module} from "contracts/modules/EIP3009Module.sol"; import {PermitModule} from "contracts/modules/PermitModule.sol"; +import {RateLimiterModule} from "contracts/modules/RateLimiterModule.sol"; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -contract FraxOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule { +contract FraxOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule, RateLimiterModule { constructor(address _lzEndpoint) OFTUpgradeable(_lzEndpoint) { _disableInitializers(); } @@ -63,10 +64,52 @@ contract FraxOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule { return _buildMsgAndOptions(_sendParam, _amountLD); } + function quoteOFT( + SendParam calldata _sendParam + ) + external + view + override + returns (OFTLimit memory oftLimit, OFTFeeDetail[] memory oftFeeDetails, OFTReceipt memory oftReceipt) + { + uint256 minAmountLD = 0; + uint256 maxAmountLD = _removeDust(_rateLimitedMaxAmountLD(_sendParam.dstEid)); + oftLimit = OFTLimit(minAmountLD, maxAmountLD); + + oftFeeDetails = new OFTFeeDetail[](0); + + (uint256 amountSentLD, uint256 amountReceivedLD) = _debitView( + _sendParam.amountLD, + _sendParam.minAmountLD, + _sendParam.dstEid + ); + oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD); + } + //============================================================================== // Overrides //============================================================================== + function _debit( + uint256 _amountLD, + uint256 _minAmountLD, + uint32 _dstEid + ) internal override returns (uint256 amountSentLD, uint256 amountReceivedLD) { + (amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid); + _consumeOutboundRateLimit(_dstEid, amountSentLD); + _burn(msg.sender, amountSentLD); + } + + function _credit( + address _to, + uint256 _amountLD, + uint32 _srcEid + ) internal override returns (uint256 amountReceivedLD) { + _consumeInboundRateLimit(_srcEid, _amountLD); + _mint(_to, _amountLD); + return _amountLD; + } + /// @dev supports EIP3009 function _transfer(address from, address to, uint256 amount) internal override(EIP3009Module, ERC20Upgradeable) { return ERC20Upgradeable._transfer(from, to, amount); @@ -76,4 +119,4 @@ contract FraxOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule { function _approve(address owner, address spender, uint256 amount) internal override(PermitModule, ERC20Upgradeable) { return ERC20Upgradeable._approve(owner, spender, amount); } -} \ No newline at end of file +} diff --git a/contracts/WFRAXTokenOFTUpgradeable.sol b/contracts/WFRAXTokenOFTUpgradeable.sol index 67496125..2aa7a631 100644 --- a/contracts/WFRAXTokenOFTUpgradeable.sol +++ b/contracts/WFRAXTokenOFTUpgradeable.sol @@ -2,14 +2,15 @@ pragma solidity ^0.8.22; import { OFTUpgradeable } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/OFTUpgradeable.sol"; -import { SendParam } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; +import { SendParam, OFTLimit, OFTFeeDetail, OFTReceipt } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; import {EIP3009Module} from "contracts/modules/EIP3009Module.sol"; import {PermitModule} from "contracts/modules/PermitModule.sol"; +import {RateLimiterModule} from "contracts/modules/RateLimiterModule.sol"; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -contract WFRAXTokenOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule { +contract WFRAXTokenOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule, RateLimiterModule { constructor(address _lzEndpoint) OFTUpgradeable(_lzEndpoint) { _disableInitializers(); } @@ -57,10 +58,52 @@ contract WFRAXTokenOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule return _buildMsgAndOptions(_sendParam, _amountLD); } + function quoteOFT( + SendParam calldata _sendParam + ) + external + view + override + returns (OFTLimit memory oftLimit, OFTFeeDetail[] memory oftFeeDetails, OFTReceipt memory oftReceipt) + { + uint256 minAmountLD = 0; + uint256 maxAmountLD = _removeDust(_rateLimitedMaxAmountLD(_sendParam.dstEid)); + oftLimit = OFTLimit(minAmountLD, maxAmountLD); + + oftFeeDetails = new OFTFeeDetail[](0); + + (uint256 amountSentLD, uint256 amountReceivedLD) = _debitView( + _sendParam.amountLD, + _sendParam.minAmountLD, + _sendParam.dstEid + ); + oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD); + } + //============================================================================== // Overrides //============================================================================== + function _debit( + uint256 _amountLD, + uint256 _minAmountLD, + uint32 _dstEid + ) internal override returns (uint256 amountSentLD, uint256 amountReceivedLD) { + (amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid); + _consumeOutboundRateLimit(_dstEid, amountSentLD); + _burn(msg.sender, amountSentLD); + } + + function _credit( + address _to, + uint256 _amountLD, + uint32 _srcEid + ) internal override returns (uint256 amountReceivedLD) { + _consumeInboundRateLimit(_srcEid, _amountLD); + _mint(_to, _amountLD); + return _amountLD; + } + /// @dev supports EIP3009 function _transfer(address from, address to, uint256 amount) internal override(EIP3009Module, ERC20Upgradeable) { return ERC20Upgradeable._transfer(from, to, amount); @@ -70,4 +113,4 @@ contract WFRAXTokenOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule function _approve(address owner, address spender, uint256 amount) internal override(PermitModule, ERC20Upgradeable) { return ERC20Upgradeable._approve(owner, spender, amount); } -} \ No newline at end of file +} diff --git a/contracts/frxUsd/FrxUSDOFTUpgradeable.sol b/contracts/frxUsd/FrxUSDOFTUpgradeable.sol index 9e391e1a..9a6704de 100644 --- a/contracts/frxUsd/FrxUSDOFTUpgradeable.sol +++ b/contracts/frxUsd/FrxUSDOFTUpgradeable.sol @@ -6,10 +6,11 @@ import { FreezeThawModule } from "contracts/modules/FreezeThawModule.sol"; import { PauseModule } from "contracts/modules/PauseModule.sol"; import { EIP3009Module } from "contracts/modules/EIP3009Module.sol"; import { PermitModule } from "contracts/modules/PermitModule.sol"; -import { SendParam } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; +import { RateLimiterModule } from "contracts/modules/RateLimiterModule.sol"; +import { SendParam, OFTLimit, OFTFeeDetail, OFTReceipt } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -contract FrxUSDOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule, FreezeThawModule, PauseModule { +contract FrxUSDOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule, FreezeThawModule, PauseModule, RateLimiterModule { constructor(address _lzEndpoint) OFTUpgradeable(_lzEndpoint) { _disableInitializers(); } @@ -164,10 +165,52 @@ contract FrxUSDOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule, Fr return _buildMsgAndOptions(_sendParam, _amountLD); } + function quoteOFT( + SendParam calldata _sendParam + ) + external + view + override + returns (OFTLimit memory oftLimit, OFTFeeDetail[] memory oftFeeDetails, OFTReceipt memory oftReceipt) + { + uint256 minAmountLD = 0; + uint256 maxAmountLD = _removeDust(_rateLimitedMaxAmountLD(_sendParam.dstEid)); + oftLimit = OFTLimit(minAmountLD, maxAmountLD); + + oftFeeDetails = new OFTFeeDetail[](0); + + (uint256 amountSentLD, uint256 amountReceivedLD) = _debitView( + _sendParam.amountLD, + _sendParam.minAmountLD, + _sendParam.dstEid + ); + oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD); + } + //============================================================================== // Overrides //============================================================================== + function _debit( + uint256 _amountLD, + uint256 _minAmountLD, + uint32 _dstEid + ) internal override returns (uint256 amountSentLD, uint256 amountReceivedLD) { + (amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid); + _consumeOutboundRateLimit(_dstEid, amountSentLD); + _burn(msg.sender, amountSentLD); + } + + function _credit( + address _to, + uint256 _amountLD, + uint32 _srcEid + ) internal override returns (uint256 amountReceivedLD) { + _consumeInboundRateLimit(_srcEid, _amountLD); + _mint(_to, _amountLD); + return _amountLD; + } + /// @dev supports EIP3009 function _transfer(address from, address to, uint256 amount) internal override(EIP3009Module, ERC20Upgradeable) { return ERC20Upgradeable._transfer(from, to, amount); diff --git a/contracts/frxUsd/SFrxUSDOFTUpgradeable.sol b/contracts/frxUsd/SFrxUSDOFTUpgradeable.sol index ee99085d..f71cdd95 100644 --- a/contracts/frxUsd/SFrxUSDOFTUpgradeable.sol +++ b/contracts/frxUsd/SFrxUSDOFTUpgradeable.sol @@ -2,14 +2,15 @@ pragma solidity ^0.8.22; import { OFTUpgradeable } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/OFTUpgradeable.sol"; -import { SendParam } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; +import { SendParam, OFTLimit, OFTFeeDetail, OFTReceipt } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; import {EIP3009Module} from "contracts/modules/EIP3009Module.sol"; import {PermitModule} from "contracts/modules/PermitModule.sol"; +import {RateLimiterModule} from "contracts/modules/RateLimiterModule.sol"; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -contract SFrxUSDOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule { +contract SFrxUSDOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule, RateLimiterModule { constructor(address _lzEndpoint) OFTUpgradeable(_lzEndpoint) { _disableInitializers(); } @@ -59,10 +60,52 @@ contract SFrxUSDOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule { return _buildMsgAndOptions(_sendParam, _amountLD); } + function quoteOFT( + SendParam calldata _sendParam + ) + external + view + override + returns (OFTLimit memory oftLimit, OFTFeeDetail[] memory oftFeeDetails, OFTReceipt memory oftReceipt) + { + uint256 minAmountLD = 0; + uint256 maxAmountLD = _removeDust(_rateLimitedMaxAmountLD(_sendParam.dstEid)); + oftLimit = OFTLimit(minAmountLD, maxAmountLD); + + oftFeeDetails = new OFTFeeDetail[](0); + + (uint256 amountSentLD, uint256 amountReceivedLD) = _debitView( + _sendParam.amountLD, + _sendParam.minAmountLD, + _sendParam.dstEid + ); + oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD); + } + //============================================================================== // Overrides //============================================================================== + function _debit( + uint256 _amountLD, + uint256 _minAmountLD, + uint32 _dstEid + ) internal override returns (uint256 amountSentLD, uint256 amountReceivedLD) { + (amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid); + _consumeOutboundRateLimit(_dstEid, amountSentLD); + _burn(msg.sender, amountSentLD); + } + + function _credit( + address _to, + uint256 _amountLD, + uint32 _srcEid + ) internal override returns (uint256 amountReceivedLD) { + _consumeInboundRateLimit(_srcEid, _amountLD); + _mint(_to, _amountLD); + return _amountLD; + } + /// @dev supports EIP3009 function _transfer(address from, address to, uint256 amount) internal override(EIP3009Module, ERC20Upgradeable) { return ERC20Upgradeable._transfer(from, to, amount); diff --git a/contracts/modules/RateLimiterModule.sol b/contracts/modules/RateLimiterModule.sol new file mode 100644 index 00000000..c01b4368 --- /dev/null +++ b/contracts/modules/RateLimiterModule.sol @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +abstract contract RateLimiterModule { + struct RateLimitGlobalConfig { + bool isGloballyDisabled; + } + + struct RateLimitConfig { + bool overrideDefaultConfig; + bool outboundEnabled; + bool inboundEnabled; + uint256 outboundLimit; + uint256 inboundLimit; + uint32 outboundWindow; + uint32 inboundWindow; + } + + struct RateLimitState { + uint256 outboundUsage; + uint256 inboundUsage; + uint40 lastUpdated; + } + + struct SetRateLimitConfigParam { + uint32 eid; + RateLimitConfig config; + } + + struct SetRateLimitStateParam { + uint32 eid; + RateLimitState state; + } + + struct RateLimiterStorage { + RateLimitGlobalConfig globalConfig; + RateLimitConfig defaultConfig; + mapping(uint32 eid => RateLimitConfig config) configs; + mapping(uint32 eid => RateLimitState state) states; + } + + /// @dev keccak256(abi.encode(uint256(keccak256("frax.storage.RateLimiterModule")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant RateLimiterStorageLocation = + 0xbc2f4e0019400f86d65d92ee20d8164c6b63b661679e24c1985de9d3497f1b00; + + event RateLimitGlobalConfigSet(bool isGloballyDisabled); + event DefaultRateLimitConfigSet( + bool outboundEnabled, + bool inboundEnabled, + uint256 outboundLimit, + uint256 inboundLimit, + uint32 outboundWindow, + uint32 inboundWindow + ); + event RateLimitConfigSet( + uint32 indexed eid, + bool overrideDefaultConfig, + bool outboundEnabled, + bool inboundEnabled, + uint256 outboundLimit, + uint256 inboundLimit, + uint32 outboundWindow, + uint32 inboundWindow + ); + event RateLimitStateSet( + uint32 indexed eid, + uint256 outboundUsage, + uint256 inboundUsage, + uint40 lastUpdated + ); + event RateLimitCheckpointed( + uint32 indexed eid, + uint256 outboundUsage, + uint256 inboundUsage, + uint40 lastUpdated + ); + event RateLimitConsumed( + uint32 indexed eid, + bool indexed isOutbound, + uint256 amountLD, + uint256 usage, + uint256 limit + ); + + error RateLimitExceeded(uint32 eid, bool isOutbound, uint256 amountLD, uint256 availableLD); + error InvalidRateLimitConfig(); + error InvalidRateLimitState(); + error RateLimitManagerOnly(); + error OwnerUnavailable(); + + function _getRateLimiterStorage() private pure returns (RateLimiterStorage storage $) { + assembly { + $.slot := RateLimiterStorageLocation + } + } + + modifier onlyRateLimitManager() { + if (msg.sender != _rateLimitOwner()) revert RateLimitManagerOnly(); + _; + } + + function rateLimitGlobalConfig() public view returns (RateLimitGlobalConfig memory config) { + RateLimiterStorage storage $ = _getRateLimiterStorage(); + return $.globalConfig; + } + + function defaultRateLimitConfig() public view returns (RateLimitConfig memory config) { + RateLimiterStorage storage $ = _getRateLimiterStorage(); + return $.defaultConfig; + } + + function storedRateLimitConfig(uint32 _eid) public view returns (RateLimitConfig memory config) { + RateLimiterStorage storage $ = _getRateLimiterStorage(); + return $.configs[_eid]; + } + + function rateLimitConfig(uint32 _eid) public view returns (RateLimitConfig memory config) { + return _effectiveRateLimitConfig(_eid); + } + + function storedRateLimitState(uint32 _eid) public view returns (RateLimitState memory state) { + RateLimiterStorage storage $ = _getRateLimiterStorage(); + return $.states[_eid]; + } + + function rateLimitState(uint32 _eid) public view returns (RateLimitState memory state) { + RateLimitConfig memory config = _effectiveRateLimitConfig(_eid); + return _currentRateLimitState(_eid, config); + } + + function outboundRateLimitAvailable(uint32 _eid) public view returns (uint256 availableLD) { + return _outboundRateLimitAvailable(_eid); + } + + function inboundRateLimitAvailable(uint32 _eid) public view returns (uint256 availableLD) { + return _inboundRateLimitAvailable(_eid); + } + + function setRateLimitGlobalConfig(RateLimitGlobalConfig calldata _globalConfig) external onlyRateLimitManager { + RateLimiterStorage storage $ = _getRateLimiterStorage(); + $.globalConfig = _globalConfig; + emit RateLimitGlobalConfigSet(_globalConfig.isGloballyDisabled); + } + + function setDefaultRateLimitConfig(RateLimitConfig calldata _defaultConfig) external onlyRateLimitManager { + _validateRateLimitConfig(_defaultConfig); + + RateLimiterStorage storage $ = _getRateLimiterStorage(); + $.defaultConfig = _defaultConfig; + + emit DefaultRateLimitConfigSet( + _defaultConfig.outboundEnabled, + _defaultConfig.inboundEnabled, + _defaultConfig.outboundLimit, + _defaultConfig.inboundLimit, + _defaultConfig.outboundWindow, + _defaultConfig.inboundWindow + ); + } + + function setRateLimitConfigs(SetRateLimitConfigParam[] calldata _params) external onlyRateLimitManager { + RateLimiterStorage storage $ = _getRateLimiterStorage(); + + uint256 length = _params.length; + for (uint256 i; i < length; ++i) { + _validateRateLimitConfig(_params[i].config); + _checkpointRateLimit(_params[i].eid, _effectiveRateLimitConfig(_params[i].eid)); + + $.configs[_params[i].eid] = _params[i].config; + + emit RateLimitConfigSet( + _params[i].eid, + _params[i].config.overrideDefaultConfig, + _params[i].config.outboundEnabled, + _params[i].config.inboundEnabled, + _params[i].config.outboundLimit, + _params[i].config.inboundLimit, + _params[i].config.outboundWindow, + _params[i].config.inboundWindow + ); + } + } + + function setRateLimitStates(SetRateLimitStateParam[] calldata _params) external onlyRateLimitManager { + RateLimiterStorage storage $ = _getRateLimiterStorage(); + + uint256 length = _params.length; + for (uint256 i; i < length; ++i) { + _validateRateLimitState(_params[i].state); + $.states[_params[i].eid] = _params[i].state; + + emit RateLimitStateSet( + _params[i].eid, + _params[i].state.outboundUsage, + _params[i].state.inboundUsage, + _params[i].state.lastUpdated + ); + } + } + + function checkpointRateLimits(uint32[] calldata _eids) external onlyRateLimitManager { + uint256 length = _eids.length; + for (uint256 i; i < length; ++i) { + _checkpointRateLimit(_eids[i], _effectiveRateLimitConfig(_eids[i])); + } + } + + function _consumeOutboundRateLimit(uint32 _dstEid, uint256 _amountLD) internal { + _consumeRateLimit(_dstEid, _amountLD, true); + } + + function _consumeInboundRateLimit(uint32 _srcEid, uint256 _amountLD) internal { + _consumeRateLimit(_srcEid, _amountLD, false); + } + + function _outboundRateLimitAvailable(uint32 _dstEid) internal view returns (uint256 availableLD) { + RateLimiterStorage storage $ = _getRateLimiterStorage(); + if ($.globalConfig.isGloballyDisabled) return type(uint256).max; + + RateLimitConfig memory config = _effectiveRateLimitConfig(_dstEid); + if (!config.outboundEnabled) return type(uint256).max; + + RateLimitState memory state = _currentRateLimitState(_dstEid, config); + if (state.outboundUsage >= config.outboundLimit) return 0; + + return config.outboundLimit - state.outboundUsage; + } + + function _inboundRateLimitAvailable(uint32 _srcEid) internal view returns (uint256 availableLD) { + RateLimiterStorage storage $ = _getRateLimiterStorage(); + if ($.globalConfig.isGloballyDisabled) return type(uint256).max; + + RateLimitConfig memory config = _effectiveRateLimitConfig(_srcEid); + if (!config.inboundEnabled) return type(uint256).max; + + RateLimitState memory state = _currentRateLimitState(_srcEid, config); + if (state.inboundUsage >= config.inboundLimit) return 0; + + return config.inboundLimit - state.inboundUsage; + } + + function _effectiveRateLimitConfig(uint32 _eid) internal view returns (RateLimitConfig memory config) { + RateLimiterStorage storage $ = _getRateLimiterStorage(); + config = $.configs[_eid]; + if (!config.overrideDefaultConfig) { + config = $.defaultConfig; + } + } + + function _currentRateLimitState( + uint32 _eid, + RateLimitConfig memory _config + ) internal view returns (RateLimitState memory state) { + RateLimiterStorage storage $ = _getRateLimiterStorage(); + state = $.states[_eid]; + + uint40 currentTimestamp = uint40(block.timestamp); + uint40 lastUpdated = state.lastUpdated; + if (lastUpdated == 0 || lastUpdated >= currentTimestamp) { + state.lastUpdated = currentTimestamp; + return state; + } + + uint256 elapsed = currentTimestamp - lastUpdated; + state.outboundUsage = _decayUsage(state.outboundUsage, _config.outboundLimit, _config.outboundWindow, elapsed); + state.inboundUsage = _decayUsage(state.inboundUsage, _config.inboundLimit, _config.inboundWindow, elapsed); + state.lastUpdated = currentTimestamp; + } + + function _checkpointRateLimit(uint32 _eid, RateLimitConfig memory _config) internal { + RateLimiterStorage storage $ = _getRateLimiterStorage(); + RateLimitState memory currentState = _currentRateLimitState(_eid, _config); + $.states[_eid] = currentState; + + emit RateLimitCheckpointed( + _eid, + currentState.outboundUsage, + currentState.inboundUsage, + currentState.lastUpdated + ); + } + + function _consumeRateLimit(uint32 _eid, uint256 _amountLD, bool _isOutbound) internal { + if (_amountLD == 0) return; + + RateLimiterStorage storage $ = _getRateLimiterStorage(); + if ($.globalConfig.isGloballyDisabled) return; + + RateLimitConfig memory config = _effectiveRateLimitConfig(_eid); + if (_isOutbound && !config.outboundEnabled) return; + if (!_isOutbound && !config.inboundEnabled) return; + + RateLimitState memory currentState = _currentRateLimitState(_eid, config); + + uint256 limit = _isOutbound ? config.outboundLimit : config.inboundLimit; + uint256 usage = _isOutbound ? currentState.outboundUsage : currentState.inboundUsage; + uint256 available = usage >= limit ? 0 : limit - usage; + if (_amountLD > available) revert RateLimitExceeded(_eid, _isOutbound, _amountLD, available); + + if (_isOutbound) { + currentState.outboundUsage = usage + _amountLD; + } else { + currentState.inboundUsage = usage + _amountLD; + } + + $.states[_eid] = currentState; + + emit RateLimitConsumed( + _eid, + _isOutbound, + _amountLD, + _isOutbound ? currentState.outboundUsage : currentState.inboundUsage, + limit + ); + } + + function _validateRateLimitConfig(RateLimitConfig memory _config) internal pure { + if (_config.outboundEnabled && (_config.outboundLimit == 0 || _config.outboundWindow == 0)) { + revert InvalidRateLimitConfig(); + } + if (_config.inboundEnabled && (_config.inboundLimit == 0 || _config.inboundWindow == 0)) { + revert InvalidRateLimitConfig(); + } + } + + function _validateRateLimitState(RateLimitState memory _state) internal view { + if (_state.lastUpdated > block.timestamp) revert InvalidRateLimitState(); + if (_state.lastUpdated == 0 && (_state.outboundUsage != 0 || _state.inboundUsage != 0)) { + revert InvalidRateLimitState(); + } + } + + function _decayUsage( + uint256 _usage, + uint256 _limit, + uint32 _window, + uint256 _elapsed + ) internal pure returns (uint256 decayedUsage) { + if (_usage == 0 || _limit == 0 || _window == 0 || _elapsed == 0) { + return _usage; + } + if (_elapsed >= _window) { + return 0; + } + + uint256 replenished = (_limit * _elapsed) / _window; + return replenished >= _usage ? 0 : _usage - replenished; + } + + function _rateLimitedMaxAmountLD(uint32 _dstEid) internal view returns (uint256 maxAmountLD) { + return _min(_outboundRateLimitAvailable(_dstEid), uint256(type(uint64).max)); + } + + function _min(uint256 _a, uint256 _b) internal pure returns (uint256) { + return _a < _b ? _a : _b; + } + + function _rateLimitOwner() internal view returns (address ownerAddress) { + (bool success, bytes memory data) = address(this).staticcall(abi.encodeWithSignature("owner()")); + if (!success || data.length < 32) revert OwnerUnavailable(); + ownerAddress = abi.decode(data, (address)); + } +} diff --git a/scripts/DeployFraxOFTProtocol/DeployFraxOFTProtocol.s.sol b/scripts/DeployFraxOFTProtocol/DeployFraxOFTProtocol.s.sol index 5955a21e..fb774670 100644 --- a/scripts/DeployFraxOFTProtocol/DeployFraxOFTProtocol.s.sol +++ b/scripts/DeployFraxOFTProtocol/DeployFraxOFTProtocol.s.sol @@ -4,13 +4,14 @@ pragma solidity ^0.8.19; import "../BaseL0Script.sol"; import { SetDVNs } from "scripts/DeployFraxOFTProtocol/inherited/SetDVNs.s.sol"; +import { SetRateLimits } from "scripts/DeployFraxOFTProtocol/inherited/SetRateLimits.s.sol"; /* TODO - Deployment handling on non-pre-deterministic chains */ -contract DeployFraxOFTProtocol is SetDVNs, BaseL0Script { +contract DeployFraxOFTProtocol is SetDVNs, SetRateLimits, BaseL0Script { using OptionsBuilder for bytes; using stdJson for string; using Strings for uint256; @@ -74,6 +75,11 @@ contract DeployFraxOFTProtocol is SetDVNs, BaseL0Script { _configs: broadcastConfigArray }); + setRateLimits({ + _connectedOfts: connectedOfts, + _configs: broadcastConfigArray + }); + setDVNs({ _connectedConfig: _connectedConfig, _connectedOfts: connectedOfts, @@ -92,6 +98,11 @@ contract DeployFraxOFTProtocol is SetDVNs, BaseL0Script { setupEvms(); setupNonEvms(); + setRateLimits({ + _connectedOfts: proxyOfts, + _configs: allConfigs + }); + /// @dev configures legacy configs as well setDVNs({ _connectedConfig: broadcastConfig, diff --git a/scripts/DeployFraxOFTProtocol/inherited/SetRateLimits.s.sol b/scripts/DeployFraxOFTProtocol/inherited/SetRateLimits.s.sol new file mode 100644 index 00000000..37bbbae7 --- /dev/null +++ b/scripts/DeployFraxOFTProtocol/inherited/SetRateLimits.s.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: ISC +pragma solidity ^0.8.19; + +import { L0Config } from "scripts/L0Constants.sol"; +import { BaseInherited } from "./BaseInherited.sol"; +import { Script } from "forge-std/Script.sol"; + +interface IRateLimitedOFT { + struct RateLimitGlobalConfig { + bool isGloballyDisabled; + } + + struct RateLimitConfig { + bool overrideDefaultConfig; + bool outboundEnabled; + bool inboundEnabled; + uint256 outboundLimit; + uint256 inboundLimit; + uint32 outboundWindow; + uint32 inboundWindow; + } + + struct SetRateLimitConfigParam { + uint32 eid; + RateLimitConfig config; + } + + function rateLimitGlobalConfig() external view returns (RateLimitGlobalConfig memory config); + function defaultRateLimitConfig() external view returns (RateLimitConfig memory config); + function storedRateLimitConfig(uint32 _eid) external view returns (RateLimitConfig memory config); + function setRateLimitGlobalConfig(RateLimitGlobalConfig calldata _globalConfig) external; + function setDefaultRateLimitConfig(RateLimitConfig calldata _defaultConfig) external; + function setRateLimitConfigs(SetRateLimitConfigParam[] calldata _params) external; +} + +contract SetRateLimits is BaseInherited, Script { + /// @notice Apply any desired rate-limit config for every OFT in `_connectedOfts`. + /// @dev Override the desired* hook methods in a downstream deployment script to opt in. + function setRateLimits( + address[] memory _connectedOfts, + L0Config[] memory _configs + ) public virtual { + for (uint256 o; o < _connectedOfts.length; ++o) { + _setRateLimitGlobalConfig(_connectedOfts[o]); + _setDefaultRateLimitConfig(_connectedOfts[o]); + _setRateLimitConfigs(_connectedOfts[o], _configs); + } + } + + function desiredRateLimitGlobalConfig( + address /* _connectedOft */ + ) public view virtual returns (bool shouldSet, IRateLimitedOFT.RateLimitGlobalConfig memory config) {} + + function desiredDefaultRateLimitConfig( + address /* _connectedOft */ + ) public view virtual returns (bool shouldSet, IRateLimitedOFT.RateLimitConfig memory config) {} + + function desiredRateLimitConfig( + address /* _connectedOft */, + uint32 /* _eid */ + ) public view virtual returns (bool shouldSet, IRateLimitedOFT.RateLimitConfig memory config) {} + + function _setRateLimitGlobalConfig(address _connectedOft) internal virtual { + (bool shouldSet, IRateLimitedOFT.RateLimitGlobalConfig memory desiredConfig) = + desiredRateLimitGlobalConfig(_connectedOft); + if (!shouldSet) return; + + IRateLimitedOFT.RateLimitGlobalConfig memory currentConfig = + IRateLimitedOFT(_connectedOft).rateLimitGlobalConfig(); + if (_rateLimitGlobalConfigsEqual(currentConfig, desiredConfig)) return; + + bytes memory data = abi.encodeCall(IRateLimitedOFT.setRateLimitGlobalConfig, (desiredConfig)); + _rateLimitSafeCall(_connectedOft, data, "setRateLimitGlobalConfig"); + pushSerializedTx({ + _name: "setRateLimitGlobalConfig", + _to: _connectedOft, + _value: 0, + _data: data + }); + } + + function _setDefaultRateLimitConfig(address _connectedOft) internal virtual { + (bool shouldSet, IRateLimitedOFT.RateLimitConfig memory desiredConfig) = + desiredDefaultRateLimitConfig(_connectedOft); + if (!shouldSet) return; + + IRateLimitedOFT.RateLimitConfig memory currentConfig = + IRateLimitedOFT(_connectedOft).defaultRateLimitConfig(); + if (_rateLimitConfigsEqual(currentConfig, desiredConfig)) return; + + bytes memory data = abi.encodeCall(IRateLimitedOFT.setDefaultRateLimitConfig, (desiredConfig)); + _rateLimitSafeCall(_connectedOft, data, "setDefaultRateLimitConfig"); + pushSerializedTx({ + _name: "setDefaultRateLimitConfig", + _to: _connectedOft, + _value: 0, + _data: data + }); + } + + function _setRateLimitConfigs(address _connectedOft, L0Config[] memory _configs) internal virtual { + uint256 numConfigsToSet; + + for (uint256 c; c < _configs.length; ++c) { + if (_configs[c].chainid == block.chainid) continue; + + (bool shouldSet, IRateLimitedOFT.RateLimitConfig memory desiredConfig) = + desiredRateLimitConfig(_connectedOft, uint32(_configs[c].eid)); + if (!shouldSet) continue; + + IRateLimitedOFT.RateLimitConfig memory currentConfig = + IRateLimitedOFT(_connectedOft).storedRateLimitConfig(uint32(_configs[c].eid)); + if (_rateLimitConfigsEqual(currentConfig, desiredConfig)) continue; + + ++numConfigsToSet; + } + + if (numConfigsToSet == 0) return; + + IRateLimitedOFT.SetRateLimitConfigParam[] memory params = + new IRateLimitedOFT.SetRateLimitConfigParam[](numConfigsToSet); + uint256 writeIndex; + + for (uint256 c; c < _configs.length; ++c) { + if (_configs[c].chainid == block.chainid) continue; + + uint32 eid = uint32(_configs[c].eid); + (bool shouldSet, IRateLimitedOFT.RateLimitConfig memory desiredConfig) = + desiredRateLimitConfig(_connectedOft, eid); + if (!shouldSet) continue; + + IRateLimitedOFT.RateLimitConfig memory currentConfig = IRateLimitedOFT(_connectedOft).storedRateLimitConfig(eid); + if (_rateLimitConfigsEqual(currentConfig, desiredConfig)) continue; + + params[writeIndex] = IRateLimitedOFT.SetRateLimitConfigParam({ + eid: eid, + config: desiredConfig + }); + ++writeIndex; + } + + bytes memory data = abi.encodeCall(IRateLimitedOFT.setRateLimitConfigs, (params)); + _rateLimitSafeCall(_connectedOft, data, "setRateLimitConfigs"); + pushSerializedTx({ + _name: "setRateLimitConfigs", + _to: _connectedOft, + _value: 0, + _data: data + }); + } + + function _rateLimitGlobalConfigsEqual( + IRateLimitedOFT.RateLimitGlobalConfig memory _a, + IRateLimitedOFT.RateLimitGlobalConfig memory _b + ) internal pure returns (bool) { + return _a.isGloballyDisabled == _b.isGloballyDisabled; + } + + function _rateLimitConfigsEqual( + IRateLimitedOFT.RateLimitConfig memory _a, + IRateLimitedOFT.RateLimitConfig memory _b + ) internal pure returns (bool) { + return _a.overrideDefaultConfig == _b.overrideDefaultConfig + && _a.outboundEnabled == _b.outboundEnabled + && _a.inboundEnabled == _b.inboundEnabled + && _a.outboundLimit == _b.outboundLimit + && _a.inboundLimit == _b.inboundLimit + && _a.outboundWindow == _b.outboundWindow + && _a.inboundWindow == _b.inboundWindow; + } + + function _rateLimitSafeCall( + address _target, + bytes memory _data, + string memory _errorContext + ) internal { + (bool success, bytes memory returnData) = _target.call(_data); + if (!success) { + if (returnData.length > 0) { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, returnData), mload(returnData)) + } + } + + revert(string.concat(_errorContext, ": call reverted without reason")); + } + } +} diff --git a/scripts/ops/review-config/get-config.ts b/scripts/ops/review-config/get-config.ts index 21f110f3..fabfeda4 100644 --- a/scripts/ops/review-config/get-config.ts +++ b/scripts/ops/review-config/get-config.ts @@ -34,6 +34,121 @@ dotenv.config(); const bytes32Zero = "0x0000000000000000000000000000000000000000000000000000000000000000"; +const RATE_LIMITER_ABI = [ + { + inputs: [], + name: "rateLimitGlobalConfig", + outputs: [ + { + components: [{ internalType: "bool", name: "isGloballyDisabled", type: "bool" }], + internalType: "struct RateLimiterModule.RateLimitGlobalConfig", + name: "config", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "defaultRateLimitConfig", + outputs: [ + { + components: [ + { internalType: "bool", name: "overrideDefaultConfig", type: "bool" }, + { internalType: "bool", name: "outboundEnabled", type: "bool" }, + { internalType: "bool", name: "inboundEnabled", type: "bool" }, + { internalType: "uint256", name: "outboundLimit", type: "uint256" }, + { internalType: "uint256", name: "inboundLimit", type: "uint256" }, + { internalType: "uint32", name: "outboundWindow", type: "uint32" }, + { internalType: "uint32", name: "inboundWindow", type: "uint32" }, + ], + internalType: "struct RateLimiterModule.RateLimitConfig", + name: "config", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint32", name: "_eid", type: "uint32" }], + name: "storedRateLimitConfig", + outputs: [ + { + components: [ + { internalType: "bool", name: "overrideDefaultConfig", type: "bool" }, + { internalType: "bool", name: "outboundEnabled", type: "bool" }, + { internalType: "bool", name: "inboundEnabled", type: "bool" }, + { internalType: "uint256", name: "outboundLimit", type: "uint256" }, + { internalType: "uint256", name: "inboundLimit", type: "uint256" }, + { internalType: "uint32", name: "outboundWindow", type: "uint32" }, + { internalType: "uint32", name: "inboundWindow", type: "uint32" }, + ], + internalType: "struct RateLimiterModule.RateLimitConfig", + name: "config", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint32", name: "_eid", type: "uint32" }], + name: "rateLimitConfig", + outputs: [ + { + components: [ + { internalType: "bool", name: "overrideDefaultConfig", type: "bool" }, + { internalType: "bool", name: "outboundEnabled", type: "bool" }, + { internalType: "bool", name: "inboundEnabled", type: "bool" }, + { internalType: "uint256", name: "outboundLimit", type: "uint256" }, + { internalType: "uint256", name: "inboundLimit", type: "uint256" }, + { internalType: "uint32", name: "outboundWindow", type: "uint32" }, + { internalType: "uint32", name: "inboundWindow", type: "uint32" }, + ], + internalType: "struct RateLimiterModule.RateLimitConfig", + name: "config", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint32", name: "_eid", type: "uint32" }], + name: "rateLimitState", + outputs: [ + { + components: [ + { internalType: "uint256", name: "outboundUsage", type: "uint256" }, + { internalType: "uint256", name: "inboundUsage", type: "uint256" }, + { internalType: "uint40", name: "lastUpdated", type: "uint40" }, + ], + internalType: "struct RateLimiterModule.RateLimitState", + name: "state", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint32", name: "_eid", type: "uint32" }], + name: "outboundRateLimitAvailable", + outputs: [{ internalType: "uint256", name: "availableLD", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint32", name: "_eid", type: "uint32" }], + name: "inboundRateLimitAvailable", + outputs: [{ internalType: "uint256", name: "availableLD", type: "uint256" }], + stateMutability: "view", + type: "function", + }, +] as const; + function decodeLzReceiveOption(encodedOption: Hex) { const optionBytes = hexToBytes(encodedOption); const length = optionBytes.length; @@ -109,6 +224,26 @@ async function main() { executorAddress: string } + type RateLimitGlobalConfigType = { + isGloballyDisabled: boolean + } + + type RateLimitConfigType = { + overrideDefaultConfig: boolean + outboundEnabled: boolean + inboundEnabled: boolean + outboundLimit: string + inboundLimit: string + outboundWindow: number + inboundWindow: number + } + + type RateLimitStateType = { + outboundUsage: string + inboundUsage: string + lastUpdated: number + } + type OFTInfo = { peerAddress: string; peerAddressBytes32: string; @@ -132,8 +267,31 @@ async function main() { receiveLibraryTimeOut: ReceiveLibraryTimeOutInfo; executorConfig: ExecutorConfigType; defaultExecutorConfig: ExecutorConfigType; + rateLimitGlobalConfig?: RateLimitGlobalConfigType; + defaultRateLimitConfig?: RateLimitConfigType; + storedRateLimitConfig?: RateLimitConfigType; + effectiveRateLimitConfig?: RateLimitConfigType; + rateLimitState?: RateLimitStateType; + outboundRateLimitAvailable?: string; + inboundRateLimitAvailable?: string; } + const normalizeRateLimitConfig = (config: any): RateLimitConfigType => ({ + overrideDefaultConfig: config.overrideDefaultConfig, + outboundEnabled: config.outboundEnabled, + inboundEnabled: config.inboundEnabled, + outboundLimit: config.outboundLimit.toString(), + inboundLimit: config.inboundLimit.toString(), + outboundWindow: Number(config.outboundWindow), + inboundWindow: Number(config.inboundWindow), + }); + + const normalizeRateLimitState = (state: any): RateLimitStateType => ({ + outboundUsage: state.outboundUsage.toString(), + inboundUsage: state.inboundUsage.toString(), + lastUpdated: Number(state.lastUpdated), + }); + const chains: Record = { ethereum: { client: createPublicClient({ @@ -543,6 +701,180 @@ async function main() { } } + const rateLimitContract = { + address: oApp, + abi: RATE_LIMITER_ABI, + } as const; + + if (srcChain !== "ink") { + contractCalls = [ + { + ...rateLimitContract, + functionName: "rateLimitGlobalConfig", + }, + { + ...rateLimitContract, + functionName: "defaultRateLimitConfig", + }, + ]; + + Object.keys(ofts[oAppName][srcChain]).forEach((destChainName) => { + contractCalls.push({ + ...rateLimitContract, + functionName: "storedRateLimitConfig", + args: [chains[destChainName].peerId], + }); + contractCalls.push({ + ...rateLimitContract, + functionName: "rateLimitConfig", + args: [chains[destChainName].peerId], + }); + contractCalls.push({ + ...rateLimitContract, + functionName: "rateLimitState", + args: [chains[destChainName].peerId], + }); + contractCalls.push({ + ...rateLimitContract, + functionName: "outboundRateLimitAvailable", + args: [chains[destChainName].peerId], + }); + contractCalls.push({ + ...rateLimitContract, + functionName: "inboundRateLimitAvailable", + args: [chains[destChainName].peerId], + }); + }); + + const rateLimitInfo = await chains[srcChain].client.multicall({ + contracts: contractCalls, + }); + + const sharedGlobalConfig = rateLimitInfo[0]?.status === "success" + ? { + isGloballyDisabled: rateLimitInfo[0].result.isGloballyDisabled, + } + : undefined; + const sharedDefaultConfig = rateLimitInfo[1]?.status === "success" + ? normalizeRateLimitConfig(rateLimitInfo[1].result) + : undefined; + + Object.keys(ofts[oAppName][srcChain]).forEach((destChainName, index) => { + if (sharedGlobalConfig) { + ofts[oAppName][srcChain][destChainName].rateLimitGlobalConfig = sharedGlobalConfig; + } + if (sharedDefaultConfig) { + ofts[oAppName][srcChain][destChainName].defaultRateLimitConfig = sharedDefaultConfig; + } + + const resultOffset = 2 + (index * 5); + const storedConfig = rateLimitInfo[resultOffset]; + const effectiveConfig = rateLimitInfo[resultOffset + 1]; + const rateLimitState = rateLimitInfo[resultOffset + 2]; + const outboundAvailable = rateLimitInfo[resultOffset + 3]; + const inboundAvailable = rateLimitInfo[resultOffset + 4]; + + if (storedConfig?.status === "success") { + ofts[oAppName][srcChain][destChainName].storedRateLimitConfig = + normalizeRateLimitConfig(storedConfig.result); + } + if (effectiveConfig?.status === "success") { + ofts[oAppName][srcChain][destChainName].effectiveRateLimitConfig = + normalizeRateLimitConfig(effectiveConfig.result); + } + if (rateLimitState?.status === "success") { + ofts[oAppName][srcChain][destChainName].rateLimitState = + normalizeRateLimitState(rateLimitState.result); + } + if (outboundAvailable?.status === "success") { + ofts[oAppName][srcChain][destChainName].outboundRateLimitAvailable = + outboundAvailable.result.toString(); + } + if (inboundAvailable?.status === "success") { + ofts[oAppName][srcChain][destChainName].inboundRateLimitAvailable = + inboundAvailable.result.toString(); + } + }); + } else { + let sharedGlobalConfig: RateLimitGlobalConfigType | undefined; + let sharedDefaultConfig: RateLimitConfigType | undefined; + + try { + const oftres = await chains[srcChain].client.readContract({ + ...rateLimitContract, + functionName: "rateLimitGlobalConfig", + }); + sharedGlobalConfig = { + isGloballyDisabled: oftres.isGloballyDisabled, + }; + } catch {} + + try { + const oftres = await chains[srcChain].client.readContract({ + ...rateLimitContract, + functionName: "defaultRateLimitConfig", + }); + sharedDefaultConfig = normalizeRateLimitConfig(oftres); + } catch {} + + for (const destChainName of Object.keys(ofts[oAppName][srcChain])) { + if (sharedGlobalConfig) { + ofts[oAppName][srcChain][destChainName].rateLimitGlobalConfig = sharedGlobalConfig; + } + if (sharedDefaultConfig) { + ofts[oAppName][srcChain][destChainName].defaultRateLimitConfig = sharedDefaultConfig; + } + + try { + const oftres = await chains[srcChain].client.readContract({ + ...rateLimitContract, + functionName: "storedRateLimitConfig", + args: [chains[destChainName].peerId], + }); + ofts[oAppName][srcChain][destChainName].storedRateLimitConfig = + normalizeRateLimitConfig(oftres); + } catch {} + + try { + const oftres = await chains[srcChain].client.readContract({ + ...rateLimitContract, + functionName: "rateLimitConfig", + args: [chains[destChainName].peerId], + }); + ofts[oAppName][srcChain][destChainName].effectiveRateLimitConfig = + normalizeRateLimitConfig(oftres); + } catch {} + + try { + const oftres = await chains[srcChain].client.readContract({ + ...rateLimitContract, + functionName: "rateLimitState", + args: [chains[destChainName].peerId], + }); + ofts[oAppName][srcChain][destChainName].rateLimitState = + normalizeRateLimitState(oftres); + } catch {} + + try { + const oftres = await chains[srcChain].client.readContract({ + ...rateLimitContract, + functionName: "outboundRateLimitAvailable", + args: [chains[destChainName].peerId], + }); + ofts[oAppName][srcChain][destChainName].outboundRateLimitAvailable = oftres.toString(); + } catch {} + + try { + const oftres = await chains[srcChain].client.readContract({ + ...rateLimitContract, + functionName: "inboundRateLimitAvailable", + args: [chains[destChainName].peerId], + }); + ofts[oAppName][srcChain][destChainName].inboundRateLimitAvailable = oftres.toString(); + } catch {} + } + } + // OFT // TODO oft.sharedDecimals : uint8 // TODO oft.token : address diff --git a/test/foundry/contracts/RateLimiterModuleTest.t.sol b/test/foundry/contracts/RateLimiterModuleTest.t.sol new file mode 100644 index 00000000..ed7189c8 --- /dev/null +++ b/test/foundry/contracts/RateLimiterModuleTest.t.sol @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import { Test } from "forge-std/Test.sol"; +import { TransparentUpgradeableProxy } from "@fraxfinance/layerzero-v2-upgradeable/messagelib/contracts/upgradeable/proxy/TransparentUpgradeableProxy.sol"; +import { SendParam, OFTLimit } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; +import { FraxOFTUpgradeable } from "contracts/FraxOFTUpgradeable.sol"; +import { FraxOFTAdapterUpgradeable } from "contracts/FraxOFTAdapterUpgradeable.sol"; +import { RateLimiterModule } from "contracts/modules/RateLimiterModule.sol"; +import { ERC20Mock } from "test/mocks/ERC20Mock.sol"; + +contract EndpointV2MockLite { + mapping(address => address) public delegates; + + function setDelegate(address _delegate) external { + delegates[msg.sender] = _delegate; + } +} + +contract FraxOFTUpgradeableRateLimitHarness is FraxOFTUpgradeable { + constructor(address _lzEndpoint) FraxOFTUpgradeable(_lzEndpoint) {} + + function mint(address _to, uint256 _amount) external { + _mint(_to, _amount); + } + + function debit( + uint256 _amountLD, + uint256 _minAmountLD, + uint32 _dstEid + ) external returns (uint256 amountSentLD, uint256 amountReceivedLD) { + return _debit(_amountLD, _minAmountLD, _dstEid); + } + + function credit( + address _to, + uint256 _amountLD, + uint32 _srcEid + ) external returns (uint256 amountReceivedLD) { + return _credit(_to, _amountLD, _srcEid); + } +} + +contract FraxOFTAdapterUpgradeableRateLimitHarness is FraxOFTAdapterUpgradeable { + constructor(address _token, address _lzEndpoint) FraxOFTAdapterUpgradeable(_token, _lzEndpoint) {} + + function debit( + uint256 _amountLD, + uint256 _minAmountLD, + uint32 _dstEid + ) external returns (uint256 amountSentLD, uint256 amountReceivedLD) { + return _debit(_amountLD, _minAmountLD, _dstEid); + } + + function credit( + address _to, + uint256 _amountLD, + uint32 _srcEid + ) external returns (uint256 amountReceivedLD) { + return _credit(_to, _amountLD, _srcEid); + } +} + +contract RateLimiterModuleTest is Test { + uint32 internal constant DST_EID = 30101; + uint32 internal constant SRC_EID = 30252; + + address internal constant PROXY_ADMIN = address(0x9999); + + address internal owner = address(this); + address internal alice = vm.addr(0x41); + address internal bob = vm.addr(0xb0b); + + EndpointV2MockLite internal endpoint; + FraxOFTUpgradeableRateLimitHarness internal oft; + FraxOFTAdapterUpgradeableRateLimitHarness internal adapter; + ERC20Mock internal token; + + function setUp() external { + endpoint = new EndpointV2MockLite(); + oft = _deployOFT(); + token = new ERC20Mock("Mock Token", "MOCK"); + adapter = _deployAdapter(address(token)); + + oft.mint(alice, 1_000e18); + token.mint(alice, 1_000e18); + + vm.prank(alice); + token.approve(address(adapter), type(uint256).max); + } + + function test_OFTOutboundRateLimit_RevertsWhenCapacityExceeded() external { + oft.setDefaultRateLimitConfig(_config(100e18, 1 hours, 0, 0)); + + vm.prank(alice); + oft.debit(60e18, 60e18, DST_EID); + + assertEq(oft.outboundRateLimitAvailable(DST_EID), 40e18); + + vm.expectRevert( + abi.encodeWithSelector( + RateLimiterModule.RateLimitExceeded.selector, + DST_EID, + true, + 41e18, + 40e18 + ) + ); + vm.prank(alice); + oft.debit(41e18, 41e18, DST_EID); + } + + function test_OFTOutboundRateLimit_RefillsLinearly() external { + oft.setDefaultRateLimitConfig(_config(100e18, 100, 0, 0)); + + vm.prank(alice); + oft.debit(100e18, 100e18, DST_EID); + + assertEq(oft.outboundRateLimitAvailable(DST_EID), 0); + + vm.warp(block.timestamp + 50); + assertEq(oft.outboundRateLimitAvailable(DST_EID), 50e18); + + vm.prank(alice); + oft.debit(50e18, 50e18, DST_EID); + + assertEq(oft.outboundRateLimitAvailable(DST_EID), 0); + } + + function test_OFTInboundRateLimit_RevertsThenRefills() external { + oft.setDefaultRateLimitConfig(_config(0, 0, 75e18, 100)); + + oft.credit(bob, 75e18, SRC_EID); + assertEq(oft.balanceOf(bob), 75e18); + assertEq(oft.inboundRateLimitAvailable(SRC_EID), 0); + + vm.expectRevert( + abi.encodeWithSelector( + RateLimiterModule.RateLimitExceeded.selector, + SRC_EID, + false, + 1e18, + 0 + ) + ); + oft.credit(bob, 1e18, SRC_EID); + + vm.warp(block.timestamp + 100); + oft.credit(bob, 75e18, SRC_EID); + assertEq(oft.balanceOf(bob), 150e18); + } + + function test_OFTQuoteOFT_ReportsCurrentOutboundCapacity() external { + oft.setDefaultRateLimitConfig(_config(10e18, 1 hours, 0, 0)); + + vm.prank(alice); + oft.debit(2.5e18, 2.5e18, DST_EID); + + SendParam memory sendParam = SendParam({ + dstEid: DST_EID, + to: bytes32(uint256(uint160(bob))), + amountLD: 20e18, + minAmountLD: 0, + extraOptions: "", + composeMsg: "", + oftCmd: "" + }); + + (OFTLimit memory limit,,) = oft.quoteOFT(sendParam); + assertEq(limit.maxAmountLD, 7.5e18); + } + + function test_AdapterRateLimits_ApplyOnDebitAndCredit() external { + adapter.setDefaultRateLimitConfig(_config(100e18, 1 hours, 50e18, 1 hours)); + + vm.prank(alice); + adapter.debit(60e18, 60e18, DST_EID); + + assertEq(token.balanceOf(address(adapter)), 60e18); + assertEq(adapter.outboundRateLimitAvailable(DST_EID), 40e18); + + vm.expectRevert( + abi.encodeWithSelector( + RateLimiterModule.RateLimitExceeded.selector, + DST_EID, + true, + 41e18, + 40e18 + ) + ); + vm.prank(alice); + adapter.debit(41e18, 41e18, DST_EID); + + adapter.credit(bob, 50e18, SRC_EID); + assertEq(token.balanceOf(bob), 50e18); + + vm.expectRevert( + abi.encodeWithSelector( + RateLimiterModule.RateLimitExceeded.selector, + SRC_EID, + false, + 1e18, + 0 + ) + ); + adapter.credit(bob, 1e18, SRC_EID); + } + + function _deployOFT() internal returns (FraxOFTUpgradeableRateLimitHarness deployed) { + FraxOFTUpgradeableRateLimitHarness implementation = new FraxOFTUpgradeableRateLimitHarness(address(endpoint)); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(implementation), + PROXY_ADMIN, + abi.encodeWithSelector(FraxOFTUpgradeable.initialize.selector, "Mock Frax", "mFRAX", owner) + ); + return FraxOFTUpgradeableRateLimitHarness(address(proxy)); + } + + function _deployAdapter(address _token) internal returns (FraxOFTAdapterUpgradeableRateLimitHarness deployed) { + FraxOFTAdapterUpgradeableRateLimitHarness implementation = new FraxOFTAdapterUpgradeableRateLimitHarness( + _token, + address(endpoint) + ); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(implementation), + PROXY_ADMIN, + abi.encodeWithSelector(FraxOFTAdapterUpgradeable.initialize.selector, owner) + ); + return FraxOFTAdapterUpgradeableRateLimitHarness(address(proxy)); + } + + function _config( + uint256 _outboundLimit, + uint32 _outboundWindow, + uint256 _inboundLimit, + uint32 _inboundWindow + ) internal pure returns (RateLimiterModule.RateLimitConfig memory config) { + config = RateLimiterModule.RateLimitConfig({ + overrideDefaultConfig: false, + outboundEnabled: _outboundLimit != 0, + inboundEnabled: _inboundLimit != 0, + outboundLimit: _outboundLimit, + inboundLimit: _inboundLimit, + outboundWindow: _outboundWindow, + inboundWindow: _inboundWindow + }); + } +}