diff --git a/.vscode/settings.json b/.vscode/settings.json index ac680625585..b2a58996d10 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -52,6 +52,7 @@ "tbody", "thead", "THENA", + "timelock", "Tobe", "tsconfigs", "typechain", diff --git a/src/config/data/ccip/data.ts b/src/config/data/ccip/data.ts index 27a8d7c709c..a11224a5378 100644 --- a/src/config/data/ccip/data.ts +++ b/src/config/data/ccip/data.ts @@ -1,4 +1,14 @@ -import { ChainsConfig, LanesConfig, TokensConfig, Environment, Version } from "./types" +import { + ChainsConfig, + LanesConfig, + TokensConfig, + Environment, + Version, + SupportedTokenConfig, + determineTokenMechanism, + TokenMechanism, + NetworkFees, +} from "." // For mainnet import chainsMainnetv120 from "@config/data/ccip/v1_2_0/mainnet/chains.json" @@ -12,7 +22,43 @@ import lanesTestnetv120 from "@config/data/ccip/v1_2_0/testnet/lanes.json" import tokensTestnetv120 from "@config/data/ccip/v1_2_0/testnet/tokens.json" import { SupportedChain } from "@config/types" -import { supportedChainToChainInRdd } from "@features/utils" +import { directoryToSupportedChain, supportedChainToChainInRdd } from "@features/utils" + +export const getAllEnvironments = () => [Environment.Mainnet, Environment.Testnet] +export const getAllVersions = () => [Version.V1_2_0] + +export const networkFees: NetworkFees = { + tokenTransfers: { + [TokenMechanism.LockAndUnlock]: { + allLanes: { gasTokenFee: "0.07 %", linkFee: "0.063 %" }, + }, + [TokenMechanism.LockAndMint]: { + fromEthereum: { gasTokenFee: "0.50 USD", linkFee: "0.45 USD" }, + toEthereum: { gasTokenFee: "5.00 USD", linkFee: "4.50 USD" }, + nonEthereum: { gasTokenFee: "0.25 USD", linkFee: "0.225 USD" }, + }, + [TokenMechanism.BurnAndMint]: { + fromEthereum: { gasTokenFee: "0.50 USD", linkFee: "0.45 USD" }, + toEthereum: { gasTokenFee: "5.00 USD", linkFee: "4.50 USD" }, + nonEthereum: { gasTokenFee: "0.25 USD", linkFee: "0.225 USD" }, + }, + [TokenMechanism.BurnAndUnlock]: { + fromEthereum: { gasTokenFee: "0.50 USD", linkFee: "0.45 USD" }, + toEthereum: { gasTokenFee: "5.00 USD", linkFee: "4.50 USD" }, + nonEthereum: { gasTokenFee: "0.25 USD", linkFee: "0.225 USD" }, + }, + [TokenMechanism.NoPoolDestinationChain]: { + allLanes: { gasTokenFee: "", linkFee: "" }, + }, + [TokenMechanism.NoPoolSourceChain]: { allLanes: { gasTokenFee: "", linkFee: "" } }, + [TokenMechanism.NoPoolsOnBothChains]: { allLanes: { gasTokenFee: "", linkFee: "" } }, + [TokenMechanism.Unsupported]: { allLanes: { gasTokenFee: "", linkFee: "" } }, + }, + messaging: { + fromToEthereum: { gasTokenFee: "0.50 USD", linkFee: "0.45 USD" }, + nonEthereum: { gasTokenFee: "0.10 USD", linkFee: "0.09 USD" }, + }, +} export const loadReferenceData = ({ environment, version }: { environment: Environment; version: Version }) => { let chainsReferenceData: ChainsConfig @@ -63,6 +109,52 @@ export const getAllChains = ({ return [...chainsMainnetKeys, ...chainsTestnetKeys] } +export const getAllSupportedTokens = (params: { environment: Environment; version: Version }) => { + const { lanesReferenceData } = loadReferenceData(params) + const tokens: Record>> = {} + Object.entries(lanesReferenceData).forEach(([sourceChainRdd, laneReferenceData]) => { + const sourceChain = directoryToSupportedChain(sourceChainRdd) + + Object.entries(laneReferenceData).forEach(([destinationChainRdd, destinationLaneReferenceData]) => { + const supportedTokens = destinationLaneReferenceData.supportedTokens + if (supportedTokens) { + Object.entries(supportedTokens).forEach(([token, tokenConfig]) => { + const destinationChain = directoryToSupportedChain(destinationChainRdd) + + tokens[token] = tokens[token] || {} + tokens[token][sourceChain] = tokens[token][sourceChain] || {} + tokens[token][sourceChain][destinationChain] = tokenConfig + }) + } + }) + }) + if (Object.keys(tokens).length === 0) { + console.warn(`No supported tokens found for ${params.environment} ${params.version}`) + return [] + } + return tokens +} + +export const getTokenMechanism = (params: { + token: string + sourceChain: SupportedChain + destinationChain: SupportedChain + environment: Environment + version: Version +}) => { + const { tokensReferenceData } = loadReferenceData(params) + const sourceChainRdd = supportedChainToChainInRdd(params.sourceChain) + const destinationChainRdd = supportedChainToChainInRdd(params.destinationChain) + + const tokenConfig = tokensReferenceData[params.token] + const sourceChainPoolInfo = tokenConfig[sourceChainRdd] + const destinationChainPoolInfo = tokenConfig[destinationChainRdd] + const sourceChainPoolType = sourceChainPoolInfo.poolType + const destinationChainPoolType = destinationChainPoolInfo.poolType + const tokenMechanism = determineTokenMechanism(sourceChainPoolType, destinationChainPoolType) + return tokenMechanism +} + const CCIPTokenImage = "https://images.prismic.io/data-chain-link/86d5bc29-7511-49f5-bbd8-18a8ebc008b0_ccip-icon-white.png?auto=compress,format" diff --git a/src/config/data/ccip/index.ts b/src/config/data/ccip/index.ts index 1ef370f3192..cb8eec2fccf 100644 --- a/src/config/data/ccip/index.ts +++ b/src/config/data/ccip/index.ts @@ -1,2 +1,3 @@ -export * from "./data" export * from "./types" +export * from "./utils" +export * from "./data" diff --git a/src/config/data/ccip/types.ts b/src/config/data/ccip/types.ts index 30fe35d01bb..de52eff11b6 100644 --- a/src/config/data/ccip/types.ts +++ b/src/config/data/ccip/types.ts @@ -1,17 +1,18 @@ -type RateLimiterConfig = { +export type RateLimiterConfig = { capacity: string isEnabled: boolean rate: string } -type SupportedTokenConfig = { - [token: string]: { - rateLimiterConfig: RateLimiterConfig - } +export type SupportedTokenConfig = { + rateLimiterConfig: RateLimiterConfig +} +export type SupportedTokensConfig = { + [token: string]: SupportedTokenConfig } export type LaneConfig = { - supportedTokens?: SupportedTokenConfig + supportedTokens?: SupportedTokensConfig rateLimiterConfig: RateLimiterConfig onRamp: string } @@ -20,10 +21,7 @@ export type DestinationsLaneConfig = { [destinationChain: string]: LaneConfig } -enum PoolType { - LockRelease = "lockRelease", - BurnMint = "burnMint", -} +export type PoolType = "lockRelease" | "burnMint" | "usdc" type PoolInfo = { tokenAddress: string @@ -50,7 +48,46 @@ export type LanesConfig = { } export type TokensConfig = { - [token: string]: PoolInfo + [token: string]: { + [chain: string]: PoolInfo + } +} + +export enum TokenMechanism { + LockAndMint = "Lock & Mint", + BurnAndUnlock = "Burn & Unlock", + LockAndUnlock = "Lock & Unlock", + BurnAndMint = "Burn & Mint", + NoPoolSourceChain = "No pool on source blockchain", + NoPoolDestinationChain = "No pool on destination blockchain", + NoPoolsOnBothChains = "No pools on both blockchains", + Unsupported = "Unsupported pool mechanism", +} + +export type NetworkFeeStructure = { + gasTokenFee: string + linkFee: string +} + +export type LaneSpecificFees = { + fromToEthereum?: NetworkFeeStructure + fromEthereum?: NetworkFeeStructure + toEthereum?: NetworkFeeStructure + nonEthereum?: NetworkFeeStructure + allLanes?: NetworkFeeStructure +} + +export type LaneSpecificFeeKey = keyof LaneSpecificFees + +export type TokenTransfersNetworkFees = { + [key in TokenMechanism]: LaneSpecificFees +} + +export type MessagingNetworkFees = LaneSpecificFees + +export type NetworkFees = { + tokenTransfers: TokenTransfersNetworkFees + messaging: MessagingNetworkFees } export enum Environment { diff --git a/src/config/data/ccip/utils.ts b/src/config/data/ccip/utils.ts new file mode 100644 index 00000000000..d2811441909 --- /dev/null +++ b/src/config/data/ccip/utils.ts @@ -0,0 +1,105 @@ +import { SupportedChain, chainToTechnology } from "@config" +import { NetworkFeeStructure, PoolType, TokenMechanism, LaneSpecificFeeKey } from "./types" +import { networkFees } from "./data" + +export const determineTokenMechanism = ( + sourcePoolType: PoolType | undefined, + destinationPoolType: PoolType | undefined +): TokenMechanism => { + if (!sourcePoolType && destinationPoolType) { + return TokenMechanism.NoPoolSourceChain + } else if (sourcePoolType && !destinationPoolType) { + return TokenMechanism.NoPoolDestinationChain + } else if (!sourcePoolType && !destinationPoolType) { + return TokenMechanism.NoPoolsOnBothChains + } + + if (sourcePoolType === "lockRelease" && destinationPoolType === "burnMint") { + return TokenMechanism.LockAndMint + } else if (sourcePoolType === "burnMint" && destinationPoolType === "lockRelease") { + return TokenMechanism.BurnAndUnlock + } else if (sourcePoolType === "lockRelease" && destinationPoolType === "lockRelease") { + return TokenMechanism.LockAndUnlock + } else if ( + (sourcePoolType === "burnMint" && destinationPoolType === "burnMint") || + (sourcePoolType === "usdc" && destinationPoolType === "usdc") + ) { + return TokenMechanism.BurnAndMint + } + + return TokenMechanism.Unsupported +} + +export const calculateNetworkFeesForTokenMechanismDirect = ( + mechanism: TokenMechanism, + laneSpecificFeeKey: LaneSpecificFeeKey +): NetworkFeeStructure => { + const feesForMechanism = networkFees.tokenTransfers[mechanism] + const specificFee = feesForMechanism ? feesForMechanism[laneSpecificFeeKey] : null + + if (specificFee) { + return specificFee + } else { + console.error(`No fees defined for mechanism: ${mechanism} and lane key: ${laneSpecificFeeKey}`) + return { gasTokenFee: "0", linkFee: "0" } + } +} + +export const calculateNetworkFeesForTokenMechanism = ( + mechanism: TokenMechanism, + sourceChain: SupportedChain, + destinationChain: SupportedChain +): NetworkFeeStructure => { + const feesForMechanism = networkFees.tokenTransfers[mechanism] + + if (feesForMechanism && feesForMechanism.allLanes) { + return feesForMechanism.allLanes + } + + // If 'allLanes' is not available, determine the fee type based on the technology of source and destination chains + const sourceTechno = chainToTechnology[sourceChain] + const destinationTechno = chainToTechnology[destinationChain] + + const isSourceEthereum = sourceTechno === "ETHEREUM" + const isDestinationEthereum = destinationTechno === "ETHEREUM" + + let laneSpecificFeeKey: LaneSpecificFeeKey + if ((isSourceEthereum || isDestinationEthereum) && feesForMechanism.fromToEthereum) { + laneSpecificFeeKey = "fromToEthereum" + } else if (isSourceEthereum && feesForMechanism.fromEthereum) { + laneSpecificFeeKey = "fromEthereum" + } else if (isDestinationEthereum && feesForMechanism.toEthereum) { + laneSpecificFeeKey = "toEthereum" + } else { + laneSpecificFeeKey = "nonEthereum" + } + + return calculateNetworkFeesForTokenMechanismDirect(mechanism, laneSpecificFeeKey) +} + +export const calculateMessagingNetworkFeesDirect = (laneSpecificFeeKey: LaneSpecificFeeKey): NetworkFeeStructure => { + const messagingFees = networkFees.messaging[laneSpecificFeeKey] + if (messagingFees) { + return messagingFees + } else { + console.error(`No fees defined for lane key: ${laneSpecificFeeKey}`) + return { gasTokenFee: "0", linkFee: "0" } + } +} + +export const calculateMessaingNetworkFees = (sourceChain: SupportedChain, destinationChain: SupportedChain) => { + const sourceTechno = chainToTechnology[sourceChain] + const destinationTechno = chainToTechnology[destinationChain] + + const isSourceEthereum = sourceTechno === "ETHEREUM" + const isDestinationEthereum = destinationTechno === "ETHEREUM" + + let laneSpecificFeeKey: LaneSpecificFeeKey + if (isSourceEthereum || isDestinationEthereum) { + laneSpecificFeeKey = "fromToEthereum" + } else { + laneSpecificFeeKey = "nonEthereum" + } + + return calculateMessagingNetworkFeesDirect(laneSpecificFeeKey) +} diff --git a/src/config/data/ccip/v1_2_0/mainnet/lanes.json b/src/config/data/ccip/v1_2_0/mainnet/lanes.json index b88547f0af1..b7e7da404e8 100644 --- a/src/config/data/ccip/v1_2_0/mainnet/lanes.json +++ b/src/config/data/ccip/v1_2_0/mainnet/lanes.json @@ -424,6 +424,13 @@ "isEnabled": false, "rate": "0" } + }, + "WETH": { + "rateLimiterConfig": { + "capacity": "114000000000000000000", + "isEnabled": true, + "rate": "32000000000000000" + } } }, "rateLimiterConfig": { @@ -505,7 +512,7 @@ "rate": "138880000000000000000" } }, - "wOETH": { + "WOETH": { "rateLimiterConfig": { "capacity": "1500000000000000000000", "isEnabled": true, @@ -519,6 +526,13 @@ "rate": "56000000000000000" } }, + "WETH": { + "rateLimiterConfig": { + "capacity": "114000000000000000000", + "isEnabled": true, + "rate": "32000000000000000" + } + }, "rsETH": { "rateLimiterConfig": { "capacity": "500000000000000000000", @@ -822,6 +836,13 @@ "isEnabled": false, "rate": "0" } + }, + "WETH": { + "rateLimiterConfig": { + "capacity": "114000000000000000000", + "isEnabled": true, + "rate": "32000000000000000" + } } }, "rateLimiterConfig": { @@ -864,6 +885,13 @@ "rate": "56000000000000000" } }, + "WETH": { + "rateLimiterConfig": { + "capacity": "114000000000000000000", + "isEnabled": true, + "rate": "32000000000000000" + } + }, "rsETH": { "rateLimiterConfig": { "capacity": "500000000000000000000", @@ -1063,7 +1091,7 @@ "rate": "138880000000000000000" } }, - "wOETH": { + "WOETH": { "rateLimiterConfig": { "capacity": "1500000000000000000000", "isEnabled": true, @@ -1077,6 +1105,13 @@ "rate": "56000000000000000" } }, + "WETH": { + "rateLimiterConfig": { + "capacity": "114000000000000000000", + "isEnabled": true, + "rate": "32000000000000000" + } + }, "rsETH": { "rateLimiterConfig": { "capacity": "500000000000000000000", @@ -1181,6 +1216,13 @@ "rate": "56000000000000000" } }, + "WETH": { + "rateLimiterConfig": { + "capacity": "114000000000000000000", + "isEnabled": true, + "rate": "32000000000000000" + } + }, "rsETH": { "rateLimiterConfig": { "capacity": "500000000000000000000", diff --git a/src/config/data/ccip/v1_2_0/mainnet/tokens.json b/src/config/data/ccip/v1_2_0/mainnet/tokens.json index 445dd115217..cbdbd21396d 100644 --- a/src/config/data/ccip/v1_2_0/mainnet/tokens.json +++ b/src/config/data/ccip/v1_2_0/mainnet/tokens.json @@ -748,7 +748,7 @@ "decimals": 18 } }, - "wOETH": { + "WOETH": { "ethereum-mainnet-arbitrum-1": { "tokenAddress": "0xD8724322f44E5c58D7A815F542036fb17DbbF839", "allowListEnabled": false, diff --git a/src/content/ccip/architecture.mdx b/src/content/ccip/architecture.mdx index 77929412a6a..9d4f1343031 100644 --- a/src/content/ccip/architecture.mdx +++ b/src/content/ccip/architecture.mdx @@ -27,8 +27,9 @@ Below is a diagram displaying the basic architecture of CCIP. Routers are smart @@ -59,7 +60,7 @@ The figure below outlines the different components involved in a cross-chain tra #### Router -The Router is the primary contract CCIP users interface with. This contract is responsible for initiating cross-chain interactions. One router contract exists per chain. When transferring tokens, callers have to approve tokens for the router contract. +The Router is the primary contract CCIP users interface with. This contract is responsible for initiating cross-chain interactions. One router contract exists per blockchain. When transferring tokens, callers have to approve tokens for the router contract. The router contract routes the instruction to the destination-specific [OnRamp](#onramp). When a message is received on the destination chain, the router is the contract that “delivers” tokens to the user's account or the message to the receiver's smart contract. @@ -89,14 +90,30 @@ One OffRamp contract per [lane](/ccip/concepts#lane) exists. This contract perfo #### Token pools -Each token has its own token pool, an abstraction layer over ERC-20 tokens that facilitates OnRamp and OffRamp token-related operations. Token pools are configurable to `lock` or `burn` at the source blockchain and `unlock` or `mint` at the destination blockchain. The mechanism for handling tokens depends on the characteristics of the token in question. Here are a few examples: - -- Blockchain-native gas tokens, such as ETH, MATIC, and AVAX, can only be minted on their native chains. These tokens cannot be burned on the source and minted at the destination to transfer these tokens between chains. Instead, the linked token pool uses a "Lock and Mint" approach that locks the token at its source and then mints a wrapped or synthetic asset on the destination blockchain. This synthetic asset represents the locked asset and is essential for redeeming the locked asset. -- A token like LINK is minted on a single chain (Ethereum mainnet) with a fixed total supply. CCIP cannot natively mint it on another chain. In this case, the "Lock and Mint" approach is required. -- Some tokens can be minted on multiple chains. Examples of such tokens include stablecoins like USDC, TUSD, USDT, and FRAX. The linked token pools use a "Burn and Mint" method to burn the token at its source and then mint it natively on the destination blockchain. Wrapped assets such as WBTC or WETH are other examples that use the "Burn and Mint" approach. -- A token with a Proof Of Reserve (PoR) feed on a specific chain poses a challenge for the "Burn and Mint" method when applied to other chains because it conflicts with the PoR feed. For these tokens, "Lock and Mint" is the preferred approach. - -Token pools provide rate limiting, which is a security feature enabling token issuers to set a maximum rate at which their token can be transferred. +Each token is associated with its own token pool, an abstraction layer over ERC-20 tokens designed to facilitate token-related operations for OnRamping and OffRamping. Token pools provide rate limiting, a security feature enabling token issuers to set a maximum rate at which their token can be transferred per lane. Token pools are configured to `lock` or `burn` tokens on the source blockchain and `unlock` or `mint` tokens on the destination blockchain. This setup results in four primary mechanisms: + +- **Burn and Mint**: Tokens are burned on the source blockchain, and an equivalent amount of tokens are minted on the destination blockchain. +- **Lock and Mint**: Tokens are locked on their issuing blockchain, and fully collateralized "wrapped" tokens are minted on the destination blockchain. These wrapped tokens can be transferred across non-issuing blockchains using the _Burn and Mint_ mechanism. +- **Burn and Unlock**: Tokens are burned on the source blockchain, and an equivalent amount of tokens are released on the destination blockchain. This mechanism is the inverse of the _Lock and Mint_ mechanism. It applies when you send tokens to their issuing source blockchain. +- **Lock and Unlock**: Tokens are locked on the source blockchain, and an equivalent amount of tokens are released on the destination blockchain. + +The mechanism for handling tokens varies depending on the characteristics of each token. Below are several examples to illustrate this: + +- LINK Token is minted on a single blockchain (Ethereum mainnet) and has a fixed total supply. Consequently, CCIP cannot natively mint it on another blockchain. For LINK, the token pool is configured to lock tokens on Ethereum mainnet (the issuing blockchain) and mint them on the destination blockchain. Conversely, when transferring from a non-issuing blockchain to Ethereum mainnet, the LINK token pool is set to burn the tokens on the source (non-issuing) blockchain and unlock them on Ethereum Mainnet (issuing). For example, transferring 10 LINK from Ethereum mainnet to Base mainnet involves the LINK token pool locking 10 LINK on Ethereum mainnet and minting 10 LINK on Base mainnet. Conversely, transferring 10 LINK from Base mainnet to Ethereum mainnet involves the LINK token pool burning 10 LINK on Base mainnet and unlocking 10 LINK on Ethereum mainnet. +- Wrapped native Assets (e.g., WETH) utilize a _Lock and Unlock_ mechanism. For instance, when transferring 10 WETH from Ethereum mainnet to Optimism mainnet, the WETH token pool will lock 10 WETH on Ethereum mainnet and unlock 10 WETH on Optimism mainnet. Conversely, transferring from Optimism mainnet back to Ethereum mainnet involves the WETH token pool locking 10 WETH on Optimism mainnet and unlocking 10 WETH on the Ethereum mainnet. +- Stablecoins (e.g., USDC) can be minted natively on multiple blockchains. Their respective token pools employ a _Burn and Mint_ mechanism, burning the token on the source blockchain and then minting it natively on the destination blockchain. +- Tokens with a Proof Of Reserve (PoR) with a PoR feed on a specific blockchain present a challenge for the _Burn and Mint_ mechanism when applied across other blockchains due to conflicts with the PoR feed. For such tokens, the _Lock and Mint_ approach is preferred. + +{/* prettier-ignore */} +