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 withiftTransfer. 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.IFTOwnablegives the contract a single owner address. It combines the base withERC20BurnableUpgradeable,OwnableUpgradeable, andUUPSUpgradeable, and its authority check resolves toonlyOwner.IFTAccessManagedhands authorization to an external OpenZeppelin AccessManager, which permissions each function by role rather than fixing it to one address. It swaps the owner forAccessManagedUpgradeable, and its authority check uses therestrictedmodifier, which defers to that manager.
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.
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 withsetIFTRateLimit(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
iftMintand 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.
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 callsiftTransfer. 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:
- The source contract calls
ICS27GMP.sendCallwith the encoded mint call, the bridge’s counterparty address, empty salt, empty memo, and the timeout. The returned packet sequence keys the pending transfer. - 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. - A relayer carries the packet to the destination chain and submits it with a proof, which the destination chain’s light client verifies.
- The destination router hands the packet to its own ICS27GMP, by port.
- On an EVM destination, ICS27GMP executes the payload through the
ICS27Accountderived from the destination client ID, packet sender, and salt, soiftMintarrives from that account rather than from GMP itself. A Cosmos SDK destination instead executes the JSON messages body produced byCosmosIFTSendCallConstructor.
Receiving a mint on EVM
On an EVM destination, the receiving entry point isiftMint(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.
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 inIIFT. 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 inIIFTErrors, 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 anIFTOwnable 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.