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.

  • 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).


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.


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.