feat: add OFT rate limiting support#135
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a reusable on-chain rate limiting module and wires it into the project’s OFT/OFTAdapter contracts, along with deployment/ops support and Foundry tests to validate expected capacity/refill behavior.
Changes:
- Added
RateLimiterModule(config/state storage + consume/checkpoint + availability getters). - Integrated rate limiting into OFT/OFTAdapter implementations by overriding
quoteOFT,_debit, and_credit. - Added deployment helper script for applying rate limit configs and extended the ops review script to fetch rate limit config/state.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
contracts/modules/RateLimiterModule.sol |
New core module implementing configurable inbound/outbound linear refill rate limiting. |
contracts/FraxOFTUpgradeable.sol |
Adds rate limiting consumption in _debit/_credit and updates quoteOFT to report rate-limited max. |
contracts/FraxOFTAdapterUpgradeable.sol |
Adds rate limiting on adapter transfers and rate-limited quoteOFT. |
contracts/FraxOFTMintableAdapterUpgradeable.sol |
Applies rate limiting to mint/burn adapter flows and rate-limited quoteOFT. |
contracts/FraxOFTMintableAdapterUpgradeableTIP20.sol |
Applies rate limiting to TIP20 adapter flows and rate-limited quoteOFT. |
contracts/frxUsd/FrxUSDOFTUpgradeable.sol |
Adds rate limiting to frxUSD OFT send/receive via _debit/_credit + quoteOFT. |
contracts/frxUsd/SFrxUSDOFTUpgradeable.sol |
Adds rate limiting to sfrxUSD OFT send/receive via _debit/_credit + quoteOFT. |
contracts/WFRAXTokenOFTUpgradeable.sol |
Adds rate limiting to WFRAX OFT send/receive via _debit/_credit + quoteOFT. |
test/foundry/contracts/RateLimiterModuleTest.t.sol |
New Foundry tests covering outbound/inbound limits, linear refill, quote capacity, and adapter behavior. |
scripts/DeployFraxOFTProtocol/inherited/SetRateLimits.s.sol |
New deployment helper to set global/default/per-eid rate limit configs and serialize txs. |
scripts/DeployFraxOFTProtocol/DeployFraxOFTProtocol.s.sol |
Integrates setRateLimits() into the deployment flow. |
scripts/ops/review-config/get-config.ts |
Extends config review output with rate limit global/default/per-eid config/state + availability. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| sharedGlobalConfig = { | ||
| isGloballyDisabled: oftres.isGloballyDisabled, | ||
| }; | ||
| } catch {} |
There was a problem hiding this comment.
Empty catch {} blocks will silently swallow RPC/ABI issues and produce partial configs without any signal, which is hard to debug operationally. Consider at least capturing the error (or toggling verbose logging) so failures are observable.
| ) 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) {} |
There was a problem hiding this comment.
These desired* hook functions have empty bodies but non-void return types; they’ll compile but implicitly return default values, which is easy to misread as an accidental omission. Consider explicitly returning (false, config) (or marking them virtual with no body in an abstract contract) to make the default behavior unambiguous.
| ) 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) {} | |
| ) public view virtual returns (bool shouldSet, IRateLimitedOFT.RateLimitGlobalConfig memory config) { | |
| return (false, config); | |
| } | |
| function desiredDefaultRateLimitConfig( | |
| address /* _connectedOft */ | |
| ) public view virtual returns (bool shouldSet, IRateLimitedOFT.RateLimitConfig memory config) { | |
| return (false, config); | |
| } | |
| function desiredRateLimitConfig( | |
| address /* _connectedOft */, | |
| uint32 /* _eid */ | |
| ) public view virtual returns (bool shouldSet, IRateLimitedOFT.RateLimitConfig memory config) { | |
| return (false, config); | |
| } |
| ) 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) {} |
There was a problem hiding this comment.
Same as above: desiredRateLimitConfig has an empty body and implicitly returns default values. An explicit return (or abstract declaration) would make it clearer that downstream scripts must override this hook to opt in.
| ) 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) {} | |
| ) public view virtual returns (bool shouldSet, IRateLimitedOFT.RateLimitGlobalConfig memory config) { | |
| return (false, IRateLimitedOFT.RateLimitGlobalConfig({ isGloballyDisabled: false })); | |
| } | |
| function desiredDefaultRateLimitConfig( | |
| address /* _connectedOft */ | |
| ) public view virtual returns (bool shouldSet, IRateLimitedOFT.RateLimitConfig memory config) { | |
| return ( | |
| false, | |
| IRateLimitedOFT.RateLimitConfig({ | |
| overrideDefaultConfig: false, | |
| outboundEnabled: false, | |
| inboundEnabled: false, | |
| outboundLimit: 0, | |
| inboundLimit: 0, | |
| outboundWindow: 0, | |
| inboundWindow: 0 | |
| }) | |
| ); | |
| } | |
| function desiredRateLimitConfig( | |
| address /* _connectedOft */, | |
| uint32 /* _eid */ | |
| ) public view virtual returns (bool shouldSet, IRateLimitedOFT.RateLimitConfig memory config) { | |
| return ( | |
| false, | |
| IRateLimitedOFT.RateLimitConfig({ | |
| overrideDefaultConfig: false, | |
| outboundEnabled: false, | |
| inboundEnabled: false, | |
| outboundLimit: 0, | |
| inboundLimit: 0, | |
| outboundWindow: 0, | |
| inboundWindow: 0 | |
| }) | |
| ); | |
| } |
| if (_elapsed >= _window) { | ||
| return 0; | ||
| } | ||
|
|
||
| uint256 replenished = (_limit * _elapsed) / _window; | ||
| return replenished >= _usage ? 0 : _usage - replenished; |
There was a problem hiding this comment.
_decayUsage computes replenished as (_limit * _elapsed) / _window, which can overflow and revert for large _limit values (owner misconfiguration), effectively bricking reads/consumption for that eid. Consider using a mulDiv-style computation to avoid overflow (e.g., full-precision mulDiv) and/or bounding _limit during config validation.
No description provided.