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-677transferAndCallsupport)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 aSkyTokenSkyNFTDeployerFacet.deployNFTChild(salt, data)— deploys aSkyNFT
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:
| Field | Value for bridge-deployed tokens |
|---|---|
s_owner | address(0) (set from tokenData.owner) |
s_ccipAdmin | Diamond address |
b_parentToken | Source chain token address |
b_parentSourceChainSelector | CCIP 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()returnsaddress(0)— the Diamond holds admin via the AccessControl role system, not theownerfield.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:
| Field | Value |
|---|---|
originToken | Address of the original NFT contract on the source chain (immutable) |
originChainSelector | CCIP chain selector of the source chain (immutable) |
| Royalties | EIP-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:
BridgeReceiverFacettriggersSkyTokenDeployerFacet.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.