Diamond & Facets
The Diamond (0x14fbb1eD5BC098B4Ea236dcE0941EDB02e967b44, identical across the 11-chain fleet - see Divergent-address chains for the one exception) is an EIP-2535 proxy that routes calls to 12 facets by function selector - 65 selectors total.
Facets are divided into two groups based on when they were added:
- Genesis facets (5): deployed and cut in during
DeployDiamond. Their implementation addresses are uniform across the fleet (same deployer + same bytecode = same CREATE address). - Init-cut facets (7): added during
InitializeDiamond. Their addresses vary by chain because they embed CCIP-immutable constructor arguments.
Genesis facets
DiamondCutFacet
Purpose: Two-step upgrade mechanism for the Diamond.
Flow:
PROPOSER_ROLEcallsdiamondCut(facetCuts[], init, calldata)→ proposal queued with an auto-incrementedidand a 7-day TTL.MULTISIG_ROLEcallsfinalizeUpgrade(id)→ change applied viaLibDiamond.diamondCut.
Safety functions (MULTISIG_ROLE):
rejectUpgrade(id)- mark a single proposal as rejected.rejectAllUpgrades()- reject all pending proposals (unbounded loop; userejectRangeif proposer key is compromised).rejectRange(start, end)- paginated rejection for spam scenarios.upgradeStatus(id)- view a proposal's state (exists, rejected, finalized, createdAt, expiresAt).
Finalized proposals cannot be re-executed. Proposals expire after 7 days (PROPOSAL_TTL).
DiamondLoupeFacet
Purpose: EIP-2535 introspection - enumerate facets, function selectors, and supported interfaces.
Key functions: facets(), facetFunctionSelectors(facet), facetAddresses(), facetAddress(selector), supportsInterface(interfaceId).
AccessControlFacet
Purpose: Role management for the Diamond. Exposes grantRole, revokeRole, renounceRole, hasRole, getRoleAdmin. Also implements the ERC-721 receiver hook so the Diamond can hold NFTs during bridge operations.
WithdrawalTimelockFacet
Purpose: 48-hour timelocked emergency withdrawals for ETH, ERC-20, and ERC-721 tokens stuck in the Diamond.
Flow: MULTISIG_ROLE submits a withdrawal request → after a 48-hour delay (WITHDRAWAL_DELAY), executes it. Any pending request can be cancelled before execution.
ReceiverSelectorWhitelist
Purpose: Allowlist of function selectors that may be invoked through the CCIP ccipReceive self-call dispatch path. The CCIP entry point checks this map before delegating, so any non-whitelisted selector is rejected before reaching the onlySelf-gated implementation. (HIGH #5 security mitigation.)
Init-cut facets
BridgeFacet
Purpose: Cross-chain ERC-20 bridging via Chainlink CCIP. Source-chain entry: lock canonical tokens or burn bridge-deployed SkyToken wrappers.
Access: All bridge functions require onlyEntryPoint. Pause/unpause via PAUSER_ROLE.
Key functions:
| Function | Description |
|---|---|
bridgeERC20(token, amount, chainSelector, receiver) | Bridge an ERC-20 (or native ETH via address(0) - Diamond wraps to WETH) |
getBridgeMessageDetails(token, amount, chainSelector, receiver) | Off-chain quote: CCIP fee + deployment flag |
setNeverWrapToken(token, status) | Block a canonical token (e.g., WETH, USDC) from getting a SkyToken wrapper. OPERATOR_ROLE. |
setNeverWrapTokens(tokens[], status) | Batch version |
isNeverWrapToken(token) | Query status |
getLockedTokenAmount(token) | Amount of a token currently locked in the Diamond |
pause() / unPause() | Pause ERC-20 bridging (PAUSER_ROLE) |
Fee-on-transfer tokens are auto-rejected. Token deployment state is written before calling the CCIP router (CEI pattern).
ERC721BridgeFacet
Purpose: Cross-chain ERC-721 bridging via Chainlink CCIP. Source-chain entry: lock or burn NFTs. Only callable by EntryPoint.
Key functions: bridgeERC721(token, tokenId, chainSelector, receiver), bridgeERC721Batch(token, tokenIds[], chainSelector, receiver), getERC721BridgeMessageDetails(...).
BridgeReceiverFacet
Purpose: Destination-chain handler for incoming CCIP ERC-20 messages (ccipReceive path). Mints SkyToken wrappers on first arrival or unlocks canonical tokens on return. Auto-deploys tokens via CREATE3 if the token does not exist on the destination.
WETH handling: On destination, if the incoming token is WETH and the chain supports native ETH, the Diamond unwraps WETH and sends native ETH to the receiver. If the unwrap/send fails, WETH is forwarded directly (fallback event WETHUnwrapFallback).
ccipReceive uses try-catch - the CCIP router expects no reverts; failures are stored in s_failedMessages for later retry.
ERC721BridgeReceiverFacet
Purpose: Destination-chain handler for incoming CCIP ERC-721 messages. Mints SkyNFT wrappers on first arrival or unlocks canonical NFTs on return.
CCIPFacet
Purpose: CCIP router integration, chain and sender allowlisting, and failed-message handling.
Key functions: allowlistDestinationChain(chainSelector, allowed), allowlistSourceChain(chainSelector, allowed), allowlistSender(sender, allowed), retryFailedMessage(messageId, token, amount, beneficiary), setWeth(weth), getCCIPFees(...).
Gating: allowlist management requires MULTISIG_ROLE. Retry requires DEFAULT_ADMIN_ROLE.
SkyTokenDeployerFacet
Purpose: Deterministic ERC-20 token deployment via CREATE3. Called internally by BridgeReceiverFacet (restricted to onlySelf).
Key functions: deployChild(salt, tokenData) → deploys a new SkyToken; predictTokenAddress(salt) → compute address without deploying.
SkyNFTDeployerFacet
Purpose: Deterministic ERC-721 wrapper deployment via CREATE3. Called internally by ERC721BridgeReceiverFacet (restricted to onlySelf).
Key functions: deployNFTChild(salt, data) → deploys a new SkyNFT; predictNFTAddress(salt) → compute address without deploying.
Selector count
All 11 chains: 12 facets / 65 selectors (verified via live on-chain facets() loupe at deploy time).
Implementation addresses
Genesis facet implementations are identical across all chains:
| Facet | Address |
|---|---|
| DiamondCutFacet | 0x2A37630aB3706B87b36e27fA1476f074AcB4f113 |
| DiamondLoupeFacet | 0x3Bcf4185443A339517aD4e580067f178d1B68E1D |
| AccessControlFacet | 0xd76d071128338d229d5687B7361D408947ad7260 |
| WithdrawalTimelockFacet | 0x5589719a0ad9112063972DBA099A1f87C4158d96 |
| ReceiverSelectorWhitelist | 0x2BBf54EBe0E0fC0714D2BB75B4F02a246c728545 |
Init-cut facet addresses vary per chain (CCIP constructor immutables + CREATE nonce). See Official Addresses for the full per-chain table.
Divergent-address chains
The Diamond's uniform address depends on every fleet chain deploying byte-identical genesis facets, from the same deployer, with the same CREATE2 salt. If the compiled genesis bytecode drifts at all - even compiler metadata that doesn't touch logic - the CREATE2 output address changes for that chain.
Robinhood Chain is the current example: its genesis facets were compiled from a later source snapshot than the fleet's 2026-05-12 deploy. Runtime bytecode is logic-identical (confirmed byte-for-byte except the trailing CBOR metadata), but the differing init-code hash puts Robinhood's Diamond at 0xEF69D1115EC4742F7CECa7cAAD8DdcB5E4695456 instead of the fleet's 0x14fbb1eD5BC098B4Ea236dcE0941EDB02e967b44.
This cascades to any CREATE3 deployment made from that Diamond - see Token Model → Divergent-address chains. Robinhood's sAFX, sAVI, and any future bridge-deployed token or NFT wrapper will all sit at Robinhood-specific addresses, never the fleet's uniform ones. Full addresses: Official Addresses.
Is this fixable? Technically yes - redeploy Robinhood's Diamond with byte-identical genesis bytecode at the same deployer/salt to land on the fleet address, then migrate all live state (roles, registered tokens, CCIP lane allowlists on every peer chain that references Robinhood as a destination) off the current Diamond. But Robinhood mainnet is already live with real value and its role handoff to the Robinhood admin Safe is complete, so that migration carries real risk for a purely cosmetic gain. It is not planned - the divergent address is treated as permanent, chain-specific configuration instead, the same way an earlier fleet/Robinhood-testnet split (sAVI) was handled.