Skip to main content
Stablecoin OFT enforces security policies at two levels (see Architecture):
  • Cross-chain extensions (Fee, Rate Limiter, PauseByID) — mixed into the OFT via OFTCoreExtendedRBACUpgradeable, configured per destination chain (EID). These intercept _debit and _credit on 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

ModuleLayerPurposeGranularityWhere Applied
FeeOFTCollect basis-point fees on outbound transfersPer-destination + default_debitView()
Rate LimiterOFTToken bucket rate limits with linear decayPer-destination + default_debit() and _credit()
PauseByIDOFTHalt transfers to/from specific destinationsPer-destination + default_debit() modifier
AllowlistTokenRestrict token holders by addressGlobal (on ERC20Plus)transfer() / transferFrom() / burn()
PauseTokenHalt all token operations globallyGlobal (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

  1. User calls send() with amountLD
  2. _debitView() calculates the fee: fee = (amountLD * feeBps) / 10_000
  3. amountReceivedLD = removeDust(amountLD - fee)
  4. amountSentLD = amountLD (the user pays the full amount)
  5. The difference (amountSentLD - amountReceivedLD) is retained as fee

Configuration

Default fee applies to all destinations unless overridden:
function setDefaultFeeBps(uint16 _feeBps) external; // FEE_CONFIG_MANAGER_ROLE
Per-destination override:
function setFeeBps(
    uint256 _id,        // Destination EID
    uint16 _feeBps,     // Fee in BPS (0-10000)
    bool _enabled       // true = use this override; false = fall back to default
) external; // FEE_CONFIG_MANAGER_ROLE
When 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

uint16 public constant BPS_DENOMINATOR = 10_000;
A fee of 50 BPS = 0.50%. Setting _feeBps > BPS_DENOMINATOR reverts with InvalidBps(feeBps).

Roles

RoleFunctions
FEE_CONFIG_MANAGER_ROLEsetDefaultFeeBps(), 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:
  1. Outbound transfers consume capacity; inbound transfers replenish it (when net accounting is enabled)
  2. Capacity regenerates linearly over the configured time window
  3. If a transfer would exceed available capacity, it reverts with RateLimitExceeded
Example: With a limit of 1,000,000 tokens and a window of 3,600 seconds (1 hour), the regeneration rate is ~277.78 tokens/second:
  • 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:
function setRateLimitGlobalConfig(RateLimitGlobalConfig memory _globalConfig) external;
// RATE_LIMITER_MANAGER_ROLE

struct RateLimitGlobalConfig {
    bool useGlobalState;      // Use single bucket for all destinations
    bool isGloballyDisabled;  // Disable all rate limiting
}
Per-destination configuration:
function setRateLimitConfigs(SetRateLimitConfigParam[] calldata _params) external;
// RATE_LIMITER_MANAGER_ROLE

struct RateLimitConfig {
    bool overrideDefaultConfig;    // true = use this config; false = use default
    bool outboundEnabled;          // Enable outbound rate limit
    bool inboundEnabled;           // Enable inbound rate limit
    bool netAccountingEnabled;     // Offset outflow with inflows
    bool addressExemptionEnabled;  // Allow per-address exemptions
    uint96 outboundLimit;          // Max outbound tokens in window
    uint96 inboundLimit;           // Max inbound tokens in window
    uint32 outboundWindow;         // Outbound decay window (seconds)
    uint32 inboundWindow;          // Inbound decay window (seconds)
}
Manual state override (for emergency adjustments):
function setRateLimitStates(SetRateLimitStateParam[] calldata _params) external;
// RATE_LIMITER_MANAGER_ROLE

struct RateLimitState {
    uint96 outboundUsage;   // Current outbound usage
    uint96 inboundUsage;    // Current inbound usage
    uint40 lastUpdated;     // Timestamp (cannot be in the future)
}
Address exemptions:
function setRateLimitAddressExemptions(
    SetRateLimitAddressExceptionParam[] calldata _exemptions
) external; // RATE_LIMITER_MANAGER_ROLE

struct SetRateLimitAddressExceptionParam {
    address user;
    bool isExempt;
}
Exemptions only apply when addressExemptionEnabled is true in the destination’s config. Checkpoint (call before changing limits or windows):
function checkpointRateLimits(uint256[] calldata _ids) external;
// RATE_LIMITER_MANAGER_ROLE
Writes decayed usages to storage so new config applies to the current state rather than stale values.

Net vs Gross Accounting

When netAccountingEnabled 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
When netAccountingEnabled is false:
  • Outbound and inbound are tracked independently
  • Each direction has its own separate bucket

Scaling

Rate limit amounts are stored as uint96. 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

RoleFunctions
RATE_LIMITER_MANAGER_ROLEsetRateLimitGlobalConfig(), 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:
  1. Is there a specific PauseConfig for this destination with enabled = true?
  2. If yes, use that config’s paused value
  3. If no, use the defaultPaused value

Configuration

Default pause (applies to all destinations without specific config):
function setDefaultPaused(bool _paused) external; // PAUSER_ROLE or UNPAUSER_ROLE
Per-destination pause:
function setPaused(SetPausedParam[] calldata _params) external;
// PAUSER_ROLE or UNPAUSER_ROLE

struct SetPausedParam {
    uint256 id;       // Destination EID
    bool paused;      // Whether to pause
    bool enabled;     // true = use this config; false = fall back to default
}

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:
ModeBehaviorWho Can Transfer
OpenNo restrictionsEveryone
BlacklistBlock specific addressesEveryone except blacklisted addresses
WhitelistAllow only specific addressesOnly whitelisted addresses
Mode transitions are controlled by 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 counts
  • getBlacklist(offset, limit) / getWhitelist(offset, limit) for paginated enumeration
  • isBlacklisted(address) / isWhitelisted(address) for individual queries
The allowlist check (isAllowlisted) is enforced on transfer, transferFrom, and burn for both sender and receiver.
RoleFunctions
DEFAULT_ADMIN_ROLEsetAllowlistMode()
BLACKLISTER_ROLEsetBlacklisted()
WHITELISTER_ROLEsetWhitelisted()

Fund Recovery

For compliance scenarios (e.g., court orders, sanctions enforcement), addresses holding DEFAULT_ADMIN_ROLE can transfer tokens away from non-allowlisted addresses:
function recoverFunds(address _from, address _to, uint256 _amount) external;
_from must NOT be allowlisted under the current mode. Attempting to recover from an allowlisted address reverts with CannotRecoverFromAllowlisted(address user).

Pause (Global)

Halts all transfer, transferFrom, and burn calls. Unlike PauseByID (which targets individual destinations), global pause blocks everything — local and cross-chain.
RoleFunctions
PAUSER_ROLEpause()
UNPAUSER_ROLEunpause()
The pause/unpause split ensures a compromised pauser key cannot also undo a legitimate security pause.

Execution Order

On an outbound send(), modules execute in this order:
1. PauseByID      → whenNotPaused(dstEid) modifier on _debit()
2. Fee            → _debitView() calculates fee, reduces amountReceivedLD
3. Rate Limiter   → _outflow() checks and updates outbound bucket
4. Token Transfer → burn/lock/wrap the tokens
On an inbound receive (_lzReceive):
1. Rate Limiter   → _inflow() checks and updates inbound bucket
2. Token Transfer → mint/unlock/unwrap the tokens

Next Steps

  • RBAC Reference for the complete role-to-function mapping
  • OFTs for deployable variants and initialization (including fee deposit)