Agent documentation index: llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
Skip to main content

Check OFT Transfer Limits

Overview​

FXRP OFT transfers are capped by per-endpoint sliding-window rate limits on the OFT contracts themselves. These are not the FAssets minting rate limits on AssetManager.

The same view API lives on:

  • FAssetOFTAdapter on Flare and Coston2 (lock/release)
  • FXRPOFT on other chains (mint/burn)

See FXRP OFT Adapter for how those contracts relate, and OFT deployments for addresses.

A send records outgoing capacity on the source contract for the destination LayerZero endpoint ID (EID). Credit records incoming capacity on the destination contract for the source EID. The two directions do not refill each other. A transfer that does not fit remaining capacity reverts with RateLimitExceeded. That is a custom error, not an event: the call fails, and no log is emitted.

The practical getters are getAmountCanBeSent and getAmountCanBeReceived. They apply the default fallback and linear decay, so they are what you should call before send().

This example queries the Coston2 adapter 0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639 for Sepolia EID 40161. On mainnet, call the Flare adapter or the destination FXRPOFT instead.

Query Functions​

All six functions are view. Anyone can call them; no wallet is required.

GoalFunctionWhat it returns
Per-eid outgoing configoutgoingRateLimit(uint32 _dstEid)Raw RateLimit storage. window == 0 means "use default", not unlimited
Per-eid incoming configincomingRateLimit(uint32 _srcEid)Raw RateLimit storage
Default outgoingdefaultOutgoingRateLimit()Fallback when the per-eid outgoing window is 0
Default incomingdefaultIncomingRateLimit()Fallback when the per-eid incoming window is 0
Remaining send capacitygetAmountCanBeSent(uint32 _dstEid)(consumed after decay, amountCanBeSent)
Remaining receive capacitygetAmountCanBeReceived(uint32 _srcEid)(consumed after decay, amountCanBeReceived)
struct RateLimit {
uint192 consumedTransferCapacity;
uint64 lastUpdated;
uint192 limit;
uint64 window;
}

struct DefaultRateLimit {
uint192 limit;
uint64 window;
}

function outgoingRateLimit(uint32 _dstEid) external view returns (RateLimit memory);
function incomingRateLimit(uint32 _srcEid) external view returns (RateLimit memory);
function defaultOutgoingRateLimit() external view returns (DefaultRateLimit memory);
function defaultIncomingRateLimit() external view returns (DefaultRateLimit memory);
function getAmountCanBeSent(uint32 _dstEid)
public view returns (uint256 currentConsumedTransferCapacity, uint256 amountCanBeSent);
function getAmountCanBeReceived(uint32 _srcEid)
public view returns (uint256 currentConsumedTransferCapacity, uint256 amountCanBeReceived);

limit and remaining capacity are in token base units. FXRP uses 6 decimals, so 1_000_000 is 1 FXRP.

window is the duration in seconds. Capacity refills with linear decay at limit / window per second:

decay = limit * timeSinceLastUpdate / window
consumed = max(0, consumedTransferCapacity - decay)
remaining = max(0, limit - consumed)

quoteOFT also surfaces remaining send capacity as oftLimit.maxAmountLD = min(amountCanBeSent, totalSupply).

Which Contract to Call​

Call the OFT on the chain that enforces capacity.

QuestionCall onFunction
How much can I send from Coston2 to Sepolia?Coston2 FAssetOFTAdaptergetAmountCanBeSent(40161)
How much can Coston2 still receive from Sepolia?Coston2 FAssetOFTAdaptergetAmountCanBeReceived(40161)
How much can I send from Sepolia to Coston2?Sepolia FXRPOFTgetAmountCanBeSent(40294)
How much can Sepolia still receive from Coston2?Sepolia FXRPOFTgetAmountCanBeReceived(40294)

A full preflight for Coston2 → Sepolia reads both legs: outgoing on the adapter and incoming on Sepolia FXRPOFT. The script below reads one contract (the Coston2 adapter) so you can see every getter in one place.

Coston2 EID is 40294 (EndpointId.FLARE_V2_TESTNET). Sepolia EID is 40161 (EndpointId.SEPOLIA_V2_TESTNET). Mainnet Flare is 30295.

Fallback Rules​

A per-eid entry is configured only when its window > 0.

Per-eid windowDefault windowEffective behavior
> 0anythingUse the per-eid limit / window
0> 0Fall back to the default
00Limiting disabled: getAmountCanBe* returns (0, type(uint256).max)
> 0, limit = 0—Blocked: remaining capacity is 0

outgoingRateLimit / incomingRateLimit return stored limit / window as-is. If window == 0, that means "use default", not "unlimited". Use getAmountCanBeSent / getAmountCanBeReceived for effective remaining capacity.

Rate-limit configuration (setOutgoingRateLimits, setDefaultOutgoingRateLimit, and the incoming equivalents) is owner/governance only.

Transfer Limits Script​

getOftTransferLimits.ts
/**
* Read FXRP OFT transfer rate limits on the Coston2 OFT Adapter.
*
* Usage (from a project with viem and @layerzerolabs/lz-definitions):
* npx tsx getOftTransferLimits.ts
*
* No wallet or private key is required.
*/

import {
createPublicClient,
formatUnits,
http,
maxUint256,
type Address,
} from "viem";
import { flareTestnet } from "viem/chains";
import { EndpointId } from "@layerzerolabs/lz-definitions";

const COSTON2_OFT_ADAPTER =
"0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639" as Address;
const DST_EID = EndpointId.SEPOLIA_V2_TESTNET;
const FXRP_DECIMALS = 6;

const transferLimitingAbi = [
{
type: "function",
name: "outgoingRateLimit",
stateMutability: "view",
inputs: [{ name: "_dstEid", type: "uint32" }],
outputs: [
{
type: "tuple",
components: [
{ name: "consumedTransferCapacity", type: "uint192" },
{ name: "lastUpdated", type: "uint64" },
{ name: "limit", type: "uint192" },
{ name: "window", type: "uint64" },
],
},
],
},
{
type: "function",
name: "incomingRateLimit",
stateMutability: "view",
inputs: [{ name: "_srcEid", type: "uint32" }],
outputs: [
{
type: "tuple",
components: [
{ name: "consumedTransferCapacity", type: "uint192" },
{ name: "lastUpdated", type: "uint64" },
{ name: "limit", type: "uint192" },
{ name: "window", type: "uint64" },
],
},
],
},
{
type: "function",
name: "defaultOutgoingRateLimit",
stateMutability: "view",
inputs: [],
outputs: [
{
type: "tuple",
components: [
{ name: "limit", type: "uint192" },
{ name: "window", type: "uint64" },
],
},
],
},
{
type: "function",
name: "defaultIncomingRateLimit",
stateMutability: "view",
inputs: [],
outputs: [
{
type: "tuple",
components: [
{ name: "limit", type: "uint192" },
{ name: "window", type: "uint64" },
],
},
],
},
{
type: "function",
name: "getAmountCanBeSent",
stateMutability: "view",
inputs: [{ name: "_dstEid", type: "uint32" }],
outputs: [
{ name: "currentConsumedTransferCapacity", type: "uint256" },
{ name: "amountCanBeSent", type: "uint256" },
],
},
{
type: "function",
name: "getAmountCanBeReceived",
stateMutability: "view",
inputs: [{ name: "_srcEid", type: "uint32" }],
outputs: [
{ name: "currentConsumedTransferCapacity", type: "uint256" },
{ name: "amountCanBeReceived", type: "uint256" },
],
},
] as const;

type RateLimit = {
consumedTransferCapacity: bigint;
lastUpdated: bigint;
limit: bigint;
window: bigint;
};

type DefaultRateLimit = {
limit: bigint;
window: bigint;
};

const publicClient = createPublicClient({
chain: flareTestnet,
transport: http("https://coston2-api.flare.network/ext/C/rpc"),
});

function formatFxrp(amount: bigint): string {
if (amount === maxUint256) {
return "unlimited";
}
return `${amount.toString()} (${formatUnits(amount, FXRP_DECIMALS)} FXRP)`;
}

function formatWindow(
windowSeconds: bigint,
zeroMeans: "use default" | "limiting disabled",
): string {
if (windowSeconds === 0n) {
return `0s (${zeroMeans})`;
}
return `${windowSeconds.toString()}s`;
}

function printRateLimit(label: string, rl: RateLimit) {
console.log(`${label}:`);
console.log(` consumed: ${formatFxrp(rl.consumedTransferCapacity)}`);
console.log(` lastUpdated: ${rl.lastUpdated.toString()}`);
console.log(` limit: ${formatFxrp(rl.limit)}`);
console.log(` window: ${formatWindow(rl.window, "use default")}`);
}

function printDefault(label: string, rl: DefaultRateLimit) {
console.log(`${label}:`);
console.log(` limit: ${formatFxrp(rl.limit)}`);
console.log(` window: ${formatWindow(rl.window, "limiting disabled")}`);
}

async function main() {
console.log("=== FXRP OFT Transfer Limits ===\n");
console.log(`OFT Adapter: ${COSTON2_OFT_ADAPTER}`);
console.log(`Network: Coston2`);
console.log(`Remote EID: ${DST_EID} (Sepolia)\n`);

const [outgoing, incoming, defaultOutgoing, defaultIncoming, sent, received] =
await Promise.all([
publicClient.readContract({
address: COSTON2_OFT_ADAPTER,
abi: transferLimitingAbi,
functionName: "outgoingRateLimit",
args: [DST_EID],
}),
publicClient.readContract({
address: COSTON2_OFT_ADAPTER,
abi: transferLimitingAbi,
functionName: "incomingRateLimit",
args: [DST_EID],
}),
publicClient.readContract({
address: COSTON2_OFT_ADAPTER,
abi: transferLimitingAbi,
functionName: "defaultOutgoingRateLimit",
}),
publicClient.readContract({
address: COSTON2_OFT_ADAPTER,
abi: transferLimitingAbi,
functionName: "defaultIncomingRateLimit",
}),
publicClient.readContract({
address: COSTON2_OFT_ADAPTER,
abi: transferLimitingAbi,
functionName: "getAmountCanBeSent",
args: [DST_EID],
}),
publicClient.readContract({
address: COSTON2_OFT_ADAPTER,
abi: transferLimitingAbi,
functionName: "getAmountCanBeReceived",
args: [DST_EID],
}),
]);

printRateLimit(`outgoingRateLimit(${DST_EID})`, outgoing);
printDefault("defaultOutgoingRateLimit()", defaultOutgoing);
printRateLimit(`incomingRateLimit(${DST_EID})`, incoming);
printDefault("defaultIncomingRateLimit()", defaultIncoming);

const [consumedOut, amountCanBeSent] = sent;
const [consumedIn, amountCanBeReceived] = received;

console.log(`\ngetAmountCanBeSent(${DST_EID}):`);
console.log(` consumed (after decay): ${formatFxrp(consumedOut)}`);
console.log(` remaining: ${formatFxrp(amountCanBeSent)}`);
if (outgoing.window === 0n) {
console.log(
" note: per-eid window is 0, so the default outgoing limit applies",
);
}

console.log(`\ngetAmountCanBeReceived(${DST_EID}):`);
console.log(` consumed (after decay): ${formatFxrp(consumedIn)}`);
console.log(` remaining: ${formatFxrp(amountCanBeReceived)}`);
if (incoming.window === 0n) {
console.log(
" note: per-eid window is 0, so the default incoming limit applies",
);
}
}

void main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

How to Run​

npx tsx getOftTransferLimits.ts

Change DST_EID to another LayerZero V2 testnet EID, or point COSTON2_OFT_ADAPTER and the client at Flare mainnet to query production.

Expected Output​

Values change as transfers consume capacity and as that consumption decays. In the snapshot below, Sepolia has no per-eid window, so the defaults apply. After decay, remaining outgoing capacity is the default outgoing limit (1,000,000 FXRP over 3600s) and remaining incoming capacity is the default incoming limit (1,500,000 FXRP over 3600s):

Expected Output
=== FXRP OFT Transfer Limits ===

OFT Adapter: 0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639
Network: Coston2
Remote EID: 40161 (Sepolia)

outgoingRateLimit(40161):
consumed: 10000000 (10 FXRP)
lastUpdated: 1790081729
limit: 0 (0 FXRP)
window: 0s (use default)
defaultOutgoingRateLimit():
limit: 1000000000000 (1000000 FXRP)
window: 3600s
incomingRateLimit(40161):
consumed: 1000000 (1 FXRP)
lastUpdated: 1790081154
limit: 0 (0 FXRP)
window: 0s (use default)
defaultIncomingRateLimit():
limit: 1500000000000 (1500000 FXRP)
window: 3600s

getAmountCanBeSent(40161):
consumed (after decay): 0 (0 FXRP)
remaining: 1000000000000 (1000000 FXRP)
note: per-eid window is 0, so the default outgoing limit applies

getAmountCanBeReceived(40161):
consumed (after decay): 0 (0 FXRP)
remaining: 1500000000000 (1500000 FXRP)
note: per-eid window is 0, so the default incoming limit applies

Raw outgoingRateLimit can still show leftover consumedTransferCapacity while getAmountCanBeSent reports 0 consumed after decay. That is expected: the raw getter does not apply decay or the default fallback.

Code Breakdown​

  1. Client. A Viem publicClient on flareTestnet is enough. The script never creates a wallet client.
  2. Target. COSTON2_OFT_ADAPTER is the documented Coston2 adapter. DST_EID is Sepolia (EndpointId.SEPOLIA_V2_TESTNET, 40161), matching Bridge FXRP to Ethereum.
  3. ABI. Only the six TransferLimiting view functions are inlined. They are not in @flarenetwork/flare-periphery-contracts.
  4. Raw vs effective. outgoingRateLimit / incomingRateLimit plus the defaults show the configured windows. getAmountCanBeSent / getAmountCanBeReceived apply the fallback and decay.
  5. Units. Amounts print as base units and as FXRP via formatUnits(..., 6). type(uint256).max remaining prints as unlimited.

RateLimitExceeded​

RateLimitExceeded is a Solidity custom error, not an event. When remaining capacity is too small, _record reverts and the transfer does not happen. There is no delay, no retry timestamp, and no RateLimitExceeded log to watch.

error RateLimitExceeded();

The error has no arguments. Remaining capacity is not in the revert data — call getAmountCanBeSent or getAmountCanBeReceived before send().

Where it revertsCause
Source sendOutgoing remaining capacity for _dstEid is below the amount being sent
Destination credit (lzReceive)Incoming remaining capacity for _srcEid is below the amount being credited

Wallets and explorers show a failed transaction with this custom error. Unlike minting rate limits, which delay execution, an OFT transfer that hits the cap is rejected outright.

Important Notes​

  • Call the contract on the chain you care about. Outgoing capacity for Flare → Base is on the Flare adapter; incoming capacity for that same transfer is on Base FXRPOFT.
  • Raw per-eid reads are not remaining capacity. window == 0 means "use default". Always prefer getAmountCanBeSent / getAmountCanBeReceived for preflight.
  • Overflow reverts. send and credit revert with RateLimitExceeded when the amount does not fit.
  • Paused sends also revert. Check paused() on the adapter if a send fails for a reason other than the rate limit. See FXRP OFT Adapter.
What's next