Skip to main content
An Interchain Fungible Token (IFT) is a fungible token that moves between chains by burning it on one and minting it on the other. Each deployment is an ERC20 with the cross-chain behavior built into the token itself. That behavior is built on GMP. An IFT defines no packet type, and instead a transfer travels as an ordinary GMP call, addressed to the counterparty IFT implementation registered for that client. An issuer deploys one of two Solidity variants, which differ only in who the contract’s authority is. The authority registers a bridge and send-call constructor for each counterparty IFT implementation. Source: IFTBaseUpgradeable.sol, with the deployable variants and the encoders listed below. The behavior behind this surface is on IFT: how it works.

What an IFT contract does

An IFT contract contains both the ERC20 logic and the logic for moving the token across chains. Sending burns the amount from the caller, then dispatches an encoded mint call through ICS27GMP. On receipt, the contract checks that the sender is a registered counterparty, then mints the amount. Any holder can send with iftTransfer. On the shipped IFTOwnable and IFTAccessManaged variants, holders can also burn their own balances through ERC20BurnableUpgradeable. Minting is the gated side: the authority mints local supply, and a registered counterparty’s mint call arrives through GMP. The authority also registers which counterparties this contract accepts those calls from. GMP carries the call and reports acknowledgement or timeout. Counterparty IFT implementations communicate through GMP, with no escrow or voucher contract between them. Interacts with: ICS27GMP and its per-sender accounts, one send-call constructor per registered bridge, and the counterparty IFT implementation.

Contracts on this page

One variant is deployed per token, with the abstract base compiled into it. Each counterparty needs a send-call constructor registered beside it. The external surface is declared in IIFT, the data structures in IIFTMsgs, and the errors in IIFTErrors. An encoder implements IIFTSendCallConstructor, which is the one interface an issuer writes against to support a new counterparty.

Authority

Every IFT deployment has a single authority. Registering and removing bridges, setting the rate limit, minting local supply, and authorizing upgrades all run through it. IBC-solidity ships two standard options: a single owner address, or an external access manager.
  • IFTOwnable gives the contract a single owner address. It combines the base with ERC20BurnableUpgradeable, OwnableUpgradeable, and UUPSUpgradeable, and its authority check resolves to onlyOwner.
  • IFTAccessManaged hands authorization to an external OpenZeppelin AccessManager, which permissions each function by role rather than fixing it to one address. It swaps the owner for AccessManagedUpgradeable, and its authority check uses the restricted modifier, which defers to that manager.
A contract that fits neither variant inherits IFTBaseUpgradeable directly. It must implement _onlyAuthority() to choose its own governance model. It can also override the ERC20 _update hook for behavior such as whitelisting. Rate limiting is built into the base, see Rate limits.

Initialize the contract

The choice of authority shows up as the initializer’s first argument:
The current implementation writes the GMP address only during initialization and has no setter. Changing it on an existing proxy requires an authorized implementation upgrade that adds explicit storage-update or reinitialization logic; upgrading the implementation alone does not change the stored address. Both variants run behind a proxy, and both disable initializers in their constructors.

Bridge registration

A bridge records the counterparty IFT implementation for a client and the constructor that encodes that destination’s mint operation. It tells this contract where to send, and whose mint calls to accept. Each contract registers its own counterparties, one at a time, and a pair can transfer only once both sides have registered the other. Bridges are keyed by client, so a contract holds one counterparty per client. Registration validates its arguments before it stores anything. The client ID and the counterparty address must be non-empty, and the send-call constructor must be a live contract that advertises IIFTSendCallConstructor through ERC-165.
The three fields are the local IBC client ID, the counterparty implementation’s address as a string, and the send-call constructor registered for that client.
For an EVM counterparty, register the address as 0x-prefixed EIP-55 checksummed hex. Registration accepts any non-empty string and does not validate its casing. On inbound mint, IFT compares the stored string byte for byte with the checksummed sender produced by GMP, so differently cased text fails IFTUnauthorizedMint. Packet receiver parsing is separate and accepts any parseable EVM address string.
Registering the same client ID again overwrites the previous entry. Removing a bridge blocks new sends and inbound EVM mints for that client. Outbound transfers already recorded as pending can still settle or refund because callbacks use the pending-transfer mapping rather than the bridge record.

Rate limits

Every IFT deployment limits how fast tokens can cross the bridge, to bound the damage an IBC exploit can do before operators respond. The authority sets the limit with setIFTRateLimit(uint208 capacity, uint48 window), and getIFTRateLimit() returns it. Limits are mandatory: until one is set, every transfer fails. The limit is a refilling bucket. A bucket holds at most capacity tokens and refills at capacity / window per second, so an empty bucket is full again after window seconds. A full bucket allows a burst of capacity at once, which means up to twice the capacity can pass within one window. getIFTRateLimitAvailable() returns what each direction can currently take. Both directions share the capacity and window but track usage separately, across every bridge of the token:
  • Outbound covers the burn in iftTransfer. A send over the limit reverts, so no packet is sent.
  • Inbound covers iftMint and refunds. A mint over the limit fails the receive and produces an error acknowledgement, which refunds the sender. A refund over the limit reverts and the pending transfer stays recorded, so the relayer can retry once the bucket has refilled.
Flow in one direction never restores allowance in the other, and the authority’s local mint is not limited. Changing the limit keeps the refill accrued under the old settings and applies the new rate from then on. The IFT rate limiting ADR explains these choices.

Sending a transfer

A transfer starts on the source IFT contract, when a holder calls iftTransfer. The contract burns the amount first, then dispatches an encoded mint call over GMP and stores the record (later this record can be used to refund a failure or timeout). An IFT has no packet type of its own: the transfer travels as an ordinary GMP call, and ICS27GMP owns the wire format. The send is one atomic EVM transaction. If bridge lookup, payload construction, or ICS27GMP.sendCall reverts, the earlier burn is rolled back and no pending transfer remains. Any holder may call either overload, since neither carries an authority check. The call consumes outbound rate limit and burns before it dispatches, stores the pending transfer under the sequence sendCall returns, and ends by emitting IFTTransferInitiated. From the dispatch onwards the transfer is out of this contract’s hands. Its path to the counterparty:
  1. The source contract calls ICS27GMP.sendCall with the encoded mint call, the bridge’s counterparty address, empty salt, empty memo, and the timeout. The returned packet sequence keys the pending transfer.
  2. ICS27GMP wraps the payload in a GMP packet whose sender field is the source contract’s address as checksummed hex, and sends it through the router with both source and destination ports set to gmpport.
  3. A relayer carries the packet to the destination chain and submits it with a proof, which the destination chain’s light client verifies.
  4. The destination router hands the packet to its own ICS27GMP, by port.
  5. On an EVM destination, ICS27GMP executes the payload through the ICS27Account derived from the destination client ID, packet sender, and salt, so iftMint arrives from that account rather than from GMP itself. A Cosmos SDK destination instead executes the JSON messages body produced by CosmosIFTSendCallConstructor.
The destination contract’s checks on that call are next. IFT: how it works explains the same path from the holder’s side.

Receiving a mint on EVM

On an EVM destination, the receiving entry point is iftMint(address receiver, uint256 amount), and the destination contract mints only for a caller it can trace back to a registered counterparty. That caller is an ICS27Account, not the GMP contract itself. The destination ICS27GMP creates or retrieves its account identifier from the destination chain’s local client ID, the packet sender string, and the packet salt. The account address is CREATE2-derived using that identifier and deployment-specific beacon-proxy bytecode. IFT authorizes the caller by reverse-looking up the identifier. Four checks stand between the call and a mint.
  • The contract asks GMP for the caller’s account identifier, and that lookup reverts when the caller is not a GMP account.
  • A bridge must be registered for the identifier’s client ID.
  • The identifier’s sender string must equal the registered counterparty address exactly.
  • The identifier’s salt must be empty, which leaves each counterparty one account to mint through.
If all four pass and the amount fits the inbound rate limit, the contract mints to the receiver, emits IFTMintReceived, and returns nothing. GMP wraps the call’s empty return data in an acknowledgement. A failure carrying revert data becomes the universal error acknowledgement. An empty revert, including an out-of-gas callback, aborts receive processing instead of writing that acknowledgement.

Acknowledgement and timeout callbacks

The two GMP callbacks settle or refund the pending record, and only the GMP contract can invoke them. The base contract inherits IBCCallbackReceiver, which advertises IIBCSenderCallbacks through ERC-165, and GMP delivers a callback only to senders that advertise it. GMP maps the universal error acknowledgement to success == false before invoking onAckPacket; IFT does not inspect the acknowledgement bytes. On success the source contract deletes the pending transfer and emits IFTTransferCompleted, minting and burning nothing. On failure it refunds instead: it mints the pending amount back to the original sender, deletes the record, and emits IFTTransferRefunded. The timeout callback refunds with no success check, once a relayer submits the timeout proof and GMP routes the callback to the source contract. Its three effects are the same: mint back to the sender, delete the record, emit IFTTransferRefunded. Either refund consumes inbound rate limit. A refund that does not fit reverts the callback and leaves the pending transfer in place, so a later retry can refund it.

Send-call constructors

A send-call constructor is registered per counterparty client, and it turns a receiver and an amount into the exact mint call that chain expects. One EVM encoder can serve every EVM counterparty because it holds no state. A Cosmos encoder stores a type URL, denom, and interchain account address; counterparties with different values need separately configured encoder deployments, while bridges with identical values may reuse one. The destination’s mint call is built on the source chain, inside the transfer, and different chain types expect different encodings. The Cosmos contract accepts a receiver that is either EVM hex or bech32-shaped; the bech32 check is a character-shape heuristic and does not validate a checksum. Its interchain account address derives from the Cosmos destination client ID, the predicted checksummed IFT contract address as the GMP sender, and the empty IFT salt. The repo’s compute-ift-addresses tool predicts the IFT deployment address from the deployer and nonce, then computes the corresponding Cosmos account before deployment. That account address is fed to the constructor’s deployment. Supporting a new counterparty means implementing constructMintCall(receiver, amount) from IIFTSendCallConstructor and advertising that interface through ERC-165, then registering the contract as a bridge’s constructor. Without the ERC-165 answer, registerIFTBridge reverts IFTInvalidConstructorInterface.

State and storage

Beyond its ERC20 balances and its access-control state, an IFT contract also stores the GMP address, the bridges by client ID, and the pending transfers by client ID and sequence. All three live in one ERC-7201 namespaced slot. A pending transfer is a PendingTransfer{sender, amount} record. It holds only the sender and the amount, because the client ID and the sequence are its mapping keys. Each has its own getter: getIFTBridge(clientId) for a bridge, getPendingTransfer(clientId, sequence) for a pending transfer, and ics27() for the GMP address.

Events

Six events cover the bridge lifecycle and every transfer outcome, all declared in IIFT. IIFTRateLimit declares a seventh, for rate limit changes.

Errors

The reverts fall into argument validation, a missing bridge or pending record, and an unauthorized caller. The contract’s errors are declared in IIFTErrors, and each send-call constructor declares its own receiver error. Receiver-format failures come from the selected constructor. On EVM, minting to the zero address instead reverts with the inherited ERC20 error.

Roles and permissions

The table names the main entry points each kind of caller may call. Holders can burn their own balance because both variants inherit ERC20BurnableUpgradeable, which also lets an approved spender burn from the balance it was approved against. A call to iftMint from the GMP contract itself reverts, because that contract has no account identifier of its own.

Deployment and upgrade

The issuer deploys the variant’s logic contract behind an ERC1967 proxy. The repo ships a Forge script that deploys an IFTOwnable logic contract and its proxy for the end-to-end tests, taking the GMP address, name, and symbol from the environment and making the deployer the owner. Both variants are UUPS-upgradeable, and the same authority that gates bridge management gates the upgrade: owner-gated on IFTOwnable, restricted on IFTAccessManaged. A CosmosIFTSendCallConstructor keeps the configuration it is deployed with. Permissions and upgrades carries the full proxy and upgrade map.