- Cross-chain extensions (Fee, Rate Limiter, PauseByID) — mixed into the OFT via
OFTCoreExtendedRBACUpgradeable, configured per destination chain (EID). These intercept_debitand_crediton cross-chain sends and receives. - Token controls (Allowlist, Pause) — built into
ERC20Plus, applied to all token operations including local transfers. Documented in detail on the ERC20Plus page; summarized here for completeness.
Overview
| Module | Layer | Purpose | Granularity | Where Applied |
|---|---|---|---|---|
| Fee | OFT | Collect basis-point fees on outbound transfers | Per-destination + default | _debitView() |
| Rate Limiter | OFT | Token bucket rate limits with linear decay | Per-destination + default | _debit() and _credit() |
| PauseByID | OFT | Halt transfers to/from specific destinations | Per-destination + default | _debit() modifier |
| Allowlist | Token | Restrict token holders by address | Global (on ERC20Plus) | transfer() / transferFrom() / burn() |
| Pause | Token | Halt all token operations globally | Global (on ERC20Plus) | transfer() / transferFrom() / burn() |
Fee Module
Collects fees in basis points (BPS) on outbound cross-chain transfers. Fees are deducted from the transfer amount before it reaches the destination chain.How It Works
- User calls
send()withamountLD _debitView()calculates the fee:fee = (amountLD * feeBps) / 10_000amountReceivedLD = removeDust(amountLD - fee)amountSentLD = amountLD(the user pays the full amount)- The difference (
amountSentLD - amountReceivedLD) is retained as fee
Configuration
Default fee applies to all destinations unless overridden:enabled is false, the destination falls back to the default fee.
Fee settlement is push-based across all OFT variants. When fees are collected during _debit, they are transferred immediately to the fee deposit address supplied at initialization. Treasury accounting should follow normal ERC20 Transfer (or native) inflows to that address rather than a separate withdrawal action.
Constants
_feeBps > BPS_DENOMINATOR reverts with InvalidBps(feeBps).
Roles
| Role | Functions |
|---|---|
FEE_CONFIG_MANAGER_ROLE | setDefaultFeeBps(), setFeeBps() |
Rate Limiter Module
Enforces transfer volume limits using a token bucket algorithm with linear decay. Supports per-destination limits for both outbound (send) and inbound (receive) directions.How It Works
The rate limiter uses a token bucket model:- Outbound transfers consume capacity; inbound transfers replenish it (when net accounting is enabled)
- Capacity regenerates linearly over the configured time window
- If a transfer would exceed available capacity, it reverts with
RateLimitExceeded
- At t=0: Available = 1,000,000
- User sends 800,000 tokens. Available = 200,000
- At t=1800 (30 min): 500,000 regenerated. Available = 700,000
- At t=3600 (1 hr): Fully regenerated. Available = 1,000,000
- If 300,000 tokens arrive inbound at any point, available increases by 300,000 (capped at limit)
Configuration
Global configuration:addressExemptionEnabled is true in the destination’s config.
Checkpoint (call before changing limits or windows):
Net vs Gross Accounting
WhennetAccountingEnabled is true:
- Outbound transfers reduce inbound usage (and vice versa)
- This allows “round-trip” capacity: if 500k tokens leave and 500k arrive, net usage is zero
netAccountingEnabled is false:
- Outbound and inbound are tracked independently
- Each direction has its own separate bucket
Scaling
Rate limit amounts are stored asuint96. For tokens with amounts exceeding type(uint96).max (~79 billion with 18 decimals), use the SCALE_DECIMALS constructor parameter to downscale amounts. For example, SCALE_DECIMALS = 6 divides all amounts by 10^6 before storing.
Roles
| Role | Functions |
|---|---|
RATE_LIMITER_MANAGER_ROLE | setRateLimitGlobalConfig(), setRateLimitConfigs(), setRateLimitStates(), setRateLimitAddressExemptions(), checkpointRateLimits() |
PauseByID Module
Per-destination pause controls that halt outbound transfers to specific chains without affecting the rest of the network.How It Works
The_debit() function includes a whenNotPaused(_dstEid) modifier. If the destination EID is paused, the transaction reverts with Paused(uint256 id).
Pause logic evaluates:
- Is there a specific
PauseConfigfor this destination withenabled = true? - If yes, use that config’s
pausedvalue - If no, use the
defaultPausedvalue
Configuration
Default pause (applies to all destinations without specific config):Use Cases
- Chain compromise: Pause a single destination without halting all cross-chain operations
- Maintenance: Temporarily halt transfers to a chain during upgrades
- Regulatory action: Block transfers to/from a specific chain
Interaction with quoteOFT
When a destination is paused,quoteOFT() returns maxAmountLD = 0 in the OFTLimit, signaling to UIs that no transfer is possible.
Token-Level Controls (on ERC20Plus)
The following controls live on the token itself, not on the OFT. They apply to all token operations — local transfers and cross-chain sends alike.
Allowlist
The allowlist system operates in one of three modes at any time:| Mode | Behavior | Who Can Transfer |
|---|---|---|
| Open | No restrictions | Everyone |
| Blacklist | Block specific addresses | Everyone except blacklisted addresses |
| Whitelist | Allow only specific addresses | Only whitelisted addresses |
DEFAULT_ADMIN_ROLE. Switching modes does not clear existing lists — the blacklist and whitelist are maintained independently and apply only when their respective mode is active.
Both lists are implemented as OpenZeppelin EnumerableSet.AddressSet, supporting:
blacklistedCount()/whitelistedCount()for total countsgetBlacklist(offset, limit)/getWhitelist(offset, limit)for paginated enumerationisBlacklisted(address)/isWhitelisted(address)for individual queries
isAllowlisted) is enforced on transfer, transferFrom, and burn for both sender and receiver.
| Role | Functions |
|---|---|
DEFAULT_ADMIN_ROLE | setAllowlistMode() |
BLACKLISTER_ROLE | setBlacklisted() |
WHITELISTER_ROLE | setWhitelisted() |
Fund Recovery
For compliance scenarios (e.g., court orders, sanctions enforcement), addresses holdingDEFAULT_ADMIN_ROLE can transfer tokens away from non-allowlisted addresses:
_from must NOT be allowlisted under the current mode. Attempting to recover from an allowlisted address reverts with CannotRecoverFromAllowlisted(address user).
Pause (Global)
Halts alltransfer, transferFrom, and burn calls. Unlike PauseByID (which targets individual destinations), global pause blocks everything — local and cross-chain.
| Role | Functions |
|---|---|
PAUSER_ROLE | pause() |
UNPAUSER_ROLE | unpause() |
Execution Order
On an outboundsend(), modules execute in this order:
_lzReceive):
Next Steps
- RBAC Reference for the complete role-to-function mapping
- OFTs for deployable variants and initialization (including fee deposit)