Skip to main content

Token Model

When a token is bridged to a destination chain where it does not natively exist, SkyBridge deploys a deterministic wrapper contract via CREATE3. There are two wrapper types:

  • SkyToken - ERC-20 (with ERC-677 transferAndCall support)
  • SkyNFT - ERC-721 with enumerable, URI storage, and EIP-2981 royalties

Deployment via CREATE3​

Both token types are deployed with a CREATE3 library, giving the address a salt-derived determinism that is independent of the deployer's nonce. For a given salt (derived from the source chain selector + source token address), the wrapper address is the same on every destination chain that shares the same deploying Diamond address. In practice that's all 11 fleet chains, since the Diamond itself is CREATE2-deployed to a uniform address there - see Divergent-address chains below for the one exception.

  • SkyTokenDeployerFacet.deployChild(salt, tokenData) - deploys a SkyToken
  • SkyNFTDeployerFacet.deployNFTChild(salt, data) - deploys a SkyNFT

Both deployer facets are restricted to onlySelf (internal Diamond self-call only; cannot be invoked directly).

Address prediction (off-chain, no gas): predictTokenAddress(salt) / predictNFTAddress(salt).

Divergent-address chains​

Create3.sol's addressOf(salt) derives the resulting address from address(this) at call time - i.e. the calling Diamond, since deployChild/deployNFTChild execute in the Diamond's delegatecall context. The real formula is f(salt, deployingDiamond), not f(salt) alone - the salt is not sufficient by itself to predict an address across chains.

This is invisible across the 11-chain fleet because every fleet Diamond sits at the same CREATE2 address (0x14fbb1eD5BC098B4Ea236dcE0941EDB02e967b44), so a fixed salt happens to produce a fixed token address everywhere.

Robinhood Chain breaks that assumption. Its Diamond (0xEF69D1115EC4742F7CECa7cAAD8DdcB5E4695456) sits at a different address than the fleet - not because the logic differs, but because its genesis facets were compiled from a later source snapshot (see Diamond & Facets → Divergent-address chains for the root cause). Since the deploying Diamond differs, every CREATE3 salt evaluated on Robinhood produces a different result than on the fleet:

TokenFleet address (11 chains)Robinhood address
sAVI (legacy)0xEA11FD3aacF901c95E5e7bF24d245a1d8b661FA70xC39bBb223F389d84D90F89062303D3F17C0bFB6F
sAFX (retired on Robinhood Chain)0x7F340bb1Bd152E70761C70665E9D7c0aA3c4d3F5 (Base and Polygon)0x2E67f423F705E23d6cBFd745c121875DEB0f1508

Both Robinhood addresses are correct, on-chain-verified deployments - not misdeploys. AFX itself no longer follows this pattern: see AFX addresses below. Any tooling, indexer, or UI config that hardcodes the fleet token address needs an explicit Robinhood override rather than assuming uniformity. Full address list: Official Addresses.


AFX addresses​

AFX, our own token, is handled differently from other bridged tokens. Since app version 2.2.2 (October 2026):

ChainAFX tokenAddress
EthereumAFX (home chain)0x9414E9af3a531E47f0F2B48d1a7fAf8178cF0BD7
Base, PolygonsAFX0x7F340bb1Bd152E70761C70665E9D7c0aA3c4d3F5
Arbitrum One, Optimism, Avalanche C-Chain, BNB Chain, Unichain, Soneium, Ink, MegaETH, Robinhood ChainAFX ("Airfox Token" / AFX)0x86A174b7Fb40D36e19fD909f5DA923823CBdA80e

On every chain in the last row, AFX is one token at one shared address, with the same name and ticker as on Ethereum. It is backed 1:1 by AFX held by the bridge on Ethereum, like any bridged copy, and pays no flat bridge fee.

The older Robinhood Chain sAFX (0x2E67f423F705E23d6cBFd745c121875DEB0f1508) is retired: it can only be sent back to Ethereum, where it arrives as AFX. A transfer of it to any other chain is refused.


SkyToken​

SkyToken is deployed with the following state at construction:

FieldValue for bridge-deployed tokens
s_owneraddress(0) (set from tokenData.owner)
s_ccipAdminDiamond address
b_parentTokenSource chain token address
b_parentSourceChainSelectorCCIP chain selector of the source chain

Ownership and admin assignment (from constructor):

When s_owner == address(0) (bridge-deployed case), the constructor grants DEFAULT_ADMIN_ROLE, CCIP_OPERATOR_ROLE, and OPERATOR_ROLE to s_ccipAdmin (the Diamond). In other words:

  • owner() returns address(0) - the Diamond holds admin via the AccessControl role system, not the owner field.
  • getCCIPAdmin() returns the Diamond address - this is how Chainlink's token admin registry identifies the bridge as the token's controller for CCIP lock/release or mint/burn.

The Diamond can rotate s_ccipAdmin via setCCIPAdmin(address) (requires CCIP_OPERATOR_ROLE).

Mint/burn: CCIP_OPERATOR_ROLE holders (the Diamond) can mint and burn tokens to process cross-chain transfers.

Name and ticker: a copy created from app version 2.2.2 (October 2026) onward is named SkyBridge <token name> and uses the token's own ticker. Copies created earlier keep the ticker they were created with, which starts with s (for example sUSD1). See CCIP Token Support.


SkyNFT​

SkyNFT preserves the original collection's name and symbol (no "wrapped" prefix). It is deployed with:

FieldValue
originTokenAddress of the original NFT contract on the source chain (immutable)
originChainSelectorCCIP chain selector of the source chain (immutable)
RoyaltiesEIP-2981 default royalty from the original collection (clamped: fractions > 10,000/10,000 are set to (address(0), 0) to prevent on-chain revert)

Ownership:

owner() is a pure function that hard-codes address(0). This is the same end state as SkyToken - the Diamond holds control - but via a different code path: the SkyNFT constructor grants DEFAULT_ADMIN_ROLE and CCIP_OPERATOR_ROLE directly to the _bridge argument (the Diamond address).

Mint/burn: CCIP_OPERATOR_ROLE (the Diamond) calls mint(to, tokenId, uri) on arrival and burn(tokenId) on departure back to the source chain.


Token bridging lifecycle​

ERC-20 (source chain)​

  • Canonical token (the original): locked in the Diamond.
  • Bridge-deployed SkyToken: burned from the user's wallet.

ERC-20 (destination chain)​

  • If token exists: unlock from Diamond or mint more SkyTokens.
  • If token does not exist: BridgeReceiverFacet triggers SkyTokenDeployerFacet.deployChild(...) in the same CCIP message execution, then mints to the receiver.

The same logic applies to ERC-721 with ERC721BridgeReceiverFacet and SkyNFTDeployerFacet.


Notes on fee-on-transfer and rebasing tokens​

  • Fee-on-transfer tokens are rejected at the EntryPoint level (pre/post balance check).
  • Rebasing tokens should be explicitly blacklisted via setTokenBlacklisted.
  • Centralized-function tokens (pause/blocklist controls) retain those controls in the wrapped form only if the original token's admin also controls the destination chain.

See CCIP Tokens for a full list of caveats.