diff --git a/.gitignore b/.gitignore index 7e32b04e..2963edda 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,6 @@ ccip-api-ref/docs-api/v1/* !ccip-api-ref/docs-api/v1/sidebar.d.ts # Canton CLI config -canton-config.json \ No newline at end of file +canton-config.json + +pnpm-lock.yaml \ No newline at end of file diff --git a/ccip-cli/package.json b/ccip-cli/package.json index 22040140..cd36d426 100644 --- a/ccip-cli/package.json +++ b/ccip-cli/package.json @@ -28,7 +28,7 @@ "typecheck": "tsc --noEmit", "check": "npm run lint && npm run typecheck", "build": "npm run clean && tsc -p ./tsconfig.build.json && npm run patch-dist", - "patch-dist": "chmod +x ./dist/index.js && find ./dist -type f -name \"*.js\" -exec sed -i.bkp 's|@chainlink/ccip-sdk/src/.*\\.ts|@chainlink/ccip-sdk|g' {} + && find ./dist -type f -name \"*.bkp\" -delete", + "patch-dist": "chmod +x ./dist/index.js && find ./dist -type f -name \"*.js\" -exec sed -i.bkp -e 's|@chainlink/ccip-sdk/src/token-admin/\\([^/]*\\)/index\\.ts|@chainlink/ccip-sdk/token-admin/\\1|g' -e 's|@chainlink/ccip-sdk/src/token-admin/types\\.ts|@chainlink/ccip-sdk/token-admin/types|g' -e 's|@chainlink/ccip-sdk/src/.*\\.ts|@chainlink/ccip-sdk|g' {} + && find ./dist -type f -name \"*.bkp\" -delete", "start": "./ccip-cli", "clean": "rm -rfv ./dist", "prepare": "npm run build" diff --git a/ccip-cli/src/commands/pool.ts b/ccip-cli/src/commands/pool.ts new file mode 100644 index 00000000..2a790f98 --- /dev/null +++ b/ccip-cli/src/commands/pool.ts @@ -0,0 +1,24 @@ +/** + * Pool operations command group. + * Dispatches to subcommands: deploy. + */ + +import type { Argv } from 'yargs' + +export const command = 'pool' +export const describe = + 'Pool operations (deploy, apply-chain-updates, append-remote-pool-addresses, remove-remote-pool-addresses, delete-chain-config, get-config, set-rate-limiter-config, set-rate-limit-admin, provide-liquidity, set-fee-config, set-finality-config, set-fee-admin, transfer-ownership, accept-ownership, execute-ownership-transfer)' + +/** + * Yargs builder for the pool command group. + * Loads subcommands from the `pool/` directory. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with subcommands. + */ +export const builder = (yargs: Argv) => + yargs + .commandDir('pool', { + extensions: [new URL(import.meta.url).pathname.split('.').pop()!], + exclude: /\.test\.[tj]s$/, + }) + .demandCommand(1) diff --git a/ccip-cli/src/commands/pool/accept-ownership.ts b/ccip-cli/src/commands/pool/accept-ownership.ts new file mode 100644 index 00000000..b6186ba0 --- /dev/null +++ b/ccip-cli/src/commands/pool/accept-ownership.ts @@ -0,0 +1,131 @@ +/** + * Pool accept-ownership subcommand. + * Accepts proposed pool ownership (2-step ownership transfer). + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AcceptOwnershipParams, + type AptosChain, + type Chain, + type EVMChain, + type SolanaChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'accept-ownership' +export const describe = 'Accept proposed pool ownership (2-step ownership transfer)' + +/** + * Yargs builder for the pool accept-ownership subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be pending/proposed owner)', + }) + .option('pool-address', { + type: 'string', + demandOption: true, + describe: 'Pool address', + }) + .example([ + [ + 'ccip-cli pool accept-ownership -n sepolia --pool-address 0x...', + 'Accept proposed pool ownership', + ], + ]) + +/** + * Handler for the pool accept-ownership subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doAcceptOwnership(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type AcceptOwnershipArgv = Awaited['argv']> & GlobalOpts + +/** Calls acceptOwnership on the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function acceptForChain( + chain: Chain, + wallet: unknown, + params: AcceptOwnershipParams, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const mgr = EVMTokenManager.fromChain(chain as EVMChain) + return mgr.acceptOwnership({ ...params, wallet }) + } + case ChainFamily.Solana: { + const mgr = SolanaTokenManager.fromChain(chain as SolanaChain) + return mgr.acceptOwnership({ ...params, wallet }) + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const { hash } = await mgr.acceptOwnership({ ...params, wallet }) + return { hash } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doAcceptOwnership(ctx: Ctx, argv: AcceptOwnershipArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const params: AcceptOwnershipParams = { + poolAddress: argv.poolAddress, + } + + logger.debug(`Accepting ownership: pool=${params.poolAddress}`) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await acceptForChain(chain, wallet, params) + + const output: Record = { + network: networkName, + poolAddress: params.poolAddress, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Ownership accepted, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/pool/append-remote-pool-addresses.ts b/ccip-cli/src/commands/pool/append-remote-pool-addresses.ts new file mode 100644 index 00000000..776ed2de --- /dev/null +++ b/ccip-cli/src/commands/pool/append-remote-pool-addresses.ts @@ -0,0 +1,159 @@ +/** + * Pool append-remote-pool-addresses subcommand. + * Appends remote pool addresses to a CCIP token pool for a given remote chain. + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AppendRemotePoolAddressesParams, + type AptosChain, + type Chain, + type EVMChain, + type SolanaChain, + CCIPArgumentInvalidError, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'append-remote-pool-addresses' +export const describe = 'Append remote pool addresses to a CCIP token pool for a given remote chain' + +/** + * Yargs builder for the pool append-remote-pool-addresses subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be pool owner)', + }) + .option('pool-address', { + type: 'string', + describe: 'Local pool address', + }) + .option('remote-chain', { + type: 'string', + describe: 'Remote chain: chainId, name, or selector', + }) + .option('remote-pool-addresses', { + type: 'string', + describe: 'Comma-separated list of remote pool addresses', + }) + .check((argv) => { + if (!argv.network) throw new CCIPArgumentInvalidError('network', 'required argument missing') + if (!argv.poolAddress) + throw new CCIPArgumentInvalidError('pool-address', 'required argument missing') + if (!argv.remoteChain) + throw new CCIPArgumentInvalidError('remote-chain', 'required argument missing') + if (!argv.remotePoolAddresses) + throw new CCIPArgumentInvalidError('remote-pool-addresses', 'required argument missing') + return true + }) + .example([ + [ + 'ccip-cli pool append-remote-pool-addresses -n sepolia --pool-address 0x... --remote-chain avalanche-fuji --remote-pool-addresses 0xaaa,0xbbb', + 'Append remote pool addresses for a remote chain', + ], + ]) + +/** + * Handler for the pool append-remote-pool-addresses subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doAppendRemotePoolAddresses(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type AppendArgv = Awaited['argv']> & GlobalOpts + +/** Calls appendRemotePoolAddresses on the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function appendForChain( + chain: Chain, + wallet: unknown, + params: AppendRemotePoolAddressesParams, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + return mgr.appendRemotePoolAddresses({ ...params, wallet }) + } + case ChainFamily.Solana: { + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + return mgr.appendRemotePoolAddresses({ ...params, wallet }) + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const { hash } = await mgr.appendRemotePoolAddresses({ ...params, wallet }) + return { hash } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doAppendRemotePoolAddresses(ctx: Ctx, argv: AppendArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network!).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const remoteChainSelector = networkInfo(argv.remoteChain!).chainSelector + const remotePoolAddresses = argv.remotePoolAddresses!.split(',').map((a) => a.trim()) + + const params: AppendRemotePoolAddressesParams = { + poolAddress: argv.poolAddress!, + remoteChainSelector, + remotePoolAddresses, + } + + logger.debug( + `Appending ${remotePoolAddresses.length} remote pool address(es) for remote chain ${remoteChainSelector}`, + ) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await appendForChain(chain, wallet, params) + + const output: Record = { + network: networkName, + poolAddress: argv.poolAddress!, + remoteChainSelector: String(remoteChainSelector), + addressesAdded: remotePoolAddresses.join(', '), + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Remote pool addresses appended, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/pool/apply-chain-updates.ts b/ccip-cli/src/commands/pool/apply-chain-updates.ts new file mode 100644 index 00000000..7cc9bba4 --- /dev/null +++ b/ccip-cli/src/commands/pool/apply-chain-updates.ts @@ -0,0 +1,254 @@ +/** + * Pool apply-chain-updates subcommand. + * Configures remote chains on a CCIP token pool. + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type ApplyChainUpdatesParams, + type AptosChain, + type Chain, + type EVMChain, + type RateLimiterConfig, + type RemoteChainConfig, + type SolanaChain, + CCIPArgumentInvalidError, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'apply-chain-updates' +export const describe = 'Configure remote chains on a CCIP token pool' + +// ── Config file schema ── + +interface ConfigFile { + chainsToRemove?: string[] + chainsToAdd?: Array<{ + remoteChainSelector: string + remotePoolAddresses: string[] + remoteTokenAddress: string + remoteTokenDecimals?: number + outboundRateLimiterConfig?: RateLimiterConfig + inboundRateLimiterConfig?: RateLimiterConfig + }> +} + +// ── Generate config template ── + +const CONFIG_TEMPLATE: ConfigFile = { + chainsToRemove: [], + chainsToAdd: [ + { + remoteChainSelector: + '', + remotePoolAddresses: [''], + remoteTokenAddress: '', + remoteTokenDecimals: 18, + outboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + inboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + }, + ], +} + +/** + * Yargs builder for the pool apply-chain-updates subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be pool owner)', + }) + .option('pool-address', { + type: 'string', + describe: 'Local pool address', + }) + .option('config', { + type: 'string', + describe: 'Path to JSON config file with remote chain configurations', + }) + .option('generate-config', { + type: 'boolean', + describe: 'Output a sample JSON config template to stdout', + }) + .check((argv) => { + if (!argv.generateConfig) { + if (!argv.network) + throw new CCIPArgumentInvalidError('network', 'required argument missing') + if (!argv.poolAddress) + throw new CCIPArgumentInvalidError('pool-address', 'required argument missing') + } + return true + }) + .example([ + [ + 'ccip-cli pool apply-chain-updates -n sepolia --pool-address 0x... --config config.json', + 'Apply chain updates from a config file', + ], + [ + 'ccip-cli pool apply-chain-updates --generate-config > config.json', + 'Generate a template config file', + ], + [ + 'cat config.json | ccip-cli pool apply-chain-updates -n sepolia --pool-address 0x...', + 'Read config from stdin', + ], + ]) + +/** + * Handler for the pool apply-chain-updates subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + // Handle --generate-config + if (argv.generateConfig) { + ctx.output.write(JSON.stringify(CONFIG_TEMPLATE, null, 2)) + destroy() + return + } + + return doApplyChainUpdates(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type ApplyArgv = Awaited['argv']> & GlobalOpts + +/** Reads and parses config from file path or stdin. */ +async function readConfig(argv: ApplyArgv): Promise { + const { readFileSync } = await import('node:fs') + + if (argv.config) { + // Read from file + const raw = readFileSync(argv.config, 'utf8') + return JSON.parse(raw) as ConfigFile + } + + // Try stdin (piped input) + if (!process.stdin.isTTY) { + const chunks: Buffer[] = [] + for await (const chunk of process.stdin) { + chunks.push(chunk as Buffer) + } + const raw = Buffer.concat(chunks).toString('utf8') + return JSON.parse(raw) as ConfigFile + } + + throw new CCIPArgumentInvalidError( + 'config', + 'No config provided. Use --config or pipe JSON via stdin. Use --generate-config to see the expected format.', + ) +} + +/** + * Resolves a chain identifier (name, chainId, or selector) to a numeric selector string. + * Uses `networkInfo()` which accepts all three formats. + */ +function resolveChainSelector(input: string): bigint { + return networkInfo(input).chainSelector +} + +/** Converts a config file to ApplyChainUpdatesParams. */ +function configToParams(poolAddress: string, config: ConfigFile): ApplyChainUpdatesParams { + const defaultRateLimit: RateLimiterConfig = { isEnabled: false, capacity: '0', rate: '0' } + + const chainsToAdd: RemoteChainConfig[] = (config.chainsToAdd ?? []).map((c) => ({ + remoteChainSelector: resolveChainSelector(c.remoteChainSelector), + remotePoolAddresses: c.remotePoolAddresses, + remoteTokenAddress: c.remoteTokenAddress, + remoteTokenDecimals: c.remoteTokenDecimals, + outboundRateLimiterConfig: c.outboundRateLimiterConfig ?? defaultRateLimit, + inboundRateLimiterConfig: c.inboundRateLimiterConfig ?? defaultRateLimit, + })) + + return { + poolAddress, + remoteChainSelectorsToRemove: (config.chainsToRemove ?? []).map(resolveChainSelector), + chainsToAdd, + } +} + +/** Calls applyChainUpdates on the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function applyForChain( + chain: Chain, + wallet: unknown, + params: ApplyChainUpdatesParams, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + return mgr.applyChainUpdates({ ...params, wallet }) + } + case ChainFamily.Solana: { + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + return mgr.applyChainUpdates({ ...params, wallet }) + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const { hash } = await mgr.applyChainUpdates({ ...params, wallet }) + return { hash } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doApplyChainUpdates(ctx: Ctx, argv: ApplyArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network!).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const config = await readConfig(argv) + const params = configToParams(argv.poolAddress!, config) + + logger.debug( + `Applying chain updates: ${params.chainsToAdd.length} add(s), ${params.remoteChainSelectorsToRemove.length} remove(s)`, + ) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await applyForChain(chain, wallet, params) + + const output: Record = { + network: networkName, + poolAddress: argv.poolAddress!, + txHash: result.hash, + chainsAdded: String(params.chainsToAdd.length), + chainsRemoved: String(params.remoteChainSelectorsToRemove.length), + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Chain updates applied, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/pool/delete-chain-config.ts b/ccip-cli/src/commands/pool/delete-chain-config.ts new file mode 100644 index 00000000..eebabdee --- /dev/null +++ b/ccip-cli/src/commands/pool/delete-chain-config.ts @@ -0,0 +1,148 @@ +/** + * Pool delete-chain-config subcommand. + * Removes a remote chain configuration from a CCIP token pool. + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type Chain, + type DeleteChainConfigParams, + type EVMChain, + type SolanaChain, + CCIPArgumentInvalidError, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'delete-chain-config' +export const describe = 'Remove a remote chain configuration from a CCIP token pool' + +/** + * Yargs builder for the pool delete-chain-config subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be pool owner)', + }) + .option('pool-address', { + type: 'string', + describe: 'Local pool address', + }) + .option('remote-chain', { + type: 'string', + describe: 'Remote chain: chainId, name, or selector', + }) + .check((argv) => { + if (!argv.network) throw new CCIPArgumentInvalidError('network', 'required argument missing') + if (!argv.poolAddress) + throw new CCIPArgumentInvalidError('pool-address', 'required argument missing') + if (!argv.remoteChain) + throw new CCIPArgumentInvalidError('remote-chain', 'required argument missing') + return true + }) + .example([ + [ + 'ccip-cli pool delete-chain-config -n sepolia --pool-address 0x... --remote-chain avalanche-fuji', + 'Remove a remote chain config from a pool', + ], + ]) + +/** + * Handler for the pool delete-chain-config subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doDeleteChainConfig(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type DeleteArgv = Awaited['argv']> & GlobalOpts + +/** Calls deleteChainConfig on the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function deleteForChain( + chain: Chain, + wallet: unknown, + params: DeleteChainConfigParams, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + return mgr.deleteChainConfig({ ...params, wallet }) + } + case ChainFamily.Solana: { + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + return mgr.deleteChainConfig({ ...params, wallet }) + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const { hash } = await mgr.deleteChainConfig({ ...params, wallet }) + return { hash } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doDeleteChainConfig(ctx: Ctx, argv: DeleteArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network!).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const remoteChainSelector = networkInfo(argv.remoteChain!).chainSelector + + const params: DeleteChainConfigParams = { + poolAddress: argv.poolAddress!, + remoteChainSelector, + } + + logger.debug(`Deleting chain config for remote chain ${remoteChainSelector}`) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await deleteForChain(chain, wallet, params) + + const output: Record = { + network: networkName, + poolAddress: argv.poolAddress!, + remoteChainSelector: String(remoteChainSelector), + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Chain config deleted, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/pool/deploy-combined.ts b/ccip-cli/src/commands/pool/deploy-combined.ts new file mode 100644 index 00000000..5caac670 --- /dev/null +++ b/ccip-cli/src/commands/pool/deploy-combined.ts @@ -0,0 +1,165 @@ +/** + * Pool deploy-combined subcommand (EVM only). + * Deploys a CrossChainPoolToken — the canonical CCT v2.0 contract that is simultaneously + * an ERC20 token and its own CCIP token pool (single deploy, no separate token/pool). + */ + +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { + type EVMChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import { parseUnits } from 'ethers' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' +import { runVerification } from '../verify-utils.ts' + +export const command = 'deploy-combined' +export const describe = 'Deploy a CrossChainPoolToken (combined token + pool, EVM v2.0)' + +/** + * Yargs builder for the pool deploy-combined subcommand. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with command options. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'EVM network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key', + }) + .option('name', { type: 'string', demandOption: true, describe: 'Token name' }) + .option('symbol', { type: 'string', demandOption: true, describe: 'Token symbol' }) + .option('decimals', { type: 'number', demandOption: true, describe: 'Token decimals' }) + .option('router-address', { + type: 'string', + demandOption: true, + describe: 'CCIP Router address (used to derive rmnProxy)', + }) + .option('max-supply', { + type: 'string', + describe: 'Max supply in whole units (omit for unlimited)', + }) + .option('initial-supply', { + type: 'string', + default: '0', + describe: 'Pre-mint amount in whole units', + }) + .option('advanced-pool-hooks', { + type: 'string', + describe: 'AdvancedPoolHooks contract address (default: none)', + }) + .option('ccip-admin', { + type: 'string', + describe: 'CCIP admin (getCCIPAdmin); defaults to signer', + }) + .option('pre-mint-recipient', { + type: 'string', + describe: 'Recipient of the initial-supply pre-mint; defaults to ccip-admin', + }) + .option('verify', { + type: 'boolean', + default: false, + describe: 'Verify the deployed CrossChainPoolToken on the explorer', + }) + .option('etherscan-api-key', { + type: 'string', + describe: 'Etherscan V2 API key for --verify (defaults to ETHERSCAN_API_KEY env)', + }) + .example([ + [ + 'ccip-cli pool deploy-combined -n ethereum-testnet-sepolia --name "My Token" --symbol MTK --decimals 18 --router-address 0x0BF3...', + 'Deploy a CrossChainPoolToken (token == pool) on Sepolia', + ], + ]) + +/** + * Handler for the pool deploy-combined subcommand. + * @param argv - Command line arguments. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doDeployCombined(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +async function doDeployCombined( + ctx: Ctx, + argv: Awaited['argv']> & GlobalOpts, +) { + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + if (chain.network.family !== ChainFamily.EVM) { + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } + + const [, wallet] = await loadChainWallet(chain, argv) + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + + const maxSupply = argv.maxSupply ? parseUnits(argv.maxSupply, argv.decimals) : undefined + const initialSupply = + argv.initialSupply !== '0' ? parseUnits(argv.initialSupply, argv.decimals) : undefined + + const result = await mgr.deployCrossChainPoolToken({ + name: argv.name, + symbol: argv.symbol, + decimals: argv.decimals, + routerAddress: argv.routerAddress, + ...(maxSupply !== undefined && { maxSupply }), + ...(initialSupply !== undefined && { initialSupply }), + ...(argv.advancedPoolHooks && { advancedPoolHooks: argv.advancedPoolHooks }), + ...(argv.ccipAdmin && { ccipAdmin: argv.ccipAdmin }), + ...(argv.preMintRecipient && { preMintRecipient: argv.preMintRecipient }), + wallet, + }) + + const output: Record = { + network: networkName, + address: result.address, + tokenAddress: result.tokenAddress, + poolAddress: result.poolAddress, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + break + case Format.log: + ctx.output.write('CrossChainPoolToken deployed (token == pool):', result.address) + ctx.output.write('tx:', result.hash) + break + case Format.pretty: + default: + prettyTable.call(ctx, output) + break + } + + // The cct facade returns the verification handle (contract key + encoded constructor args) + // for the deployed CrossChainPoolToken. + if (argv.verify) { + await runVerification(ctx, networkName, [{ ...result.verification, address: result.address }], { + etherscanApiKey: argv.etherscanApiKey, + }) + } +} diff --git a/ccip-cli/src/commands/pool/deploy-via-factory.ts b/ccip-cli/src/commands/pool/deploy-via-factory.ts new file mode 100644 index 00000000..5d7f8bac --- /dev/null +++ b/ccip-cli/src/commands/pool/deploy-via-factory.ts @@ -0,0 +1,218 @@ +/** + * Pool deploy-via-factory subcommand. + * + * Deploys CCT v2 contracts through a `TokenPoolFactory 2.0.0` (CREATE2) on EVM, in either mode: + * - no `--token-address` → deploy a NEW CrossChainToken **and** its pool (deployTokenAndTokenPool) + * - with `--token-address`→ deploy a pool for an EXISTING token (deployTokenPoolWithExistingToken) + * + * With `--verify`, every contract the factory created (token, pool, and the auto-deployed lockbox + * for lock-release) is verified on the source-chain explorer using the exact constructor args + * (the factory contracts are born in internal CREATE2 calls, so the args are carried through from + * the deploy rather than recovered from a creation tx). + */ + +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { + type EVMChain, + CCIPArgumentInvalidError, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' +import { type VerifyTarget, runVerification } from '../verify-utils.ts' + +export const command = 'deploy-via-factory' +export const describe = 'Deploy CCT v2 token/pool through a TokenPoolFactory 2.0.0 (EVM, CREATE2)' + +/** + * Yargs builder for the pool deploy-via-factory subcommand. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with command options. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'EVM network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key', + }) + .option('factory', { + type: 'string', + demandOption: true, + describe: 'TokenPoolFactory 2.0.0 address on this chain', + }) + .option('pool-type', { + type: 'string', + choices: ['burn-mint', 'lock-release'] as const, + demandOption: true, + describe: 'Local pool type to deploy', + }) + .option('decimals', { + type: 'number', + default: 18, + describe: 'Local token decimals', + }) + .option('token-address', { + type: 'string', + describe: 'Existing token address (existing-token mode); omit to deploy a new token + pool', + }) + // New-token mode (omit --token-address) + .option('name', { type: 'string', describe: 'Token name (new-token mode)' }) + .option('symbol', { type: 'string', describe: 'Token symbol (new-token mode)' }) + .option('max-supply', { + type: 'string', + describe: 'Max supply in smallest units (new-token mode); defaults to uint256 max', + }) + .option('pre-mint', { + type: 'string', + describe: 'Initial supply minted at deploy, smallest units (new-token mode)', + }) + .option('pre-mint-recipient', { + type: 'string', + describe: 'Recipient of the pre-mint; defaults to the future owner', + }) + // Shared + .option('lock-box', { + type: 'string', + describe: 'Existing ERC20LockBox (lock-release); the factory auto-deploys one if omitted', + }) + .option('salt', { type: 'string', describe: 'CREATE2 salt (random 32 bytes if omitted)' }) + .option('future-owner', { + type: 'string', + describe: 'Final owner of the deployed contracts; defaults to the signer', + }) + .option('verify', { + type: 'boolean', + default: false, + describe: 'Verify every deployed contract on the source-chain explorer', + }) + .option('etherscan-api-key', { + type: 'string', + describe: 'Etherscan V2 API key for --verify (defaults to ETHERSCAN_API_KEY env)', + }) + .check((argv) => { + if (!argv.tokenAddress && (!argv.name || !argv.symbol)) { + throw new Error('new-token mode requires --name and --symbol (or pass --token-address)') + } + return true + }) + .example([ + [ + 'ccip-cli pool deploy-via-factory -n ethereum-testnet-sepolia --factory 0x93c5... --pool-type burn-mint --name "My Token" --symbol MTK --verify', + 'Deploy a new token + burn-mint pool via the factory and verify both', + ], + [ + 'ccip-cli pool deploy-via-factory -n ethereum-testnet-sepolia --factory 0x93c5... --pool-type lock-release --token-address 0xabc... --verify', + 'Deploy a lock-release pool (+ auto lockbox) for an existing token and verify', + ], + ]) + +type FactoryArgv = Awaited['argv']> & GlobalOpts + +/** + * Handler for the pool deploy-via-factory subcommand. + * @param argv - Command line arguments. + */ +export async function handler(argv: FactoryArgv) { + const [ctx, destroy] = getCtx(argv) + return doDeploy(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +async function doDeploy(ctx: Ctx, argv: FactoryArgv) { + const net = networkInfo(argv.network) + if (net.family !== ChainFamily.EVM) { + throw new CCIPChainFamilyUnsupportedError(net.family) + } + const networkName = net.name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = (await getChain(networkName)) as EVMChain + const [, wallet] = await loadChainWallet(chain, argv) + const mgr = EVMTokenManager.fromChain(chain) + + const poolType = argv.poolType + const shared = { + factoryAddress: argv.factory, + decimals: argv.decimals, + poolType, + ...(argv.lockBox && { lockBoxAddress: argv.lockBox }), + ...(argv.salt && { salt: argv.salt }), + ...(argv.futureOwner && { futureOwner: argv.futureOwner }), + } + + const output: Record = { network: networkName } + // Verification handles for every contract the factory created (pool, token, and the + // auto-deployed lockbox for lock-release); each already carries its address. + let verifications: VerifyTarget[] + + if (argv.tokenAddress) { + // Existing-token mode → pool only. + const result = await mgr.deployPoolViaFactory({ + ...shared, + tokenAddress: argv.tokenAddress, + wallet, + }) + output.poolAddress = result.poolAddress + output.txHash = result.hash + if (result.lockBoxAddress) output.lockBoxAddress = result.lockBoxAddress + verifications = result.verifications + } else { + // New-token mode → token + pool. + if (!argv.name || !argv.symbol) { + throw new CCIPArgumentInvalidError('name', 'new-token mode requires --name and --symbol') + } + const result = await mgr.deployTokenAndPoolViaFactory({ + ...shared, + name: argv.name, + symbol: argv.symbol, + maxSupply: argv.maxSupply ? BigInt(argv.maxSupply) : (1n << 256n) - 1n, + ...(argv.preMint && { preMint: BigInt(argv.preMint) }), + ...(argv.preMintRecipient && { preMintRecipient: argv.preMintRecipient }), + wallet, + }) + output.tokenAddress = result.tokenAddress + output.poolAddress = result.poolAddress + output.txHash = result.hash + if (result.lockBoxAddress) output.lockBoxAddress = result.lockBoxAddress + verifications = result.verifications + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + break + case Format.log: + if (output.tokenAddress) ctx.output.write('Token:', output.tokenAddress) + ctx.output.write('Pool:', output.poolAddress, 'tx:', output.txHash) + if (output.lockBoxAddress) ctx.output.write('LockBox:', output.lockBoxAddress) + break + case Format.pretty: + default: + prettyTable.call(ctx, output) + break + } + + // The factory-deploy facade returns a verification handle (contract key + encoded + // constructor args + address) for every contract it created in an internal CREATE2 call. + if (argv.verify) { + await runVerification(ctx, networkName, verifications, { + etherscanApiKey: argv.etherscanApiKey, + }) + } +} diff --git a/ccip-cli/src/commands/pool/deploy.test.ts b/ccip-cli/src/commands/pool/deploy.test.ts new file mode 100644 index 00000000..a9320c47 --- /dev/null +++ b/ccip-cli/src/commands/pool/deploy.test.ts @@ -0,0 +1,27 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import * as deploy from './deploy.ts' + +// ============================================================================= +// Module shape +// ============================================================================= + +describe('pool deploy — module shape', () => { + it('should export command as "deploy"', () => { + assert.equal(deploy.command, 'deploy') + }) + + it('should export a describe string', () => { + assert.equal(typeof deploy.describe, 'string') + assert.ok(deploy.describe.length > 0) + }) + + it('should export a builder function', () => { + assert.equal(typeof deploy.builder, 'function') + }) + + it('should export a handler function', () => { + assert.equal(typeof deploy.handler, 'function') + }) +}) diff --git a/ccip-cli/src/commands/pool/deploy.ts b/ccip-cli/src/commands/pool/deploy.ts new file mode 100644 index 00000000..e4dc422e --- /dev/null +++ b/ccip-cli/src/commands/pool/deploy.ts @@ -0,0 +1,298 @@ +/** + * Pool deploy subcommand. + * Deploys a new CCIP token pool (BurnMintTokenPool / LockReleaseTokenPool / Aptos pool). + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { type DeployVerification, EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type Chain, + type EVMChain, + type SolanaChain, + CCIPArgumentInvalidError, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' +import { type VerifyTarget, runVerification } from '../verify-utils.ts' + +export const command = 'deploy' +export const describe = 'Deploy a new CCIP token pool' + +/** + * Yargs builder for the pool deploy subcommand. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with command options. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key', + }) + .option('pool-type', { + type: 'string', + choices: ['burn-mint', 'lock-release'] as const, + demandOption: true, + describe: + 'Pool type: burn-mint (burns on source, mints on dest) or lock-release (locks on source, releases on dest)', + }) + .option('token-address', { + type: 'string', + demandOption: true, + describe: 'Token address (ERC20, SPL mint, or Aptos FA metadata)', + }) + .option('local-token-decimals', { + type: 'number', + demandOption: true, + describe: 'Token decimals on this chain', + }) + // EVM-specific + .option('router-address', { + type: 'string', + describe: 'CCIP Router address (required for EVM and Aptos)', + }) + .option('advanced-pool-hooks', { + type: 'string', + describe: 'AdvancedPoolHooks contract address (EVM v2.0; default: none)', + }) + .option('lock-box', { + type: 'string', + describe: + 'Existing ERC20LockBox address for lock-release (EVM only; auto-deployed when omitted)', + }) + // Solana-specific + .option('pool-program-id', { + type: 'string', + describe: 'Pre-deployed pool program ID (required for Solana)', + }) + // Aptos-specific + .option('token-module', { + type: 'string', + choices: ['managed', 'generic', 'regulated'] as const, + describe: "Aptos token module variant (default: 'managed')", + }) + .option('mcms-address', { + type: 'string', + describe: 'Deployed mcms package address (required for Aptos)', + }) + .option('admin-address', { + type: 'string', + describe: 'Admin address for regulated token access control (Aptos regulated only)', + }) + .option('verify', { + type: 'boolean', + default: false, + describe: 'Verify the deployed pool (and auto-deployed lockbox) on the explorer (EVM only)', + }) + .option('etherscan-api-key', { + type: 'string', + describe: 'Etherscan V2 API key for --verify (defaults to ETHERSCAN_API_KEY env)', + }) + .check((argv) => { + const { family } = networkInfo(argv.network) + if (family === ChainFamily.EVM) { + if (!argv.routerAddress) + throw new CCIPArgumentInvalidError( + 'router-address', + '--router-address is required for EVM and Aptos networks', + ) + } else if (family === ChainFamily.Aptos) { + if (!argv.routerAddress) + throw new CCIPArgumentInvalidError( + 'router-address', + '--router-address is required for EVM and Aptos networks', + ) + if (!argv.mcmsAddress) + throw new CCIPArgumentInvalidError( + 'mcms-address', + '--mcms-address is required for Aptos networks', + ) + } else if (family === ChainFamily.Solana) { + if (!argv.poolProgramId) + throw new CCIPArgumentInvalidError( + 'pool-program-id', + '--pool-program-id is required for Solana networks', + ) + } + return true + }) + .example([ + [ + 'ccip-cli pool deploy -n ethereum-testnet-sepolia --pool-type burn-mint --token-address 0xa42B... --local-token-decimals 18 --router-address 0x0BF3...', + 'Deploy BurnMintTokenPool on Sepolia', + ], + [ + 'ccip-cli pool deploy -n solana-devnet --pool-type burn-mint --token-address J6fE... --local-token-decimals 9 --pool-program-id ', + 'Deploy pool on Solana devnet', + ], + [ + 'ccip-cli pool deploy -n aptos-testnet --pool-type burn-mint --token-address 0x89fd... --local-token-decimals 8 --router-address 0xabc... --mcms-address 0x123...', + 'Deploy managed_token_pool on Aptos testnet', + ], + ]) + +/** + * Handler for the pool deploy subcommand. + * @param argv - Command line arguments. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doDeployPool(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type DeployArgv = Awaited['argv']> & GlobalOpts + +/** Chain-family-agnostic deploy result normalized for output. */ +type NormalizedDeploy = { + hash: string + poolAddress?: string + lockBoxAddress?: string + /** Aptos generic pools only: `false` when the pool needs a follow-up `initialize()`. */ + initialized?: boolean + /** EVM-only block-explorer verification handle for the pool. */ + verification?: DeployVerification + /** EVM lock-release only: verification handle for the auto-deployed `ERC20LockBox`. */ + lockBoxVerification?: DeployVerification +} + +/** Deploys a pool via the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function deployPoolForChain( + chain: Chain, + wallet: unknown, + argv: DeployArgv, +): Promise { + const poolType = argv.poolType + + switch (chain.network.family) { + case ChainFamily.EVM: { + const mgr = EVMTokenManager.fromChain(chain as EVMChain) + const result = await mgr.deployPool({ + poolType, + tokenAddress: argv.tokenAddress, + localTokenDecimals: argv.localTokenDecimals, + routerAddress: argv.routerAddress!, + ...(argv.advancedPoolHooks && { advancedPoolHooks: argv.advancedPoolHooks }), + ...(argv.lockBox && { lockBoxAddress: argv.lockBox }), + wallet, + }) + return { + hash: result.hash, + poolAddress: result.poolAddress, + verification: result.verification, + ...(result.lockBoxAddress && { lockBoxAddress: result.lockBoxAddress }), + ...(result.lockBoxVerification && { lockBoxVerification: result.lockBoxVerification }), + } + } + case ChainFamily.Solana: { + const mgr = SolanaTokenManager.fromChain(chain as SolanaChain) + const result = await mgr.deployTokenPool({ + tokenAddress: argv.tokenAddress, + poolProgramAddress: argv.poolProgramId!, + wallet, + }) + return { hash: result.hash, poolAddress: result.poolAddress } + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const result = await mgr.deployPool({ + poolType, + tokenAddress: argv.tokenAddress, + localTokenDecimals: argv.localTokenDecimals, + routerAddress: argv.routerAddress!, + mcmsAddress: argv.mcmsAddress!, + ...(argv.tokenModule && { tokenModule: argv.tokenModule }), + ...(argv.adminAddress && { adminAddress: argv.adminAddress }), + wallet, + }) + return { + hash: result.hash, + poolAddress: result.poolAddress, + ...(result.initialized === false && { initialized: false }), + } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doDeployPool(ctx: Ctx, argv: DeployArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await deployPoolForChain(chain, wallet, argv) + + const output: Record = { network: networkName } + if (result.poolAddress) output.poolAddress = result.poolAddress + output.txHash = result.hash + if (result.lockBoxAddress) output.lockBoxAddress = result.lockBoxAddress + + if (result.initialized === false) { + const poolModule = + argv.poolType === 'burn-mint' ? 'burn_mint_token_pool' : 'lock_release_token_pool' + const warning = + `WARNING: Generic pool deployed but NOT initialized. ` + + `The token creator module must call ${poolModule}::initialize() ` + + `with stored capability refs (BurnRef/MintRef/TransferRef) ` + + `before this pool can be used for CCIP operations.` + output.initialized = 'false' + output.warning = warning + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + break + case Format.log: + if (result.poolAddress) { + ctx.output.write('Pool deployed:', result.poolAddress, 'tx:', result.hash) + } else { + ctx.output.write('Pool deployed, tx:', result.hash) + } + if (result.initialized === false) { + logger.warn(output.warning) + } + break + case Format.pretty: + default: + prettyTable.call(ctx, output) + break + } + + // Contract verification is EVM-only; the cct facade returns the pool's (and, for + // lock-release, the auto-deployed lockbox's) verification handles on the deploy result. + if (argv.verify) { + const targets: VerifyTarget[] = [] + if (result.verification && result.poolAddress) { + targets.push({ ...result.verification, address: result.poolAddress }) + } + if (result.lockBoxVerification && result.lockBoxAddress) { + targets.push({ ...result.lockBoxVerification, address: result.lockBoxAddress }) + } + await runVerification(ctx, networkName, targets, { etherscanApiKey: argv.etherscanApiKey }) + } +} diff --git a/ccip-cli/src/commands/pool/execute-ownership-transfer.ts b/ccip-cli/src/commands/pool/execute-ownership-transfer.ts new file mode 100644 index 00000000..13d5299f --- /dev/null +++ b/ccip-cli/src/commands/pool/execute-ownership-transfer.ts @@ -0,0 +1,116 @@ +/** + * Pool execute-ownership-transfer subcommand. + * Aptos-only: finalizes pool ownership transfer (3rd step of Aptos 3-step process). + * + * Aptos ownership transfer flow: + * 1. `transfer-ownership` — current owner proposes new owner + * 2. `accept-ownership` — proposed owner signals acceptance + * 3. `execute-ownership-transfer` — current owner finalizes the AptosFramework object transfer + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { + type AptosChain, + type ExecuteOwnershipTransferParams, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'execute-ownership-transfer' +export const describe = + 'Aptos-only: finalize pool ownership transfer (3rd step after transfer + accept)' + +/** + * Yargs builder for the pool execute-ownership-transfer subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (must be an Aptos network)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be current pool owner)', + }) + .option('pool-address', { + type: 'string', + demandOption: true, + describe: 'Pool address', + }) + .option('new-owner', { + type: 'string', + demandOption: true, + describe: 'Address of the new owner (must match the address that called accept-ownership)', + }) + .example([ + [ + 'ccip-cli pool execute-ownership-transfer -n aptos-testnet --pool-address 0x... --new-owner 0x...', + 'Finalize Aptos pool ownership transfer', + ], + ]) + +/** + * Handler for the pool execute-ownership-transfer subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doExecuteOwnershipTransfer(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type ExecuteOwnershipTransferArgv = Awaited['argv']> & GlobalOpts + +async function doExecuteOwnershipTransfer(ctx: Ctx, argv: ExecuteOwnershipTransferArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + + const params: ExecuteOwnershipTransferParams = { + poolAddress: argv.poolAddress, + newOwner: argv.newOwner, + } + + logger.debug( + `Executing ownership transfer: pool=${params.poolAddress}, newOwner=${params.newOwner}`, + ) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await mgr.executeOwnershipTransfer({ ...params, wallet }) + + const output: Record = { + network: networkName, + poolAddress: params.poolAddress, + newOwner: params.newOwner, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Ownership transfer executed, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/pool/get-config.ts b/ccip-cli/src/commands/pool/get-config.ts new file mode 100644 index 00000000..46b73016 --- /dev/null +++ b/ccip-cli/src/commands/pool/get-config.ts @@ -0,0 +1,184 @@ +/** + * Pool get-config subcommand. + * Reads pool configuration and remote chain settings from on-chain state. + */ + +import { + type RateLimiterState, + CCIPArgumentInvalidError, + jsonStringify, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import { formatUnits } from 'ethers' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { formatDuration, getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'get-config' +export const describe = 'Show pool configuration and remote chain settings' + +/** + * Yargs builder for the pool get-config subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + describe: 'Network: chainId, selector, or name (e.g., ethereum-testnet-sepolia)', + }) + .option('pool-address', { + type: 'string', + describe: 'Pool address', + }) + .option('remote-chain', { + type: 'string', + describe: 'Filter remotes by chain name, selector, or chainId (shows only this remote)', + }) + .check((argv) => { + if (!argv.network) throw new CCIPArgumentInvalidError('network', 'required argument missing') + if (!argv.poolAddress) + throw new CCIPArgumentInvalidError('pool-address', 'required argument missing') + return true + }) + .example([ + [ + 'ccip-cli pool get-config -n sepolia --pool-address 0x...', + 'Show pool config and all remotes', + ], + [ + 'ccip-cli pool get-config -n sepolia --pool-address 0x... --remote-chain solana-devnet', + 'Show config for a specific remote chain only', + ], + [ + 'ccip-cli pool get-config -n solana-devnet --pool-address -f json', + 'Show pool config as JSON', + ], + ]) + +/** + * Handler for the pool get-config subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doGetConfig(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +function prettyRateLimiter(state: RateLimiterState, info: { decimals: number; symbol: string }) { + if (!state) return null + return { + capacity: formatUnits(state.capacity, info.decimals) + ' ' + info.symbol, + tokens: `${formatUnits(state.tokens, info.decimals)} (${Math.round((Number(state.tokens) / Number(state.capacity)) * 100)}%)`, + rate: `${formatUnits(state.rate, info.decimals)}/s (0-to-full in ${formatDuration(Number(state.capacity / state.rate))})`, + ...(state.tokens < state.capacity && { + timeToFull: formatDuration(Number(state.capacity - state.tokens) / Number(state.rate)), + }), + } +} + +async function doGetConfig( + ctx: Ctx, + argv: Awaited['argv']> & GlobalOpts, +) { + const { logger } = ctx + const networkName = networkInfo(argv.network!).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const poolAddress = argv.poolAddress! + + // Resolve optional --remote-chain filter to a chain selector + const remoteFilter = argv.remoteChain ? networkInfo(argv.remoteChain).chainSelector : undefined + + const [poolConfig, remotes, tokenInfo] = await Promise.all([ + chain.getTokenPoolConfig(poolAddress), + chain.getTokenPoolRemotes(poolAddress, remoteFilter), + chain.getTokenPoolConfig(poolAddress).then((c) => chain.getTokenInfo(c.token)), + ]) + + const remotesEntries = Object.entries(remotes) + + switch (argv.format) { + case Format.json: { + const output = { + network: networkName, + poolAddress, + token: poolConfig.token, + ...tokenInfo, + owner: poolConfig.owner, + ...('proposedOwner' in poolConfig && { proposedOwner: poolConfig.proposedOwner }), + ...(poolConfig.rateLimitAdmin && { rateLimitAdmin: poolConfig.rateLimitAdmin }), + ...(poolConfig.feeAdmin && { feeAdmin: poolConfig.feeAdmin }), + router: poolConfig.router, + typeAndVersion: poolConfig.typeAndVersion, + remotes: Object.fromEntries(remotesEntries.map(([name, remote]) => [name, remote])), + } + ctx.output.write(jsonStringify(output, 2)) + return + } + case Format.log: + ctx.output.write('Pool:', poolAddress) + ctx.output.write('Token:', poolConfig.token, tokenInfo) + ctx.output.write('Owner:', poolConfig.owner) + if (poolConfig.proposedOwner) ctx.output.write('Proposed Owner:', poolConfig.proposedOwner) + if (poolConfig.rateLimitAdmin) + ctx.output.write('Rate Limit Admin:', poolConfig.rateLimitAdmin) + if (poolConfig.feeAdmin) ctx.output.write('Fee Admin:', poolConfig.feeAdmin) + ctx.output.write('Router:', poolConfig.router) + ctx.output.write('Type:', poolConfig.typeAndVersion) + ctx.output.write('Remotes:', remotesEntries.length) + for (const [name, remote] of remotesEntries) { + ctx.output.write(` ${name}:`, remote) + } + return + case Format.pretty: + default: { + prettyTable.call(ctx, { + network: `${networkName} [${networkInfo(networkName).chainSelector}]`, + poolAddress, + token: poolConfig.token, + symbol: tokenInfo.symbol, + name: tokenInfo.name, + decimals: tokenInfo.decimals, + owner: poolConfig.owner, + ...(poolConfig.proposedOwner && { proposedOwner: poolConfig.proposedOwner }), + ...(poolConfig.rateLimitAdmin && { rateLimitAdmin: poolConfig.rateLimitAdmin }), + ...(poolConfig.feeAdmin && { feeAdmin: poolConfig.feeAdmin }), + typeAndVersion: poolConfig.typeAndVersion, + router: poolConfig.router, + }) + + if (remotesEntries.length > 0) logger.info('Remotes [', remotesEntries.length, ']:') + for (const [name, remote] of remotesEntries) { + prettyTable.call(ctx, { + remoteNetwork: `${name} [${networkInfo(name).chainSelector}]`, + remoteToken: remote.remoteToken, + remotePool: remote.remotePools, + outbound: prettyRateLimiter(remote.outboundRateLimiterState, tokenInfo), + inbound: prettyRateLimiter(remote.inboundRateLimiterState, tokenInfo), + // FTF = Faster-Than-Finality: separate rate limiters for messages confirmed + // with fewer block confirmations (EVM v2.0+ pools only) + ...('fastOutboundRateLimiterState' in remote && { + ['[ftf: Faster-Than-Finality]outbound']: prettyRateLimiter( + remote.fastOutboundRateLimiterState, + tokenInfo, + ), + ['[ftf: Faster-Than-Finality]inbound']: prettyRateLimiter( + remote.fastInboundRateLimiterState, + tokenInfo, + ), + }), + }) + } + return + } + } +} diff --git a/ccip-cli/src/commands/pool/provide-liquidity.ts b/ccip-cli/src/commands/pool/provide-liquidity.ts new file mode 100644 index 00000000..f13f1d78 --- /dev/null +++ b/ccip-cli/src/commands/pool/provide-liquidity.ts @@ -0,0 +1,133 @@ +/** + * Pool provide-liquidity subcommand. + * Funds a lock-release CCIP token pool with liquidity (EVM only). + */ + +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { + type Chain, + type EVMChain, + type ProvideLiquidityParams, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import { Contract, parseUnits } from 'ethers' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'provide-liquidity' +export const describe = 'Provide liquidity to a lock-release CCIP token pool (EVM only)' + +/** + * Yargs builder for the pool provide-liquidity subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key', + }) + .option('pool-address', { + type: 'string', + demandOption: true, + describe: 'Lock-release pool address', + }) + .option('amount', { + type: 'string', + demandOption: true, + describe: 'Amount of liquidity to provide, in whole token units (e.g., 1000)', + }) + .example([ + [ + 'ccip-cli pool provide-liquidity -n sepolia --pool-address 0x... --amount 1000', + 'Provide 1000 whole tokens of liquidity to a lock-release pool', + ], + ]) + +/** + * Handler for the pool provide-liquidity subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doProvideLiquidity(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type ProvideLiquidityArgv = Awaited['argv']> & GlobalOpts + +/** Minimal pool/token ABI for resolving the pool's token and its decimals. */ +const POOL_TOKEN_ABI = [ + 'function getToken() view returns (address)', + 'function decimals() view returns (uint8)', +] as const + +async function doProvideLiquidity(ctx: Ctx, argv: ProvideLiquidityArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain: Chain = await getChain(networkName) + + // provide-liquidity is EVM-only (lock-release liquidity is an EVM pool concept here). + if (chain.network.family !== ChainFamily.EVM) { + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } + const evmChain = chain as EVMChain + + // Resolve the pool's token decimals so a whole-unit `--amount` can be scaled. + const pool = new Contract(argv.poolAddress, POOL_TOKEN_ABI, evmChain.provider) + const tokenAddress = (await pool.getFunction('getToken')()) as string + const token = new Contract(tokenAddress, POOL_TOKEN_ABI, evmChain.provider) + const decimals = Number((await token.getFunction('decimals')()) as bigint) + const amount = parseUnits(argv.amount, decimals) + + const params: ProvideLiquidityParams = { + poolAddress: argv.poolAddress, + amount, + } + + logger.debug( + `Providing liquidity: pool=${params.poolAddress}, token=${tokenAddress}, amount=${amount} (${argv.amount} @ ${decimals} decimals)`, + ) + + const mgr = EVMTokenManager.fromChain(evmChain) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await mgr.provideLiquidity({ ...params, wallet }) + + const output: Record = { + network: networkName, + poolAddress: params.poolAddress, + token: tokenAddress, + amount: amount.toString(), + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Liquidity provided, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/pool/remove-remote-pool-addresses.ts b/ccip-cli/src/commands/pool/remove-remote-pool-addresses.ts new file mode 100644 index 00000000..cdd01efa --- /dev/null +++ b/ccip-cli/src/commands/pool/remove-remote-pool-addresses.ts @@ -0,0 +1,160 @@ +/** + * Pool remove-remote-pool-addresses subcommand. + * Removes specific remote pool addresses from a CCIP token pool for a given remote chain. + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type Chain, + type EVMChain, + type RemoveRemotePoolAddressesParams, + type SolanaChain, + CCIPArgumentInvalidError, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'remove-remote-pool-addresses' +export const describe = + 'Remove specific remote pool addresses from a CCIP token pool for a given remote chain' + +/** + * Yargs builder for the pool remove-remote-pool-addresses subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be pool owner)', + }) + .option('pool-address', { + type: 'string', + describe: 'Local pool address', + }) + .option('remote-chain', { + type: 'string', + describe: 'Remote chain: chainId, name, or selector', + }) + .option('remote-pool-addresses', { + type: 'string', + describe: 'Comma-separated list of remote pool addresses to remove', + }) + .check((argv) => { + if (!argv.network) throw new CCIPArgumentInvalidError('network', 'required argument missing') + if (!argv.poolAddress) + throw new CCIPArgumentInvalidError('pool-address', 'required argument missing') + if (!argv.remoteChain) + throw new CCIPArgumentInvalidError('remote-chain', 'required argument missing') + if (!argv.remotePoolAddresses) + throw new CCIPArgumentInvalidError('remote-pool-addresses', 'required argument missing') + return true + }) + .example([ + [ + 'ccip-cli pool remove-remote-pool-addresses -n sepolia --pool-address 0x... --remote-chain avalanche-fuji --remote-pool-addresses 0xaaa,0xbbb', + 'Remove remote pool addresses for a remote chain', + ], + ]) + +/** + * Handler for the pool remove-remote-pool-addresses subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doRemoveRemotePoolAddresses(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type RemoveArgv = Awaited['argv']> & GlobalOpts + +/** Calls removeRemotePoolAddresses on the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function removeForChain( + chain: Chain, + wallet: unknown, + params: RemoveRemotePoolAddressesParams, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + return mgr.removeRemotePoolAddresses({ ...params, wallet }) + } + case ChainFamily.Solana: { + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + return mgr.removeRemotePoolAddresses({ ...params, wallet }) + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const { hash } = await mgr.removeRemotePoolAddresses({ ...params, wallet }) + return { hash } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doRemoveRemotePoolAddresses(ctx: Ctx, argv: RemoveArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network!).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const remoteChainSelector = networkInfo(argv.remoteChain!).chainSelector + const remotePoolAddresses = argv.remotePoolAddresses!.split(',').map((a) => a.trim()) + + const params: RemoveRemotePoolAddressesParams = { + poolAddress: argv.poolAddress!, + remoteChainSelector, + remotePoolAddresses, + } + + logger.debug( + `Removing ${remotePoolAddresses.length} remote pool address(es) for remote chain ${remoteChainSelector}`, + ) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await removeForChain(chain, wallet, params) + + const output: Record = { + network: networkName, + poolAddress: argv.poolAddress!, + remoteChainSelector: String(remoteChainSelector), + addressesRemoved: remotePoolAddresses.join(', '), + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Remote pool addresses removed, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/pool/set-fee-admin.ts b/ccip-cli/src/commands/pool/set-fee-admin.ts new file mode 100644 index 00000000..e1190469 --- /dev/null +++ b/ccip-cli/src/commands/pool/set-fee-admin.ts @@ -0,0 +1,124 @@ +/** + * Pool set-fee-admin subcommand. + * Sets the fee admin on a CCIP token pool (EVM v2.0+ only). + */ + +import { type SetFeeAdminParams, EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { + type Chain, + type EVMChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'set-fee-admin' +export const describe = 'Set the fee admin on a CCIP token pool (EVM v2.0+ only)' + +/** + * Yargs builder for the pool set-fee-admin subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be pool owner)', + }) + .option('pool-address', { + type: 'string', + demandOption: true, + describe: 'Local pool address', + }) + .option('fee-admin', { + type: 'string', + demandOption: true, + describe: 'Address of the new fee admin', + }) + .example([ + [ + 'ccip-cli pool set-fee-admin -n sepolia --pool-address 0x... --fee-admin 0x...', + 'Set the fee admin on a v2.0 pool', + ], + ]) + +/** + * Handler for the pool set-fee-admin subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doSetFeeAdmin(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type SetFeeAdminArgv = Awaited['argv']> & GlobalOpts + +/** Calls setFeeAdmin on the appropriate chain-family facade (EVM v2.0+ only), normalizing to `{ hash }`. */ +function setForChain( + chain: Chain, + wallet: unknown, + params: SetFeeAdminParams, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + return mgr.setFeeAdmin({ ...params, wallet }) + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doSetFeeAdmin(ctx: Ctx, argv: SetFeeAdminArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const params: SetFeeAdminParams = { + poolAddress: argv.poolAddress, + feeAdmin: argv.feeAdmin, + } + + logger.debug(`Setting fee admin: pool=${params.poolAddress}, admin=${params.feeAdmin}`) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await setForChain(chain, wallet, params) + + const output: Record = { + network: networkName, + poolAddress: params.poolAddress, + feeAdmin: params.feeAdmin, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Fee admin updated, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/pool/set-fee-config.ts b/ccip-cli/src/commands/pool/set-fee-config.ts new file mode 100644 index 00000000..a3f6c347 --- /dev/null +++ b/ccip-cli/src/commands/pool/set-fee-config.ts @@ -0,0 +1,239 @@ +/** + * Pool set-fee-config subcommand. + * Sets per-destination token-transfer fee configs on a CCIP token pool (EVM v2.0+ only). + */ + +import { + type SetTokenTransferFeeConfigParams, + EVMTokenManager, +} from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { + type Chain, + type EVMChain, + CCIPArgumentInvalidError, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'set-fee-config' +export const describe = + 'Set per-destination token-transfer fee configs on a CCIP token pool (EVM v2.0+ only)' + +// ── Config file schema ── + +interface FeeConfigEntry { + remoteChainSelector: string + destGasOverhead: number + destBytesOverhead: number + finalityFeeUSDCents: number + fastFinalityFeeUSDCents: number + finalityTransferFeeBps: number + fastFinalityTransferFeeBps: number + isEnabled: boolean +} + +interface ConfigFile { + feeConfigs: FeeConfigEntry[] + disable?: string[] +} + +// ── Generate config template ── + +const CONFIG_TEMPLATE: ConfigFile = { + feeConfigs: [ + { + remoteChainSelector: + '', + destGasOverhead: 90000, + destBytesOverhead: 32, + finalityFeeUSDCents: 10, + fastFinalityFeeUSDCents: 50, + finalityTransferFeeBps: 5, + fastFinalityTransferFeeBps: 25, + isEnabled: true, + }, + ], + disable: [], +} + +/** + * Yargs builder for the pool set-fee-config subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be pool owner or fee admin)', + }) + .option('pool-address', { + type: 'string', + describe: 'Local pool address', + }) + .option('config', { + type: 'string', + describe: 'Path to JSON config file with token-transfer fee configurations', + }) + .option('generate-config', { + type: 'boolean', + describe: 'Output a sample JSON config template to stdout', + }) + .check((argv) => { + if (!argv.generateConfig) { + if (!argv.network) + throw new CCIPArgumentInvalidError('network', 'required argument missing') + if (!argv.poolAddress) + throw new CCIPArgumentInvalidError('pool-address', 'required argument missing') + } + return true + }) + .example([ + [ + 'ccip-cli pool set-fee-config -n sepolia --pool-address 0x... --config config.json', + 'Set token-transfer fee config from a config file', + ], + [ + 'ccip-cli pool set-fee-config --generate-config > config.json', + 'Generate a template config file', + ], + ]) + +/** + * Handler for the pool set-fee-config subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + // Handle --generate-config + if (argv.generateConfig) { + ctx.output.write(JSON.stringify(CONFIG_TEMPLATE, null, 2)) + destroy() + return + } + + return doSetFeeConfig(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type SetFeeConfigArgv = Awaited['argv']> & GlobalOpts + +/** Reads and parses config from file path or stdin. */ +async function readConfig(argv: SetFeeConfigArgv): Promise { + const { readFileSync } = await import('node:fs') + + if (argv.config) { + const raw = readFileSync(argv.config, 'utf8') + return JSON.parse(raw) as ConfigFile + } + + // Try stdin (piped input) + if (!process.stdin.isTTY) { + const chunks: Buffer[] = [] + for await (const chunk of process.stdin) { + chunks.push(chunk as Buffer) + } + const raw = Buffer.concat(chunks).toString('utf8') + return JSON.parse(raw) as ConfigFile + } + + throw new CCIPArgumentInvalidError( + 'config', + 'No config provided. Use --config or pipe JSON via stdin. Use --generate-config to see the expected format.', + ) +} + +/** + * Resolves a chain identifier (name, chainId, or selector) to a numeric selector. + */ +function resolveChainSelector(input: string): bigint { + return networkInfo(input).chainSelector +} + +/** Converts a config file to SetTokenTransferFeeConfigParams. */ +function configToParams(poolAddress: string, config: ConfigFile): SetTokenTransferFeeConfigParams { + const updates: SetTokenTransferFeeConfigParams['updates'] = config.feeConfigs.map((c) => ({ + remoteChainSelector: resolveChainSelector(c.remoteChainSelector), + config: { + destGasOverhead: c.destGasOverhead, + destBytesOverhead: c.destBytesOverhead, + finalityFeeUSDCents: c.finalityFeeUSDCents, + fastFinalityFeeUSDCents: c.fastFinalityFeeUSDCents, + finalityTransferFeeBps: c.finalityTransferFeeBps, + fastFinalityTransferFeeBps: c.fastFinalityTransferFeeBps, + isEnabled: c.isEnabled, + }, + })) + const disable = (config.disable ?? []).map(resolveChainSelector) + + return { poolAddress, updates, disable } +} + +/** Calls setTokenTransferFeeConfig on the appropriate chain-family facade (EVM v2.0+ only), normalizing to `{ hash }`. */ +function setForChain( + chain: Chain, + wallet: unknown, + params: SetTokenTransferFeeConfigParams, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + return mgr.setTokenTransferFeeConfig({ ...params, wallet }) + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doSetFeeConfig(ctx: Ctx, argv: SetFeeConfigArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network!).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const config = await readConfig(argv) + const params = configToParams(argv.poolAddress!, config) + + logger.debug( + `Setting token transfer fee config: ${params.updates.length} update(s), ${params.disable?.length ?? 0} disable(s)`, + ) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await setForChain(chain, wallet, params) + + const output: Record = { + network: networkName, + poolAddress: argv.poolAddress!, + txHash: result.hash, + updatesApplied: String(params.updates.length), + disabled: String(params.disable?.length ?? 0), + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Token transfer fee config updated, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/pool/set-finality-config.ts b/ccip-cli/src/commands/pool/set-finality-config.ts new file mode 100644 index 00000000..be8ad3de --- /dev/null +++ b/ccip-cli/src/commands/pool/set-finality-config.ts @@ -0,0 +1,150 @@ +/** + * Pool set-finality-config subcommand. + * Sets the allowed-finality config on a CCIP token pool (EVM v2.0+ only). + */ + +import { + type SetAllowedFinalityConfigParams, + EVMTokenManager, +} from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { + type Chain, + type EVMChain, + CCIPArgumentInvalidError, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'set-finality-config' +export const describe = 'Set the allowed-finality config on a CCIP token pool (EVM v2.0+ only)' + +/** + * Yargs builder for the pool set-finality-config subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be pool owner)', + }) + .option('pool-address', { + type: 'string', + demandOption: true, + describe: 'Local pool address', + }) + .option('finality', { + type: 'string', + demandOption: true, + describe: + 'Allowed finality: "finalized" (full finality), "safe" (safe head), or a block depth NUMBER [0-65535] for Faster-Than-Finality', + }) + .example([ + [ + 'ccip-cli pool set-finality-config -n sepolia --pool-address 0x... --finality finalized', + 'Require full finality', + ], + [ + 'ccip-cli pool set-finality-config -n sepolia --pool-address 0x... --finality 5', + 'Allow Faster-Than-Finality down to 5 block confirmations', + ], + ]) + +/** + * Handler for the pool set-finality-config subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doSetFinalityConfig(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type SetFinalityConfigArgv = Awaited['argv']> & GlobalOpts + +/** Parses the --finality flag into the SDK finality value. */ +function parseFinality(input: string): SetAllowedFinalityConfigParams['finality'] { + const normalized = input.trim().toLowerCase() + if (normalized === 'finalized') return 'finalized' + if (normalized === 'safe') return 'safe' + const depth = Number(normalized) + if (!Number.isInteger(depth) || depth < 0 || depth > 65535) { + throw new CCIPArgumentInvalidError( + 'finality', + 'must be "finalized", "safe", or a block depth integer between 0 and 65535', + ) + } + return depth +} + +/** Calls setAllowedFinalityConfig on the appropriate chain-family facade (EVM v2.0+ only), normalizing to `{ hash }`. */ +function setForChain( + chain: Chain, + wallet: unknown, + params: SetAllowedFinalityConfigParams, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + return mgr.setAllowedFinalityConfig({ ...params, wallet }) + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doSetFinalityConfig(ctx: Ctx, argv: SetFinalityConfigArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const params: SetAllowedFinalityConfigParams = { + poolAddress: argv.poolAddress, + finality: parseFinality(argv.finality), + } + + logger.debug( + `Setting allowed finality config: pool=${params.poolAddress}, finality=${argv.finality}`, + ) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await setForChain(chain, wallet, params) + + const output: Record = { + network: networkName, + poolAddress: params.poolAddress, + finality: argv.finality, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Allowed finality config updated, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/pool/set-rate-limit-admin.ts b/ccip-cli/src/commands/pool/set-rate-limit-admin.ts new file mode 100644 index 00000000..787eb6a3 --- /dev/null +++ b/ccip-cli/src/commands/pool/set-rate-limit-admin.ts @@ -0,0 +1,142 @@ +/** + * Pool set-rate-limit-admin subcommand. + * Sets the rate limit admin on a CCIP token pool. + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type Chain, + type EVMChain, + type SetRateLimitAdminParams, + type SolanaChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'set-rate-limit-admin' +export const describe = 'Set the rate limit admin on a CCIP token pool' + +/** + * Yargs builder for the pool set-rate-limit-admin subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be pool owner)', + }) + .option('pool-address', { + type: 'string', + demandOption: true, + describe: 'Local pool address', + }) + .option('rate-limit-admin', { + type: 'string', + demandOption: true, + describe: 'Address of the new rate limit admin', + }) + .example([ + [ + 'ccip-cli pool set-rate-limit-admin -n sepolia --pool-address 0x... --rate-limit-admin 0x...', + 'Set the rate limit admin on a pool', + ], + ]) + +/** + * Handler for the pool set-rate-limit-admin subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doSetRateLimitAdmin(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type SetRateLimitAdminArgv = Awaited['argv']> & GlobalOpts + +/** Calls setRateLimitAdmin on the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function setForChain( + chain: Chain, + wallet: unknown, + params: SetRateLimitAdminParams, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + return mgr.setRateLimitAdmin({ ...params, wallet }) + } + case ChainFamily.Solana: { + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + return mgr.setRateLimitAdmin({ ...params, wallet }) + } + case ChainFamily.Aptos: { + // setRateLimitAdmin is unsupported on Aptos — the facade throws at call time (rate limiting is owner-managed). + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + return mgr.setRateLimitAdmin({ ...params, wallet }) + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doSetRateLimitAdmin(ctx: Ctx, argv: SetRateLimitAdminArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const params: SetRateLimitAdminParams = { + poolAddress: argv.poolAddress, + rateLimitAdmin: argv.rateLimitAdmin, + } + + logger.debug( + `Setting rate limit admin: pool=${params.poolAddress}, admin=${params.rateLimitAdmin}`, + ) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await setForChain(chain, wallet, params) + + const output: Record = { + network: networkName, + poolAddress: params.poolAddress, + rateLimitAdmin: params.rateLimitAdmin, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Rate limit admin updated, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/pool/set-rate-limiter-config.ts b/ccip-cli/src/commands/pool/set-rate-limiter-config.ts new file mode 100644 index 00000000..e0e31d94 --- /dev/null +++ b/ccip-cli/src/commands/pool/set-rate-limiter-config.ts @@ -0,0 +1,241 @@ +/** + * Pool set-rate-limiter-config subcommand. + * Updates rate limiter configurations on a CCIP token pool. + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type Chain, + type ChainRateLimiterConfig, + type EVMChain, + type RateLimiterConfig, + type SetChainRateLimiterConfigParams, + type SolanaChain, + CCIPArgumentInvalidError, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'set-rate-limiter-config' +export const describe = 'Update rate limiter configurations on a CCIP token pool' + +// ── Config file schema ── + +interface ConfigFile { + chainConfigs: Array<{ + remoteChainSelector: string + outboundRateLimiterConfig: RateLimiterConfig + inboundRateLimiterConfig: RateLimiterConfig + customBlockConfirmations?: boolean + }> +} + +// ── Generate config template ── + +const CONFIG_TEMPLATE: ConfigFile = { + chainConfigs: [ + { + remoteChainSelector: + '', + outboundRateLimiterConfig: { + isEnabled: true, + capacity: '100000000000000000000000', + rate: '167000000000000000000', + }, + inboundRateLimiterConfig: { + isEnabled: true, + capacity: '100000000000000000000000', + rate: '167000000000000000000', + }, + // customBlockConfirmations: true, + // ^ Faster-Than-Finality (FTF) — set to true to apply these rate limiters + // to the FTF (customBlockConfirmations) path. EVM v2.0+ pools only. + }, + ], +} + +/** + * Yargs builder for the pool set-rate-limiter-config subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be pool owner or rate-limit admin)', + }) + .option('pool-address', { + type: 'string', + describe: 'Local pool address', + }) + .option('config', { + type: 'string', + describe: 'Path to JSON config file with rate limiter configurations', + }) + .option('generate-config', { + type: 'boolean', + describe: 'Output a sample JSON config template to stdout', + }) + .check((argv) => { + if (!argv.generateConfig) { + if (!argv.network) + throw new CCIPArgumentInvalidError('network', 'required argument missing') + if (!argv.poolAddress) + throw new CCIPArgumentInvalidError('pool-address', 'required argument missing') + } + return true + }) + .example([ + [ + 'ccip-cli pool set-rate-limiter-config -n sepolia --pool-address 0x... --config config.json', + 'Set rate limiter config from a config file', + ], + [ + 'ccip-cli pool set-rate-limiter-config --generate-config > config.json', + 'Generate a template config file', + ], + ]) + +/** + * Handler for the pool set-rate-limiter-config subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + // Handle --generate-config + if (argv.generateConfig) { + ctx.output.write(JSON.stringify(CONFIG_TEMPLATE, null, 2)) + destroy() + return + } + + return doSetRateLimiterConfig(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type SetRateLimiterArgv = Awaited['argv']> & GlobalOpts + +/** Reads and parses config from file path or stdin. */ +async function readConfig(argv: SetRateLimiterArgv): Promise { + const { readFileSync } = await import('node:fs') + + if (argv.config) { + const raw = readFileSync(argv.config, 'utf8') + return JSON.parse(raw) as ConfigFile + } + + // Try stdin (piped input) + if (!process.stdin.isTTY) { + const chunks: Buffer[] = [] + for await (const chunk of process.stdin) { + chunks.push(chunk as Buffer) + } + const raw = Buffer.concat(chunks).toString('utf8') + return JSON.parse(raw) as ConfigFile + } + + throw new CCIPArgumentInvalidError( + 'config', + 'No config provided. Use --config or pipe JSON via stdin. Use --generate-config to see the expected format.', + ) +} + +/** + * Resolves a chain identifier (name, chainId, or selector) to a numeric selector string. + */ +function resolveChainSelector(input: string): bigint { + return networkInfo(input).chainSelector +} + +/** Converts a config file to SetChainRateLimiterConfigParams. */ +function configToParams(poolAddress: string, config: ConfigFile): SetChainRateLimiterConfigParams { + const chainConfigs: ChainRateLimiterConfig[] = config.chainConfigs.map((c) => ({ + remoteChainSelector: resolveChainSelector(c.remoteChainSelector), + outboundRateLimiterConfig: c.outboundRateLimiterConfig, + inboundRateLimiterConfig: c.inboundRateLimiterConfig, + customBlockConfirmations: c.customBlockConfirmations, + })) + + return { poolAddress, chainConfigs } +} + +/** Calls setChainRateLimiterConfig on the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function setForChain( + chain: Chain, + wallet: unknown, + params: SetChainRateLimiterConfigParams, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + return mgr.setChainRateLimiterConfig({ ...params, wallet }) + } + case ChainFamily.Solana: { + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + return mgr.setChainRateLimiterConfig({ ...params, wallet }) + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const { hash } = await mgr.setChainRateLimiterConfig({ ...params, wallet }) + return { hash } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doSetRateLimiterConfig(ctx: Ctx, argv: SetRateLimiterArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network!).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const config = await readConfig(argv) + const params = configToParams(argv.poolAddress!, config) + + logger.debug(`Setting rate limiter config: ${params.chainConfigs.length} chain config(s)`) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await setForChain(chain, wallet, params) + + const output: Record = { + network: networkName, + poolAddress: argv.poolAddress!, + txHash: result.hash, + chainsConfigured: String(params.chainConfigs.length), + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Rate limiter config updated, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/pool/transfer-ownership.ts b/ccip-cli/src/commands/pool/transfer-ownership.ts new file mode 100644 index 00000000..fef77ed4 --- /dev/null +++ b/ccip-cli/src/commands/pool/transfer-ownership.ts @@ -0,0 +1,138 @@ +/** + * Pool transfer-ownership subcommand. + * Proposes a new owner for a CCIP token pool (2-step ownership transfer). + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type Chain, + type EVMChain, + type SolanaChain, + type TransferOwnershipParams, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'transfer-ownership' +export const describe = 'Propose a new owner for a CCIP token pool (2-step ownership transfer)' + +/** + * Yargs builder for the pool transfer-ownership subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be current pool owner)', + }) + .option('pool-address', { + type: 'string', + demandOption: true, + describe: 'Pool address', + }) + .option('new-owner', { + type: 'string', + demandOption: true, + describe: 'Address of the proposed new owner', + }) + .example([ + [ + 'ccip-cli pool transfer-ownership -n sepolia --pool-address 0x... --new-owner 0x...', + 'Propose a new pool owner', + ], + ]) + +/** + * Handler for the pool transfer-ownership subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doTransferOwnership(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type TransferOwnershipArgv = Awaited['argv']> & GlobalOpts + +/** Calls transferOwnership on the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function transferForChain( + chain: Chain, + wallet: unknown, + params: TransferOwnershipParams, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const mgr = EVMTokenManager.fromChain(chain as EVMChain) + return mgr.transferOwnership({ ...params, wallet }) + } + case ChainFamily.Solana: { + const mgr = SolanaTokenManager.fromChain(chain as SolanaChain) + return mgr.transferOwnership({ ...params, wallet }) + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const { hash } = await mgr.transferOwnership({ ...params, wallet }) + return { hash } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doTransferOwnership(ctx: Ctx, argv: TransferOwnershipArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const params: TransferOwnershipParams = { + poolAddress: argv.poolAddress, + newOwner: argv.newOwner, + } + + logger.debug(`Transferring ownership: pool=${params.poolAddress}, newOwner=${params.newOwner}`) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await transferForChain(chain, wallet, params) + + const output: Record = { + network: networkName, + poolAddress: params.poolAddress, + newOwner: params.newOwner, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Ownership transfer proposed, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/token-admin.ts b/ccip-cli/src/commands/token-admin.ts new file mode 100644 index 00000000..a7be3b75 --- /dev/null +++ b/ccip-cli/src/commands/token-admin.ts @@ -0,0 +1,24 @@ +/** + * Token admin operations command group. + * Dispatches to subcommands: propose-admin, accept-admin, get-config. + */ + +import type { Argv } from 'yargs' + +export const command = 'token-admin' +export const describe = + 'Token admin operations (propose-admin, accept-admin, transfer-admin, get-config, create-token-alt, set-pool)' + +/** + * Yargs builder for the token-admin command group. + * Loads subcommands from the `token-admin/` directory. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with subcommands. + */ +export const builder = (yargs: Argv) => + yargs + .commandDir('token-admin', { + extensions: [new URL(import.meta.url).pathname.split('.').pop()!], + exclude: /\.test\.[tj]s$/, + }) + .demandCommand(1) diff --git a/ccip-cli/src/commands/token-admin/accept-admin.ts b/ccip-cli/src/commands/token-admin/accept-admin.ts new file mode 100644 index 00000000..ae71f036 --- /dev/null +++ b/ccip-cli/src/commands/token-admin/accept-admin.ts @@ -0,0 +1,154 @@ +/** + * Accept admin subcommand. + * Accepts an administrator role for a token in the TokenAdminRegistry. + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type Chain, + type EVMChain, + type SolanaChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'accept-admin' +export const describe = 'Accept an administrator role for a token in the TokenAdminRegistry' + +/** + * Yargs builder for the accept-admin subcommand. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with command options. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be pending administrator)', + }) + .option('token-address', { + type: 'string', + demandOption: true, + describe: 'Token address to accept admin role for', + }) + .option('router-address', { + type: 'string', + demandOption: true, + describe: + 'CCIP Router address (EVM/Aptos: discovers registry; Solana: router is the registry)', + }) + .example([ + [ + 'ccip-cli token-admin accept-admin -n ethereum-testnet-sepolia --token-address 0xa42B... --router-address 0x0BF3...', + 'Accept admin on Sepolia', + ], + [ + 'ccip-cli token-admin accept-admin -n solana-devnet --wallet ~/.config/solana/id.json --token-address J6fE... --router-address Ccip...', + 'Accept admin on Solana devnet', + ], + [ + 'ccip-cli token-admin accept-admin -n aptos-testnet --token-address 0x89fd... --router-address 0xc748...', + 'Accept admin on Aptos testnet', + ], + ]) + +/** + * Handler for the accept-admin subcommand. + * @param argv - Command line arguments. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doAcceptAdmin(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type AcceptArgv = Awaited['argv']> & GlobalOpts + +/** Accepts admin using the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function acceptAdminForChain( + chain: Chain, + wallet: unknown, + argv: AcceptArgv, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + // EVM resolves the TokenAdminRegistry from `address` (router/pool/registry). + return mgr.acceptAdminRole({ + tokenAddress: argv.tokenAddress, + address: argv.routerAddress, + wallet, + }) + } + case ChainFamily.Solana: { + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + return mgr.acceptAdminRole({ + tokenAddress: argv.tokenAddress, + routerAddress: argv.routerAddress, + wallet, + }) + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const { hash } = await mgr.acceptAdminRole({ + tokenAddress: argv.tokenAddress, + routerAddress: argv.routerAddress, + wallet, + }) + return { hash } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doAcceptAdmin(ctx: Ctx, argv: AcceptArgv) { + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await acceptAdminForChain(chain, wallet, argv) + + const output: Record = { + network: networkName, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Admin accepted, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/token-admin/create-token-alt.ts b/ccip-cli/src/commands/token-admin/create-token-alt.ts new file mode 100644 index 00000000..0022a5f4 --- /dev/null +++ b/ccip-cli/src/commands/token-admin/create-token-alt.ts @@ -0,0 +1,133 @@ +/** + * Token-admin create-token-alt subcommand (Solana only). + * Creates an Address Lookup Table (ALT) with CCIP base addresses for a token's pool. + */ + +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type SolanaChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'create-token-alt' +export const describe = 'Create Address Lookup Table for a token pool (Solana only)' + +/** + * Yargs builder for the token-admin create-token-alt subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., solana-devnet)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key', + }) + .option('token-address', { + type: 'string', + demandOption: true, + describe: 'SPL token mint address (base58)', + }) + .option('pool-address', { + type: 'string', + demandOption: true, + describe: 'Pool state PDA (base58). SDK derives pool program ID from its on-chain owner.', + }) + .option('router-address', { + type: 'string', + demandOption: true, + describe: 'CCIP Router program ID (base58). SDK discovers feeQuoter from config.', + }) + .option('authority', { + type: 'string', + describe: + 'ALT authority (base58). Defaults to wallet. Can differ for multisig setups (e.g., Squads vault).', + }) + .option('additional-addresses', { + type: 'array', + string: true, + describe: + 'Extra addresses for ALT (e.g., SPL Token Multisig address when using multisig mint authority for burn-mint pools)', + }) + .example([ + [ + 'ccip-cli token-admin create-token-alt -n solana-devnet --token-address J6fE... --pool-address 2pGY... --router-address Ccip...', + 'Create ALT with 10 base CCIP addresses', + ], + [ + 'ccip-cli token-admin create-token-alt -n solana-devnet --token-address J6fE... --pool-address 2pGY... --router-address Ccip... --additional-addresses 6c5U...', + 'Create ALT with base addresses + SPL Token Multisig', + ], + ]) + +/** + * Handler for the token-admin create-token-alt subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doCreateTokenAlt(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type CreateTokenAltArgv = Awaited['argv']> & GlobalOpts + +async function doCreateTokenAlt(ctx: Ctx, argv: CreateTokenAltArgv) { + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + if (chain.network.family !== ChainFamily.Solana) { + throw new CCIPChainFamilyUnsupportedError(chain.network.family, { + context: { reason: 'create-token-alt is only supported on Solana' }, + }) + } + + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + + const [, wallet] = await loadChainWallet(chain, argv) + // create+extend by default; `poolAddress` argv maps to the new `poolProgramAddress` param. + const result = await mgr.createLookupTable({ + tokenAddress: argv.tokenAddress, + poolProgramAddress: argv.poolAddress, + ...(argv.authority && { authority: argv.authority }), + ...(argv.additionalAddresses && { additionalAddresses: argv.additionalAddresses }), + wallet, + }) + + const output: Record = { + network: networkName, + lookupTableAddress: result.lookupTableAddress, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('ALT created:', result.lookupTableAddress, 'tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/token-admin/get-config.ts b/ccip-cli/src/commands/token-admin/get-config.ts new file mode 100644 index 00000000..df19115a --- /dev/null +++ b/ccip-cli/src/commands/token-admin/get-config.ts @@ -0,0 +1,140 @@ +/** + * Get config subcommand. + * Queries the TokenAdminRegistry for a token's admin configuration. + */ + +import { + type Chain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'get-config' +export const describe = 'Query token admin configuration from the TokenAdminRegistry' + +/** + * Yargs builder for the get-config subcommand. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with command options. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('token-address', { + type: 'string', + demandOption: true, + describe: 'Token address to query config for', + }) + .option('router-address', { + type: 'string', + demandOption: true, + describe: + 'CCIP Router address (EVM/Aptos: discovers registry; Solana: router is the registry)', + }) + .example([ + [ + 'ccip-cli token-admin get-config -n ethereum-testnet-sepolia --token-address 0xa42B... --router-address 0x0BF3...', + 'Query token admin config on Sepolia', + ], + [ + 'ccip-cli token-admin get-config -n solana-devnet --token-address J6fE... --router-address Ccip...', + 'Query token admin config on Solana devnet', + ], + ]) + +/** + * Handler for the get-config subcommand. + * @param argv - Command line arguments. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doGetConfig(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type GetConfigArgv = Awaited['argv']> & GlobalOpts + +/** Queries the TokenAdminRegistry for a token's config. */ +async function getConfigForChain(chain: Chain, argv: GetConfigArgv) { + switch (chain.network.family) { + case ChainFamily.EVM: + case ChainFamily.Solana: + case ChainFamily.Aptos: { + const registryAddress = await chain.getTokenAdminRegistryFor(argv.routerAddress) + return { + registryAddress, + ...(await chain.getRegistryTokenConfig(registryAddress, argv.tokenAddress)), + } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doGetConfig(ctx: Ctx, argv: GetConfigArgv) { + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const result = await getConfigForChain(chain, argv) + + const output: Record = { + network: networkName, + registryAddress: result.registryAddress, + administrator: result.administrator, + } + if (result.pendingAdministrator) { + output.pendingAdministrator = result.pendingAdministrator + } + if (result.tokenPool) { + output.tokenPool = result.tokenPool + } + if (result.poolLookupTable) { + output.poolLookupTable = result.poolLookupTable + } + + switch (argv.format) { + case Format.json: + ctx.output.write( + JSON.stringify( + result.poolLookupTableEntries + ? { ...output, poolLookupTableEntries: result.poolLookupTableEntries } + : output, + null, + 2, + ), + ) + return + case Format.log: + ctx.output.write('administrator:', result.administrator) + if (result.pendingAdministrator) + ctx.output.write('pendingAdministrator:', result.pendingAdministrator) + if (result.tokenPool) ctx.output.write('tokenPool:', result.tokenPool) + if (output.poolLookupTable) ctx.output.write('poolLookupTable:', output.poolLookupTable) + if (result.poolLookupTableEntries) { + ctx.output.write('poolLookupTableEntries:') + result.poolLookupTableEntries.forEach((entry, i) => ctx.output.write(` [${i}]: ${entry}`)) + } + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/token-admin/propose-admin.ts b/ccip-cli/src/commands/token-admin/propose-admin.ts new file mode 100644 index 00000000..76300e53 --- /dev/null +++ b/ccip-cli/src/commands/token-admin/propose-admin.ts @@ -0,0 +1,184 @@ +/** + * Propose admin subcommand. + * Proposes an administrator for a token in the TokenAdminRegistry. + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { + type EVMRegistrationMethod, + EVMTokenManager, +} from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type Chain, + type EVMChain, + type SolanaChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'propose-admin' +export const describe = 'Propose an administrator for a token in the TokenAdminRegistry' + +/** + * Yargs builder for the propose-admin subcommand. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with command options. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be token owner)', + }) + .option('token-address', { + type: 'string', + demandOption: true, + describe: 'Token address to propose admin for', + }) + // Solana & Aptos only — on EVM the admin is always the caller + .option('administrator', { + type: 'string', + describe: 'Address of the proposed administrator (Solana, Aptos only)', + }) + // EVM-specific + .option('registry-module-address', { + type: 'string', + describe: 'RegistryModuleOwnerCustom address (EVM only, from CCIP API registryModule field)', + }) + .option('registration-method', { + type: 'string', + choices: ['owner', 'get-ccip-admin', 'access-control-default-admin'] as const, + default: 'owner', + describe: 'EVM registration method (EVM only)', + }) + // Solana & Aptos + .option('router-address', { + type: 'string', + describe: 'CCIP Router address (Solana, Aptos)', + }) + .example([ + [ + 'ccip-cli token-admin propose-admin -n ethereum-testnet-sepolia --token-address 0xa42B... --registry-module-address 0xa3c7...', + 'Propose admin on Sepolia (owner method, default)', + ], + [ + 'ccip-cli token-admin propose-admin -n ethereum-testnet-sepolia --token-address 0xa42B... --registry-module-address 0xa3c7... --registration-method get-ccip-admin', + 'Propose admin via getCCIPAdmin method', + ], + [ + 'ccip-cli token-admin propose-admin -n solana-devnet --wallet ~/.config/solana/id.json --token-address J6fE... --administrator 5YNm... --router-address Ccip...', + 'Propose admin on Solana devnet', + ], + [ + 'ccip-cli token-admin propose-admin -n aptos-testnet --token-address 0x89fd... --administrator 0x0650... --router-address 0xc748...', + 'Propose admin on Aptos testnet', + ], + ]) + +/** + * Handler for the propose-admin subcommand. + * @param argv - Command line arguments. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doProposeAdmin(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type ProposeArgv = Awaited['argv']> & GlobalOpts + +/** Proposes an admin using the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function proposeAdminForChain( + chain: Chain, + wallet: unknown, + argv: ProposeArgv, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + // Map CLI kebab-case to SDK type + const cliToSdk: Record = { + owner: 'owner', + 'get-ccip-admin': 'getCCIPAdmin', + 'access-control-default-admin': 'accessControlDefaultAdmin', + } + return mgr.proposeAdminRole({ + tokenAddress: argv.tokenAddress, + registryModuleAddress: argv.registryModuleAddress!, + registrationMethod: cliToSdk[argv.registrationMethod], + wallet, + }) + } + case ChainFamily.Solana: { + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + return mgr.proposeAdminRole({ + tokenAddress: argv.tokenAddress, + administrator: argv.administrator!, + routerAddress: argv.routerAddress!, + wallet, + }) + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const { hash } = await mgr.proposeAdminRole({ + tokenAddress: argv.tokenAddress, + administrator: argv.administrator!, + routerAddress: argv.routerAddress!, + wallet, + }) + return { hash } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doProposeAdmin(ctx: Ctx, argv: ProposeArgv) { + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await proposeAdminForChain(chain, wallet, argv) + + const output: Record = { + network: networkName, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Admin proposed, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/token-admin/set-pool.ts b/ccip-cli/src/commands/token-admin/set-pool.ts new file mode 100644 index 00000000..ea1d45c7 --- /dev/null +++ b/ccip-cli/src/commands/token-admin/set-pool.ts @@ -0,0 +1,173 @@ +/** + * Set pool subcommand. + * Registers a pool in the TokenAdminRegistry for a token. + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type Chain, + type EVMChain, + type SolanaChain, + CCIPArgumentInvalidError, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'set-pool' +export const describe = 'Register a pool in the TokenAdminRegistry for a token' + +/** + * Yargs builder for the set-pool subcommand. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with command options. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be token administrator)', + }) + .option('token-address', { + type: 'string', + demandOption: true, + describe: 'Token address to register pool for', + }) + .option('pool-address', { + type: 'string', + demandOption: true, + describe: 'Pool address to register', + }) + .option('router-address', { + type: 'string', + demandOption: true, + describe: + 'CCIP Router address (EVM/Aptos: discovers registry; Solana: router is the registry)', + }) + .option('pool-lookup-table', { + type: 'string', + describe: 'Address Lookup Table (Solana only, required)', + }) + .example([ + [ + 'ccip-cli token-admin set-pool -n ethereum-testnet-sepolia --token-address 0xa42B... --pool-address 0xd7BF... --router-address 0x0BF3...', + 'Set pool on Sepolia', + ], + [ + 'ccip-cli token-admin set-pool -n solana-devnet --wallet ~/.config/solana/id.json --token-address J6fE... --pool-address 99Ux... --router-address Ccip... --pool-lookup-table C6jB...', + 'Set pool on Solana devnet', + ], + [ + 'ccip-cli token-admin set-pool -n aptos-testnet --token-address 0x89fd... --pool-address 0xeb63... --router-address 0xc748...', + 'Set pool on Aptos testnet', + ], + ]) + +/** + * Handler for the set-pool subcommand. + * @param argv - Command line arguments. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doSetPool(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type SetPoolArgv = Awaited['argv']> & GlobalOpts + +/** Sets pool using the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function setPoolForChain( + chain: Chain, + wallet: unknown, + argv: SetPoolArgv, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + // EVM resolves the TokenAdminRegistry from `address` (router/pool/registry). + return mgr.setPool({ + tokenAddress: argv.tokenAddress, + poolAddress: argv.poolAddress, + address: argv.routerAddress, + wallet, + }) + } + case ChainFamily.Solana: { + const solanaChain = chain as SolanaChain + if (!argv.poolLookupTable) { + throw new CCIPArgumentInvalidError('pool-lookup-table', 'required for Solana') + } + const mgr = SolanaTokenManager.fromChain(solanaChain) + // Solana derives the pool from its lookup table; `address` is the router (the registry). + return mgr.setPool({ + tokenAddress: argv.tokenAddress, + address: argv.routerAddress, + poolLookupTableAddress: argv.poolLookupTable, + wallet, + }) + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const { hash } = await mgr.setPool({ + tokenAddress: argv.tokenAddress, + poolAddress: argv.poolAddress, + routerAddress: argv.routerAddress, + wallet, + }) + return { hash } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doSetPool(ctx: Ctx, argv: SetPoolArgv) { + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await setPoolForChain(chain, wallet, argv) + + const output: Record = { + network: networkName, + tokenAddress: argv.tokenAddress, + poolAddress: argv.poolAddress, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Pool registered, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/token-admin/transfer-admin.ts b/ccip-cli/src/commands/token-admin/transfer-admin.ts new file mode 100644 index 00000000..fa743fec --- /dev/null +++ b/ccip-cli/src/commands/token-admin/transfer-admin.ts @@ -0,0 +1,163 @@ +/** + * Transfer admin subcommand. + * Transfers the administrator role for a token in the TokenAdminRegistry to a new address. + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type Chain, + type EVMChain, + type SolanaChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'transfer-admin' +export const describe = + 'Transfer the administrator role for a token in the TokenAdminRegistry to a new address' + +/** + * Yargs builder for the transfer-admin subcommand. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with command options. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be current administrator)', + }) + .option('token-address', { + type: 'string', + demandOption: true, + describe: 'Token address to transfer admin role for', + }) + .option('new-admin', { + type: 'string', + demandOption: true, + describe: 'Address of the new administrator', + }) + .option('router-address', { + type: 'string', + demandOption: true, + describe: + 'CCIP Router address (EVM/Aptos: discovers registry; Solana: router is the registry)', + }) + .example([ + [ + 'ccip-cli token-admin transfer-admin -n ethereum-testnet-sepolia --token-address 0xa42B... --new-admin 0x1234... --router-address 0x0BF3...', + 'Transfer admin on Sepolia', + ], + [ + 'ccip-cli token-admin transfer-admin -n solana-devnet --wallet ~/.config/solana/id.json --token-address J6fE... --new-admin 5y76... --router-address Ccip...', + 'Transfer admin on Solana devnet', + ], + [ + 'ccip-cli token-admin transfer-admin -n aptos-testnet --token-address 0x89fd... --new-admin 0xabe0... --router-address 0xc748...', + 'Transfer admin on Aptos testnet', + ], + ]) + +/** + * Handler for the transfer-admin subcommand. + * @param argv - Command line arguments. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doTransferAdmin(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type TransferArgv = Awaited['argv']> & GlobalOpts + +/** Transfers admin using the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function transferAdminForChain( + chain: Chain, + wallet: unknown, + argv: TransferArgv, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + // EVM resolves the TokenAdminRegistry from `address` (router/pool/registry). + return mgr.transferAdminRole({ + tokenAddress: argv.tokenAddress, + newAdmin: argv.newAdmin, + address: argv.routerAddress, + wallet, + }) + } + case ChainFamily.Solana: { + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + return mgr.transferAdminRole({ + tokenAddress: argv.tokenAddress, + newAdmin: argv.newAdmin, + routerAddress: argv.routerAddress, + wallet, + }) + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const { hash } = await mgr.transferAdminRole({ + tokenAddress: argv.tokenAddress, + newAdmin: argv.newAdmin, + routerAddress: argv.routerAddress, + wallet, + }) + return { hash } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doTransferAdmin(ctx: Ctx, argv: TransferArgv) { + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await transferAdminForChain(chain, wallet, argv) + + const output: Record = { + network: networkName, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Admin transferred, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/token.ts b/ccip-cli/src/commands/token.ts index 7a7f65ff..34af0a76 100644 --- a/ccip-cli/src/commands/token.ts +++ b/ccip-cli/src/commands/token.ts @@ -1,131 +1,24 @@ /** - * Token balance query command. - * Queries native or token balance for an address. + * Token operations command group. + * Dispatches to subcommands: balance (default), deploy. */ -import { type ChainStatic, jsonStringify, networkInfo } from '@chainlink/ccip-sdk/src/index.ts' -import { formatUnits } from 'ethers' import type { Argv } from 'yargs' -import { type Ctx, Format } from './types.ts' -import { getCtx, logParsedError, prettyTable } from './utils.ts' -import type { GlobalOpts } from '../index.ts' -import { fetchChainsFromRpcs } from '../providers/index.ts' - export const command = 'token' -export const describe = 'Query token balance for an address' +export const describe = + 'Token operations (balance, deploy, create-multisig, transfer-mint-authority, grant-mint-burn-access, revoke-mint-burn-access, get-mint-burn-info)' /** - * Yargs builder for the token command. + * Yargs builder for the token command group. + * Loads subcommands from the `token/` directory. * @param yargs - Yargs instance. - * @returns Configured yargs instance with command options. + * @returns Configured yargs instance with subcommands. */ export const builder = (yargs: Argv) => yargs - .option('network', { - alias: 'n', - type: 'string', - demandOption: true, - describe: 'Network: chainId or name (e.g., ethereum-mainnet, solana-devnet)', - }) - .option('holder', { - alias: 'H', - type: 'string', - demandOption: true, - describe: 'Wallet address to query balance for', - }) - .option('token', { - alias: 't', - type: 'string', - demandOption: false, - describe: 'Token address (omit for native token balance)', - }) - .example([ - ['ccip-cli token -n ethereum-mainnet -H 0x1234...abcd', 'Query native ETH balance'], - [ - 'ccip-cli token -n ethereum-mainnet -H 0x1234... -t 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - 'Query USDC token balance', - ], - [ - 'ccip-cli token -n solana-devnet -H EPUjBP3Xf76K1VKsDSc6GupBWE8uykNksCLJgXZn87CB', - 'Query native SOL balance', - ], - ]) - -/** - * Handler for the token command. - * @param argv - Command line arguments. - */ -export async function handler(argv: Awaited['argv']> & GlobalOpts) { - const [ctx, destroy] = getCtx(argv) - return queryTokenBalance(ctx, argv) - .catch((err) => { - process.exitCode = 1 - if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + .commandDir('token', { + extensions: [new URL(import.meta.url).pathname.split('.').pop()!], + exclude: /\.test\.[tj]s$/, }) - .finally(destroy) -} - -async function queryTokenBalance(ctx: Ctx, argv: Parameters[0]) { - const { output } = ctx - const networkName = networkInfo(argv.network).name - const getChain = fetchChainsFromRpcs(ctx, argv) - const chain = await getChain(networkName) - - const balance = await chain.getBalance({ - holder: argv.holder, - token: argv.token, - }) - - // Get token info for formatting (only for tokens, not native) - let tokenInfo - if (argv.token) { - argv.token = (chain.constructor as ChainStatic).getAddress(argv.token) - tokenInfo = await chain.getTokenInfo(argv.token) - } - - const tokenLabel = tokenInfo?.symbol ?? 'native' - const formatted = formatUnits( - balance, - tokenInfo ? tokenInfo.decimals : (chain.constructor as ChainStatic).decimals, - ) - - switch (argv.format) { - case Format.json: - output.write( - jsonStringify( - { - network: networkName, - holder: argv.holder, - token: tokenLabel, - balance: balance.toString(), - formatted, - ...tokenInfo, - }, - 2, - ), - ) - return - case Format.log: - output.write( - `Balance of`, - tokenInfo ? argv.token : tokenLabel, - ':', - balance, - `=`, - tokenInfo ? `${formatted} ${tokenLabel}` : formatted, - ) - return - case Format.pretty: - default: - prettyTable.call(ctx, { - network: networkName, - holder: argv.holder, - token: argv.token ?? tokenLabel, - balance, - formatted, - ...tokenInfo, - }) - return - } -} + .demandCommand(0) diff --git a/ccip-cli/src/commands/token/balance.ts b/ccip-cli/src/commands/token/balance.ts new file mode 100644 index 00000000..545c929c --- /dev/null +++ b/ccip-cli/src/commands/token/balance.ts @@ -0,0 +1,131 @@ +/** + * Token balance query subcommand (default for `ccip-cli token`). + * Queries native or token balance for an address. + */ + +import { type ChainStatic, jsonStringify, networkInfo } from '@chainlink/ccip-sdk/src/index.ts' +import { formatUnits } from 'ethers' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = '$0' +export const describe = 'Query token balance for an address' + +/** + * Yargs builder for the token balance subcommand. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with command options. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-mainnet, solana-devnet)', + }) + .option('holder', { + alias: 'H', + type: 'string', + demandOption: true, + describe: 'Wallet address to query balance for', + }) + .option('token', { + alias: 't', + type: 'string', + demandOption: false, + describe: 'Token address (omit for native token balance)', + }) + .example([ + ['ccip-cli token -n ethereum-mainnet -H 0x1234...abcd', 'Query native ETH balance'], + [ + 'ccip-cli token -n ethereum-mainnet -H 0x1234... -t 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + 'Query USDC token balance', + ], + [ + 'ccip-cli token -n solana-devnet -H EPUjBP3Xf76K1VKsDSc6GupBWE8uykNksCLJgXZn87CB', + 'Query native SOL balance', + ], + ]) + +/** + * Handler for the token balance subcommand. + * @param argv - Command line arguments. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return queryTokenBalance(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +async function queryTokenBalance(ctx: Ctx, argv: Parameters[0]) { + const { output } = ctx + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const balance = await chain.getBalance({ + holder: argv.holder, + token: argv.token, + }) + + // Get token info for formatting (only for tokens, not native) + let tokenInfo + if (argv.token) { + argv.token = (chain.constructor as ChainStatic).getAddress(argv.token) + tokenInfo = await chain.getTokenInfo(argv.token) + } + + const tokenLabel = tokenInfo?.symbol ?? 'native' + const formatted = formatUnits( + balance, + tokenInfo ? tokenInfo.decimals : (chain.constructor as ChainStatic).decimals, + ) + + switch (argv.format) { + case Format.json: + output.write( + jsonStringify( + { + network: networkName, + holder: argv.holder, + token: tokenLabel, + balance: balance.toString(), + formatted, + ...tokenInfo, + }, + 2, + ), + ) + return + case Format.log: + output.write( + `Balance of`, + tokenInfo ? argv.token : tokenLabel, + ':', + balance, + `=`, + tokenInfo ? `${formatted} ${tokenLabel}` : formatted, + ) + return + case Format.pretty: + default: + prettyTable.call(ctx, { + network: networkName, + holder: argv.holder, + token: argv.token ?? tokenLabel, + balance, + formatted, + ...tokenInfo, + }) + return + } +} diff --git a/ccip-cli/src/commands/token/create-multisig.ts b/ccip-cli/src/commands/token/create-multisig.ts new file mode 100644 index 00000000..d7c3e503 --- /dev/null +++ b/ccip-cli/src/commands/token/create-multisig.ts @@ -0,0 +1,131 @@ +/** + * Pool create-multisig subcommand (Solana only). + * Creates an SPL Token native multisig with the Pool Signer PDA auto-included. + */ + +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type SolanaChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'create-multisig' +export const describe = 'Create SPL Token multisig with Pool Signer PDA (Solana only)' + +/** + * Yargs builder for the token create-multisig subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., solana-devnet)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key', + }) + .option('mint', { + alias: 'token-address', + type: 'string', + demandOption: true, + describe: 'SPL token mint address (base58)', + }) + .option('pool-program-id', { + type: 'string', + demandOption: true, + describe: 'Pool program ID for PDA derivation', + }) + .option('additional-signers', { + type: 'array', + string: true, + demandOption: true, + describe: 'Additional signer pubkeys (Pool Signer PDA is auto-included)', + }) + .option('threshold', { + type: 'number', + demandOption: true, + describe: 'Required number of signers (m-of-n)', + }) + .option('seed', { + type: 'string', + describe: 'Optional seed for deterministic multisig address derivation', + }) + .example([ + [ + 'ccip-cli token create-multisig -n solana-devnet --mint J6fE... --pool-program-id 41FG... --additional-signers 59eN... --threshold 1', + 'Create multisig with Pool Signer PDA + one additional signer, threshold 1', + ], + ]) + +/** + * Handler for the token create-multisig subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doCreateMultisig(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type CreateMultisigArgv = Awaited['argv']> & GlobalOpts + +async function doCreateMultisig(ctx: Ctx, argv: CreateMultisigArgv) { + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + if (chain.network.family !== ChainFamily.Solana) { + throw new CCIPChainFamilyUnsupportedError(chain.network.family, { + context: { reason: 'create-multisig is only supported on Solana' }, + }) + } + + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await mgr.createPoolMintAuthorityMultisig({ + mint: argv.mint, + poolProgramId: argv.poolProgramId, + additionalSigners: argv.additionalSigners, + threshold: argv.threshold, + ...(argv.seed && { seed: argv.seed }), + wallet, + }) + + const output: Record = { + network: networkName, + multisigAddress: result.multisigAddress, + poolSignerPda: result.poolSignerPda, + allSigners: result.allSigners, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Multisig created:', result.multisigAddress, 'tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, { ...output, allSigners: result.allSigners.join(', ') }) + return + } +} diff --git a/ccip-cli/src/commands/token/deploy.test.ts b/ccip-cli/src/commands/token/deploy.test.ts new file mode 100644 index 00000000..0ef62e53 --- /dev/null +++ b/ccip-cli/src/commands/token/deploy.test.ts @@ -0,0 +1,50 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import * as balance from './balance.ts' +import * as deploy from './deploy.ts' + +// ============================================================================= +// Module shape +// ============================================================================= + +describe('token deploy — module shape', () => { + it('should export command as "deploy"', () => { + assert.equal(deploy.command, 'deploy') + }) + + it('should export a describe string', () => { + assert.equal(typeof deploy.describe, 'string') + assert.ok(deploy.describe.length > 0) + }) + + it('should export a builder function', () => { + assert.equal(typeof deploy.builder, 'function') + }) + + it('should export a handler function', () => { + assert.equal(typeof deploy.handler, 'function') + }) +}) + +// ============================================================================= +// token balance — module shape (backward compat) +// ============================================================================= + +describe('token balance — module shape', () => { + it('should export command as "$0" (default subcommand)', () => { + assert.equal(balance.command, '$0') + }) + + it('should export a describe string', () => { + assert.equal(typeof balance.describe, 'string') + }) + + it('should export a builder function', () => { + assert.equal(typeof balance.builder, 'function') + }) + + it('should export a handler function', () => { + assert.equal(typeof balance.handler, 'function') + }) +}) diff --git a/ccip-cli/src/commands/token/deploy.ts b/ccip-cli/src/commands/token/deploy.ts new file mode 100644 index 00000000..e6b675c1 --- /dev/null +++ b/ccip-cli/src/commands/token/deploy.ts @@ -0,0 +1,280 @@ +/** + * Token deploy subcommand. + * Deploys a new CCIP-compatible token (CrossChainToken / SPL mint / managed_token). + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { type DeployVerification, EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type Chain, + type EVMChain, + type SolanaChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import { parseUnits } from 'ethers' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' +import { runVerification } from '../verify-utils.ts' + +export const command = 'deploy' +export const describe = 'Deploy a new CCIP-compatible token' + +/** + * Yargs builder for the token deploy subcommand. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with command options. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia, solana-devnet)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key', + }) + .option('name', { + type: 'string', + demandOption: true, + describe: 'Token name', + }) + .option('symbol', { + type: 'string', + demandOption: true, + describe: 'Token symbol', + }) + .option('decimals', { + type: 'number', + demandOption: true, + describe: 'Token decimals', + }) + .option('max-supply', { + type: 'string', + describe: 'Max supply in whole units (omit for unlimited; Solana: must fit in u64)', + }) + .option('initial-supply', { + type: 'string', + default: '0', + describe: 'Initial supply in whole units (Solana: must fit in u64)', + }) + // EVM-specific (CrossChainToken v2.0) + .option('owner', { + type: 'string', + describe: 'Owner address (2-step admin); defaults to signer. EVM only', + }) + .option('ccip-admin', { + type: 'string', + describe: 'CCIP admin (getCCIPAdmin); defaults to owner/signer. EVM only', + }) + .option('burn-mint-role-admin', { + type: 'string', + describe: 'Admin allowed to grant/revoke MINTER/BURNER roles; defaults to owner. EVM only', + }) + .option('pre-mint-recipient', { + type: 'string', + describe: 'Recipient of the initial-supply pre-mint; defaults to owner. EVM only', + }) + // Solana-specific + .option('token-program', { + type: 'string', + choices: ['spl-token', 'token-2022'] as const, + default: 'spl-token', + describe: 'Solana token program (Solana only)', + }) + .option('metadata-uri', { + type: 'string', + describe: 'Metaplex metadata JSON URI (Solana only)', + }) + // Aptos-specific + .option('icon', { + type: 'string', + describe: 'Token icon URI (Aptos only)', + }) + .option('project', { + type: 'string', + describe: 'Project URL (Aptos only)', + }) + // Verification (EVM only) + .option('verify', { + type: 'boolean', + default: false, + describe: 'Verify the deployed contract on the source-chain explorer (EVM only)', + }) + .option('etherscan-api-key', { + type: 'string', + describe: 'Etherscan V2 API key for --verify (defaults to ETHERSCAN_API_KEY env)', + }) + .example([ + [ + 'ccip-cli token deploy -n ethereum-testnet-sepolia --name "My Token" --symbol MTK --decimals 18', + 'Deploy ERC20 on Sepolia', + ], + [ + 'ccip-cli token deploy -n solana-devnet --name "My Token" --symbol MTK --decimals 9', + 'Deploy SPL token on Solana devnet', + ], + [ + 'ccip-cli token deploy -n aptos-testnet --name "My Token" --symbol MTK --decimals 8', + 'Deploy managed_token on Aptos testnet', + ], + ]) + +/** + * Handler for the token deploy subcommand. + * @param argv - Command line arguments. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doDeployToken(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type DeployArgv = Awaited['argv']> & GlobalOpts + +/** Normalized deploy outcome across chain families. `tokenAddress` is absent on Solana. */ +type DeployOutcome = { + hash: string + tokenAddress?: string + codeObjectAddress?: string + metadataAddress?: string + /** EVM-only block-explorer verification handle for `--verify`. */ + verification?: DeployVerification +} + +/** Deploys via the appropriate chain-family facade, normalizing to a common outcome. */ +async function deployForChain( + chain: Chain, + wallet: unknown, + argv: DeployArgv, + maxSupply: bigint | undefined, + initialSupply: bigint | undefined, +): Promise { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + const result = await mgr.deployToken({ + name: argv.name, + symbol: argv.symbol, + decimals: argv.decimals, + ...(maxSupply !== undefined && { maxSupply }), + ...(initialSupply !== undefined && { initialSupply }), + ...(argv.owner && { ownerAddress: argv.owner }), + ...(argv.ccipAdmin && { ccipAdmin: argv.ccipAdmin }), + ...(argv.burnMintRoleAdmin && { burnMintRoleAdmin: argv.burnMintRoleAdmin }), + ...(argv.preMintRecipient && { preMintRecipient: argv.preMintRecipient }), + wallet, + }) + return { + hash: result.hash, + tokenAddress: result.tokenAddress, + verification: result.verification, + } + } + case ChainFamily.Solana: { + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + const result = await mgr.deployToken({ + decimals: argv.decimals, + tokenProgram: argv.tokenProgram as 'spl-token' | 'token-2022', + withMetaplex: true, + name: argv.name, + symbol: argv.symbol, + ...(argv.metadataUri && { uri: argv.metadataUri }), + ...(initialSupply !== undefined && { initialSupply }), + wallet, + }) + return { hash: result.hash, tokenAddress: result.tokenAddress } + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const result = await mgr.deployToken({ + name: argv.name, + symbol: argv.symbol, + decimals: argv.decimals, + maxSupply, + initialSupply, + ...(argv.icon && { icon: argv.icon }), + ...(argv.project && { project: argv.project }), + wallet, + }) + return { + hash: result.hash, + tokenAddress: result.tokenAddress, + ...(result.codeObjectAddress && { codeObjectAddress: result.codeObjectAddress }), + } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doDeployToken(ctx: Ctx, argv: DeployArgv) { + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const [, wallet] = await loadChainWallet(chain, argv) + + const maxSupply = argv.maxSupply ? parseUnits(argv.maxSupply, argv.decimals) : undefined + const initialSupply = + argv.initialSupply !== '0' ? parseUnits(argv.initialSupply, argv.decimals) : undefined + + const result = await deployForChain(chain, wallet, argv, maxSupply, initialSupply) + + // Build output object, including chain-specific optional fields when present + const output: Record = { + network: networkName, + txHash: result.hash, + } + if (result.tokenAddress) output.tokenAddress = result.tokenAddress + if (result.codeObjectAddress) output.codeObjectAddress = result.codeObjectAddress + if (result.metadataAddress) output.metadataAddress = result.metadataAddress + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + break + case Format.log: + if (result.tokenAddress) { + ctx.output.write('Token deployed:', result.tokenAddress, 'tx:', result.hash) + } else { + ctx.output.write('Token deployed, tx:', result.hash) + } + if (result.codeObjectAddress) ctx.output.write('Code object:', result.codeObjectAddress) + if (result.metadataAddress) ctx.output.write('Metadata:', result.metadataAddress) + break + case Format.pretty: + default: + prettyTable.call(ctx, output) + break + } + + // Contract verification is EVM-only; the cct facade returns the verification handle + // (contract key + encoded constructor args) on the deploy result. + if (argv.verify && result.verification && result.tokenAddress) { + await runVerification( + ctx, + networkName, + [{ ...result.verification, address: result.tokenAddress }], + { etherscanApiKey: argv.etherscanApiKey }, + ) + } +} diff --git a/ccip-cli/src/commands/token/get-mint-burn-info.ts b/ccip-cli/src/commands/token/get-mint-burn-info.ts new file mode 100644 index 00000000..8934bd44 --- /dev/null +++ b/ccip-cli/src/commands/token/get-mint-burn-info.ts @@ -0,0 +1,270 @@ +/** + * Token get-mint-burn-info subcommand. + * Read-only command that shows mint/burn role holders on a token. + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type EVMChain, + type SolanaChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'get-mint-burn-info' +export const describe = 'Show mint/burn role holders on a token (read-only)' + +/** + * Yargs builder for the token get-mint-burn-info subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia, solana-devnet)', + }) + .option('token-address', { + type: 'string', + demandOption: true, + describe: 'Token address (EVM contract, Solana mint, Aptos FA metadata)', + }) + .example([ + [ + 'ccip-cli token get-mint-burn-info -n sepolia --token-address 0x...', + 'Show minters and burners on an EVM BurnMintERC20', + ], + [ + 'ccip-cli token get-mint-burn-info -n solana-devnet --token-address J6fE...', + 'Show Solana mint authority and multisig members', + ], + [ + 'ccip-cli token get-mint-burn-info -n aptos-testnet --token-address 0x...', + 'Show Aptos managed/regulated token roles', + ], + ]) + +/** + * Handler for the token get-mint-burn-info subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doGetMintBurnInfo(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type GetMintBurnInfoArgv = Awaited['argv']> & GlobalOpts + +async function doGetMintBurnInfo(ctx: Ctx, argv: GetMintBurnInfoArgv) { + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + const tokenAddress = argv.tokenAddress + + switch (chain.network.family) { + case ChainFamily.EVM: + return handleEVM(ctx, chain as EVMChain, networkName, tokenAddress, argv) + case ChainFamily.Solana: + return handleSolana(ctx, chain as SolanaChain, networkName, tokenAddress, argv) + case ChainFamily.Aptos: + return handleAptos(ctx, chain as AptosChain, networkName, tokenAddress, argv) + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function handleEVM( + ctx: Ctx, + chain: EVMChain, + networkName: string, + tokenAddress: string, + argv: GetMintBurnInfoArgv, +) { + const mgr = EVMTokenManager.fromChain(chain) + const result = await mgr.getMintBurnRoles(tokenAddress) + + const output = { + network: networkName, + tokenAddress, + minters: result.minters, + burners: result.burners, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Token:', tokenAddress) + ctx.output.write('Minters:', result.minters.length ? result.minters.join(', ') : '(none)') + ctx.output.write('Burners:', result.burners.length ? result.burners.join(', ') : '(none)') + return + case Format.pretty: + default: + prettyTable.call(ctx, { + network: networkName, + tokenAddress, + ['minters (' + result.minters.length + ')']: result.minters.length + ? result.minters.join('\n') + : '(none)', + ['burners (' + result.burners.length + ')']: result.burners.length + ? result.burners.join('\n') + : '(none)', + }) + return + } +} + +async function handleSolana( + ctx: Ctx, + chain: SolanaChain, + networkName: string, + tokenAddress: string, + argv: GetMintBurnInfoArgv, +) { + const mgr = SolanaTokenManager.fromChain(chain) + const result = await mgr.getMintBurnRoles(tokenAddress) + + const output: Record = { + network: networkName, + tokenAddress, + mintAuthority: result.mintAuthority ?? '(disabled)', + isMultisig: result.isMultisig, + } + + if (result.isMultisig) { + output.multisigThreshold = `${result.multisigThreshold}-of-${result.multisigMembers!.length}` + output.multisigMembers = result.multisigMembers + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Token:', tokenAddress) + ctx.output.write('Mint Authority:', result.mintAuthority ?? '(disabled)') + if (result.isMultisig) { + ctx.output.write( + 'Multisig:', + `${result.multisigThreshold}-of-${result.multisigMembers!.length}`, + ) + for (const member of result.multisigMembers!) { + ctx.output.write(' -', member.address) + } + } + return + case Format.pretty: + default: { + const table: Record = { + network: networkName, + tokenAddress, + mintAuthority: result.mintAuthority ?? '(disabled)', + } + if (result.isMultisig) { + table.multisigThreshold = `${result.multisigThreshold}-of-${result.multisigMembers!.length}` + for (let i = 0; i < result.multisigMembers!.length; i++) { + table[`member[${i}]`] = result.multisigMembers![i]!.address + } + } + prettyTable.call(ctx, table) + return + } + } +} + +async function handleAptos( + ctx: Ctx, + chain: AptosChain, + networkName: string, + tokenAddress: string, + argv: GetMintBurnInfoArgv, +) { + const mgr = AptosTokenManager.fromChain(chain) + const result = await mgr.getMintBurnRoles(tokenAddress) + + const output: Record = { + network: networkName, + tokenAddress, + tokenModule: result.tokenModule, + } + + if (result.owner) output.owner = result.owner + if (result.allowedMinters) output.allowedMinters = result.allowedMinters + if (result.allowedBurners) output.allowedBurners = result.allowedBurners + if (result.bridgeMintersOrBurners) output.bridgeMintersOrBurners = result.bridgeMintersOrBurners + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Token:', tokenAddress) + ctx.output.write('Module:', result.tokenModule) + if (result.owner) ctx.output.write('Owner:', result.owner) + if (result.allowedMinters) { + ctx.output.write( + 'Minters:', + result.allowedMinters.length ? result.allowedMinters.join(', ') : '(none)', + ) + } + if (result.allowedBurners) { + ctx.output.write( + 'Burners:', + result.allowedBurners.length ? result.allowedBurners.join(', ') : '(none)', + ) + } + if (result.bridgeMintersOrBurners) { + ctx.output.write( + 'Bridge Minters/Burners:', + result.bridgeMintersOrBurners.length + ? result.bridgeMintersOrBurners.join(', ') + : '(none)', + ) + } + return + case Format.pretty: + default: { + const table: Record = { + network: networkName, + tokenAddress, + tokenModule: result.tokenModule, + } + if (result.owner) table.owner = result.owner + if (result.allowedMinters) { + table['minters (' + result.allowedMinters.length + ')'] = result.allowedMinters.length + ? result.allowedMinters.join('\n') + : '(none)' + } + if (result.allowedBurners) { + table['burners (' + result.allowedBurners.length + ')'] = result.allowedBurners.length + ? result.allowedBurners.join('\n') + : '(none)' + } + if (result.bridgeMintersOrBurners) { + table['bridge minters/burners (' + result.bridgeMintersOrBurners.length + ')'] = result + .bridgeMintersOrBurners.length + ? result.bridgeMintersOrBurners.join('\n') + : '(none)' + } + prettyTable.call(ctx, table) + return + } + } +} diff --git a/ccip-cli/src/commands/token/grant-mint-burn-access.ts b/ccip-cli/src/commands/token/grant-mint-burn-access.ts new file mode 100644 index 00000000..6f1c0e02 --- /dev/null +++ b/ccip-cli/src/commands/token/grant-mint-burn-access.ts @@ -0,0 +1,167 @@ +/** + * Token grant-mint-burn-access subcommand. + * Grants mint and burn permissions on a token to a pool or address. + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type Chain, + type EVMChain, + type SolanaChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'grant-mint-burn-access' +export const describe = 'Grant mint and burn permissions on a token to a pool or address' + +/** + * Yargs builder for the token grant-mint-burn-access subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia, solana-devnet)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be token owner/authority)', + }) + .option('token-address', { + type: 'string', + demandOption: true, + describe: 'Token address (EVM contract, Solana mint, Aptos FA metadata)', + }) + .option('authority', { + type: 'string', + demandOption: true, + describe: 'Address to grant mint/burn access to (pool, multisig, etc.)', + }) + .option('role', { + type: 'string', + choices: ['mint', 'burn', 'mintAndBurn'] as const, + default: 'mintAndBurn' as const, + describe: 'Which role(s) to grant (default: mintAndBurn)', + }) + .option('token-type', { + type: 'string', + choices: ['burnMintERC20', 'factoryBurnMintERC20'] as const, + default: 'burnMintERC20', + describe: 'EVM token type — controls grant ABI (EVM only)', + }) + .example([ + [ + 'ccip-cli token grant-mint-burn-access -n sepolia --token-address 0x... --authority 0x...', + 'Grant pool mint/burn roles on EVM', + ], + [ + 'ccip-cli token grant-mint-burn-access -n solana-devnet --token-address J6fE... --authority 2e8X...', + 'Transfer Solana mint authority to multisig', + ], + ]) + +/** + * Handler for the token grant-mint-burn-access subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doGrantMintBurnAccess(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type GrantMintBurnAccessArgv = Awaited['argv']> & GlobalOpts + +/** Grants mint/burn access using the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function grantForChain( + chain: Chain, + wallet: unknown, + argv: GrantMintBurnAccessArgv, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + return mgr.grantMintBurnAccess({ + tokenAddress: argv.tokenAddress, + authority: argv.authority, + role: argv.role, + wallet, + }) + } + case ChainFamily.Solana: { + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + return mgr.grantMintBurnAccess({ + tokenAddress: argv.tokenAddress, + authority: argv.authority, + role: argv.role, + wallet, + }) + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const { hash } = await mgr.grantMintBurnAccess({ + tokenAddress: argv.tokenAddress, + authority: argv.authority, + role: argv.role, + wallet, + }) + return { hash } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doGrantMintBurnAccess(ctx: Ctx, argv: GrantMintBurnAccessArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + logger.debug( + `Granting mint/burn access: token=${argv.tokenAddress}, authority=${argv.authority}, role=${argv.role}`, + ) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await grantForChain(chain, wallet, argv) + + const output: Record = { + network: networkName, + tokenAddress: argv.tokenAddress, + authority: argv.authority, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Mint/burn access granted, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/token/revoke-mint-burn-access.ts b/ccip-cli/src/commands/token/revoke-mint-burn-access.ts new file mode 100644 index 00000000..9fc9fa62 --- /dev/null +++ b/ccip-cli/src/commands/token/revoke-mint-burn-access.ts @@ -0,0 +1,165 @@ +/** + * Token revoke-mint-burn-access subcommand. + * Revokes mint or burn permissions on a token from a pool or address. + */ + +import { AptosTokenManager } from '@chainlink/ccip-sdk/src/cct/aptos/index.ts' +import { EVMTokenManager } from '@chainlink/ccip-sdk/src/cct/evm/index.ts' +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type AptosChain, + type Chain, + type EVMChain, + type SolanaChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'revoke-mint-burn-access' +export const describe = 'Revoke mint or burn permissions on a token from a pool or address' + +/** + * Yargs builder for the token revoke-mint-burn-access subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be token owner/authority)', + }) + .option('token-address', { + type: 'string', + demandOption: true, + describe: 'Token address (EVM contract, Aptos FA metadata)', + }) + .option('authority', { + type: 'string', + demandOption: true, + describe: 'Address to revoke mint/burn access from (pool, multisig, etc.)', + }) + .option('role', { + type: 'string', + choices: ['mint', 'burn'] as const, + demandOption: true, + describe: 'Which role to revoke: mint or burn', + }) + .option('token-type', { + type: 'string', + choices: ['burnMintERC20', 'factoryBurnMintERC20'] as const, + default: 'burnMintERC20', + describe: 'EVM token type — controls revoke ABI (EVM only)', + }) + .example([ + [ + 'ccip-cli token revoke-mint-burn-access -n sepolia --token-address 0x... --authority 0x... --role mint', + 'Revoke mint role on EVM', + ], + ]) + +/** + * Handler for the token revoke-mint-burn-access subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doRevokeMintBurnAccess(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type RevokeMintBurnAccessArgv = Awaited['argv']> & GlobalOpts + +/** Revokes mint/burn access using the appropriate chain-family facade, normalizing to `{ hash }`. */ +async function revokeForChain( + chain: Chain, + wallet: unknown, + argv: RevokeMintBurnAccessArgv, +): Promise<{ hash: string }> { + switch (chain.network.family) { + case ChainFamily.EVM: { + const evmChain = chain as EVMChain + const mgr = EVMTokenManager.fromChain(evmChain) + return mgr.revokeMintBurnAccess({ + tokenAddress: argv.tokenAddress, + authority: argv.authority, + role: argv.role, + wallet, + }) + } + case ChainFamily.Solana: { + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + // Solana has no role-based revoke — this rejects with a typed CCTParamsInvalidError. + return mgr.revokeMintBurnAccess({ + tokenAddress: argv.tokenAddress, + authority: argv.authority, + role: argv.role, + wallet, + }) + } + case ChainFamily.Aptos: { + const aptosChain = chain as AptosChain + const mgr = AptosTokenManager.fromChain(aptosChain) + const { hash } = await mgr.revokeMintBurnAccess({ + tokenAddress: argv.tokenAddress, + authority: argv.authority, + role: argv.role, + wallet, + }) + return { hash } + } + default: + throw new CCIPChainFamilyUnsupportedError(chain.network.family) + } +} + +async function doRevokeMintBurnAccess(ctx: Ctx, argv: RevokeMintBurnAccessArgv) { + const { logger } = ctx + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + logger.debug( + `Revoking ${argv.role} access: token=${argv.tokenAddress}, authority=${argv.authority}`, + ) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await revokeForChain(chain, wallet, argv) + + const output: Record = { + network: networkName, + tokenAddress: argv.tokenAddress, + authority: argv.authority, + role: argv.role, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write(`${argv.role} access revoked, tx:`, result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/token/transfer-mint-authority.ts b/ccip-cli/src/commands/token/transfer-mint-authority.ts new file mode 100644 index 00000000..26dca05b --- /dev/null +++ b/ccip-cli/src/commands/token/transfer-mint-authority.ts @@ -0,0 +1,111 @@ +/** + * Token transfer-mint-authority subcommand (Solana only). + * Transfers SPL token mint authority to a new address (typically a multisig). + */ + +import { SolanaTokenManager } from '@chainlink/ccip-sdk/src/cct/solana/index.ts' +import { + type SolanaChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../../index.ts' +import { fetchChainsFromRpcs, loadChainWallet } from '../../providers/index.ts' +import { type Ctx, Format } from '../types.ts' +import { getCtx, logParsedError, prettyTable } from '../utils.ts' + +export const command = 'transfer-mint-authority' +export const describe = 'Transfer SPL token mint authority to a new address (Solana only)' + +/** + * Yargs builder for the token transfer-mint-authority subcommand. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'Network: chainId or name (e.g., solana-devnet)', + }) + .option('wallet', { + alias: 'w', + type: 'string', + describe: 'Wallet: ledger[:index] or private key (must be current mint authority)', + }) + .option('mint', { + type: 'string', + demandOption: true, + describe: 'SPL token mint address (base58)', + }) + .option('new-mint-authority', { + type: 'string', + demandOption: true, + describe: 'New mint authority address (base58) — typically a multisig', + }) + .example([ + [ + 'ccip-cli token transfer-mint-authority -n solana-devnet --mint J6fE... --new-mint-authority 2e8X...', + 'Transfer mint authority to a multisig', + ], + ]) + +/** + * Handler for the token transfer-mint-authority subcommand. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doTransferMintAuthority(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +type TransferMintAuthorityArgv = Awaited['argv']> & GlobalOpts + +async function doTransferMintAuthority(ctx: Ctx, argv: TransferMintAuthorityArgv) { + const networkName = networkInfo(argv.network).name + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = await getChain(networkName) + + if (chain.network.family !== ChainFamily.Solana) { + throw new CCIPChainFamilyUnsupportedError(chain.network.family, { + context: { reason: 'transfer-mint-authority is only supported on Solana' }, + }) + } + + const solanaChain = chain as SolanaChain + const mgr = SolanaTokenManager.fromChain(solanaChain) + + const [, wallet] = await loadChainWallet(chain, argv) + const result = await mgr.transferMintAuthority({ + mint: argv.mint, + newMintAuthority: argv.newMintAuthority, + wallet, + }) + + const output: Record = { + network: networkName, + mint: argv.mint, + newMintAuthority: argv.newMintAuthority, + txHash: result.hash, + } + + switch (argv.format) { + case Format.json: + ctx.output.write(JSON.stringify(output, null, 2)) + return + case Format.log: + ctx.output.write('Mint authority transferred, tx:', result.hash) + return + case Format.pretty: + default: + prettyTable.call(ctx, output) + return + } +} diff --git a/ccip-cli/src/commands/verify-utils.ts b/ccip-cli/src/commands/verify-utils.ts new file mode 100644 index 00000000..113a30a3 --- /dev/null +++ b/ccip-cli/src/commands/verify-utils.ts @@ -0,0 +1,221 @@ +/** + * Shared helper for the in-CLI contract-verification flow. + * + * Used by the `--verify` flag on the deploy commands and by the standalone `verify` command. + * The verify module (and its ~1.3MB bundled standard-json fixtures) is lazy-imported so it + * only loads when verification is actually requested. + */ + +import { + CCIPArgumentInvalidError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { JsonRpcApiProvider } from 'ethers' + +import type { Ctx } from './types.ts' + +/** Lazy loaders for the deployable contracts' creation bytecode (used to derive ctor args). */ +const BYTECODE_LOADERS: Record Promise> = { + CrossChainToken: async () => + (await import('@chainlink/ccip-sdk/src/cct/evm/token/bytecodes/CrossChainToken.ts')) + .CROSS_CHAIN_TOKEN_BYTECODE, + CrossChainPoolToken: async () => + (await import('@chainlink/ccip-sdk/src/cct/evm/token/bytecodes/CrossChainPoolToken.ts')) + .CROSS_CHAIN_POOL_TOKEN_BYTECODE, + BurnMintTokenPool: async () => + (await import('@chainlink/ccip-sdk/src/cct/evm/token-pool/bytecodes/BurnMintTokenPool.ts')) + .BURN_MINT_TOKEN_POOL_BYTECODE, + LockReleaseTokenPool: async () => + (await import('@chainlink/ccip-sdk/src/cct/evm/token-pool/bytecodes/LockReleaseTokenPool.ts')) + .LOCK_RELEASE_TOKEN_POOL_BYTECODE, + ERC20LockBox: async () => + (await import('@chainlink/ccip-sdk/src/cct/evm/token-pool/bytecodes/ERC20LockBox.ts')) + .ERC20_LOCK_BOX_BYTECODE, +} + +/** Asks the Etherscan-v2 directory for a contract's creation transaction hash. */ +async function fetchCreationTxHash( + chainId: number, + address: string, + apiKey: string, +): Promise { + const url = new URL('https://api.etherscan.io/v2/api') + url.searchParams.set('chainid', String(chainId)) + url.searchParams.set('module', 'contract') + url.searchParams.set('action', 'getcontractcreation') + url.searchParams.set('contractaddresses', address) + url.searchParams.set('apikey', apiKey) + const res = await fetch(url) + const json = (await res.json()) as { status: string; result?: Array<{ txHash?: string }> } + const txHash = json.result?.[0]?.txHash + if (json.status !== '1' || !txHash) { + throw new CCIPArgumentInvalidError( + 'address', + `could not find the creation transaction for ${address} via the explorer; pass --creation-tx or --constructor-args`, + ) + } + return txHash +} + +/** One frame of a `callTracer` trace tree. */ +interface CallFrame { + type?: string + to?: string + input?: string + calls?: CallFrame[] +} + +/** + * Walks a `callTracer` trace for the CREATE/CREATE2 frame that produced `address` and returns its + * init code. Needed for factory deploys, where the contract is born in an internal call (so the + * top-level tx input is the factory calldata, not the contract's init code). + */ +async function traceCreatedInitCode( + provider: JsonRpcApiProvider, + txHash: string, + address: string, +): Promise { + const target = address.toLowerCase() + const root = (await provider.send('debug_traceTransaction', [ + txHash, + { tracer: 'callTracer' }, + ])) as CallFrame + const stack: CallFrame[] = [root] + while (stack.length) { + const frame = stack.pop()! + if ( + (frame.type === 'CREATE' || frame.type === 'CREATE2') && + frame.to?.toLowerCase() === target + ) { + return frame.input + } + if (frame.calls) stack.push(...frame.calls) + } + return undefined +} + +/** + * Recovers a deployed contract's ABI-encoded constructor args (for an ALREADY-deployed contract) + * by reading its on-chain creation code and stripping the known SDK creation bytecode. Used by the + * standalone `verify` command so users only supply `--contract` + `--address`. + * + * Handles both direct deploys (init code IS the creation tx input) and factory deploys (the + * contract is created in an internal CREATE2 call — recovered via `debug_traceTransaction`). + */ +export async function deriveEncodedConstructorArgs(opts: { + contract: string + chainId: number + address: string + apiKey: string + provider: JsonRpcApiProvider + creationTx?: string +}): Promise { + const loader = BYTECODE_LOADERS[opts.contract] + if (!loader) { + throw new CCIPArgumentInvalidError( + 'contract', + `cannot auto-derive constructor args for "${opts.contract}"; pass --constructor-args`, + ) + } + const bytecode = await loader() + const txHash = + opts.creationTx ?? (await fetchCreationTxHash(opts.chainId, opts.address, opts.apiKey)) + + // Direct deploy: the creation tx input is the init code (bytecode || args). + const tx = await opts.provider.getTransaction(txHash) + let initCode = tx?.data ?? '' + + // Factory deploy: the contract is created in an internal CREATE2 call — recover via a trace. + if (!initCode.startsWith(bytecode)) { + const traced = await traceCreatedInitCode(opts.provider, txHash, opts.address).catch( + () => undefined, + ) + if (traced) initCode = traced + } + + if (!initCode.startsWith(bytecode)) { + throw new CCIPArgumentInvalidError( + 'contract', + `could not recover ${opts.contract} init code from ${txHash} (wrong --contract/version, or the RPC lacks debug_traceTransaction for this factory deploy); pass --constructor-args`, + ) + } + return `0x${initCode.slice(bytecode.length)}` +} + +/** A single contract to verify on a source-chain explorer. */ +export interface VerifyTarget { + /** Bundled verification-registry key, e.g. `CrossChainToken` (see SDK `listDeployableContracts()`). */ + contract: string + /** Deployed contract address. */ + address: string + /** ABI-encoded constructor args, `0x`-prefixed (omit / `'0x'` for no-arg constructors). */ + encodedConstructorArgs?: string +} + +/** + * Verifies one or more freshly-deployed contracts on the network's explorer (Etherscan v2 / + * Blockscout / Sourcify, auto-routed by chainId). EVM-only; logs and skips otherwise. + * Never throws — verification failures are reported but don't fail the deploy. + */ +export async function runVerification( + ctx: Ctx, + networkName: string, + targets: readonly VerifyTarget[], + opts: { etherscanApiKey?: string }, +): Promise { + const net = networkInfo(networkName) + if (net.family !== ChainFamily.EVM) { + ctx.logger.warn(`verify: contract verification is EVM-only; skipping for ${networkName}`) + return + } + const apiKey = opts.etherscanApiKey ?? process.env.ETHERSCAN_API_KEY + if (!apiKey) { + ctx.logger.warn( + 'verify: no Etherscan API key — pass --etherscan-api-key or set ETHERSCAN_API_KEY; skipping verification', + ) + return + } + const chainId = Number(net.chainId) + + // Lazy-load the verify module (pulls the bundled fixtures) only when actually verifying. + const { verifyDeployedContract } = await import('@chainlink/ccip-sdk/src/verify/index.ts') + + // A freshly-deployed contract isn't indexed by the explorer for a few seconds — retry the + // submit on the "not yet indexed" error before giving up. + const notIndexed = /unable to locate contractcode|does not exist|not.*found/i + const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) + + for (const t of targets) { + ctx.logger.info(`verify: submitting ${t.contract} at ${t.address} (chainId ${chainId})...`) + for (let attempt = 1; ; attempt++) { + try { + const result = await verifyDeployedContract({ + contract: t.contract, + chainId, + contractAddress: t.address, + apiKey, + constructorArgs: t.encodedConstructorArgs + ? { kind: 'encoded', hex: t.encodedConstructorArgs } + : { kind: 'none' }, + }) + const link = result.explorerUrl ? ` ${result.explorerUrl}` : '' + ctx.output.write( + `verify[${t.contract} @ ${t.address}]: ${result.status} — ${result.message}${link}`, + ) + break + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + if (notIndexed.test(msg) && attempt <= 8) { + ctx.logger.info( + `verify: contract not indexed yet, retrying in 8s (attempt ${attempt}/8)...`, + ) + await sleep(8_000) + continue + } + ctx.logger.error(`verify[${t.contract} @ ${t.address}] errored:`, msg) + break + } + } + } +} diff --git a/ccip-cli/src/commands/verify.ts b/ccip-cli/src/commands/verify.ts new file mode 100644 index 00000000..2bf76e26 --- /dev/null +++ b/ccip-cli/src/commands/verify.ts @@ -0,0 +1,133 @@ +/** + * Standalone contract-verification command for ALREADY-deployed CCIP v2 contracts + * (CrossChainToken, BurnMintTokenPool, LockReleaseTokenPool, CrossChainPoolToken, ERC20LockBox). + * + * Zero-friction: given just `--contract` + `--address`, it derives the constructor args from the + * contract's on-chain creation code (stripping the known SDK bytecode). `--constructor-args` and + * `--creation-tx` are escape hatches. EVM only. + */ + +import { + type EVMChain, + CCIPChainFamilyUnsupportedError, + ChainFamily, + networkInfo, +} from '@chainlink/ccip-sdk/src/index.ts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../index.ts' +import type { Ctx } from './types.ts' +import { getCtx, logParsedError } from './utils.ts' +import { deriveEncodedConstructorArgs, runVerification } from './verify-utils.ts' +import { fetchChainsFromRpcs } from '../providers/index.ts' + +const DEPLOYABLE = [ + 'CrossChainToken', + 'BurnMintTokenPool', + 'LockReleaseTokenPool', + 'CrossChainPoolToken', + 'ERC20LockBox', + 'AdvancedPoolHooks', +] as const + +export const command = 'verify' +export const describe = 'Verify an already-deployed CCIP v2 contract on the source-chain explorer' + +/** + * Yargs builder for the verify command. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with command options. + */ +export const builder = (yargs: Argv) => + yargs + .option('network', { + alias: 'n', + type: 'string', + demandOption: true, + describe: 'EVM network: chainId or name (e.g., ethereum-testnet-sepolia)', + }) + .option('contract', { + type: 'string', + demandOption: true, + choices: DEPLOYABLE, + describe: 'Which bundled CCIP v2 contract this address is', + }) + .option('address', { + type: 'string', + demandOption: true, + describe: 'The deployed contract address', + }) + .option('constructor-args', { + type: 'string', + describe: 'ABI-encoded constructor args (0x-hex). Omit to auto-derive from creation code', + }) + .option('creation-tx', { + type: 'string', + describe: + 'Creation tx hash (used to auto-derive constructor args without an explorer lookup)', + }) + .option('etherscan-api-key', { + type: 'string', + describe: 'Etherscan V2 API key (defaults to ETHERSCAN_API_KEY env)', + }) + .example([ + [ + 'ccip-cli verify -n ethereum-testnet-sepolia --contract CrossChainToken --address 0x302F...', + 'Verify a deployed CrossChainToken (constructor args auto-derived)', + ], + [ + 'ccip-cli verify -n ethereum-testnet-sepolia --contract BurnMintTokenPool --address 0xb857... --constructor-args 0x...', + 'Verify a pool with explicit constructor args', + ], + ]) + +/** + * Handler for the verify command. + * @param argv - Command line arguments. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return doVerify(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +async function doVerify(ctx: Ctx, argv: Awaited['argv']> & GlobalOpts) { + const net = networkInfo(argv.network) + if (net.family !== ChainFamily.EVM) { + throw new CCIPChainFamilyUnsupportedError(net.family) + } + const apiKey = argv.etherscanApiKey ?? process.env.ETHERSCAN_API_KEY + const chainId = Number(net.chainId) + + let encodedConstructorArgs = argv.constructorArgs + if (!encodedConstructorArgs) { + if (!apiKey) { + ctx.logger.warn( + 'verify: assuming no constructor args (set --constructor-args or an API key to auto-derive)', + ) + } else { + const getChain = fetchChainsFromRpcs(ctx, argv) + const chain = (await getChain(net.name)) as EVMChain + ctx.logger.info('verify: deriving constructor args from on-chain creation code...') + encodedConstructorArgs = await deriveEncodedConstructorArgs({ + contract: argv.contract, + chainId, + address: argv.address, + apiKey, + provider: chain.provider, + creationTx: argv.creationTx, + }) + } + } + + await runVerification( + ctx, + net.name, + [{ contract: argv.contract, address: argv.address, encodedConstructorArgs }], + { etherscanApiKey: argv.etherscanApiKey }, + ) +} diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 7cfb30c3..ad55a2d5 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -28,7 +28,7 @@ Error.stackTraceLimit = 50 // show more stack frames for better debugging // generate:nofail // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -const VERSION = '1.10.2-73e631c' +const VERSION = '1.10.2-671a7e3' // generate:end const require = createRequire(import.meta.url) diff --git a/ccip-sdk/package.json b/ccip-sdk/package.json index eef22468..19acc273 100644 --- a/ccip-sdk/package.json +++ b/ccip-sdk/package.json @@ -27,7 +27,27 @@ "types": "./dist/all-chains.d.ts", "default": "./dist/all-chains.js" }, + "./cct/evm": { + "types": "./dist/cct/evm/index.d.ts", + "default": "./dist/cct/evm/index.js" + }, + "./cct/solana": { + "types": "./dist/cct/solana/index.d.ts", + "default": "./dist/cct/solana/index.js" + }, + "./cct/aptos": { + "types": "./dist/cct/aptos/index.d.ts", + "default": "./dist/cct/aptos/index.js" + }, "./dist/*": "./dist/*", + "./token-admin/types": { + "types": "./dist/token-admin/types.d.ts", + "default": "./dist/token-admin/types.js" + }, + "./verify": { + "types": "./dist/verify/index.d.ts", + "default": "./dist/verify/index.js" + }, "./src/*": "./src/*" }, "scripts": { @@ -36,7 +56,8 @@ "lint:fix": "prettier --write ./src && eslint --fix ./src", "typecheck": "tsc --noEmit", "check": "npm run lint && npm run typecheck", - "build": "npm run clean && tsc -p ./tsconfig.build.json", + "build": "npm run clean && tsc -p ./tsconfig.build.json && npm run copy-verify-fixtures", + "copy-verify-fixtures": "mkdir -p dist/verify/fixtures && cp src/verify/fixtures/*.json dist/verify/fixtures/", "clean": "rm -rfv ./dist", "prepare": "npm run build" }, @@ -67,6 +88,10 @@ "dependencies": { "@aptos-labs/ts-sdk": "^6.3.1", "@coral-xyz/anchor": "^0.29.0", + "@metaplex-foundation/mpl-token-metadata": "^3.4.0", + "@metaplex-foundation/umi": "^1.5.1", + "@metaplex-foundation/umi-bundle-defaults": "^1.5.1", + "@metaplex-foundation/umi-web3js-adapters": "^1.5.1", "@mysten/bcs": "^2.1.0", "@mysten/sui": "^2.19.0", "@noble/hashes": "^2.2.0", diff --git a/ccip-sdk/src/api/index.ts b/ccip-sdk/src/api/index.ts index fdd4cbed..26112b67 100644 --- a/ccip-sdk/src/api/index.ts +++ b/ccip-sdk/src/api/index.ts @@ -62,7 +62,7 @@ export const DEFAULT_TIMEOUT_MS = 30000 /** SDK version string for telemetry header */ // generate:nofail // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -export const SDK_VERSION = '1.10.2-73e631c' +export const SDK_VERSION = '1.10.2-671a7e3' // generate:end /** SDK telemetry header name */ diff --git a/ccip-sdk/src/aptos/index.ts b/ccip-sdk/src/aptos/index.ts index b722db04..3f2a2b25 100644 --- a/ccip-sdk/src/aptos/index.ts +++ b/ccip-sdk/src/aptos/index.ts @@ -34,12 +34,12 @@ import { CCIPAptosExtraArgsV2RequiredError, CCIPAptosNetworkUnknownError, CCIPAptosRegistryTypeInvalidError, + CCIPAptosTokenNotRegisteredError, CCIPAptosTransactionInvalidError, CCIPAptosTransactionTypeInvalidError, CCIPError, CCIPExtraArgsEncodingUnsupportedError, CCIPLogDataInvalidError, - CCIPTokenNotRegisteredError, CCIPTokenPoolChainConfigNotFoundError, CCIPWalletInvalidError, } from '../errors/index.ts' @@ -821,7 +821,8 @@ export class AptosChain extends Chain { functionArguments: [token], }, }) - if (administrator.match(/^0x0*$/)) throw new CCIPTokenNotRegisteredError(token, registry) + if (administrator.match(/^0x0*$/) && pendingAdministrator.match(/^0x0*$/)) + throw new CCIPAptosTokenNotRegisteredError(token, registry) return { administrator, ...(!pendingAdministrator.match(/^0x0*$/) && { pendingAdministrator }), @@ -836,6 +837,7 @@ export class AptosChain extends Chain { ): Promise<{ token: string router: string + owner: string typeAndVersion?: string }> { const modulesNames = (await this._getAccountModulesNames(tokenPool)) @@ -844,7 +846,7 @@ export class AptosChain extends Chain { let firstErr for (const name of modulesNames) { try { - const [typeAndVersion, token, router] = await Promise.all([ + const [typeAndVersion, token, router, owner] = await Promise.all([ this.typeAndVersion(`${tokenPool}::${name}`), this.provider.view<[string]>({ payload: { @@ -858,10 +860,17 @@ export class AptosChain extends Chain { functionArguments: [], }, }), + this.provider.view<[string]>({ + payload: { + function: `${tokenPool}::${name}::owner`, + functionArguments: [], + }, + }), ]) return { token: token[0], router: router[0], + owner: owner[0], typeAndVersion: typeAndVersion[2], } } catch (err) { diff --git a/ccip-sdk/src/aptos/types.ts b/ccip-sdk/src/aptos/types.ts index 93f64c46..ca2bc154 100644 --- a/ccip-sdk/src/aptos/types.ts +++ b/ccip-sdk/src/aptos/types.ts @@ -93,5 +93,5 @@ export function serializeExecutionReport( */ export type UnsignedAptosTx = { family: typeof ChainFamily.Aptos - transactions: [Uint8Array] + transactions: [Uint8Array, ...Uint8Array[]] } diff --git a/ccip-sdk/src/cct/aptos/bytecodes/burn_mint_token_pool.ts b/ccip-sdk/src/cct/aptos/bytecodes/burn_mint_token_pool.ts new file mode 100644 index 00000000..f53ebfb1 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/bytecodes/burn_mint_token_pool.ts @@ -0,0 +1,811 @@ +/** + * BurnMintTokenPool Move package source files. + * + * Source: chainlink-aptos contracts/ccip/ccip_token_pools/burn_mint_token_pool + * AptosFramework rev: 16beac69835f3a71564c96164a606a23f259099a + * ChainlinkCCIP + MCMS: embedded as local dependencies + * + * For standard Aptos Fungible Asset tokens with BurnRef/MintRef. + * Use managed_token_pool.ts for tokens deployed with the managed_token package. + * + * Vendored as source (not compiled bytecodes) because Aptos Move modules + * must be compiled with the deployer's address at deploy time. + * + * Lazy-loaded via dynamic import() — same pattern as EVM BurnMintERC20 bytecode. + */ + +/** Move.toml for the BurnMintTokenPool package. */ +export const BURN_MINT_POOL_MOVE_TOML = `[package] +name = "BurnMintTokenPool" +version = "1.0.0" +authors = [] + +[addresses] +ccip = "_" +ccip_token_pool = "_" +burn_mint_token_pool = "_" +mcms = "_" +mcms_register_entrypoints = "_" +burn_mint_local_token = "_" + +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "16beac69835f3a71564c96164a606a23f259099a", subdir = "aptos-move/framework/aptos-framework" } +ChainlinkCCIP = { local = "../ccip" } +CCIPTokenPool = { local = "../token_pool" } +` + +/** burn_mint_token_pool.move — pool logic (test functions stripped). */ +export const BURN_MINT_TOKEN_POOL_MOVE = `module burn_mint_token_pool::burn_mint_token_pool { + use std::account::{Self, SignerCapability}; + use std::error; + use std::fungible_asset::{Self, FungibleAsset, Metadata, TransferRef}; + use std::primary_fungible_store; + use std::object::{Self, Object, ObjectCore}; + use std::option::{Self, Option}; + use std::signer; + use std::string::{Self, String}; + use std::fungible_asset::{BurnRef, MintRef}; + + use ccip::token_admin_registry::{Self, ReleaseOrMintInputV1, LockOrBurnInputV1}; + use ccip_token_pool::ownable; + use ccip_token_pool::rate_limiter; + use ccip_token_pool::token_pool; + + use mcms::mcms_registry; + use mcms::bcs_stream; + + const STORE_OBJECT_SEED: vector = b"CcipBurnMintTokenPool"; + + struct BurnMintTokenPoolDeployment has key { + store_signer_cap: SignerCapability, + ownable_state: ownable::OwnableState, + token_pool_state: token_pool::TokenPoolState + } + + struct BurnMintTokenPoolState has key, store { + store_signer_cap: SignerCapability, + ownable_state: ownable::OwnableState, + token_pool_state: token_pool::TokenPoolState, + store_signer_address: address, + burn_ref: Option, + mint_ref: Option + } + + const E_NOT_PUBLISHER: u64 = 1; + const E_ALREADY_INITIALIZED: u64 = 2; + const E_INVALID_FUNGIBLE_ASSET: u64 = 3; + const E_LOCAL_TOKEN_MISMATCH: u64 = 4; + const E_INVALID_ARGUMENTS: u64 = 5; + const E_UNKNOWN_FUNCTION: u64 = 6; + const E_MINT_REF_NOT_SET: u64 = 7; + const E_BURN_REF_NOT_SET: u64 = 8; + + // ================================================================ + // | Init | + // ================================================================ + #[view] + public fun type_and_version(): String { + string::utf8(b"BurnMintTokenPool 1.6.0") + } + + fun init_module(publisher: &signer) { + // register the pool on deployment, because in the case of object code deployment, + // this is the only time we have a signer ref to @ccip_burn_mint_pool. + assert!( + object::object_exists(@burn_mint_local_token), + error::invalid_argument(E_INVALID_FUNGIBLE_ASSET) + ); + let metadata = object::address_to_object(@burn_mint_local_token); + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist(@burn_mint_token_pool); + + // the name of this module. if incorrect, callbacks will fail to be registered and + // register_pool will revert. + let token_pool_module_name = b"burn_mint_token_pool"; + + // Register the entrypoint with mcms + if (@mcms_register_entrypoints == @0x1) { + register_mcms_entrypoint(publisher, token_pool_module_name); + }; + + // Register V2 pool with closure-based callbacks + register_v2_callbacks(publisher); + + // create a resource account to be the owner of the primary FungibleStore we will use. + let (store_signer, store_signer_cap) = + account::create_resource_account(publisher, STORE_OBJECT_SEED); + + // make sure this is a valid fungible asset that is primary fungible store enabled, + // ie. created with primary_fungible_store::create_primary_store_enabled_fungible_asset + primary_fungible_store::ensure_primary_store_exists( + signer::address_of(&store_signer), metadata + ); + + move_to( + publisher, + BurnMintTokenPoolDeployment { + store_signer_cap, + ownable_state: ownable::new(&store_signer, @burn_mint_token_pool), + token_pool_state: token_pool::initialize( + &store_signer, @burn_mint_local_token, vector[] + ) + } + ); + } + + public fun initialize( + caller: &signer, burn_ref: BurnRef, mint_ref: MintRef + ) acquires BurnMintTokenPoolDeployment { + assert_can_initialize(signer::address_of(caller)); + + assert!( + exists(@burn_mint_token_pool), + error::invalid_argument(E_ALREADY_INITIALIZED) + ); + + let metadata = object::address_to_object(@burn_mint_local_token); + let burn_ref_metadata = fungible_asset::burn_ref_metadata(&burn_ref); + let mint_ref_metadata = fungible_asset::mint_ref_metadata(&mint_ref); + + assert!( + metadata == burn_ref_metadata && metadata == mint_ref_metadata, + error::invalid_argument(E_LOCAL_TOKEN_MISMATCH) + ); + + let BurnMintTokenPoolDeployment { + store_signer_cap, + ownable_state, + token_pool_state + } = move_from(@burn_mint_token_pool); + + let store_signer = account::create_signer_with_capability(&store_signer_cap); + + let pool = BurnMintTokenPoolState { + ownable_state, + store_signer_address: signer::address_of(&store_signer), + store_signer_cap, + token_pool_state, + burn_ref: option::some(burn_ref), + mint_ref: option::some(mint_ref) + }; + + move_to(&store_signer, pool); + } + + public fun register_v2_callbacks(publisher: &signer) { + assert!( + signer::address_of(publisher) == @burn_mint_token_pool, + error::permission_denied(E_NOT_PUBLISHER) + ); + token_admin_registry::register_pool_v2( + publisher, + @burn_mint_local_token, + lock_or_burn_v2, + release_or_mint_v2 + ); + } + + // ================================================================ + // | Exposing token_pool functions | + // ================================================================ + #[view] + public fun get_token(): address acquires BurnMintTokenPoolState { + token_pool::get_token(&borrow_pool().token_pool_state) + } + + #[view] + public fun get_router(): address { + token_pool::get_router() + } + + #[view] + public fun get_token_decimals(): u8 acquires BurnMintTokenPoolState { + token_pool::get_token_decimals(&borrow_pool().token_pool_state) + } + + #[view] + public fun get_remote_pools( + remote_chain_selector: u64 + ): vector> acquires BurnMintTokenPoolState { + token_pool::get_remote_pools( + &borrow_pool().token_pool_state, remote_chain_selector + ) + } + + #[view] + public fun is_remote_pool( + remote_chain_selector: u64, remote_pool_address: vector + ): bool acquires BurnMintTokenPoolState { + token_pool::is_remote_pool( + &borrow_pool().token_pool_state, + remote_chain_selector, + remote_pool_address + ) + } + + #[view] + public fun get_remote_token( + remote_chain_selector: u64 + ): vector acquires BurnMintTokenPoolState { + let pool = borrow_pool(); + token_pool::get_remote_token(&pool.token_pool_state, remote_chain_selector) + } + + public entry fun add_remote_pool( + caller: &signer, remote_chain_selector: u64, remote_pool_address: vector + ) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::add_remote_pool( + &mut pool.token_pool_state, + remote_chain_selector, + remote_pool_address + ); + } + + public entry fun remove_remote_pool( + caller: &signer, remote_chain_selector: u64, remote_pool_address: vector + ) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::remove_remote_pool( + &mut pool.token_pool_state, + remote_chain_selector, + remote_pool_address + ); + } + + #[view] + public fun is_supported_chain(remote_chain_selector: u64): bool acquires BurnMintTokenPoolState { + let pool = borrow_pool(); + token_pool::is_supported_chain(&pool.token_pool_state, remote_chain_selector) + } + + #[view] + public fun get_supported_chains(): vector acquires BurnMintTokenPoolState { + let pool = borrow_pool(); + token_pool::get_supported_chains(&pool.token_pool_state) + } + + public entry fun apply_chain_updates( + caller: &signer, + remote_chain_selectors_to_remove: vector, + remote_chain_selectors_to_add: vector, + remote_pool_addresses_to_add: vector>>, + remote_token_addresses_to_add: vector> + ) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::apply_chain_updates( + &mut pool.token_pool_state, + remote_chain_selectors_to_remove, + remote_chain_selectors_to_add, + remote_pool_addresses_to_add, + remote_token_addresses_to_add + ); + } + + #[view] + public fun get_allowlist_enabled(): bool acquires BurnMintTokenPoolState { + let pool = borrow_pool(); + token_pool::get_allowlist_enabled(&pool.token_pool_state) + } + + public entry fun set_allowlist_enabled( + caller: &signer, enabled: bool + ) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + token_pool::set_allowlist_enabled(&mut pool.token_pool_state, enabled); + } + + #[view] + public fun get_allowlist(): vector
acquires BurnMintTokenPoolState { + let pool = borrow_pool(); + token_pool::get_allowlist(&pool.token_pool_state) + } + + public entry fun apply_allowlist_updates( + caller: &signer, removes: vector
, adds: vector
+ ) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + token_pool::apply_allowlist_updates(&mut pool.token_pool_state, removes, adds); + } + + // ================================================================ + // | Burn/Mint | + // ================================================================ + + // the callback proof type used as authentication to retrieve and set input and output arguments. + struct CallbackProof has drop {} + + public fun lock_or_burn( + _store: Object, fa: FungibleAsset, _transfer_ref: &TransferRef + ) acquires BurnMintTokenPoolState { + // retrieve the input for this lock or burn operation. if this function is invoked + // outside of ccip::token_admin_registry, the transaction will abort. + let input = + token_admin_registry::get_lock_or_burn_input_v1( + @burn_mint_token_pool, CallbackProof {} + ); + + let pool = borrow_pool_mut(); + let fa_amount = fungible_asset::amount(&fa); + + // This method validates various aspects of the lock or burn operation. If any of the + // validations fail, the transaction will abort. + let dest_token_address = + token_pool::validate_lock_or_burn( + &mut pool.token_pool_state, + &fa, + &input, + fa_amount + ); + + // Construct lock_or_burn output before we lose access to fa + let dest_pool_data = token_pool::encode_local_decimals(&pool.token_pool_state); + + // Burn the funds + assert!(pool.burn_ref.is_some(), E_BURN_REF_NOT_SET); + fungible_asset::burn(pool.burn_ref.borrow(), fa); + + // set the output for this lock or burn operation. + token_admin_registry::set_lock_or_burn_output_v1( + @burn_mint_token_pool, + CallbackProof {}, + dest_token_address, + dest_pool_data + ); + + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(&input); + + token_pool::emit_locked_or_burned( + &mut pool.token_pool_state, fa_amount, remote_chain_selector + ); + } + + public fun release_or_mint( + _store: Object, _amount: u64, _transfer_ref: &TransferRef + ): FungibleAsset acquires BurnMintTokenPoolState { + // retrieve the input for this release or mint operation. if this function is invoked + // outside of ccip::token_admin_registry, the transaction will abort. + let input = + token_admin_registry::get_release_or_mint_input_v1( + @burn_mint_token_pool, CallbackProof {} + ); + let pool = borrow_pool_mut(); + let local_amount = + token_pool::calculate_release_or_mint_amount(&pool.token_pool_state, &input); + + token_pool::validate_release_or_mint( + &mut pool.token_pool_state, &input, local_amount + ); + + // Mint the amount for release. + assert!(pool.mint_ref.is_some(), E_MINT_REF_NOT_SET); + let fa = fungible_asset::mint(pool.mint_ref.borrow(), local_amount); + + // set the output for this release or mint operation. + token_admin_registry::set_release_or_mint_output_v1( + @burn_mint_token_pool, CallbackProof {}, local_amount + ); + + let recipient = token_admin_registry::get_release_or_mint_receiver(&input); + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(&input); + + token_pool::emit_released_or_minted( + &mut pool.token_pool_state, + recipient, + local_amount, + remote_chain_selector + ); + + // return the withdrawn fungible asset. + fa + } + + #[persistent] + fun lock_or_burn_v2( + fa: FungibleAsset, input: LockOrBurnInputV1 + ): (vector, vector) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + let fa_amount = fungible_asset::amount(&fa); + + let dest_token_address = + token_pool::validate_lock_or_burn( + &mut pool.token_pool_state, + &fa, + &input, + fa_amount + ); + + // Burn the token + assert!(pool.burn_ref.is_some(), E_BURN_REF_NOT_SET); + fungible_asset::burn(pool.burn_ref.borrow(), fa); + + // Emit event + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(&input); + + token_pool::emit_locked_or_burned( + &mut pool.token_pool_state, fa_amount, remote_chain_selector + ); + + (dest_token_address, token_pool::encode_local_decimals(&pool.token_pool_state)) + } + + #[persistent] + fun release_or_mint_v2( + input: ReleaseOrMintInputV1 + ): (FungibleAsset, u64) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + let local_amount = + token_pool::calculate_release_or_mint_amount(&pool.token_pool_state, &input); + + token_pool::validate_release_or_mint( + &mut pool.token_pool_state, &input, local_amount + ); + + // Mint the amount for release + assert!(pool.mint_ref.is_some(), E_MINT_REF_NOT_SET); + let fa = fungible_asset::mint(pool.mint_ref.borrow(), local_amount); + + let recipient = token_admin_registry::get_release_or_mint_receiver(&input); + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(&input); + + token_pool::emit_released_or_minted( + &mut pool.token_pool_state, + recipient, + local_amount, + remote_chain_selector + ); + + (fa, local_amount) + } + + // ================================================================ + // | Rate limit config | + // ================================================================ + public entry fun set_chain_rate_limiter_configs( + caller: &signer, + remote_chain_selectors: vector, + outbound_is_enableds: vector, + outbound_capacities: vector, + outbound_rates: vector, + inbound_is_enableds: vector, + inbound_capacities: vector, + inbound_rates: vector + ) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + let number_of_chains = remote_chain_selectors.length(); + + assert!( + number_of_chains == outbound_is_enableds.length() + && number_of_chains == outbound_capacities.length() + && number_of_chains == outbound_rates.length() + && number_of_chains == inbound_is_enableds.length() + && number_of_chains == inbound_capacities.length() + && number_of_chains == inbound_rates.length(), + error::invalid_argument(E_INVALID_ARGUMENTS) + ); + + for (i in 0..number_of_chains) { + token_pool::set_chain_rate_limiter_config( + &mut pool.token_pool_state, + remote_chain_selectors[i], + outbound_is_enableds[i], + outbound_capacities[i], + outbound_rates[i], + inbound_is_enableds[i], + inbound_capacities[i], + inbound_rates[i] + ); + }; + } + + public entry fun set_chain_rate_limiter_config( + caller: &signer, + remote_chain_selector: u64, + outbound_is_enabled: bool, + outbound_capacity: u64, + outbound_rate: u64, + inbound_is_enabled: bool, + inbound_capacity: u64, + inbound_rate: u64 + ) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::set_chain_rate_limiter_config( + &mut pool.token_pool_state, + remote_chain_selector, + outbound_is_enabled, + outbound_capacity, + outbound_rate, + inbound_is_enabled, + inbound_capacity, + inbound_rate + ); + } + + #[view] + public fun get_current_inbound_rate_limiter_state( + remote_chain_selector: u64 + ): rate_limiter::TokenBucket acquires BurnMintTokenPoolState { + token_pool::get_current_inbound_rate_limiter_state( + &borrow_pool().token_pool_state, remote_chain_selector + ) + } + + #[view] + public fun get_current_outbound_rate_limiter_state( + remote_chain_selector: u64 + ): rate_limiter::TokenBucket acquires BurnMintTokenPoolState { + token_pool::get_current_outbound_rate_limiter_state( + &borrow_pool().token_pool_state, remote_chain_selector + ) + } + + // ================================================================ + // | Storage helpers | + // ================================================================ + #[view] + public fun get_store_address(): address { + store_address() + } + + inline fun store_address(): address { + account::create_resource_address(&@burn_mint_token_pool, STORE_OBJECT_SEED) + } + + fun assert_can_initialize(caller_address: address) { + if (caller_address == @burn_mint_token_pool) { return }; + + if (object::is_object(@burn_mint_token_pool)) { + let burn_mint_token_pool_object = + object::address_to_object(@burn_mint_token_pool); + if (caller_address == object::owner(burn_mint_token_pool_object) + || caller_address == object::root_owner(burn_mint_token_pool_object)) { + return + }; + }; + + abort error::permission_denied(E_NOT_PUBLISHER) + } + + inline fun borrow_pool(): &BurnMintTokenPoolState { + borrow_global(store_address()) + } + + inline fun borrow_pool_mut(): &mut BurnMintTokenPoolState { + borrow_global_mut(store_address()) + } + + // ================================================================ + // | Expose ownable | + // ================================================================ + #[view] + public fun owner(): address acquires BurnMintTokenPoolState { + ownable::owner(&borrow_pool().ownable_state) + } + + #[view] + public fun has_pending_transfer(): bool acquires BurnMintTokenPoolState { + ownable::has_pending_transfer(&borrow_pool().ownable_state) + } + + #[view] + public fun pending_transfer_from(): Option
acquires BurnMintTokenPoolState { + ownable::pending_transfer_from(&borrow_pool().ownable_state) + } + + #[view] + public fun pending_transfer_to(): Option
acquires BurnMintTokenPoolState { + ownable::pending_transfer_to(&borrow_pool().ownable_state) + } + + #[view] + public fun pending_transfer_accepted(): Option acquires BurnMintTokenPoolState { + ownable::pending_transfer_accepted(&borrow_pool().ownable_state) + } + + public entry fun transfer_ownership(caller: &signer, to: address) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + ownable::transfer_ownership(caller, &mut pool.ownable_state, to) + } + + public entry fun accept_ownership(caller: &signer) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + ownable::accept_ownership(caller, &mut pool.ownable_state) + } + + public entry fun execute_ownership_transfer( + caller: &signer, to: address + ) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + ownable::execute_ownership_transfer(caller, &mut pool.ownable_state, to) + } + + // ================================================================ + // | Ref Migration | + // ================================================================ + public fun migrate_mint_ref(caller: &signer): MintRef acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + assert!(pool.mint_ref.is_some(), E_MINT_REF_NOT_SET); + + pool.mint_ref.extract() + } + + public fun migrate_burn_ref(caller: &signer): BurnRef acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + assert!(pool.burn_ref.is_some(), E_BURN_REF_NOT_SET); + + pool.burn_ref.extract() + } + + // ================================================================ + // | MCMS entrypoint | + // ================================================================ + struct McmsCallback has drop {} + + public fun mcms_entrypoint( + _metadata: object::Object + ): option::Option acquires BurnMintTokenPoolState { + let (caller, function, data) = + mcms_registry::get_callback_params(@burn_mint_token_pool, McmsCallback {}); + + let function_bytes = *function.bytes(); + let stream = bcs_stream::new(data); + + if (function_bytes == b"add_remote_pool") { + let remote_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let remote_pool_address = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + add_remote_pool(&caller, remote_chain_selector, remote_pool_address); + } else if (function_bytes == b"remove_remote_pool") { + let remote_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let remote_pool_address = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + remove_remote_pool(&caller, remote_chain_selector, remote_pool_address); + } else if (function_bytes == b"apply_chain_updates") { + let remote_chain_selectors_to_remove = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let remote_chain_selectors_to_add = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let remote_pool_addresses_to_add = + bcs_stream::deserialize_vector( + &mut stream, + |stream| bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ) + ); + let remote_token_addresses_to_add = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + bcs_stream::assert_is_consumed(&stream); + apply_chain_updates( + &caller, + remote_chain_selectors_to_remove, + remote_chain_selectors_to_add, + remote_pool_addresses_to_add, + remote_token_addresses_to_add + ); + } else if (function_bytes == b"set_allowlist_enabled") { + let enabled = bcs_stream::deserialize_bool(&mut stream); + bcs_stream::assert_is_consumed(&stream); + set_allowlist_enabled(&caller, enabled); + } else if (function_bytes == b"apply_allowlist_updates") { + let removes = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + let adds = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + apply_allowlist_updates(&caller, removes, adds); + } else if (function_bytes == b"set_chain_rate_limiter_configs") { + let remote_chain_selectors = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let outbound_is_enableds = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_bool(stream) + ); + let outbound_capacities = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let outbound_rates = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let inbound_is_enableds = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_bool(stream) + ); + let inbound_capacities = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let inbound_rates = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + bcs_stream::assert_is_consumed(&stream); + set_chain_rate_limiter_configs( + &caller, + remote_chain_selectors, + outbound_is_enableds, + outbound_capacities, + outbound_rates, + inbound_is_enableds, + inbound_capacities, + inbound_rates + ); + } else if (function_bytes == b"set_chain_rate_limiter_config") { + let remote_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let outbound_is_enabled = bcs_stream::deserialize_bool(&mut stream); + let outbound_capacity = bcs_stream::deserialize_u64(&mut stream); + let outbound_rate = bcs_stream::deserialize_u64(&mut stream); + let inbound_is_enabled = bcs_stream::deserialize_bool(&mut stream); + let inbound_capacity = bcs_stream::deserialize_u64(&mut stream); + let inbound_rate = bcs_stream::deserialize_u64(&mut stream); + bcs_stream::assert_is_consumed(&stream); + set_chain_rate_limiter_config( + &caller, + remote_chain_selector, + outbound_is_enabled, + outbound_capacity, + outbound_rate, + inbound_is_enabled, + inbound_capacity, + inbound_rate + ); + } else if (function_bytes == b"transfer_ownership") { + let to = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + transfer_ownership(&caller, to); + } else if (function_bytes == b"accept_ownership") { + bcs_stream::assert_is_consumed(&stream); + accept_ownership(&caller); + } else if (function_bytes == b"execute_ownership_transfer") { + let to = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + execute_ownership_transfer(&caller, to) + } else { + abort error::invalid_argument(E_UNKNOWN_FUNCTION) + }; + + option::none() + } + + /// Callable during upgrades + public(friend) fun register_mcms_entrypoint( + publisher: &signer, module_name: vector + ) { + mcms_registry::register_entrypoint( + publisher, string::utf8(module_name), McmsCallback {} + ); + } +} +` diff --git a/ccip-sdk/src/cct/aptos/bytecodes/ccip.ts b/ccip-sdk/src/cct/aptos/bytecodes/ccip.ts new file mode 100644 index 00000000..bba40d29 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/bytecodes/ccip.ts @@ -0,0 +1,5891 @@ +/** + * ChainlinkCCIP Move sources — embedded from chainlink-aptos. + * + * These sources are compiled locally alongside pool packages so that + * the compiled bytecode matches the on-chain modules exactly. + * + * @packageDocumentation + */ + +/** Move.toml for ChainlinkCCIP — uses local path for MCMS dependency. */ +export const CCIP_MOVE_TOML = `[package] +name = "ChainlinkCCIP" +version = "1.0.0" +upgrade_policy = "compatible" + +[addresses] +ccip = "_" +mcms = "_" +mcms_owner = "0x0" +mcms_register_entrypoints = "_" + +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "16beac69835f3a71564c96164a606a23f259099a", subdir = "aptos-move/framework/aptos-framework" } +ChainlinkManyChainMultisig = { local = "../mcms" } +` + +/** sources/allowlist.move */ +export const CCIP_ALLOWLIST_MOVE = `module ccip::allowlist { + use std::account; + use std::event::{Self, EventHandle}; + use std::error; + use std::string::{Self, String}; + + struct AllowlistState has store { + allowlist_name: String, + allowlist_enabled: bool, + allowlist: vector
, + allowlist_add_events: EventHandle, + allowlist_remove_events: EventHandle + } + + #[event] + struct AllowlistRemove has store, drop { + allowlist_name: String, + removed_address: address + } + + #[event] + struct AllowlistAdd has store, drop { + allowlist_name: String, + added_address: address + } + + const E_ALLOWLIST_NOT_ENABLED: u64 = 1; + + public fun new(event_account: &signer, allowlist: vector
): AllowlistState { + new_with_name(event_account, allowlist, string::utf8(b"default")) + } + + public fun new_with_name( + event_account: &signer, allowlist: vector
, allowlist_name: String + ): AllowlistState { + AllowlistState { + allowlist_name, + allowlist_enabled: !allowlist.is_empty(), + allowlist, + allowlist_add_events: account::new_event_handle(event_account), + allowlist_remove_events: account::new_event_handle(event_account) + } + } + + public fun get_allowlist_enabled(state: &AllowlistState): bool { + state.allowlist_enabled + } + + public fun set_allowlist_enabled( + state: &mut AllowlistState, enabled: bool + ) { + state.allowlist_enabled = enabled; + } + + public fun get_allowlist(state: &AllowlistState): vector
{ + state.allowlist + } + + public fun is_allowed(state: &AllowlistState, sender: address): bool { + if (!state.allowlist_enabled) { + return true + }; + + state.allowlist.contains(&sender) + } + + public fun apply_allowlist_updates( + state: &mut AllowlistState, removes: vector
, adds: vector
+ ) { + removes.for_each_ref( + |removed_address| { + let removed_address: address = *removed_address; + let (found, i) = state.allowlist.index_of(&removed_address); + if (found) { + state.allowlist.swap_remove(i); + event::emit_event( + &mut state.allowlist_remove_events, + AllowlistRemove { + allowlist_name: state.allowlist_name, + removed_address + } + ); + } + } + ); + + if (!adds.is_empty()) { + assert!( + state.allowlist_enabled, + error::invalid_state(E_ALLOWLIST_NOT_ENABLED) + ); + + adds.for_each_ref( + |added_address| { + let added_address: address = *added_address; + if (added_address != @0x0 + && !state.allowlist.contains(&added_address)) { + state.allowlist.push_back(added_address); + event::emit_event( + &mut state.allowlist_add_events, + AllowlistAdd { + allowlist_name: state.allowlist_name, + added_address + } + ); + } + } + ); + } + } + + public fun destroy_allowlist(state: AllowlistState) { + let AllowlistState { + allowlist_name: _, + allowlist_enabled: _, + allowlist: _, + allowlist_add_events: add_events, + allowlist_remove_events: remove_events + } = state; + + event::destroy_handle(add_events); + event::destroy_handle(remove_events); + } + + #[test_only] + public fun new_add_event(add: address): AllowlistAdd { + AllowlistAdd { + added_address: add, + allowlist_name: string::utf8(b"default") + } + } + + #[test_only] + public fun new_remove_event(remove: address): AllowlistRemove { + AllowlistRemove { + removed_address: remove, + allowlist_name: string::utf8(b"default") + } + } + + #[test_only] + public fun get_allowlist_add_events(state: &AllowlistState): &EventHandle { + &state.allowlist_add_events + } + + #[test_only] + public fun get_allowlist_remove_events(state: &AllowlistState) + : &EventHandle { + &state.allowlist_remove_events + } +} + +#[test_only] +module ccip::allowlist_test { + use std::account; + use std::event; + use std::signer; + use std::vector; + + use ccip::allowlist::{Self, AllowlistAdd, AllowlistRemove}; + + #[test(owner = @0x0)] + fun init_empty_is_empty_and_disabled(owner: &signer) { + let state = set_up_test(owner, vector::empty()); + + assert!(!allowlist::get_allowlist_enabled(&state)); + assert!(allowlist::get_allowlist(&state).is_empty()); + + // Any address is allowed when the allowlist is disabled + assert!(allowlist::is_allowed(&state, @0x1111111111111)); + + allowlist::destroy_allowlist(state); + } + + #[test(owner = @0x0)] + fun init_non_empty_is_non_empty_and_enabled(owner: &signer) { + let init_allowlist = vector[@0x1, @0x2]; + + let state = set_up_test(owner, init_allowlist); + + assert!(allowlist::get_allowlist_enabled(&state)); + assert!(allowlist::get_allowlist(&state).length() == 2); + + // The given addresses are allowed + assert!(allowlist::is_allowed(&state, init_allowlist[0])); + assert!(allowlist::is_allowed(&state, init_allowlist[1])); + + // Other addresses are not allowed + assert!(!allowlist::is_allowed(&state, @0x3)); + + allowlist::destroy_allowlist(state); + } + + #[test(owner = @0x0)] + #[expected_failure(abort_code = 0x30001, location = allowlist)] + fun cannot_add_to_disabled_allowlist(owner: &signer) { + let state = set_up_test(owner, vector::empty()); + + let adds = vector[@0x1]; + + allowlist::apply_allowlist_updates(&mut state, vector::empty(), adds); + + allowlist::destroy_allowlist(state); + } + + #[test(owner = @0x0)] + fun apply_allowlist_updates_mutates_state(owner: &signer) { + let state = set_up_test(owner, vector::empty()); + allowlist::set_allowlist_enabled(&mut state, true); + + assert!(allowlist::get_allowlist(&state).is_empty()); + + allowlist::apply_allowlist_updates(&mut state, vector::empty(), vector::empty()); + + assert!(allowlist::get_allowlist(&state).is_empty()); + + let adds = vector[@0x1, @0x2]; + + allowlist::apply_allowlist_updates(&mut state, vector::empty(), adds); + + assert_add_events_emitted(adds, &state); + + let removes = vector[@0x1]; + + allowlist::apply_allowlist_updates(&mut state, removes, vector::empty()); + + assert_remove_events_emitted(removes, &state); + + assert!(allowlist::get_allowlist(&state).length() == 1); + assert!(allowlist::is_allowed(&state, @0x2)); + assert!(!allowlist::is_allowed(&state, @0x1)); + + allowlist::destroy_allowlist(state); + } + + #[test(owner = @0x0)] + fun apply_allowlist_updates_removes_before_adds(owner: &signer) { + let account_to_allow = @0x1; + let state = set_up_test(owner, vector::empty()); + allowlist::set_allowlist_enabled(&mut state, true); + + let adds_and_removes = vector[account_to_allow]; + + allowlist::apply_allowlist_updates(&mut state, vector::empty(), adds_and_removes); + + assert!(allowlist::get_allowlist(&state).length() == 1); + assert!(allowlist::is_allowed(&state, account_to_allow)); + + allowlist::apply_allowlist_updates(&mut state, adds_and_removes, adds_and_removes); + + // Since removes happen before adds, the account should still be allowed + assert!(allowlist::is_allowed(&state, account_to_allow)); + + assert_remove_events_emitted(adds_and_removes, &state); + // Events don't get purged after calling event::emitted_events so we'll have + // both the first and the second add event in the emitted events + adds_and_removes.push_back(account_to_allow); + assert_add_events_emitted(adds_and_removes, &state); + + allowlist::destroy_allowlist(state); + } + + inline fun assert_add_events_emitted( + added_addresses: vector
, state: &allowlist::AllowlistState + ) { + let expected = + added_addresses.map:: ( + |add| allowlist::new_add_event(add) + ); + let got = + event::emitted_events_by_handle( + allowlist::get_allowlist_add_events(state) + ); + let number_of_adds = expected.length(); + + // Assert that exactly one event was emitted for each add + assert!(got.length() == number_of_adds); + + // Assert that the emitted events match the expected events + for (i in 0..number_of_adds) { + assert!(expected.borrow(i) == got.borrow(i)); + } + } + + inline fun assert_remove_events_emitted( + added_addresses: vector
, state: &allowlist::AllowlistState + ) { + let expected = + added_addresses.map:: ( + |add| allowlist::new_remove_event(add) + ); + let got = + event::emitted_events_by_handle( + allowlist::get_allowlist_remove_events(state) + ); + let number_of_adds = expected.length(); + + // Assert that exactly one event was emitted for each add + assert!(got.length() == number_of_adds); + + // Assert that the emitted events match the expected events + for (i in 0..number_of_adds) { + assert!(expected.borrow(i) == got.borrow(i)); + } + } + + inline fun set_up_test(owner: &signer, allowlist: vector
) + : allowlist::AllowlistState { + account::create_account_for_test(signer::address_of(owner)); + + allowlist::new(owner, allowlist) + } +} +` + +/** sources/auth.move */ +export const CCIP_AUTH_MOVE = `module ccip::auth { + use std::error; + use std::object; + use std::option::{Self, Option}; + use std::signer; + use std::string; + + use ccip::allowlist; + use ccip::ownable; + use ccip::state_object; + + use mcms::bcs_stream; + use mcms::mcms_registry; + + struct AuthState has key { + ownable_state: ownable::OwnableState, + allowed_onramps: allowlist::AllowlistState, + allowed_offramps: allowlist::AllowlistState + } + + const E_UNKNOWN_FUNCTION: u64 = 1; + const E_NOT_ALLOWED_ONRAMP: u64 = 2; + const E_NOT_ALLOWED_OFFRAMP: u64 = 3; + const E_NOT_OWNER_OR_CCIP: u64 = 4; + + fun init_module(publisher: &signer) { + let state_object_signer = &state_object::object_signer(); + + let allowed_onramps = + allowlist::new_with_name( + state_object_signer, vector[], string::utf8(b"onramps") + ); + allowlist::set_allowlist_enabled(&mut allowed_onramps, true); + + let allowed_offramps = + allowlist::new_with_name( + state_object_signer, vector[], string::utf8(b"offramps") + ); + allowlist::set_allowlist_enabled(&mut allowed_offramps, true); + + move_to( + state_object_signer, + AuthState { + ownable_state: ownable::new(state_object_signer, @ccip), + allowed_onramps, + allowed_offramps + } + ); + + // Register the entrypoint with mcms + if (@mcms_register_entrypoints == @0x1) { + register_mcms_entrypoint(publisher); + }; + } + + #[view] + public fun get_allowed_onramps(): vector
acquires AuthState { + allowlist::get_allowlist(&borrow_state().allowed_onramps) + } + + #[view] + public fun get_allowed_offramps(): vector
acquires AuthState { + allowlist::get_allowlist(&borrow_state().allowed_offramps) + } + + #[view] + public fun is_onramp_allowed(onramp_address: address): bool acquires AuthState { + allowlist::is_allowed(&borrow_state().allowed_onramps, onramp_address) + } + + #[view] + public fun is_offramp_allowed(offramp_address: address): bool acquires AuthState { + allowlist::is_allowed(&borrow_state().allowed_offramps, offramp_address) + } + + public entry fun apply_allowed_onramp_updates( + caller: &signer, onramps_to_remove: vector
, onramps_to_add: vector
+ ) acquires AuthState { + let state = borrow_state_mut(); + + assert_is_owner_or_ccip(signer::address_of(caller), &state.ownable_state); + + allowlist::apply_allowlist_updates( + &mut state.allowed_onramps, onramps_to_remove, onramps_to_add + ); + } + + public entry fun apply_allowed_offramp_updates( + caller: &signer, + offramps_to_remove: vector
, + offramps_to_add: vector
+ ) acquires AuthState { + let state = borrow_state_mut(); + + assert_is_owner_or_ccip(signer::address_of(caller), &state.ownable_state); + + allowlist::apply_allowlist_updates( + &mut state.allowed_offramps, offramps_to_remove, offramps_to_add + ); + } + + inline fun borrow_state(): &AuthState { + borrow_global(state_object::object_address()) + } + + inline fun borrow_state_mut(): &mut AuthState { + borrow_global_mut(state_object::object_address()) + } + + inline fun assert_is_owner_or_ccip( + caller: address, ownable_state: &ownable::OwnableState + ) { + assert!( + caller == @ccip || caller == ownable::owner(ownable_state), + error::permission_denied(E_NOT_OWNER_OR_CCIP) + ); + } + + public fun assert_is_allowed_onramp(caller: address) acquires AuthState { + assert!( + allowlist::is_allowed(&borrow_state().allowed_onramps, caller), + error::permission_denied(E_NOT_ALLOWED_ONRAMP) + ); + } + + public fun assert_is_allowed_offramp(caller: address) acquires AuthState { + assert!( + allowlist::is_allowed(&borrow_state().allowed_offramps, caller), + error::permission_denied(E_NOT_ALLOWED_OFFRAMP) + ); + } + + // ================================================================ + // | Ownable | + // ================================================================ + #[view] + public fun owner(): address acquires AuthState { + ownable::owner(&borrow_state().ownable_state) + } + + #[view] + public fun has_pending_transfer(): bool acquires AuthState { + ownable::has_pending_transfer(&borrow_state().ownable_state) + } + + #[view] + public fun pending_transfer_from(): Option
acquires AuthState { + ownable::pending_transfer_from(&borrow_state().ownable_state) + } + + #[view] + public fun pending_transfer_to(): Option
acquires AuthState { + ownable::pending_transfer_to(&borrow_state().ownable_state) + } + + #[view] + public fun pending_transfer_accepted(): Option acquires AuthState { + ownable::pending_transfer_accepted(&borrow_state().ownable_state) + } + + public fun assert_only_owner(caller: address) acquires AuthState { + ownable::assert_only_owner(caller, &borrow_state().ownable_state) + } + + public entry fun transfer_ownership(caller: &signer, to: address) acquires AuthState { + let state = borrow_state_mut(); + ownable::transfer_ownership(caller, &mut state.ownable_state, to) + } + + public entry fun accept_ownership(caller: &signer) acquires AuthState { + let state = borrow_state_mut(); + ownable::accept_ownership(caller, &mut state.ownable_state) + } + + public entry fun execute_ownership_transfer( + caller: &signer, to: address + ) acquires AuthState { + let state = borrow_state_mut(); + ownable::execute_ownership_transfer(caller, &mut state.ownable_state, to) + } + + // ================================================================ + // | MCMS Entrypoint | + // ================================================================ + struct McmsCallback has drop {} + + public fun mcms_entrypoint( + _metadata: object::Object + ): option::Option acquires AuthState { + let (caller, function, data) = + mcms_registry::get_callback_params(@ccip, McmsCallback {}); + + let function_bytes = *function.bytes(); + let stream = bcs_stream::new(data); + + if (function_bytes == b"apply_allowed_onramp_updates") { + let onramps_to_remove = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + let onramps_to_add = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + apply_allowed_onramp_updates(&caller, onramps_to_remove, onramps_to_add) + } else if (function_bytes == b"apply_allowed_offramp_updates") { + let offramps_to_remove = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + let offramps_to_add = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + apply_allowed_offramp_updates(&caller, offramps_to_remove, offramps_to_add) + } else if (function_bytes == b"transfer_ownership") { + let to = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + transfer_ownership(&caller, to) + } else if (function_bytes == b"accept_ownership") { + bcs_stream::assert_is_consumed(&stream); + accept_ownership(&caller) + } else if (function_bytes == b"execute_ownership_transfer") { + let to = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + execute_ownership_transfer(&caller, to) + } else { + abort error::invalid_argument(E_UNKNOWN_FUNCTION) + }; + + option::none() + } + + /// Callable during upgrades + public(friend) fun register_mcms_entrypoint(publisher: &signer) { + mcms_registry::register_entrypoint( + publisher, string::utf8(b"auth"), McmsCallback {} + ); + } + + // ========================== TEST ONLY ========================== + #[test_only] + public fun test_init_module(publisher: &signer) { + init_module(publisher); + } + + #[test_only] + public fun test_register_mcms_entrypoint(publisher: &signer) { + mcms_registry::register_entrypoint( + publisher, string::utf8(b"auth"), McmsCallback {} + ); + } +} +` + +/** sources/client.move */ +export const CCIP_CLIENT_MOVE = `/// This module defines messages for end users to interact with Aptos CCIP. +module ccip::client { + use std::bcs; + + const GENERIC_EXTRA_ARGS_V2_TAG: vector = x"181dcf10"; + const SVM_EXTRA_ARGS_V1_TAG: vector = x"1f3b3aba"; + + const E_INVALID_SVM_TOKEN_RECEIVER_LENGTH: u64 = 1; + const E_INVALID_SVM_ACCOUNT_LENGTH: u64 = 2; + + #[view] + public fun generic_extra_args_v2_tag(): vector { + GENERIC_EXTRA_ARGS_V2_TAG + } + + #[view] + public fun svm_extra_args_v1_tag(): vector { + SVM_EXTRA_ARGS_V1_TAG + } + + #[view] + public fun encode_generic_extra_args_v2( + gas_limit: u256, allow_out_of_order_execution: bool + ): vector { + let extra_args = vector[]; + extra_args.append(GENERIC_EXTRA_ARGS_V2_TAG); + extra_args.append(bcs::to_bytes(&gas_limit)); + extra_args.append(bcs::to_bytes(&allow_out_of_order_execution)); + extra_args + } + + #[view] + public fun encode_svm_extra_args_v1( + compute_units: u32, + account_is_writable_bitmap: u64, + allow_out_of_order_execution: bool, + token_receiver: vector, + accounts: vector> + ): vector { + let extra_args = vector[]; + extra_args.append(SVM_EXTRA_ARGS_V1_TAG); + extra_args.append(bcs::to_bytes(&compute_units)); + extra_args.append(bcs::to_bytes(&account_is_writable_bitmap)); + extra_args.append(bcs::to_bytes(&allow_out_of_order_execution)); + + assert!(token_receiver.length() == 32, E_INVALID_SVM_TOKEN_RECEIVER_LENGTH); + accounts.for_each_ref( + |account| { + assert!(account.length() == 32, E_INVALID_SVM_ACCOUNT_LENGTH); + } + ); + + extra_args.append(bcs::to_bytes(&token_receiver)); + extra_args.append(bcs::to_bytes(&accounts)); + extra_args + } + + struct Any2AptosMessage has store, drop, copy { + message_id: vector, + source_chain_selector: u64, + sender: vector, + data: vector, + dest_token_amounts: vector + } + + struct Any2AptosTokenAmount has store, drop, copy { + token: address, + amount: u64 + } + + public fun new_any2aptos_message( + message_id: vector, + source_chain_selector: u64, + sender: vector, + data: vector, + dest_token_amounts: vector + ): Any2AptosMessage { + Any2AptosMessage { + message_id, + source_chain_selector, + sender, + data, + dest_token_amounts + } + } + + public fun new_dest_token_amounts( + token_addresses: vector
, token_amounts: vector + ): vector { + token_addresses.zip_map_ref( + &token_amounts, + |token_address, token_amount| { + Any2AptosTokenAmount { token: *token_address, amount: *token_amount } + } + ) + } + + // Any2AptosMessage accessors + public fun get_message_id(input: &Any2AptosMessage): vector { + input.message_id + } + + public fun get_source_chain_selector(input: &Any2AptosMessage): u64 { + input.source_chain_selector + } + + public fun get_sender(input: &Any2AptosMessage): vector { + input.sender + } + + public fun get_data(input: &Any2AptosMessage): vector { + input.data + } + + public fun get_dest_token_amounts(input: &Any2AptosMessage) + : vector { + input.dest_token_amounts + } + + // Any2AptosTokenAmount accessors + public fun get_token(input: &Any2AptosTokenAmount): address { + input.token + } + + public fun get_amount(input: &Any2AptosTokenAmount): u64 { + input.amount + } +} +` + +/** sources/eth_abi.move */ +export const CCIP_ETH_ABI_MOVE = `// module to do the equivalent packing as ethereum's abi.encode and abi.encodePacked +module ccip::eth_abi { + use std::bcs; + use std::error; + use std::from_bcs; + use std::vector; + + const ENCODED_BOOL_FALSE: vector = vector[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + const ENCODED_BOOL_TRUE: vector = vector[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; + + const E_OUT_OF_BYTES: u64 = 1; + const E_INVALID_ADDRESS: u64 = 2; + const E_INVALID_BOOL: u64 = 3; + const E_INVALID_SELECTOR: u64 = 4; + const E_INVALID_U256_LENGTH: u64 = 5; + const E_INTEGER_OVERFLOW: u64 = 6; + const E_INVALID_BYTES32_LENGTH: u64 = 7; + + public inline fun encode_address(out: &mut vector, value: address) { + out.append(bcs::to_bytes(&value)) + } + + public inline fun encode_u8(out: &mut vector, value: u8) { + encode_u256(out, value as u256); + } + + public inline fun encode_u32(out: &mut vector, value: u32) { + encode_u256(out, value as u256) + } + + public inline fun encode_u64(out: &mut vector, value: u64) { + encode_u256(out, value as u256) + } + + public inline fun encode_u256(out: &mut vector, value: u256) { + let value_bytes = bcs::to_bytes(&value); + // little endian to big endian + value_bytes.reverse(); + out.append(value_bytes) + } + + public fun encode_bool(out: &mut vector, value: bool) { + out.append(if (value) ENCODED_BOOL_TRUE else ENCODED_BOOL_FALSE) + } + + /// For numeric types (address, uint, int) - left padded with zeros + public inline fun encode_left_padded_bytes32( + out: &mut vector, value: vector + ) { + assert!(value.length() <= 32, error::invalid_argument(E_INVALID_U256_LENGTH)); + + let padding_len = 32 - value.length(); + for (i in 0..padding_len) { + out.push_back(0); + }; + out.append(value); + } + + /// For byte array types (bytes32, bytes4, etc.) - right padded with zeros + public inline fun encode_right_padded_bytes32( + out: &mut vector, value: vector + ) { + assert!(value.length() <= 32, E_INVALID_BYTES32_LENGTH); + + out.append(value); + let padding_len = 32 - value.length(); + for (i in 0..padding_len) { + out.push_back(0); + }; + } + + public inline fun encode_bytes(out: &mut vector, value: vector) { + encode_u256(out, (value.length() as u256)); + + out.append(value); + if (value.length() % 32 != 0) { + let padding_len = 32 - (value.length() % 32); + for (i in 0..padding_len) { + out.push_back(0); + } + } + } + + public fun encode_selector(out: &mut vector, value: vector) { + assert!(value.length() == 4, error::invalid_argument(E_INVALID_SELECTOR)); + out.append(value); + } + + public inline fun encode_packed_address( + out: &mut vector, value: address + ) { + out.append(bcs::to_bytes(&value)) + } + + public inline fun encode_packed_bytes( + out: &mut vector, value: vector + ) { + out.append(value) + } + + public inline fun encode_packed_bytes32( + out: &mut vector, value: vector + ) { + assert!(value.length() <= 32, E_INVALID_BYTES32_LENGTH); + + out.append(value); + let padding_len = 32 - value.length(); + for (i in 0..padding_len) { + out.push_back(0); + }; + } + + public inline fun encode_packed_u8(out: &mut vector, value: u8) { + out.push_back(value) + } + + public inline fun encode_packed_u32(out: &mut vector, value: u32) { + let value_bytes = bcs::to_bytes(&value); + // little endian to big endian + value_bytes.reverse(); + out.append(value_bytes) + } + + public inline fun encode_packed_u64(out: &mut vector, value: u64) { + let value_bytes = bcs::to_bytes(&value); + // little endian to big endian + value_bytes.reverse(); + out.append(value_bytes) + } + + public inline fun encode_packed_u256(out: &mut vector, value: u256) { + let value_bytes = bcs::to_bytes(&value); + // little endian to big endian + value_bytes.reverse(); + out.append(value_bytes) + } + + struct ABIStream has drop { + data: vector, + cur: u64 + } + + public fun new_stream(data: vector): ABIStream { + ABIStream { data, cur: 0 } + } + + public fun decode_address(stream: &mut ABIStream): address { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 32 <= data.length(), error::out_of_range(E_OUT_OF_BYTES) + ); + + // Verify first 12 bytes are zero + for (i in 0..12) { + assert!( + data[cur + i] == 0, error::invalid_argument(E_INVALID_ADDRESS) + ); + }; + + // Extract last 20 bytes for address + let addr_bytes = data.slice(cur + 12, cur + 32); + stream.cur = cur + 32; + + from_bcs::to_address(addr_bytes) + } + + public fun decode_u256(stream: &mut ABIStream): u256 { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 32 <= data.length(), error::out_of_range(E_OUT_OF_BYTES) + ); + + let value_bytes = data.slice(cur, cur + 32); + // Convert from big endian to little endian + value_bytes.reverse(); + + stream.cur = cur + 32; + from_bcs::to_u256(value_bytes) + } + + public fun decode_u8(stream: &mut ABIStream): u8 { + let value = decode_u256(stream); + assert!(value <= 0xFF, error::invalid_argument(E_INTEGER_OVERFLOW)); + (value as u8) + } + + public fun decode_u32(stream: &mut ABIStream): u32 { + let value = decode_u256(stream); + assert!(value <= 0xFFFFFFFF, error::invalid_argument(E_INTEGER_OVERFLOW)); + (value as u32) + } + + public fun decode_u64(stream: &mut ABIStream): u64 { + let value = decode_u256(stream); + assert!(value <= 0xFFFFFFFFFFFFFFFF, error::invalid_argument(E_INTEGER_OVERFLOW)); + (value as u64) + } + + public fun decode_bool(stream: &mut ABIStream): bool { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 32 <= data.length(), error::out_of_range(E_OUT_OF_BYTES) + ); + + let value = data.slice(cur, cur + 32); + stream.cur = cur + 32; + + if (value == ENCODED_BOOL_FALSE) { false } + else if (value == ENCODED_BOOL_TRUE) { true } + else { + abort error::invalid_argument(E_INVALID_BOOL) + } + } + + public fun decode_bytes32(stream: &mut ABIStream): vector { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 32 <= data.length(), error::out_of_range(E_OUT_OF_BYTES) + ); + + let bytes = data.slice(cur, cur + 32); + stream.cur = cur + 32; + bytes + } + + public fun decode_bytes(stream: &mut ABIStream): vector { + // First read length as u256 + let length = (decode_u256(stream) as u64); + + let padding_len = if (length % 32 == 0) { 0 } + else { + 32 - (length % 32) + }; + + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + length + padding_len <= data.length(), + error::out_of_range(E_OUT_OF_BYTES) + ); + + let bytes = data.slice(cur, cur + length); + + // Skip padding bytes + stream.cur = cur + length + padding_len; + + bytes + } + + public inline fun decode_vector( + stream: &mut ABIStream, elem_decoder: |&mut ABIStream| E + ): vector { + let len = decode_u256(stream); + let v = vector::empty(); + + for (i in 0..len) { + v.push_back(elem_decoder(stream)); + }; + + v + } + + public fun decode_u256_value(value_bytes: vector): u256 { + assert!( + value_bytes.length() == 32, + error::invalid_argument(E_INVALID_U256_LENGTH) + ); + value_bytes.reverse(); + from_bcs::to_u256(value_bytes) + } +} +` + +/** sources/fee_quoter.move */ +export const CCIP_FEE_QUOTER_MOVE = `/// This module is responsible for storage and retrieval of fee token and token transfer +/// information and pricing. +module ccip::fee_quoter { + use std::account; + use std::bcs; + use std::error; + use std::event::{Self, EventHandle}; + use std::fungible_asset::Metadata; + use std::object; + use std::option; + use std::signer; + use std::string::{Self, String}; + use std::smart_table::{Self, SmartTable}; + use std::timestamp; + + use ccip::auth; + use ccip::client; + use ccip::eth_abi; + use ccip::state_object; + + use mcms::bcs_stream; + use mcms::mcms_registry; + + const CHAIN_FAMILY_SELECTOR_EVM: vector = x"2812d52c"; + const CHAIN_FAMILY_SELECTOR_SVM: vector = x"1e10bdc4"; + const CHAIN_FAMILY_SELECTOR_APTOS: vector = x"ac77ffec"; + const CHAIN_FAMILY_SELECTOR_SUI: vector = x"c4e05953"; + + /// @dev We disallow the first 1024 addresses to avoid calling into a range known for hosting precompiles. Calling + /// into precompiles probably won't cause any issues, but to be safe we can disallow this range. It is extremely + /// unlikely that anyone would ever be able to generate an address in this range. There is no official range of + /// precompiles, but EIP-7587 proposes to reserve the range 0x100 to 0x1ff. Our range is more conservative, even + /// though it might not be exhaustive for all chains, which is OK. We also disallow the zero address, which is a + /// common practice. + const EVM_PRECOMPILE_SPACE: u256 = 1024; + + /// @dev According to the Aptos docs, the first 0xa addresses are reserved for precompiles. + /// https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/doc/account.md#function-create_framework_reserved_account-1 + /// We use the same range for SUI, even though there is one documented reserved address outside of this range. + /// Since sending a message to this address would not cause any negative side effects, as it would never register + /// a callback with CCIP, there is no negative impact. + /// https://move-book.com/appendix/reserved-addresses.html + const MOVE_PRECOMPILE_SPACE: u256 = 0x0b; + + const ALLOW_OUT_OF_ORDER_EXECUTION: bool = true; + + const GAS_PRICE_BITS: u8 = 112; + const GAS_PRICE_MASK_112_BITS: u256 = 0xffffffffffffffffffffffffffff; // 28 f's + + const MESSAGE_FIXED_BYTES: u64 = 32 * 15; + const MESSAGE_FIXED_BYTES_PER_TOKEN: u64 = 32 * (4 + (3 + 2)); + + const CCIP_LOCK_OR_BURN_V1_RET_BYTES: u32 = 32; + + /// The maximum number of accounts that can be passed in SVMExtraArgs. + const SVM_EXTRA_ARGS_MAX_ACCOUNTS: u64 = 64; + + /// Number of overhead accounts needed for message execution on SVM. + /// These are message.receiver, and the OffRamp Signer PDA specific to the receiver. + const SVM_MESSAGING_ACCOUNTS_OVERHEAD: u64 = 2; + + /// The size of each SVM account (in bytes). + const SVM_ACCOUNT_BYTE_SIZE: u64 = 32; + + /// The expected static payload size of a token transfer when Borsh encoded and submitted to SVM. + /// TokenPool extra data and offchain data sizes are dynamic, and should be accounted for separately. + const SVM_TOKEN_TRANSFER_DATA_OVERHEAD: u64 = (4 + 32) // source_pool + + 32 // token_address + + 4 // gas_amount + + 4 // extra_data overhead + + 32 // amount + + 32 // size of the token lookup table account + + 32 // token-related accounts in the lookup table, over-estimated to 32, typically between 11 - 13 + + 32 // token account belonging to the token receiver, e.g ATA, not included in the token lookup table + + 32 // per-chain token pool config, not included in the token lookup table + + 32 // per-chain token billing config, not always included in the token lookup table + + 32; // OffRamp pool signer PDA, not included in the token lookup table; + + const MAX_U64: u256 = 18446744073709551615; + const MAX_U160: u256 = 1461501637330902918203684832716283019655932542975; + const MAX_U256: u256 = + 115792089237316195423570985008687907853269984665640564039457584007913129639935; + const VAL_1E5: u256 = 100_000; + const VAL_1E14: u256 = 100_000_000_000_000; + const VAL_1E16: u256 = 10_000_000_000_000_000; + const VAL_1E18: u256 = 1_000_000_000_000_000_000; + + // Link has 8 decimals on Aptos and 18 decimals on it's native chain, Ethereum. We want to emit + // the fee in juels (1e18) denomination for consistency across chains. This means we multiply + // the fee by 1e10 on Aptos before we emit it in the event. + const LOCAL_8_TO_18_DECIMALS_LINK_MULTIPLIER: u256 = 10_000_000_000; + + struct FeeQuoterState has key, store { + // max_fee_juels_per_msg is in juels (1e18) denomination for consistency across chains. + max_fee_juels_per_msg: u256, + link_token: address, + token_price_staleness_threshold: u64, + fee_tokens: vector
, + usd_per_unit_gas_by_dest_chain: SmartTable, + usd_per_token: SmartTable, + dest_chain_configs: SmartTable, + // dest chain selector -> local token -> TokenTransferFeeConfig + token_transfer_fee_configs: SmartTable>, + premium_multiplier_wei_per_eth: SmartTable, + fee_token_added_events: EventHandle, + fee_token_removed_events: EventHandle, + token_transfer_fee_config_added_events: EventHandle, + token_transfer_fee_config_removed_events: EventHandle, + usd_per_token_updated_events: EventHandle, + usd_per_unit_gas_updated_events: EventHandle, + dest_chain_added_events: EventHandle, + dest_chain_config_updated_events: EventHandle, + premium_multiplier_wei_per_eth_updated_events: EventHandle< + PremiumMultiplierWeiPerEthUpdated> + } + + struct StaticConfig has drop { + max_fee_juels_per_msg: u256, + link_token: address, + token_price_staleness_threshold: u64 + } + + struct DestChainConfig has store, drop, copy { + is_enabled: bool, + max_number_of_tokens_per_msg: u16, + max_data_bytes: u32, + max_per_msg_gas_limit: u32, + dest_gas_overhead: u32, + dest_gas_per_payload_byte_base: u8, + dest_gas_per_payload_byte_high: u8, + dest_gas_per_payload_byte_threshold: u16, + dest_data_availability_overhead_gas: u32, + dest_gas_per_data_availability_byte: u16, + dest_data_availability_multiplier_bps: u16, + chain_family_selector: vector, + enforce_out_of_order: bool, + default_token_fee_usd_cents: u16, + default_token_dest_gas_overhead: u32, + default_tx_gas_limit: u32, + // Multiplier for gas costs, 1e18 based so 11e17 = 10% extra cost. + gas_multiplier_wei_per_eth: u64, + gas_price_staleness_threshold: u32, + network_fee_usd_cents: u32 + } + + struct TokenTransferFeeConfig has store, drop, copy { + min_fee_usd_cents: u32, + max_fee_usd_cents: u32, + deci_bps: u16, + dest_gas_overhead: u32, + dest_bytes_overhead: u32, + is_enabled: bool + } + + struct TimestampedPrice has store, drop, copy { + value: u256, + timestamp: u64 + } + + #[event] + struct FeeTokenAdded has store, drop { + fee_token: address + } + + #[event] + struct FeeTokenRemoved has store, drop { + fee_token: address + } + + #[event] + struct TokenTransferFeeConfigAdded has store, drop { + dest_chain_selector: u64, + token: address, + token_transfer_fee_config: TokenTransferFeeConfig + } + + #[event] + struct TokenTransferFeeConfigRemoved has store, drop { + dest_chain_selector: u64, + token: address + } + + #[event] + struct UsdPerTokenUpdated has store, drop { + token: address, + usd_per_token: u256, + timestamp: u64 + } + + #[event] + struct UsdPerUnitGasUpdated has store, drop { + dest_chain_selector: u64, + usd_per_unit_gas: u256, + timestamp: u64 + } + + #[event] + struct DestChainAdded has store, drop { + dest_chain_selector: u64, + dest_chain_config: DestChainConfig + } + + #[event] + struct DestChainConfigUpdated has store, drop { + dest_chain_selector: u64, + dest_chain_config: DestChainConfig + } + + #[event] + struct PremiumMultiplierWeiPerEthUpdated has store, drop { + token: address, + premium_multiplier_wei_per_eth: u64 + } + + const E_ALREADY_INITIALIZED: u64 = 1; + const E_INVALID_LINK_TOKEN: u64 = 2; + const E_UNKNOWN_DEST_CHAIN_SELECTOR: u64 = 3; + const E_UNKNOWN_TOKEN: u64 = 4; + const E_DEST_CHAIN_NOT_ENABLED: u64 = 5; + const E_TOKEN_UPDATE_MISMATCH: u64 = 6; + const E_GAS_UPDATE_MISMATCH: u64 = 7; + const E_TOKEN_TRANSFER_FEE_CONFIG_MISMATCH: u64 = 8; + const E_FEE_TOKEN_NOT_SUPPORTED: u64 = 9; + const E_TOKEN_NOT_SUPPORTED: u64 = 10; + const E_UNKNOWN_CHAIN_FAMILY_SELECTOR: u64 = 11; + const E_STALE_GAS_PRICE: u64 = 12; + const E_MESSAGE_TOO_LARGE: u64 = 13; + const E_UNSUPPORTED_NUMBER_OF_TOKENS: u64 = 14; + const E_INVALID_EVM_ADDRESS: u64 = 15; + const E_INVALID_32BYTES_ADDRESS: u64 = 16; + const E_FEE_TOKEN_COST_TOO_HIGH: u64 = 17; + const E_MESSAGE_GAS_LIMIT_TOO_HIGH: u64 = 18; + const E_EXTRA_ARG_OUT_OF_ORDER_EXECUTION_MUST_BE_TRUE: u64 = 19; + const E_INVALID_EXTRA_ARGS_TAG: u64 = 20; + const E_INVALID_EXTRA_ARGS_DATA: u64 = 21; + const E_INVALID_TOKEN_RECEIVER: u64 = 22; + const E_MESSAGE_COMPUTE_UNIT_LIMIT_TOO_HIGH: u64 = 23; + const E_MESSAGE_FEE_TOO_HIGH: u64 = 24; + const E_SOURCE_TOKEN_DATA_TOO_LARGE: u64 = 25; + const E_INVALID_DEST_CHAIN_SELECTOR: u64 = 26; + const E_INVALID_GAS_LIMIT: u64 = 27; + const E_INVALID_CHAIN_FAMILY_SELECTOR: u64 = 28; + const E_TO_TOKEN_AMOUNT_TOO_LARGE: u64 = 29; + const E_UNKNOWN_FUNCTION: u64 = 30; + const E_ZERO_TOKEN_PRICE: u64 = 31; + const E_TOO_MANY_SVM_EXTRA_ARGS_ACCOUNTS: u64 = 32; + const E_INVALID_SVM_EXTRA_ARGS_WRITABLE_BITMAP: u64 = 33; + const E_INVALID_FEE_RANGE: u64 = 34; + const E_INVALID_DEST_BYTES_OVERHEAD: u64 = 35; + const E_INVALID_SVM_RECEIVER_LENGTH: u64 = 36; + const E_TOKEN_AMOUNT_MISMATCH: u64 = 37; + const E_INVALID_SVM_ACCOUNT_LENGTH: u64 = 38; + + #[view] + public fun type_and_version(): String { + string::utf8(b"FeeQuoter 1.6.0") + } + + fun init_module(publisher: &signer) { + // Register the entrypoint with mcms + if (@mcms_register_entrypoints == @0x1) { + register_mcms_entrypoint(publisher); + }; + } + + public entry fun initialize( + caller: &signer, + max_fee_juels_per_msg: u256, + link_token: address, + token_price_staleness_threshold: u64, + fee_tokens: vector
+ ) { + auth::assert_only_owner(signer::address_of(caller)); + + assert!( + !exists(state_object::object_address()), + error::invalid_argument(E_ALREADY_INITIALIZED) + ); + + assert!( + object::object_exists(link_token), + error::invalid_argument(E_INVALID_LINK_TOKEN) + ); + + let state_object_signer = state_object::object_signer(); + + let state = FeeQuoterState { + max_fee_juels_per_msg, + link_token, + token_price_staleness_threshold, + fee_tokens, + usd_per_unit_gas_by_dest_chain: smart_table::new(), + usd_per_token: smart_table::new(), + dest_chain_configs: smart_table::new(), + token_transfer_fee_configs: smart_table::new(), + premium_multiplier_wei_per_eth: smart_table::new(), + fee_token_added_events: account::new_event_handle(&state_object_signer), + fee_token_removed_events: account::new_event_handle(&state_object_signer), + token_transfer_fee_config_added_events: account::new_event_handle( + &state_object_signer + ), + token_transfer_fee_config_removed_events: account::new_event_handle( + &state_object_signer + ), + usd_per_token_updated_events: account::new_event_handle(&state_object_signer), + usd_per_unit_gas_updated_events: account::new_event_handle( + &state_object_signer + ), + dest_chain_added_events: account::new_event_handle(&state_object_signer), + dest_chain_config_updated_events: account::new_event_handle( + &state_object_signer + ), + premium_multiplier_wei_per_eth_updated_events: account::new_event_handle( + &state_object_signer + ) + }; + move_to(&state_object_signer, state); + } + + #[view] + public fun get_token_price(token: address): TimestampedPrice acquires FeeQuoterState { + get_token_price_internal(borrow_state(), token) + } + + public fun timestamped_price_value( + timestamped_price: &TimestampedPrice + ): u256 { + timestamped_price.value + } + + public fun timestamped_price_timestamp( + timestamped_price: &TimestampedPrice + ): u64 { + timestamped_price.timestamp + } + + #[view] + public fun get_token_prices( + tokens: vector
+ ): (vector) acquires FeeQuoterState { + let state = borrow_state(); + tokens.map_ref(|token| get_token_price_internal(state, *token)) + } + + #[view] + public fun get_dest_chain_gas_price( + dest_chain_selector: u64 + ): TimestampedPrice acquires FeeQuoterState { + get_dest_chain_gas_price_internal(borrow_state(), dest_chain_selector) + } + + #[view] + public fun get_token_and_gas_prices( + token: address, dest_chain_selector: u64 + ): (u256, u256) acquires FeeQuoterState { + let state = borrow_state(); + let dest_chain_config = get_dest_chain_config_internal( + state, dest_chain_selector + ); + assert!( + dest_chain_config.is_enabled, + error::invalid_argument(E_DEST_CHAIN_NOT_ENABLED) + ); + let token_price = get_token_price_internal(state, token); + let gas_price_value = + get_validated_gas_price_internal( + state, dest_chain_config, dest_chain_selector + ); + (token_price.value, gas_price_value) + } + + #[view] + public fun convert_token_amount( + from_token: address, from_token_amount: u64, to_token: address + ): u64 acquires FeeQuoterState { + let state = borrow_state(); + convert_token_amount_internal(state, from_token, from_token_amount, to_token) + } + + #[view] + public fun get_fee_tokens(): vector
acquires FeeQuoterState { + borrow_state().fee_tokens + } + + public entry fun apply_fee_token_updates( + caller: &signer, + fee_tokens_to_remove: vector
, + fee_tokens_to_add: vector
+ ) acquires FeeQuoterState { + auth::assert_only_owner(signer::address_of(caller)); + + let state = borrow_state_mut(); + + // Remove tokens + fee_tokens_to_remove.for_each_ref( + |fee_token| { + let fee_token = *fee_token; + let (found, index) = state.fee_tokens.index_of(&fee_token); + if (found) { + state.fee_tokens.remove(index); + event::emit_event( + &mut state.fee_token_removed_events, FeeTokenRemoved { fee_token } + ); + }; + } + ); + + // Add new tokens + fee_tokens_to_add.for_each_ref( + |fee_token| { + let fee_token = *fee_token; + let (found, _) = state.fee_tokens.index_of(&fee_token); + if (!found) { + state.fee_tokens.push_back(fee_token); + event::emit_event( + &mut state.fee_token_added_events, FeeTokenAdded { fee_token } + ); + }; + } + ); + } + + #[view] + public fun get_token_transfer_fee_config( + dest_chain_selector: u64, token: address + ): TokenTransferFeeConfig acquires FeeQuoterState { + *get_token_transfer_fee_config_internal( + borrow_state(), dest_chain_selector, token + ) + } + + inline fun get_token_transfer_fee_config_internal( + state: &FeeQuoterState, dest_chain_selector: u64, token: address + ): &TokenTransferFeeConfig { + let empty_fee_config = TokenTransferFeeConfig { + min_fee_usd_cents: 0, + max_fee_usd_cents: 0, + deci_bps: 0, + dest_gas_overhead: 0, + dest_bytes_overhead: 0, + is_enabled: false + }; + + if (!state.token_transfer_fee_configs.contains(dest_chain_selector)) { + &empty_fee_config + } else { + let dest_chain_fee_configs = + state.token_transfer_fee_configs.borrow(dest_chain_selector); + + dest_chain_fee_configs.borrow_with_default(token, &empty_fee_config) + } + } + + // Note that unlike EVM, this only allows changes for a single dest chain selector + // at a time. + public entry fun apply_token_transfer_fee_config_updates( + caller: &signer, + dest_chain_selector: u64, + add_tokens: vector
, + add_min_fee_usd_cents: vector, + add_max_fee_usd_cents: vector, + add_deci_bps: vector, + add_dest_gas_overhead: vector, + add_dest_bytes_overhead: vector, + add_is_enabled: vector, + remove_tokens: vector
+ ) acquires FeeQuoterState { + auth::assert_only_owner(signer::address_of(caller)); + + let state = borrow_state_mut(); + + if (!state.token_transfer_fee_configs.contains(dest_chain_selector)) { + state.token_transfer_fee_configs.add( + dest_chain_selector, smart_table::new() + ); + }; + let token_transfer_fee_configs = + state.token_transfer_fee_configs.borrow_mut(dest_chain_selector); + + let add_tokens_len = add_tokens.length(); + assert!( + add_tokens_len == add_min_fee_usd_cents.length(), + error::invalid_argument(E_TOKEN_TRANSFER_FEE_CONFIG_MISMATCH) + ); + assert!( + add_tokens_len == add_max_fee_usd_cents.length(), + error::invalid_argument(E_TOKEN_TRANSFER_FEE_CONFIG_MISMATCH) + ); + assert!( + add_tokens_len == add_deci_bps.length(), + error::invalid_argument(E_TOKEN_TRANSFER_FEE_CONFIG_MISMATCH) + ); + assert!( + add_tokens_len == add_dest_gas_overhead.length(), + error::invalid_argument(E_TOKEN_TRANSFER_FEE_CONFIG_MISMATCH) + ); + assert!( + add_tokens_len == add_dest_bytes_overhead.length(), + error::invalid_argument(E_TOKEN_TRANSFER_FEE_CONFIG_MISMATCH) + ); + assert!( + add_tokens_len == add_is_enabled.length(), + error::invalid_argument(E_TOKEN_TRANSFER_FEE_CONFIG_MISMATCH) + ); + + for (i in 0..add_tokens_len) { + let token = add_tokens[i]; + let min_fee_usd_cents = add_min_fee_usd_cents[i]; + let max_fee_usd_cents = add_max_fee_usd_cents[i]; + let deci_bps = add_deci_bps[i]; + let dest_gas_overhead = add_dest_gas_overhead[i]; + let dest_bytes_overhead = add_dest_bytes_overhead[i]; + let is_enabled = add_is_enabled[i]; + + let token_transfer_fee_config = TokenTransferFeeConfig { + min_fee_usd_cents, + max_fee_usd_cents, + deci_bps, + dest_gas_overhead, + dest_bytes_overhead, + is_enabled + }; + + if (token_transfer_fee_config.min_fee_usd_cents + >= token_transfer_fee_config.max_fee_usd_cents) { + abort error::invalid_argument(E_INVALID_FEE_RANGE); + }; + if (token_transfer_fee_config.dest_bytes_overhead + < CCIP_LOCK_OR_BURN_V1_RET_BYTES) { + abort error::invalid_argument(E_INVALID_DEST_BYTES_OVERHEAD); + }; + + token_transfer_fee_configs.upsert(token, token_transfer_fee_config); + + event::emit_event( + &mut state.token_transfer_fee_config_added_events, + TokenTransferFeeConfigAdded { + dest_chain_selector, + token, + token_transfer_fee_config + } + ); + }; + + remove_tokens.for_each_ref( + |token| { + let token: address = *token; + if (token_transfer_fee_configs.contains(token)) { + token_transfer_fee_configs.remove(token); + + event::emit_event( + &mut state.token_transfer_fee_config_removed_events, + TokenTransferFeeConfigRemoved { dest_chain_selector, token } + ); + } + } + ); + } + + public fun update_prices( + caller: &signer, + source_tokens: vector
, + source_usd_per_token: vector, + gas_dest_chain_selectors: vector, + gas_usd_per_unit_gas: vector + ) acquires FeeQuoterState { + auth::assert_is_allowed_offramp(signer::address_of(caller)); + + assert!( + source_tokens.length() == source_usd_per_token.length(), + error::invalid_argument(E_TOKEN_UPDATE_MISMATCH) + ); + assert!( + gas_dest_chain_selectors.length() == gas_usd_per_unit_gas.length(), + error::invalid_argument(E_GAS_UPDATE_MISMATCH) + ); + + let state = borrow_state_mut(); + let timestamp = timestamp::now_seconds(); + + source_tokens.zip_ref( + &source_usd_per_token, + |token, usd_per_token| { + let timestamped_price = TimestampedPrice { value: *usd_per_token, timestamp }; + state.usd_per_token.upsert(*token, timestamped_price); + event::emit_event( + &mut state.usd_per_token_updated_events, + UsdPerTokenUpdated { + token: *token, + usd_per_token: *usd_per_token, + timestamp + } + ); + } + ); + + gas_dest_chain_selectors.zip_ref( + &gas_usd_per_unit_gas, + |dest_chain_selector, usd_per_unit_gas| { + let timestamped_price = + TimestampedPrice { value: *usd_per_unit_gas, timestamp }; + state.usd_per_unit_gas_by_dest_chain.upsert( + *dest_chain_selector, timestamped_price + ); + + event::emit_event( + &mut state.usd_per_unit_gas_updated_events, + UsdPerUnitGasUpdated { + dest_chain_selector: *dest_chain_selector, + usd_per_unit_gas: *usd_per_unit_gas, + timestamp + } + ); + } + ); + } + + #[view] + public fun get_validated_fee( + dest_chain_selector: u64, + receiver: vector, + data: vector, + local_token_addresses: vector
, + local_token_amounts: vector, + _token_store_addresses: vector
, + fee_token: address, + _fee_token_store: address, + extra_args: vector + ): u64 acquires FeeQuoterState { + let state = borrow_state(); + + let dest_chain_config = get_dest_chain_config_internal( + state, dest_chain_selector + ); + assert!( + dest_chain_config.is_enabled, + error::invalid_argument(E_DEST_CHAIN_NOT_ENABLED) + ); + + assert!( + state.fee_tokens.contains(&fee_token), + error::invalid_argument(E_FEE_TOKEN_NOT_SUPPORTED) + ); + + let chain_family_selector = dest_chain_config.chain_family_selector; + + let data_len = data.length(); + let tokens_len = local_token_addresses.length(); + validate_message(dest_chain_config, data_len, tokens_len); + + let gas_limit = + if (chain_family_selector == CHAIN_FAMILY_SELECTOR_EVM + || chain_family_selector == CHAIN_FAMILY_SELECTOR_APTOS + || chain_family_selector == CHAIN_FAMILY_SELECTOR_SUI) { + resolve_generic_gas_limit(dest_chain_config, extra_args) + } else if (chain_family_selector == CHAIN_FAMILY_SELECTOR_SVM) { + resolve_svm_gas_limit( + dest_chain_config, + state, + dest_chain_selector, + extra_args, + receiver, + data_len, + tokens_len, + local_token_addresses + ) + } else { + abort error::invalid_argument(E_UNKNOWN_CHAIN_FAMILY_SELECTOR) + }; + + validate_dest_family_address(chain_family_selector, receiver, gas_limit); + + let fee_token_price = get_token_price_internal(state, fee_token); + assert!(fee_token_price.value > 0, error::invalid_state(E_ZERO_TOKEN_PRICE)); + + let packed_gas_price = + get_validated_gas_price_internal( + state, dest_chain_config, dest_chain_selector + ); + + let (premium_fee_usd_wei, token_transfer_gas, token_transfer_bytes_overhead) = + if (tokens_len > 0) { + get_token_transfer_cost( + state, + dest_chain_config, + dest_chain_selector, + fee_token, + fee_token_price, + local_token_addresses, + local_token_amounts + ) + } else { + ((dest_chain_config.network_fee_usd_cents as u256) * VAL_1E16, 0, 0) + }; + let premium_multiplier = + get_premium_multiplier_wei_per_eth_internal(state, fee_token); + premium_fee_usd_wei *=(premium_multiplier as u256); // Apply premium multiplier in wei/eth units + + let data_availability_cost_usd_36_decimals = + if (dest_chain_config.dest_data_availability_multiplier_bps > 0) { + // Extract data availability gas price (upper 112 bits) - matches EVM uint112 behavior + let data_availability_gas_price = + (packed_gas_price >> GAS_PRICE_BITS) & GAS_PRICE_MASK_112_BITS; + get_data_availability_cost( + dest_chain_config, + data_availability_gas_price, + data_len, + tokens_len, + token_transfer_bytes_overhead + ) + } else { 0 }; + + let call_data_length: u256 = + (data_len as u256) + (token_transfer_bytes_overhead as u256); + let dest_call_data_cost = + call_data_length + * (dest_chain_config.dest_gas_per_payload_byte_base as u256); + if (call_data_length + > (dest_chain_config.dest_gas_per_payload_byte_threshold as u256)) { + dest_call_data_cost = + (dest_chain_config.dest_gas_per_payload_byte_base as u256) + * (dest_chain_config.dest_gas_per_payload_byte_threshold as u256) + + ( + call_data_length + - (dest_chain_config.dest_gas_per_payload_byte_threshold as u256) + ) * (dest_chain_config.dest_gas_per_payload_byte_high as u256); + }; + + let total_dest_chain_gas = + (dest_chain_config.dest_gas_overhead as u256) + (token_transfer_gas as u256) + + dest_call_data_cost + gas_limit; + + let gas_cost = packed_gas_price & GAS_PRICE_MASK_112_BITS; + + let total_cost_usd = + ( + total_dest_chain_gas * gas_cost + * (dest_chain_config.gas_multiplier_wei_per_eth as u256) + ) + premium_fee_usd_wei + data_availability_cost_usd_36_decimals; + + let fee_token_cost = total_cost_usd / fee_token_price.value; + + // we need to convert back to a u64 which is what the fungible asset module uses for amounts. + assert!( + fee_token_cost <= MAX_U64, + error::invalid_state(E_FEE_TOKEN_COST_TOO_HIGH) + ); + fee_token_cost as u64 + } + + public entry fun apply_premium_multiplier_wei_per_eth_updates( + caller: &signer, tokens: vector
, premium_multiplier_wei_per_eth: vector + ) acquires FeeQuoterState { + auth::assert_only_owner(signer::address_of(caller)); + + let state = borrow_state_mut(); + + tokens.zip_ref( + &premium_multiplier_wei_per_eth, + |token, premium_multiplier_wei_per_eth| { + let token: address = *token; + let premium_multiplier_wei_per_eth: u64 = *premium_multiplier_wei_per_eth; + state.premium_multiplier_wei_per_eth.upsert( + token, premium_multiplier_wei_per_eth + ); + event::emit_event( + &mut state.premium_multiplier_wei_per_eth_updated_events, + PremiumMultiplierWeiPerEthUpdated { + token, + premium_multiplier_wei_per_eth + } + ); + } + ); + } + + #[view] + public fun get_premium_multiplier_wei_per_eth(token: address): u64 acquires FeeQuoterState { + let state = borrow_state(); + get_premium_multiplier_wei_per_eth_internal(state, token) + } + + inline fun get_premium_multiplier_wei_per_eth_internal( + state: &FeeQuoterState, token: address + ): u64 { + assert!( + state.premium_multiplier_wei_per_eth.contains(token), + error::invalid_argument(E_UNKNOWN_TOKEN) + ); + *state.premium_multiplier_wei_per_eth.borrow(token) + } + + inline fun resolve_generic_gas_limit( + dest_chain_config: &DestChainConfig, extra_args: vector + ): u256 { + let (gas_limit, _allow_out_of_order_execution) = + decode_generic_extra_args(dest_chain_config, extra_args); + assert!( + gas_limit <= (dest_chain_config.max_per_msg_gas_limit as u256), + error::invalid_argument(E_MESSAGE_GAS_LIMIT_TOO_HIGH) + ); + gas_limit + } + + inline fun resolve_svm_gas_limit( + dest_chain_config: &DestChainConfig, + state: &FeeQuoterState, + dest_chain_selector: u64, + extra_args: vector, + receiver: vector, + data_len: u64, + tokens_len: u64, + local_token_addresses: vector
+ ): u256 { + let extra_args_len = extra_args.length(); + assert!(extra_args_len > 0, error::invalid_argument(E_INVALID_EXTRA_ARGS_DATA)); + + let ( + compute_units, + account_is_writable_bitmap, + _allow_out_of_order_execution, + token_receiver, + accounts + ) = decode_svm_extra_args(extra_args); + + let gas_limit = compute_units; + + assert!( + gas_limit <= dest_chain_config.max_per_msg_gas_limit, + error::invalid_argument(E_MESSAGE_COMPUTE_UNIT_LIMIT_TOO_HIGH) + ); + + let accounts_length = accounts.length(); + // The max payload size for SVM is heavily dependent on the accounts passed into extra args and the number of + // tokens. Below, token and account overhead will count towards maxDataBytes. + let svm_expanded_data_length = data_len; + + // The receiver length has not yet been validated before this point. + assert!( + receiver.length() == 32, + error::invalid_argument(E_INVALID_SVM_RECEIVER_LENGTH) + ); + let receiver_uint = eth_abi::decode_u256_value(receiver); + if (receiver_uint == 0) { + // When message receiver is zero, CCIP receiver is not invoked on SVM. + // There should not be additional accounts specified for the receiver. + assert!( + accounts_length == 0, + error::invalid_argument(E_TOO_MANY_SVM_EXTRA_ARGS_ACCOUNTS) + ); + } else { + // The messaging accounts needed for CCIP receiver on SVM are: + // message receiver, offramp PDA signer, + // plus remaining accounts specified in SVM extraArgs. Each account is 32 bytes. + svm_expanded_data_length +=((accounts_length + + SVM_MESSAGING_ACCOUNTS_OVERHEAD) * SVM_ACCOUNT_BYTE_SIZE); + }; + + for (i in 0..accounts_length) { + assert!( + accounts[i].length() == 32, + error::invalid_argument(E_INVALID_SVM_ACCOUNT_LENGTH) + ); + }; + + if (tokens_len > 0) { + assert!( + token_receiver.length() == 32 + && eth_abi::decode_u256_value(token_receiver) != 0, + error::invalid_argument(E_INVALID_TOKEN_RECEIVER) + ); + }; + assert!( + accounts_length <= SVM_EXTRA_ARGS_MAX_ACCOUNTS, + error::invalid_argument(E_TOO_MANY_SVM_EXTRA_ARGS_ACCOUNTS) + ); + assert!( + (account_is_writable_bitmap >> (accounts_length as u8)) == 0, + error::invalid_argument(E_INVALID_SVM_EXTRA_ARGS_WRITABLE_BITMAP) + ); + + svm_expanded_data_length += tokens_len * SVM_TOKEN_TRANSFER_DATA_OVERHEAD; + + // The token destBytesOverhead can be very different per token so we have to take it into account as well. + for (i in 0..tokens_len) { + let local_token_address = local_token_addresses[i]; + let destBytesOverhead = + get_token_transfer_fee_config_internal( + state, dest_chain_selector, local_token_address + ).dest_bytes_overhead; + + // Pools get CCIP_LOCK_OR_BURN_V1_RET_BYTES by default, but if an override is set we use that instead. + if (destBytesOverhead > 0) { + svm_expanded_data_length +=(destBytesOverhead as u64); + } else { + svm_expanded_data_length +=(CCIP_LOCK_OR_BURN_V1_RET_BYTES as u64); + } + }; + + assert!( + svm_expanded_data_length <= (dest_chain_config.max_data_bytes as u64), + error::invalid_argument(E_MESSAGE_TOO_LARGE) + ); + + gas_limit as u256 + } + + inline fun decode_generic_extra_args( + dest_chain_config: &DestChainConfig, extra_args: vector + ): (u256, bool) { + let extra_args_len = extra_args.length(); + if (extra_args_len == 0) { + // If extra args are empty, generate default values. Out-of-order is always true. + ( + dest_chain_config.default_tx_gas_limit as u256, + ALLOW_OUT_OF_ORDER_EXECUTION + ) + } else { + assert!( + extra_args_len >= 4, + error::invalid_argument(E_INVALID_EXTRA_ARGS_DATA) + ); + + let args_tag = extra_args.slice(0, 4); + assert!( + args_tag == client::generic_extra_args_v2_tag(), + error::invalid_argument(E_INVALID_EXTRA_ARGS_TAG) + ); + + let args_data = extra_args.slice(4, extra_args_len); + decode_generic_extra_args_v2(args_data) + } + } + + inline fun decode_generic_extra_args_v2(extra_args: vector): (u256, bool) { + let stream = bcs_stream::new(extra_args); + let gas_limit = bcs_stream::deserialize_u256(&mut stream); + let allow_out_of_order_execution = bcs_stream::deserialize_bool(&mut stream); + bcs_stream::assert_is_consumed(&stream); + (gas_limit, allow_out_of_order_execution) + } + + inline fun decode_svm_extra_args( + extra_args: vector + ): ( + u32, u64, bool, vector, vector> + ) { + let extra_args_len = extra_args.length(); + assert!(extra_args_len >= 4, error::invalid_argument(E_INVALID_EXTRA_ARGS_DATA)); + + let args_tag = extra_args.slice(0, 4); + assert!( + args_tag == client::svm_extra_args_v1_tag(), + error::invalid_argument(E_INVALID_EXTRA_ARGS_TAG) + ); + let args_data = extra_args.slice(4, extra_args_len); + decode_svm_extra_args_v1(args_data) + } + + inline fun decode_svm_extra_args_v1( + extra_args: vector + ): ( + u32, u64, bool, vector, vector> + ) { + let stream = bcs_stream::new(extra_args); + let compute_units = bcs_stream::deserialize_u32(&mut stream); + let account_is_writable_bitmap = bcs_stream::deserialize_u64(&mut stream); + let allow_out_of_order_execution = bcs_stream::deserialize_bool(&mut stream); + let token_receiver = bcs_stream::deserialize_vector_u8(&mut stream); + let accounts = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + bcs_stream::assert_is_consumed(&stream); + ( + compute_units, + account_is_writable_bitmap, + allow_out_of_order_execution, + token_receiver, + accounts + ) + } + + inline fun get_data_availability_cost( + dest_chain_config: &DestChainConfig, + data_availability_gas_price: u256, + data_len: u64, + tokens_len: u64, + total_transfer_bytes_overhead: u32 + ): u256 { + let data_availability_length_bytes = + MESSAGE_FIXED_BYTES + data_len + (tokens_len + * MESSAGE_FIXED_BYTES_PER_TOKEN) + + (total_transfer_bytes_overhead as u64); + + let data_availability_gas = + ((data_availability_length_bytes as u256) + * (dest_chain_config.dest_gas_per_data_availability_byte as u256)) + ( + dest_chain_config.dest_data_availability_overhead_gas as u256 + ); + + data_availability_gas * data_availability_gas_price + * (dest_chain_config.dest_data_availability_multiplier_bps as u256) + * VAL_1E14 + } + + inline fun get_token_transfer_cost( + state: &FeeQuoterState, + dest_chain_config: &DestChainConfig, + dest_chain_selector: u64, + fee_token: address, + fee_token_price: TimestampedPrice, + local_token_addresses: vector
, + local_token_amounts: vector + ): (u256, u32, u32) { + let token_transfer_fee_wei: u256 = 0; + let token_transfer_gas: u32 = 0; + let token_transfer_bytes_overhead: u32 = 0; + + local_token_addresses.zip_ref( + &local_token_amounts, + |local_token_address, local_token_amount| { + let local_token_address: address = *local_token_address; + let local_token_amount: u64 = *local_token_amount; + + let transfer_fee_config = + get_token_transfer_fee_config_internal( + state, dest_chain_selector, local_token_address + ); + + if (!transfer_fee_config.is_enabled) { + token_transfer_fee_wei +=( + (dest_chain_config.default_token_fee_usd_cents as u256) + * VAL_1E16 + ); + token_transfer_gas += dest_chain_config.default_token_dest_gas_overhead; + token_transfer_bytes_overhead += CCIP_LOCK_OR_BURN_V1_RET_BYTES; + } else { + let bps_fee_usd_wei = 0; + if (transfer_fee_config.deci_bps > 0) { + let token_price = + if (local_token_address == fee_token) { + fee_token_price + } else { + get_token_price_internal(state, local_token_address) + }; + let token_usd_value = + calc_usd_value_from_token_amount( + local_token_amount, token_price.value + ); + bps_fee_usd_wei = + (token_usd_value * (transfer_fee_config.deci_bps as u256)) + / VAL_1E5; + }; + + token_transfer_gas += transfer_fee_config.dest_gas_overhead; + token_transfer_bytes_overhead += transfer_fee_config.dest_bytes_overhead; + + let min_fee_usd_wei = + (transfer_fee_config.min_fee_usd_cents as u256) * VAL_1E16; + let max_fee_usd_wei = + (transfer_fee_config.max_fee_usd_cents as u256) * VAL_1E16; + let selected_fee_usd_wei = + if (bps_fee_usd_wei < min_fee_usd_wei) { + min_fee_usd_wei + } else if (bps_fee_usd_wei > max_fee_usd_wei) { + max_fee_usd_wei + } else { + bps_fee_usd_wei + }; + token_transfer_fee_wei += selected_fee_usd_wei; + } + } + ); + + (token_transfer_fee_wei, token_transfer_gas, token_transfer_bytes_overhead) + } + + inline fun calc_usd_value_from_token_amount( + token_amount: u64, token_price: u256 + ): u256 { + (token_amount as u256) * token_price / VAL_1E18 + } + + #[view] + public fun get_token_receiver( + dest_chain_selector: u64, extra_args: vector, message_receiver: vector + ): vector acquires FeeQuoterState { + let chain_family_selector = + get_dest_chain_config_internal(borrow_state(), dest_chain_selector).chain_family_selector; + if (chain_family_selector == CHAIN_FAMILY_SELECTOR_EVM + || chain_family_selector == CHAIN_FAMILY_SELECTOR_APTOS + || chain_family_selector == CHAIN_FAMILY_SELECTOR_SUI) { + message_receiver + } else if (chain_family_selector == CHAIN_FAMILY_SELECTOR_SVM) { + let ( + _compute_units, + _account_is_writable_bitmap, + _allow_out_of_order_execution, + token_receiver, + _accounts + ) = decode_svm_extra_args(extra_args); + token_receiver + } else { + abort error::invalid_argument(E_UNKNOWN_CHAIN_FAMILY_SELECTOR) + } + } + + #[view] + /// @returns (msg_fee_juels, is_out_of_order_execution, converted_extra_args, dest_exec_data_per_token) + public fun process_message_args( + dest_chain_selector: u64, + fee_token: address, + fee_token_amount: u64, + extra_args: vector, + local_token_addresses: vector
, + dest_token_addresses: vector>, + dest_pool_datas: vector> + ): ( + u256, bool, vector, vector> + ) acquires FeeQuoterState { + let state = borrow_state(); + // This is the fee in Aptos denomination. We convert it to juels (1e18 based) below. + let msg_fee_link_local_denomination = + if (fee_token == state.link_token) { + fee_token_amount + } else { + convert_token_amount_internal( + state, + fee_token, + fee_token_amount, + state.link_token + ) + }; + + // We convert the local denomination to juels here. This means that the offchain monitoring will always + // get a consistent juels amount regardless of the token denomination on the chain. + let msg_fee_juels = + (msg_fee_link_local_denomination as u256) + * LOCAL_8_TO_18_DECIMALS_LINK_MULTIPLIER; + + // max_fee_juels_per_msg is in juels denomination for consistency across chains. + assert!( + msg_fee_juels <= state.max_fee_juels_per_msg, + error::invalid_argument(E_MESSAGE_FEE_TOO_HIGH) + ); + + let dest_chain_config = get_dest_chain_config_internal( + state, dest_chain_selector + ); + + let (converted_extra_args, is_out_of_order_execution) = + process_chain_family_selector( + dest_chain_config, !dest_token_addresses.is_empty(), extra_args + ); + + let dest_exec_data_per_token = + process_pool_return_data( + state, + dest_chain_config, + dest_chain_selector, + local_token_addresses, + dest_token_addresses, + dest_pool_datas + ); + + ( + msg_fee_juels, + is_out_of_order_execution, + converted_extra_args, + dest_exec_data_per_token + ) + } + + inline fun process_chain_family_selector( + dest_chain_config: &DestChainConfig, + is_message_with_token_transfers: bool, + extra_args: vector + ): (vector, bool) { + let chain_family_selector = dest_chain_config.chain_family_selector; + if (chain_family_selector == CHAIN_FAMILY_SELECTOR_EVM + || chain_family_selector == CHAIN_FAMILY_SELECTOR_APTOS + || chain_family_selector == CHAIN_FAMILY_SELECTOR_SUI) { + let (gas_limit, _allow_out_of_order_execution) = + decode_generic_extra_args(dest_chain_config, extra_args); + let extra_args_v2 = + client::encode_generic_extra_args_v2( + gas_limit, ALLOW_OUT_OF_ORDER_EXECUTION + ); + (extra_args_v2, ALLOW_OUT_OF_ORDER_EXECUTION) + } else if (chain_family_selector == CHAIN_FAMILY_SELECTOR_SVM) { + let ( + compute_units, + _account_is_writable_bitmap, + _allow_out_of_order_execution, + token_receiver, + _accounts + ) = decode_svm_extra_args(extra_args); + if (is_message_with_token_transfers) { + assert!( + token_receiver.length() == 32, + error::invalid_argument(E_INVALID_TOKEN_RECEIVER) + ); + let token_receiver_uint = eth_abi::decode_u256_value(token_receiver); + assert!( + token_receiver_uint > 0, + error::invalid_argument(E_INVALID_TOKEN_RECEIVER) + ); + }; + + assert!( + compute_units <= dest_chain_config.max_per_msg_gas_limit, + error::invalid_argument(E_MESSAGE_COMPUTE_UNIT_LIMIT_TOO_HIGH) + ); + + (extra_args, ALLOW_OUT_OF_ORDER_EXECUTION) + } else { + abort error::invalid_argument(E_UNKNOWN_CHAIN_FAMILY_SELECTOR) + } + } + + inline fun process_pool_return_data( + state: &FeeQuoterState, + dest_chain_config: &DestChainConfig, + dest_chain_selector: u64, + local_token_addresses: vector
, + dest_token_addresses: vector>, + dest_pool_datas: vector> + ): vector> { + let chain_family_selector = dest_chain_config.chain_family_selector; + + let tokens_len = dest_token_addresses.length(); + assert!( + tokens_len == dest_pool_datas.length(), + error::invalid_argument(E_TOKEN_AMOUNT_MISMATCH) + ); + + let dest_exec_data_per_token = vector[]; + for (i in 0..tokens_len) { + let local_token_address = local_token_addresses[i]; + let dest_token_address = dest_token_addresses[i]; + let dest_pool_data_len = dest_pool_datas[i].length(); + + let token_transfer_fee_config = + get_token_transfer_fee_config_internal( + state, dest_chain_selector, local_token_address + ); + if (dest_pool_data_len > (CCIP_LOCK_OR_BURN_V1_RET_BYTES as u64)) { + assert!( + dest_pool_data_len + <= (token_transfer_fee_config.dest_bytes_overhead as u64), + error::invalid_argument(E_SOURCE_TOKEN_DATA_TOO_LARGE) + ); + }; + + // We pass in 1 as gas_limit as this only matters for SVM address validation. This ensures the address + // may not be 0x0. + validate_dest_family_address(chain_family_selector, dest_token_address, 1); + + let dest_gas_amount = + if (token_transfer_fee_config.is_enabled) { + token_transfer_fee_config.dest_gas_overhead + } else { + dest_chain_config.default_token_dest_gas_overhead + }; + + let dest_exec_data = bcs::to_bytes(&dest_gas_amount); + dest_exec_data_per_token.push_back(dest_exec_data); + }; + + dest_exec_data_per_token + } + + #[view] + public fun get_dest_chain_config( + dest_chain_selector: u64 + ): DestChainConfig acquires FeeQuoterState { + *get_dest_chain_config_internal(borrow_state(), dest_chain_selector) + } + + inline fun get_dest_chain_config_internal( + state: &FeeQuoterState, dest_chain_selector: u64 + ): &DestChainConfig { + assert!( + state.dest_chain_configs.contains(dest_chain_selector), + error::invalid_argument(E_UNKNOWN_DEST_CHAIN_SELECTOR) + ); + state.dest_chain_configs.borrow(dest_chain_selector) + } + + public entry fun apply_dest_chain_config_updates( + caller: &signer, + dest_chain_selector: u64, + is_enabled: bool, + max_number_of_tokens_per_msg: u16, + max_data_bytes: u32, + max_per_msg_gas_limit: u32, + dest_gas_overhead: u32, + dest_gas_per_payload_byte_base: u8, + dest_gas_per_payload_byte_high: u8, + dest_gas_per_payload_byte_threshold: u16, + dest_data_availability_overhead_gas: u32, + dest_gas_per_data_availability_byte: u16, + dest_data_availability_multiplier_bps: u16, + chain_family_selector: vector, + enforce_out_of_order: bool, + default_token_fee_usd_cents: u16, + default_token_dest_gas_overhead: u32, + default_tx_gas_limit: u32, + gas_multiplier_wei_per_eth: u64, + gas_price_staleness_threshold: u32, + network_fee_usd_cents: u32 + ) acquires FeeQuoterState { + auth::assert_only_owner(signer::address_of(caller)); + + let state = borrow_state_mut(); + + assert!( + dest_chain_selector != 0, + error::invalid_argument(E_INVALID_DEST_CHAIN_SELECTOR) + ); + assert!( + default_tx_gas_limit != 0 && default_tx_gas_limit <= max_per_msg_gas_limit, + error::invalid_argument(E_INVALID_GAS_LIMIT) + ); + + assert!( + chain_family_selector == CHAIN_FAMILY_SELECTOR_EVM + || chain_family_selector == CHAIN_FAMILY_SELECTOR_SVM + || chain_family_selector == CHAIN_FAMILY_SELECTOR_APTOS + || chain_family_selector == CHAIN_FAMILY_SELECTOR_SUI, + error::invalid_argument(E_INVALID_CHAIN_FAMILY_SELECTOR) + ); + + let dest_chain_config = DestChainConfig { + is_enabled, + max_number_of_tokens_per_msg, + max_data_bytes, + max_per_msg_gas_limit, + dest_gas_overhead, + dest_gas_per_payload_byte_base, + dest_gas_per_payload_byte_high, + dest_gas_per_payload_byte_threshold, + dest_data_availability_overhead_gas, + dest_gas_per_data_availability_byte, + dest_data_availability_multiplier_bps, + chain_family_selector, + enforce_out_of_order, + default_token_fee_usd_cents, + default_token_dest_gas_overhead, + default_tx_gas_limit, + gas_multiplier_wei_per_eth, + gas_price_staleness_threshold, + network_fee_usd_cents + }; + + if (state.dest_chain_configs.contains(dest_chain_selector)) { + let dest_chain_config_ref = + state.dest_chain_configs.borrow_mut(dest_chain_selector); + *dest_chain_config_ref = dest_chain_config; + event::emit_event( + &mut state.dest_chain_config_updated_events, + DestChainConfigUpdated { dest_chain_selector, dest_chain_config } + ); + } else { + state.dest_chain_configs.add(dest_chain_selector, dest_chain_config); + event::emit_event( + &mut state.dest_chain_added_events, + DestChainAdded { dest_chain_selector, dest_chain_config } + ); + } + } + + #[view] + public fun get_static_config(): StaticConfig acquires FeeQuoterState { + let state = borrow_state(); + StaticConfig { + max_fee_juels_per_msg: state.max_fee_juels_per_msg, + link_token: state.link_token, + token_price_staleness_threshold: state.token_price_staleness_threshold + } + } + + inline fun borrow_state(): &FeeQuoterState { + borrow_global(state_object::object_address()) + } + + inline fun borrow_state_mut(): &mut FeeQuoterState { + borrow_global_mut(state_object::object_address()) + } + + inline fun get_validated_token_price( + state: &FeeQuoterState, token: address + ): TimestampedPrice { + let token_price = get_token_price_internal(state, token); + assert!( + token_price.value > 0 && token_price.timestamp > 0, + error::invalid_state(E_TOKEN_NOT_SUPPORTED) + ); + token_price + } + + // Token prices can be stale. On EVM we have additional fallbacks to a price feed, if configured. Since these + // fallbacks don't exist on Aptos, we simply return the price as is. + inline fun get_token_price_internal( + state: &FeeQuoterState, token: address + ): TimestampedPrice { + assert!( + state.usd_per_token.contains(token), + error::invalid_argument(E_UNKNOWN_TOKEN) + ); + *state.usd_per_token.borrow(token) + } + + inline fun get_dest_chain_gas_price_internal( + state: &FeeQuoterState, dest_chain_selector: u64 + ): TimestampedPrice { + assert!( + state.usd_per_unit_gas_by_dest_chain.contains(dest_chain_selector), + error::invalid_argument(E_UNKNOWN_DEST_CHAIN_SELECTOR) + ); + *state.usd_per_unit_gas_by_dest_chain.borrow(dest_chain_selector) + } + + inline fun get_validated_gas_price_internal( + state: &FeeQuoterState, dest_chain_config: &DestChainConfig, dest_chain_selector: u64 + ): u256 { + let gas_price = get_dest_chain_gas_price_internal(state, dest_chain_selector); + if (dest_chain_config.gas_price_staleness_threshold > 0) { + let time_passed_seconds = timestamp::now_seconds() - gas_price.timestamp; + assert!( + time_passed_seconds + <= (dest_chain_config.gas_price_staleness_threshold as u64), + error::invalid_state(E_STALE_GAS_PRICE) + ); + }; + gas_price.value + } + + inline fun convert_token_amount_internal( + state: &FeeQuoterState, + from_token: address, + from_token_amount: u64, + to_token: address + ): u64 { + let from_token_price = get_validated_token_price(state, from_token); + let to_token_price = get_validated_token_price(state, to_token); + let to_token_amount = + ((from_token_amount as u256) * from_token_price.value) / to_token_price.value; + assert!( + to_token_amount <= MAX_U64, + error::invalid_argument(E_TO_TOKEN_AMOUNT_TOO_LARGE) + ); + to_token_amount as u64 + } + + inline fun validate_message( + dest_chain_config: &DestChainConfig, data_len: u64, tokens_len: u64 + ) { + assert!( + data_len <= (dest_chain_config.max_data_bytes as u64), + error::invalid_argument(E_MESSAGE_TOO_LARGE) + ); + assert!( + tokens_len <= (dest_chain_config.max_number_of_tokens_per_msg as u64), + error::invalid_argument(E_UNSUPPORTED_NUMBER_OF_TOKENS) + ); + } + + inline fun validate_dest_family_address( + chain_family_selector: vector, encoded_address: vector, gas_limit: u256 + ) { + if (chain_family_selector == CHAIN_FAMILY_SELECTOR_EVM) { + validate_evm_address(encoded_address); + } else if (chain_family_selector == CHAIN_FAMILY_SELECTOR_SVM) { + // SVM addresses don't have a precompile space at the first X addresses, instead we validate that if the gasLimit + // is non-zero, the address must not be 0x0. + let min_address = 0; + if (gas_limit > 0) { + min_address = 1; + }; + validate_32byte_address(encoded_address, min_address); + } else if (chain_family_selector == CHAIN_FAMILY_SELECTOR_APTOS + || chain_family_selector == CHAIN_FAMILY_SELECTOR_SUI) { + validate_32byte_address(encoded_address, MOVE_PRECOMPILE_SPACE); + }; + } + + inline fun validate_evm_address(encoded_address: vector) { + assert!( + encoded_address.length() == 32, + error::invalid_argument(E_INVALID_EVM_ADDRESS) + ); + + let encoded_address_uint = eth_abi::decode_u256_value(encoded_address); + + assert!( + encoded_address_uint >= EVM_PRECOMPILE_SPACE, + error::invalid_argument(E_INVALID_EVM_ADDRESS) + ); + assert!( + encoded_address_uint <= MAX_U160, + error::invalid_argument(E_INVALID_EVM_ADDRESS) + ); + } + + inline fun validate_32byte_address( + encoded_address: vector, min_value: u256 + ) { + assert!( + encoded_address.length() == 32, + error::invalid_argument(E_INVALID_32BYTES_ADDRESS) + ); + + let encoded_address_uint = eth_abi::decode_u256_value(encoded_address); + assert!( + encoded_address_uint >= min_value, + error::invalid_argument(E_INVALID_32BYTES_ADDRESS) + ); + } + + // ================================================================ + // | MCMS Entrypoint | + // ================================================================ + struct McmsCallback has drop {} + + public fun mcms_entrypoint( + _metadata: object::Object + ): option::Option acquires FeeQuoterState { + let (caller, function, data) = + mcms_registry::get_callback_params(@ccip, McmsCallback {}); + + let function_bytes = *function.bytes(); + let stream = bcs_stream::new(data); + + if (function_bytes == b"initialize") { + let max_fee_juels_per_msg = bcs_stream::deserialize_u256(&mut stream); + let link_token = bcs_stream::deserialize_address(&mut stream); + let token_price_staleness_threshold = bcs_stream::deserialize_u64( + &mut stream + ); + let fee_tokens = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + initialize( + &caller, + max_fee_juels_per_msg, + link_token, + token_price_staleness_threshold, + fee_tokens + ) + } else if (function_bytes == b"apply_fee_token_updates") { + let fee_tokens_to_remove = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + let fee_tokens_to_add = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + apply_fee_token_updates(&caller, fee_tokens_to_remove, fee_tokens_to_add) + } else if (function_bytes == b"apply_token_transfer_fee_config_updates") { + let dest_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let add_tokens = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + let add_min_fee_usd_cents = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u32(stream) + ); + let add_max_fee_usd_cents = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u32(stream) + ); + let add_deci_bps = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u16(stream) + ); + let add_dest_gas_overhead = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u32(stream) + ); + let add_dest_bytes_overhead = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u32(stream) + ); + let add_is_enabled = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_bool(stream) + ); + let remove_tokens = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + apply_token_transfer_fee_config_updates( + &caller, + dest_chain_selector, + add_tokens, + add_min_fee_usd_cents, + add_max_fee_usd_cents, + add_deci_bps, + add_dest_gas_overhead, + add_dest_bytes_overhead, + add_is_enabled, + remove_tokens + ) + } else if (function_bytes == b"update_prices") { + let source_tokens = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + let source_usd_per_token = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u256(stream) + ); + let gas_dest_chain_selectors = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let gas_usd_per_unit_gas = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u256(stream) + ); + bcs_stream::assert_is_consumed(&stream); + update_prices( + &caller, + source_tokens, + source_usd_per_token, + gas_dest_chain_selectors, + gas_usd_per_unit_gas + ) + } else if (function_bytes == b"apply_premium_multiplier_wei_per_eth_updates") { + let tokens = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + let premium_multiplier_wei_per_eth = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + bcs_stream::assert_is_consumed(&stream); + apply_premium_multiplier_wei_per_eth_updates( + &caller, tokens, premium_multiplier_wei_per_eth + ) + } else if (function_bytes == b"apply_dest_chain_config_updates") { + let dest_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let is_enabled = bcs_stream::deserialize_bool(&mut stream); + let max_number_of_tokens_per_msg = bcs_stream::deserialize_u16(&mut stream); + let max_data_bytes = bcs_stream::deserialize_u32(&mut stream); + let max_per_msg_gas_limit = bcs_stream::deserialize_u32(&mut stream); + let dest_gas_overhead = bcs_stream::deserialize_u32(&mut stream); + let dest_gas_per_payload_byte_base = bcs_stream::deserialize_u8(&mut stream); + let dest_gas_per_payload_byte_high = bcs_stream::deserialize_u8(&mut stream); + let dest_gas_per_payload_byte_threshold = + bcs_stream::deserialize_u16(&mut stream); + let dest_data_availability_overhead_gas = + bcs_stream::deserialize_u32(&mut stream); + let dest_gas_per_data_availability_byte = + bcs_stream::deserialize_u16(&mut stream); + let dest_data_availability_multiplier_bps = + bcs_stream::deserialize_u16(&mut stream); + let chain_family_selector = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u8(stream) + ); + let enforce_out_of_order = bcs_stream::deserialize_bool(&mut stream); + let default_token_fee_usd_cents = bcs_stream::deserialize_u16(&mut stream); + let default_token_dest_gas_overhead = bcs_stream::deserialize_u32( + &mut stream + ); + let default_tx_gas_limit = bcs_stream::deserialize_u32(&mut stream); + let gas_multiplier_wei_per_eth = bcs_stream::deserialize_u64(&mut stream); + let gas_price_staleness_threshold = bcs_stream::deserialize_u32(&mut stream); + let network_fee_usd_cents = bcs_stream::deserialize_u32(&mut stream); + bcs_stream::assert_is_consumed(&stream); + apply_dest_chain_config_updates( + &caller, + dest_chain_selector, + is_enabled, + max_number_of_tokens_per_msg, + max_data_bytes, + max_per_msg_gas_limit, + dest_gas_overhead, + dest_gas_per_payload_byte_base, + dest_gas_per_payload_byte_high, + dest_gas_per_payload_byte_threshold, + dest_data_availability_overhead_gas, + dest_gas_per_data_availability_byte, + dest_data_availability_multiplier_bps, + chain_family_selector, + enforce_out_of_order, + default_token_fee_usd_cents, + default_token_dest_gas_overhead, + default_tx_gas_limit, + gas_multiplier_wei_per_eth, + gas_price_staleness_threshold, + network_fee_usd_cents + ) + } else { + abort error::invalid_argument(E_UNKNOWN_FUNCTION) + }; + + option::none() + } + + /// Callable during upgrades + public(friend) fun register_mcms_entrypoint(publisher: &signer) { + mcms_registry::register_entrypoint( + publisher, string::utf8(b"fee_quoter"), McmsCallback {} + ); + } + + public fun dest_chain_config_values( + config: DestChainConfig + ): ( + bool, + u16, + u32, + u32, + u32, + u8, + u8, + u16, + u32, + u16, + u16, + vector, + bool, + u16, + u32, + u32, + u64, + u32, + u32 + ) { + ( + config.is_enabled, + config.max_number_of_tokens_per_msg, + config.max_data_bytes, + config.max_per_msg_gas_limit, + config.dest_gas_overhead, + config.dest_gas_per_payload_byte_base, + config.dest_gas_per_payload_byte_high, + config.dest_gas_per_payload_byte_threshold, + config.dest_data_availability_overhead_gas, + config.dest_gas_per_data_availability_byte, + config.dest_data_availability_multiplier_bps, + config.chain_family_selector, + config.enforce_out_of_order, + config.default_token_fee_usd_cents, + config.default_token_dest_gas_overhead, + config.default_tx_gas_limit, + config.gas_multiplier_wei_per_eth, + config.gas_price_staleness_threshold, + config.network_fee_usd_cents + ) + } + + public fun token_transfer_fee_config_values( + config: TokenTransferFeeConfig + ): (u32, u32, u16, u32, u32, bool) { + ( + config.min_fee_usd_cents, + config.max_fee_usd_cents, + config.deci_bps, + config.dest_gas_overhead, + config.dest_bytes_overhead, + config.is_enabled + ) + } + + // ========================== TEST ONLY ========================== + #[test_only] + public fun test_register_mcms_entrypoint(publisher: &signer) { + mcms_registry::register_entrypoint( + publisher, string::utf8(b"fee_quoter"), McmsCallback {} + ); + } + + #[test_only] + public fun test_decode_svm_extra_args( + extra_args: vector + ): ( + u32, u64, bool, vector, vector> + ) { + decode_svm_extra_args(extra_args) + } + + #[test_only] + public fun test_decode_generic_extra_args( + dest_chain_config: &DestChainConfig, extra_args: vector + ): (u256, bool) { + decode_generic_extra_args(dest_chain_config, extra_args) + } + + #[test_only] + public fun test_decode_generic_extra_args_v2(extra_args: vector): (u256, bool) { + decode_generic_extra_args_v2(extra_args) + } + + #[test_only] + public fun test_decode_svm_extra_args_v1( + extra_args: vector + ): ( + u32, u64, bool, vector, vector> + ) { + decode_svm_extra_args_v1(extra_args) + } + + #[test_only] + public fun test_create_dest_chain_config( + is_enabled: bool, + max_number_of_tokens_per_msg: u16, + max_data_bytes: u32, + max_per_msg_gas_limit: u32, + dest_gas_overhead: u32, + dest_gas_per_payload_byte_base: u8, + dest_gas_per_payload_byte_high: u8, + dest_gas_per_payload_byte_threshold: u16, + dest_data_availability_overhead_gas: u32, + dest_gas_per_data_availability_byte: u16, + dest_data_availability_multiplier_bps: u16, + chain_family_selector: vector, + enforce_out_of_order: bool, + default_token_fee_usd_cents: u16, + default_token_dest_gas_overhead: u32, + default_tx_gas_limit: u32, + gas_multiplier_wei_per_eth: u64, + gas_price_staleness_threshold: u32, + network_fee_usd_cents: u32 + ): DestChainConfig { + DestChainConfig { + is_enabled, + max_number_of_tokens_per_msg, + max_data_bytes, + max_per_msg_gas_limit, + dest_gas_overhead, + dest_gas_per_payload_byte_base, + dest_gas_per_payload_byte_high, + dest_gas_per_payload_byte_threshold, + dest_data_availability_overhead_gas, + dest_gas_per_data_availability_byte, + dest_data_availability_multiplier_bps, + chain_family_selector, + enforce_out_of_order, + default_token_fee_usd_cents, + default_token_dest_gas_overhead, + default_tx_gas_limit, + gas_multiplier_wei_per_eth, + gas_price_staleness_threshold, + network_fee_usd_cents + } + } +} +` + +/** sources/merkle_proof.move */ +export const CCIP_MERKLE_PROOF_MOVE = `module ccip::merkle_proof { + use std::aptos_hash; + use std::error; + + const LEAF_DOMAIN_SEPARATOR: vector = x"0000000000000000000000000000000000000000000000000000000000000000"; + const INTERNAL_DOMAIN_SEPARATOR: vector = x"0000000000000000000000000000000000000000000000000000000000000001"; + + const E_VECTOR_LENGTH_MISMATCH: u64 = 1; + + public fun leaf_domain_separator(): vector { + LEAF_DOMAIN_SEPARATOR + } + + public fun merkle_root(leaf: vector, proofs: vector>): vector { + proofs.fold(leaf, |acc, proof| hash_pair(acc, proof)) + } + + public fun vector_u8_gt(a: &vector, b: &vector): bool { + let len = a.length(); + assert!(len == b.length(), error::invalid_argument(E_VECTOR_LENGTH_MISMATCH)); + + // compare each byte until not equal + for (i in 0..len) { + let byte_a = a[i]; + let byte_b = b[i]; + if (byte_a > byte_b) { + return true + } else if (byte_a < byte_b) { + return false + }; + }; + + // vectors are equal, a == b + false + } + + /// Hashes two byte vectors using SHA3-256 after concatenating them with the internal domain separator + inline fun hash_internal_node(left: vector, right: vector): vector { + let data = INTERNAL_DOMAIN_SEPARATOR; + data.append(left); + data.append(right); + aptos_hash::keccak256(data) + } + + /// Hashes a pair of byte vectors, ordering them lexographically + inline fun hash_pair(a: vector, b: vector): vector { + if (!vector_u8_gt(&a, &b)) { + hash_internal_node(a, b) + } else { + hash_internal_node(b, a) + } + } +} +` + +/** sources/nonce_manager.move */ +export const CCIP_NONCE_MANAGER_MOVE = `module ccip::nonce_manager { + use std::signer; + use std::smart_table::{Self, SmartTable}; + use std::string::{Self, String}; + + use ccip::auth; + use ccip::state_object; + + struct NonceManagerState has key, store { + // dest chain selector -> sender -> nonce + outbound_nonces: SmartTable> + } + + #[view] + public fun type_and_version(): String { + string::utf8(b"NonceManager 1.6.0") + } + + fun init_module(_publisher: &signer) { + let state_object_signer = state_object::object_signer(); + + move_to( + &state_object_signer, + NonceManagerState { outbound_nonces: smart_table::new() } + ); + } + + #[view] + public fun get_outbound_nonce( + dest_chain_selector: u64, sender: address + ): u64 acquires NonceManagerState { + let state = borrow_state(); + + if (!state.outbound_nonces.contains(dest_chain_selector)) { + return 0; + }; + + let dest_chain_nonces = state.outbound_nonces.borrow(dest_chain_selector); + *dest_chain_nonces.borrow_with_default(sender, &0) + } + + public fun get_incremented_outbound_nonce( + caller: &signer, dest_chain_selector: u64, sender: address + ): u64 acquires NonceManagerState { + auth::assert_is_allowed_onramp(signer::address_of(caller)); + + let state = borrow_state_mut(); + + if (!state.outbound_nonces.contains(dest_chain_selector)) { + state.outbound_nonces.add(dest_chain_selector, smart_table::new()); + }; + + let dest_chain_nonces = state.outbound_nonces.borrow_mut(dest_chain_selector); + let nonce_ref = dest_chain_nonces.borrow_mut_with_default(sender, 0); + let incremented_nonce = *nonce_ref + 1; + *nonce_ref = incremented_nonce; + incremented_nonce + } + + inline fun borrow_state(): &NonceManagerState { + borrow_global(state_object::object_address()) + } + + inline fun borrow_state_mut(): &mut NonceManagerState { + borrow_global_mut(state_object::object_address()) + } + + // ========================== TEST ONLY ========================== + #[test_only] + public fun test_init_module(publisher: &signer) { + init_module(publisher); + } +} +` + +/** sources/ownable.move */ +export const CCIP_OWNABLE_MOVE = `/// This module implements an Ownable component similar to Ownable2Step.sol for managing +/// object ownership. +/// +/// Due to Aptos's security model requiring the original owner's signer for 0x1::object::transfer, +/// this implementation uses a 3-step ownership transfer flow: +/// +/// 1. Initial owner calls transfer_ownership with the new owner's address +/// 2. Pending owner calls accept_ownership to confirm the transfer +/// 3. Initial owner calls execute_ownership_transfer to complete the transfer +/// +/// The execute_ownership_transfer function requires a signer in order to perform the +/// object transfer, while other operations only require the caller address to maintain the +/// principle of least privilege. +/// +/// Note that direct ownership transfers via 0x1::object::transfer are still possible. +/// This module handles such cases gracefully by reading the current owner directly +/// from the object. +module ccip::ownable { + use std::account; + use std::error; + use std::event::{Self, EventHandle}; + use std::object::{Self, Object, ObjectCore}; + use std::option::{Self, Option}; + use std::signer; + + struct OwnableState has store { + target_object: Object, + pending_transfer: Option, + ownership_transfer_requested_events: EventHandle, + ownership_transfer_accepted_events: EventHandle, + ownership_transferred_events: EventHandle + } + + struct PendingTransfer has store, drop { + from: address, + to: address, + accepted: bool + } + + const E_MUST_BE_PROPOSED_OWNER: u64 = 1; + const E_CANNOT_TRANSFER_TO_SELF: u64 = 2; + const E_ONLY_CALLABLE_BY_OWNER: u64 = 3; + const E_PROPOSED_OWNER_MISMATCH: u64 = 4; + const E_OWNER_CHANGED: u64 = 5; + const E_NO_PENDING_TRANSFER: u64 = 6; + const E_TRANSFER_NOT_ACCEPTED: u64 = 7; + const E_TRANSFER_ALREADY_ACCEPTED: u64 = 8; + + #[event] + struct OwnershipTransferRequested has store, drop { + from: address, + to: address + } + + #[event] + struct OwnershipTransferAccepted has store, drop { + from: address, + to: address + } + + #[event] + struct OwnershipTransferred has store, drop { + from: address, + to: address + } + + public fun new(event_account: &signer, object_address: address): OwnableState { + let new_state = OwnableState { + target_object: object::address_to_object(object_address), + pending_transfer: option::none(), + ownership_transfer_requested_events: account::new_event_handle(event_account), + ownership_transfer_accepted_events: account::new_event_handle(event_account), + ownership_transferred_events: account::new_event_handle(event_account) + }; + + new_state + } + + public fun owner(state: &OwnableState): address { + owner_internal(state) + } + + public fun has_pending_transfer(state: &OwnableState): bool { + state.pending_transfer.is_some() + } + + public fun pending_transfer_from(state: &OwnableState): Option
{ + state.pending_transfer.map_ref(|pending_transfer| pending_transfer.from) + } + + public fun pending_transfer_to(state: &OwnableState): Option
{ + state.pending_transfer.map_ref(|pending_transfer| pending_transfer.to) + } + + public fun pending_transfer_accepted(state: &OwnableState): Option { + state.pending_transfer.map_ref(|pending_transfer| pending_transfer.accepted) + } + + inline fun owner_internal(state: &OwnableState): address { + object::owner(state.target_object) + } + + public fun transfer_ownership( + caller: &signer, state: &mut OwnableState, to: address + ) { + let caller_address = signer::address_of(caller); + assert_only_owner_internal(caller_address, state); + assert!(caller_address != to, error::invalid_argument(E_CANNOT_TRANSFER_TO_SELF)); + + state.pending_transfer = option::some( + PendingTransfer { from: caller_address, to, accepted: false } + ); + + event::emit_event( + &mut state.ownership_transfer_requested_events, + OwnershipTransferRequested { from: caller_address, to } + ); + } + + public fun accept_ownership(caller: &signer, state: &mut OwnableState) { + let caller_address = signer::address_of(caller); + assert!( + state.pending_transfer.is_some(), + error::permission_denied(E_NO_PENDING_TRANSFER) + ); + + let current_owner = owner_internal(state); + let pending_transfer = state.pending_transfer.borrow_mut(); + + // check that the owner has not changed from a direct call to 0x1::object::transfer, + // in which case the transfer flow should be restarted. + assert!( + pending_transfer.from == current_owner, + error::permission_denied(E_OWNER_CHANGED) + ); + assert!( + pending_transfer.to == caller_address, + error::permission_denied(E_MUST_BE_PROPOSED_OWNER) + ); + assert!( + !pending_transfer.accepted, + error::invalid_state(E_TRANSFER_ALREADY_ACCEPTED) + ); + + pending_transfer.accepted = true; + + event::emit_event( + &mut state.ownership_transfer_accepted_events, + OwnershipTransferAccepted { from: pending_transfer.from, to: caller_address } + ); + } + + public fun execute_ownership_transfer( + caller: &signer, state: &mut OwnableState, to: address + ) { + let caller_address = signer::address_of(caller); + assert_only_owner_internal(caller_address, state); + + let current_owner = owner_internal(state); + let pending_transfer = state.pending_transfer.extract(); + + // check that the owner has not changed from a direct call to 0x1::object::transfer, + // in which case the transfer flow should be restarted. + assert!( + pending_transfer.from == current_owner, + error::permission_denied(E_OWNER_CHANGED) + ); + assert!( + pending_transfer.to == to, + error::permission_denied(E_PROPOSED_OWNER_MISMATCH) + ); + assert!( + pending_transfer.accepted, + error::invalid_state(E_TRANSFER_NOT_ACCEPTED) + ); + + object::transfer(caller, state.target_object, pending_transfer.to); + state.pending_transfer = option::none(); + + event::emit_event( + &mut state.ownership_transferred_events, + OwnershipTransferred { from: caller_address, to } + ); + } + + public fun assert_only_owner(caller: address, state: &OwnableState) { + assert_only_owner_internal(caller, state) + } + + inline fun assert_only_owner_internal( + caller: address, state: &OwnableState + ) { + assert!( + caller == owner_internal(state), + error::permission_denied(E_ONLY_CALLABLE_BY_OWNER) + ); + } + + public fun destroy(state: OwnableState) { + let OwnableState { + target_object: _, + pending_transfer: _, + ownership_transfer_requested_events, + ownership_transfer_accepted_events, + ownership_transferred_events + } = state; + + event::destroy_handle(ownership_transfer_requested_events); + event::destroy_handle(ownership_transfer_accepted_events); + event::destroy_handle(ownership_transferred_events); + } + + #[test_only] + public fun get_ownership_transfer_requested_events( + state: &OwnableState + ): &EventHandle { + &state.ownership_transfer_requested_events + } + + #[test_only] + public fun get_ownership_transfer_accepted_events( + state: &OwnableState + ): &EventHandle { + &state.ownership_transfer_accepted_events + } + + #[test_only] + public fun get_ownership_transferred_events( + state: &OwnableState + ): &EventHandle { + &state.ownership_transferred_events + } +} +` + +/** sources/receiver_dispatcher.move */ +export const CCIP_RECEIVER_DISPATCHER_MOVE = `module ccip::receiver_dispatcher { + use std::dispatchable_fungible_asset; + use std::signer; + + use ccip::auth; + use ccip::client; + use ccip::receiver_registry; + + public fun dispatch_receive( + caller: &signer, receiver_address: address, message: client::Any2AptosMessage + ) { + auth::assert_is_allowed_offramp(signer::address_of(caller)); + + if (receiver_registry::is_registered_receiver_v2(receiver_address)) { + receiver_registry::invoke_ccip_receive_v2(receiver_address, message); + } else { + let dispatch_metadata = + receiver_registry::start_receive(receiver_address, message); + dispatchable_fungible_asset::derived_supply(dispatch_metadata); + receiver_registry::finish_receive(receiver_address); + } + } +} +` + +/** sources/receiver_registry.move */ +export const CCIP_RECEIVER_REGISTRY_MOVE = `module ccip::receiver_registry { + use std::account; + use std::bcs; + use std::dispatchable_fungible_asset; + use std::error; + use std::event::{Self, EventHandle}; + use std::function_info::{Self, FunctionInfo}; + use std::type_info::{Self, TypeInfo}; + use std::fungible_asset::{Self, Metadata}; + use std::object::{Self, ExtendRef, Object, TransferRef}; + use std::option::{Self, Option}; + use std::signer; + use std::string::{Self, String}; + + use ccip::client; + use ccip::state_object; + + friend ccip::receiver_dispatcher; + + struct ReceiverRegistryState has key, store { + extend_ref: ExtendRef, + transfer_ref: TransferRef, + receiver_registered_events: EventHandle + } + + struct ReceiverRegistryEventsV2 has key { + receiver_registered_v2_events: EventHandle + } + + struct CCIPReceiverRegistration has key { + ccip_receive_function: FunctionInfo, + proof_typeinfo: TypeInfo, + dispatch_metadata: Object, + dispatch_extend_ref: ExtendRef, + dispatch_transfer_ref: TransferRef, + executing_input: Option + } + + struct CCIPReceiverRegistrationV2 has key { + callback: |client::Any2AptosMessage| has copy + drop + store + } + + #[event] + struct ReceiverRegistered has store, drop { + receiver_address: address, + receiver_module_name: vector + } + + #[event] + struct ReceiverRegisteredV2 has drop, store { + receiver_address: address, + callback: |client::Any2AptosMessage| has copy + drop + store + } + + const E_ALREADY_REGISTERED: u64 = 1; + const E_UNKNOWN_RECEIVER: u64 = 2; + const E_UNKNOWN_PROOF_TYPE: u64 = 3; + const E_MISSING_INPUT: u64 = 4; + const E_NON_EMPTY_INPUT: u64 = 5; + const E_PROOF_TYPE_ACCOUNT_MISMATCH: u64 = 6; + const E_PROOF_TYPE_MODULE_MISMATCH: u64 = 7; + const E_UNAUTHORIZED: u64 = 8; + + #[view] + public fun type_and_version(): String { + string::utf8(b"ReceiverRegistry 1.6.0") + } + + fun init_module(_publisher: &signer) { + let state_object_signer = state_object::object_signer(); + let constructor_ref = + object::create_named_object(&state_object_signer, b"CCIPReceiverRegistry"); + let extend_ref = object::generate_extend_ref(&constructor_ref); + let transfer_ref = object::generate_transfer_ref(&constructor_ref); + + let state = ReceiverRegistryState { + extend_ref, + transfer_ref, + receiver_registered_events: account::new_event_handle(&state_object_signer) + }; + + move_to(&state_object_signer, state); + } + + public fun register_receiver( + receiver_account: &signer, receiver_module_name: vector, _proof: ProofType + ) acquires ReceiverRegistryState { + let receiver_address = signer::address_of(receiver_account); + assert!( + !exists(receiver_address) + && !exists(receiver_address), + error::invalid_argument(E_ALREADY_REGISTERED) + ); + + let ccip_receive_function = + function_info::new_function_info( + receiver_account, + string::utf8(receiver_module_name), + string::utf8(b"ccip_receive") + ); + let proof_typeinfo = type_info::type_of(); + assert!( + proof_typeinfo.account_address() == receiver_address, + E_PROOF_TYPE_ACCOUNT_MISMATCH + ); + assert!( + proof_typeinfo.module_name() == receiver_module_name, + E_PROOF_TYPE_MODULE_MISMATCH + ); + + let state = borrow_state_mut(); + let dispatch_signer = object::generate_signer_for_extending(&state.extend_ref); + + let dispatch_object_seed = bcs::to_bytes(&receiver_address); + dispatch_object_seed.append(b"CCIPReceiverRegistration"); + + let dispatch_constructor_ref = + object::create_named_object(&dispatch_signer, dispatch_object_seed); + let dispatch_extend_ref = object::generate_extend_ref(&dispatch_constructor_ref); + let dispatch_transfer_ref = + object::generate_transfer_ref(&dispatch_constructor_ref); + let dispatch_metadata = + fungible_asset::add_fungibility( + &dispatch_constructor_ref, + option::none(), + // max name length is 32 chars + string::utf8(b"CCIPReceiverRegistration"), + // max symbol length is 10 chars + string::utf8(b"CCIPRR"), + 0, + string::utf8(b""), + string::utf8(b"") + ); + + dispatchable_fungible_asset::register_derive_supply_dispatch_function( + &dispatch_constructor_ref, option::some(ccip_receive_function) + ); + + move_to( + receiver_account, + CCIPReceiverRegistration { + ccip_receive_function, + proof_typeinfo, + dispatch_metadata, + dispatch_extend_ref, + dispatch_transfer_ref, + executing_input: option::none() + } + ); + + event::emit_event( + &mut state.receiver_registered_events, + ReceiverRegistered { receiver_address, receiver_module_name } + ); + } + + /// Registers a V2 CCIP receiver using a function-value callback (closure). + /// + /// Upgrade path: existing legacy receivers can upgrade to V2 by calling this function, + /// which supersedes the legacy registration without requiring unregistration. + /// New receivers should use V2 directly. Once V2 is registered, legacy registration + /// via \`register_receiver()\` is rejected. + /// + /// SECURITY: The callback MUST wrap a private \`#[persistent]\` function. Exposing the + /// receive function as \`public fun\` allows any caller to construct an \`Any2AptosMessage\` + /// and invoke the receiver directly, + /// + /// Correct pattern: + /// \`\`\` + /// #[persistent] + /// fun ccip_receive_v2(message: client::Any2AptosMessage) { ... } + /// + /// fun init_module(publisher: &signer) { + /// receiver_registry::register_receiver_v2( + /// publisher, |message| ccip_receive_v2(message) + /// ); + /// } + /// \`\`\` + public fun register_receiver_v2( + receiver_account: &signer, callback: |client::Any2AptosMessage| has copy + drop + store + ) { + let receiver_address = signer::address_of(receiver_account); + assert!( + !exists(receiver_address), + error::invalid_argument(E_ALREADY_REGISTERED) + ); + + move_to(receiver_account, CCIPReceiverRegistrationV2 { callback }); + + event::emit_event( + &mut borrow_events_v2_mut().receiver_registered_v2_events, + ReceiverRegisteredV2 { receiver_address, callback } + ); + } + + #[view] + public fun is_registered_receiver(receiver_address: address): bool { + exists(receiver_address) + || exists(receiver_address) + } + + #[view] + public fun is_registered_receiver_v2(receiver_address: address): bool { + exists(receiver_address) + } + + public fun get_receiver_input( + receiver_address: address, _proof: ProofType + ): client::Any2AptosMessage acquires CCIPReceiverRegistration { + let registration = get_registration_mut(receiver_address); + + assert!( + registration.proof_typeinfo == type_info::type_of(), + error::permission_denied(E_UNKNOWN_PROOF_TYPE) + ); + + assert!( + registration.executing_input.is_some(), + error::invalid_state(E_MISSING_INPUT) + ); + + registration.executing_input.extract() + } + + public(friend) fun start_receive( + receiver_address: address, message: client::Any2AptosMessage + ): Object acquires CCIPReceiverRegistration { + let registration = get_registration_mut(receiver_address); + + assert!( + registration.executing_input.is_none(), + error::invalid_state(E_NON_EMPTY_INPUT) + ); + + registration.executing_input.fill(message); + + registration.dispatch_metadata + } + + public(friend) fun finish_receive(receiver_address: address) acquires CCIPReceiverRegistration { + let registration = get_registration_mut(receiver_address); + + assert!( + registration.executing_input.is_none(), + error::invalid_state(E_NON_EMPTY_INPUT) + ); + } + + public(friend) fun invoke_ccip_receive_v2( + receiver_address: address, message: client::Any2AptosMessage + ) acquires CCIPReceiverRegistrationV2 { + assert!( + exists(receiver_address), + error::invalid_argument(E_UNKNOWN_RECEIVER) + ); + + let registration = borrow_global(receiver_address); + (registration.callback) (message); + } + + inline fun borrow_state(): &ReceiverRegistryState { + borrow_global(state_object::object_address()) + } + + inline fun borrow_state_mut(): &mut ReceiverRegistryState { + borrow_global_mut(state_object::object_address()) + } + + inline fun get_registration_mut(receiver_address: address) + : &mut CCIPReceiverRegistration { + assert!( + exists(receiver_address), + error::invalid_argument(E_UNKNOWN_RECEIVER) + ); + borrow_global_mut(receiver_address) + } + + inline fun borrow_events_v2_mut(): &mut ReceiverRegistryEventsV2 { + let state_signer = &state_object::object_signer(); + let state_address = state_object::object_address(); + + if (!exists(state_address)) { + move_to( + state_signer, + ReceiverRegistryEventsV2 { + receiver_registered_v2_events: account::new_event_handle(state_signer) + } + ); + }; + + borrow_global_mut(state_address) + } + + #[test_only] + public fun init_module_for_testing(publisher: &signer) { + init_module(publisher); + } +} +` + +/** sources/rmn_remote.move */ +export const CCIP_RMN_REMOTE_MOVE = `module ccip::rmn_remote { + use std::account; + use std::aptos_hash; + use std::bcs; + use std::chain_id; + use std::error; + use std::event::{Self, EventHandle}; + use std::object; + use std::option; + use std::secp256k1; + use std::signer; + use std::string::{Self, String}; + use std::smart_table::{Self, SmartTable}; + use std::ordered_map::{Self, OrderedMap}; + + use ccip::auth; + use ccip::eth_abi; + use ccip::merkle_proof; + use ccip::state_object; + + use mcms::bcs_stream; + use mcms::mcms_registry; + + const GLOBAL_CURSE_SUBJECT: vector = x"01000000000000000000000000000001"; + + struct RMNRemoteState has key { + local_chain_selector: u64, + config: Config, + config_count: u32, + signers: SmartTable, bool>, + cursed_subjects: SmartTable, bool>, + config_set_events: EventHandle, + cursed_events: EventHandle, + uncursed_events: EventHandle + } + + struct Config has copy, drop, store { + rmn_home_contract_config_digest: vector, + signers: vector, + f_sign: u64 + } + + struct Signer has copy, drop, store { + onchain_public_key: vector, + node_index: u64 + } + + struct Report has drop { + dest_chain_id: u64, + dest_chain_selector: u64, + rmn_remote_contract_address: address, + off_ramp_address: address, + rmn_home_contract_config_digest: vector, + merkle_roots: vector + } + + struct MerkleRoot has drop { + source_chain_selector: u64, + on_ramp_address: vector, + min_seq_nr: u64, + max_seq_nr: u64, + merkle_root: vector + } + + #[event] + struct ConfigSet has store, drop { + version: u32, + config: Config + } + + #[event] + struct Cursed has store, drop { + subjects: vector> + } + + #[event] + struct Uncursed has store, drop { + subjects: vector> + } + + // ================================================================ + // | AllowedCursersV2 (Fast Cursing) | + // ================================================================ + struct AllowedCursersV2 has key { + allowed_cursers: OrderedMap, + allowed_cursers_added_events: EventHandle, + allowed_cursers_removed_events: EventHandle + } + + #[event] + struct AllowedCursersAdded has store, drop { + cursers: vector
+ } + + #[event] + struct AllowedCursersRemoved has store, drop { + cursers: vector
+ } + + const E_ALREADY_INITIALIZED: u64 = 1; + const E_ALREADY_CURSED: u64 = 2; + const E_CONFIG_NOT_SET: u64 = 3; + const E_DUPLICATE_SIGNER: u64 = 4; + const E_INVALID_SIGNATURE: u64 = 5; + const E_INVALID_SIGNER_ORDER: u64 = 6; + const E_NOT_ENOUGH_SIGNERS: u64 = 7; + const E_NOT_CURSED: u64 = 8; + const E_OUT_OF_ORDER_SIGNATURES: u64 = 9; + const E_THRESHOLD_NOT_MET: u64 = 10; + const E_UNEXPECTED_SIGNER: u64 = 11; + const E_ZERO_VALUE_NOT_ALLOWED: u64 = 12; + const E_MERKLE_ROOT_LENGTH_MISMATCH: u64 = 13; + const E_INVALID_DIGEST_LENGTH: u64 = 14; + const E_SIGNERS_MISMATCH: u64 = 15; + const E_INVALID_SUBJECT_LENGTH: u64 = 16; + const E_INVALID_PUBLIC_KEY_LENGTH: u64 = 17; + const E_UNKNOWN_FUNCTION: u64 = 18; + const E_NOT_OWNER_OR_ALLOWED_CURSER: u64 = 19; + const E_ALLOWED_CURSERS_V2_ALREADY_INITIALIZED: u64 = 20; + const E_ALLOWED_CURSERS_V2_NOT_INITIALIZED: u64 = 21; + const E_CURSER_ALREADY_ALLOWED: u64 = 22; + const E_CURSER_NOT_ALLOWED: u64 = 23; + + #[view] + public fun type_and_version(): String { + string::utf8(b"RMNRemote 1.6.0") + } + + fun init_module(publisher: &signer) { + // Register the entrypoint with mcms + if (@mcms_register_entrypoints == @0x1) { + register_mcms_entrypoint(publisher); + }; + } + + public entry fun initialize(caller: &signer, local_chain_selector: u64) { + auth::assert_only_owner(signer::address_of(caller)); + + assert!( + local_chain_selector != 0, + error::invalid_argument(E_ZERO_VALUE_NOT_ALLOWED) + ); + assert!( + !exists(state_object::object_address()), + error::invalid_argument(E_ALREADY_INITIALIZED) + ); + + let state_object_signer = state_object::object_signer(); + + // Create V1 state (RMNRemoteState) + let state = RMNRemoteState { + local_chain_selector, + config: Config { + rmn_home_contract_config_digest: vector[], + signers: vector[], + f_sign: 0 + }, + config_count: 0, + signers: smart_table::new(), + cursed_subjects: smart_table::new(), + config_set_events: account::new_event_handle(&state_object_signer), + cursed_events: account::new_event_handle(&state_object_signer), + uncursed_events: account::new_event_handle(&state_object_signer) + }; + move_to(&state_object_signer, state); + + // Create V2 state (AllowedCursersV2) - new deployments get both + move_to( + &state_object_signer, + AllowedCursersV2 { + allowed_cursers: ordered_map::new(), + allowed_cursers_added_events: account::new_event_handle( + &state_object_signer + ), + allowed_cursers_removed_events: account::new_event_handle( + &state_object_signer + ) + } + ); + } + + #[test_only] + /// Legacy initialization that only creates RMNRemoteState (V1). + /// Used for testing migration scenarios from V1 to V2. + public entry fun initialize_v1( + caller: &signer, local_chain_selector: u64 + ) { + auth::assert_only_owner(signer::address_of(caller)); + + assert!( + local_chain_selector != 0, + error::invalid_argument(E_ZERO_VALUE_NOT_ALLOWED) + ); + assert!( + !exists(state_object::object_address()), + error::invalid_argument(E_ALREADY_INITIALIZED) + ); + + let state_object_signer = state_object::object_signer(); + let state = RMNRemoteState { + local_chain_selector, + config: Config { + rmn_home_contract_config_digest: vector[], + signers: vector[], + f_sign: 0 + }, + config_count: 0, + signers: smart_table::new(), + cursed_subjects: smart_table::new(), + config_set_events: account::new_event_handle(&state_object_signer), + cursed_events: account::new_event_handle(&state_object_signer), + uncursed_events: account::new_event_handle(&state_object_signer) + }; + + move_to(&state_object_signer, state); + } + + inline fun calculate_digest(report: &Report): vector { + let digest = vector[]; + eth_abi::encode_right_padded_bytes32(&mut digest, get_report_digest_header()); + eth_abi::encode_u64(&mut digest, report.dest_chain_id); + eth_abi::encode_u64(&mut digest, report.dest_chain_selector); + eth_abi::encode_address(&mut digest, report.rmn_remote_contract_address); + eth_abi::encode_address(&mut digest, report.off_ramp_address); + eth_abi::encode_right_padded_bytes32( + &mut digest, report.rmn_home_contract_config_digest + ); + report.merkle_roots.for_each_ref( + |merkle_root| { + let merkle_root: &MerkleRoot = merkle_root; + eth_abi::encode_u64(&mut digest, merkle_root.source_chain_selector); + eth_abi::encode_bytes(&mut digest, merkle_root.on_ramp_address); + eth_abi::encode_u64(&mut digest, merkle_root.min_seq_nr); + eth_abi::encode_u64(&mut digest, merkle_root.max_seq_nr); + eth_abi::encode_right_padded_bytes32( + &mut digest, merkle_root.merkle_root + ); + } + ); + aptos_hash::keccak256(digest) + } + + #[view] + public fun verify( + off_ramp_address: address, + merkle_root_source_chain_selectors: vector, + merkle_root_on_ramp_addresses: vector>, + merkle_root_min_seq_nrs: vector, + merkle_root_max_seq_nrs: vector, + merkle_root_values: vector>, + signatures: vector> + ): bool acquires RMNRemoteState { + let state = borrow_state(); + + assert!(state.config_count > 0, error::invalid_argument(E_CONFIG_NOT_SET)); + + let signatures_len = signatures.length(); + assert!( + signatures_len >= (state.config.f_sign + 1), + error::invalid_argument(E_THRESHOLD_NOT_MET) + ); + + let merkle_root_len = merkle_root_source_chain_selectors.length(); + assert!( + merkle_root_len == merkle_root_on_ramp_addresses.length(), + error::invalid_argument(E_MERKLE_ROOT_LENGTH_MISMATCH) + ); + assert!( + merkle_root_len == merkle_root_min_seq_nrs.length(), + error::invalid_argument(E_MERKLE_ROOT_LENGTH_MISMATCH) + ); + assert!( + merkle_root_len == merkle_root_max_seq_nrs.length(), + error::invalid_argument(E_MERKLE_ROOT_LENGTH_MISMATCH) + ); + assert!( + merkle_root_len == merkle_root_values.length(), + error::invalid_argument(E_MERKLE_ROOT_LENGTH_MISMATCH) + ); + + // Since we cannot pass structs, we need to reconstruct it from the individual components. + let merkle_roots = vector[]; + for (i in 0..merkle_root_len) { + let source_chain_selector = merkle_root_source_chain_selectors[i]; + let on_ramp_address = merkle_root_on_ramp_addresses[i]; + let min_seq_nr = merkle_root_min_seq_nrs[i]; + let max_seq_nr = merkle_root_max_seq_nrs[i]; + let merkle_root = merkle_root_values[i]; + merkle_roots.push_back( + MerkleRoot { + source_chain_selector, + on_ramp_address, + min_seq_nr, + max_seq_nr, + merkle_root + } + ); + }; + + let report = Report { + dest_chain_id: (chain_id::get() as u64), + dest_chain_selector: state.local_chain_selector, + rmn_remote_contract_address: @ccip, + off_ramp_address, + rmn_home_contract_config_digest: state.config.rmn_home_contract_config_digest, + merkle_roots + }; + + let digest = calculate_digest(&report); + + let previous_eth_address = vector[]; + for (i in 0..signatures_len) { + let signature_bytes = signatures[i]; + let signature = secp256k1::ecdsa_signature_from_bytes(signature_bytes); + + // rmn only generates signatures with v = 27, subtract the ethereum recover id offset of 27 to get zero. + let v = 0; + let maybe_public_key = secp256k1::ecdsa_recover(digest, v, &signature); + assert!( + maybe_public_key.is_some(), + error::invalid_argument(E_INVALID_SIGNATURE) + ); + + let public_key_bytes = + secp256k1::ecdsa_raw_public_key_to_bytes(&maybe_public_key.extract()); + // trim the first 12 bytes of the hash to recover the ethereum address. + let eth_address = aptos_hash::keccak256(public_key_bytes).trim(12); + + assert!( + state.signers.contains(eth_address), + error::invalid_argument(E_UNEXPECTED_SIGNER) + ); + if (i > 0) { + assert!( + merkle_proof::vector_u8_gt(ð_address, &previous_eth_address), + error::invalid_argument(E_OUT_OF_ORDER_SIGNATURES) + ); + }; + previous_eth_address = eth_address; + }; + + true + } + + #[view] + public fun get_arm(): address { + @ccip + } + + public entry fun set_config( + caller: &signer, + rmn_home_contract_config_digest: vector, + signer_onchain_public_keys: vector>, + node_indexes: vector, + f_sign: u64 + ) acquires RMNRemoteState { + auth::assert_only_owner(signer::address_of(caller)); + + let state = borrow_state_mut(); + + assert!( + rmn_home_contract_config_digest.length() == 32, + error::invalid_argument(E_INVALID_DIGEST_LENGTH) + ); + + assert!( + eth_abi::decode_u256_value(rmn_home_contract_config_digest) != 0, + error::invalid_argument(E_ZERO_VALUE_NOT_ALLOWED) + ); + + let signers_len = signer_onchain_public_keys.length(); + assert!( + signers_len == node_indexes.length(), + error::invalid_argument(E_SIGNERS_MISMATCH) + ); + + for (i in 1..signers_len) { + let previous_node_index = node_indexes[i - 1]; + let current_node_index = node_indexes[i]; + assert!( + previous_node_index < current_node_index, + error::invalid_argument(E_INVALID_SIGNER_ORDER) + ); + }; + + assert!( + signers_len >= (2 * f_sign + 1), + error::invalid_argument(E_NOT_ENOUGH_SIGNERS) + ); + + state.signers.clear(); + + let signers = + signer_onchain_public_keys.zip_map_ref( + &node_indexes, + |signer_public_key_bytes, node_indexes| { + let signer_public_key_bytes: vector = *signer_public_key_bytes; + let node_index: u64 = *node_indexes; + // expect an ethereum address of 20 bytes. + assert!( + signer_public_key_bytes.length() == 20, + error::invalid_argument(E_INVALID_PUBLIC_KEY_LENGTH) + ); + assert!( + !state.signers.contains(signer_public_key_bytes), + error::invalid_argument(E_DUPLICATE_SIGNER) + ); + state.signers.add(signer_public_key_bytes, true); + Signer { + onchain_public_key: signer_public_key_bytes, + node_index + } + } + ); + + let new_config = Config { + rmn_home_contract_config_digest, + signers, + f_sign + }; + state.config = new_config; + + let new_config_count = state.config_count + 1; + state.config_count = new_config_count; + + event::emit_event( + &mut state.config_set_events, + ConfigSet { version: new_config_count, config: new_config } + ); + } + + #[view] + public fun get_versioned_config(): (u32, Config) acquires RMNRemoteState { + let state = borrow_state(); + (state.config_count, state.config) + } + + #[view] + public fun get_local_chain_selector(): u64 acquires RMNRemoteState { + borrow_state().local_chain_selector + } + + #[view] + public fun get_report_digest_header(): vector { + aptos_hash::keccak256(b"RMN_V1_6_ANY2APTOS_REPORT") + } + + public entry fun curse( + caller: &signer, subject: vector + ) acquires RMNRemoteState, AllowedCursersV2 { + curse_multiple(caller, vector[subject]); + } + + public entry fun curse_multiple( + caller: &signer, subjects: vector> + ) acquires RMNRemoteState, AllowedCursersV2 { + assert_owner_or_allowed_curser(signer::address_of(caller)); + + let state = borrow_state_mut(); + + subjects.for_each_ref( + |subject| { + let subject: vector = *subject; + assert!( + subject.length() == 16, + error::invalid_argument(E_INVALID_SUBJECT_LENGTH) + ); + assert!( + !state.cursed_subjects.contains(subject), + error::invalid_argument(E_ALREADY_CURSED) + ); + state.cursed_subjects.add(subject, true); + } + ); + event::emit_event(&mut state.cursed_events, Cursed { subjects }); + } + + public entry fun uncurse( + caller: &signer, subject: vector + ) acquires RMNRemoteState, AllowedCursersV2 { + uncurse_multiple(caller, vector[subject]); + } + + public entry fun uncurse_multiple( + caller: &signer, subjects: vector> + ) acquires RMNRemoteState, AllowedCursersV2 { + assert_owner_or_allowed_curser(signer::address_of(caller)); + + let state = borrow_state_mut(); + + subjects.for_each_ref( + |subject| { + let subject: vector = *subject; + assert!( + state.cursed_subjects.contains(subject), + error::invalid_argument(E_NOT_CURSED) + ); + state.cursed_subjects.remove(subject); + } + ); + event::emit_event(&mut state.uncursed_events, Uncursed { subjects }); + } + + #[view] + public fun get_cursed_subjects(): vector> acquires RMNRemoteState { + borrow_state().cursed_subjects.keys() + } + + #[view] + public fun is_cursed_global(): bool acquires RMNRemoteState { + borrow_state().cursed_subjects.contains(GLOBAL_CURSE_SUBJECT) + } + + #[view] + public fun is_cursed(subject: vector): bool acquires RMNRemoteState { + borrow_state().cursed_subjects.contains(subject) || is_cursed_global() + } + + #[view] + public fun is_cursed_u128(subject_value: u128): bool acquires RMNRemoteState { + let subject = bcs::to_bytes(&subject_value); + subject.reverse(); + is_cursed(subject) + } + + inline fun borrow_state(): &RMNRemoteState { + borrow_global(state_object::object_address()) + } + + inline fun borrow_state_mut(): &mut RMNRemoteState { + borrow_global_mut(state_object::object_address()) + } + + // ================================================================ + // | AllowedCursersV2 Helper Functions | + // ================================================================ + inline fun borrow_allowed_cursers_v2(): &AllowedCursersV2 { + borrow_global(state_object::object_address()) + } + + inline fun borrow_allowed_cursers_v2_mut(): &mut AllowedCursersV2 { + borrow_global_mut(state_object::object_address()) + } + + #[view] + /// Check if an address is an allowed curser. + /// Returns false if AllowedCursersV2 is not initialized (V1 behavior: only owner can curse). + public fun is_allowed_curser(curser: address): bool acquires AllowedCursersV2 { + if (!exists(state_object::object_address())) { false } + else { + borrow_allowed_cursers_v2().allowed_cursers.contains(&curser) + } + } + + #[view] + /// Get the list of allowed cursers. + /// Returns empty vector if AllowedCursersV2 is not initialized. + public fun get_allowed_cursers(): vector
acquires AllowedCursersV2 { + if (!exists(state_object::object_address())) { + vector[] + } else { + borrow_allowed_cursers_v2().allowed_cursers.keys() + } + } + + inline fun assert_owner_or_allowed_curser(caller: address) { + assert!( + caller == auth::owner() || is_allowed_curser(caller), + error::permission_denied(E_NOT_OWNER_OR_ALLOWED_CURSER) + ); + } + + // ================================================================ + // | AllowedCursersV2 Admin Functions (Owner Only) | + // ================================================================ + + /// Initialize the AllowedCursersV2 resource. Owner only. + /// This must be called before adding allowed cursers. + public entry fun initialize_allowed_cursers_v2( + caller: &signer, initial_cursers: vector
+ ) { + auth::assert_only_owner(signer::address_of(caller)); + + assert!( + !exists(state_object::object_address()), + error::already_exists(E_ALLOWED_CURSERS_V2_ALREADY_INITIALIZED) + ); + + let state_object_signer = state_object::object_signer(); + let allowed_cursers = ordered_map::new(); + + initial_cursers.for_each_ref( + |curser| { + allowed_cursers.add(*curser, true); + } + ); + + move_to( + &state_object_signer, + AllowedCursersV2 { + allowed_cursers, + allowed_cursers_added_events: account::new_event_handle( + &state_object_signer + ), + allowed_cursers_removed_events: account::new_event_handle( + &state_object_signer + ) + } + ); + + if (!initial_cursers.is_empty()) { + event::emit(AllowedCursersAdded { cursers: initial_cursers }); + }; + } + + /// Add allowed cursers. Owner only. + /// AllowedCursersV2 must be initialized first. + public entry fun add_allowed_cursers( + caller: &signer, cursers_to_add: vector
+ ) acquires AllowedCursersV2 { + auth::assert_only_owner(signer::address_of(caller)); + + assert!( + exists(state_object::object_address()), + error::invalid_state(E_ALLOWED_CURSERS_V2_NOT_INITIALIZED) + ); + + let state = borrow_allowed_cursers_v2_mut(); + + cursers_to_add.for_each_ref( + |curser| { + assert!( + !state.allowed_cursers.contains(curser), + error::already_exists(E_CURSER_ALREADY_ALLOWED) + ); + state.allowed_cursers.add(*curser, true); + } + ); + + event::emit_event( + &mut state.allowed_cursers_added_events, + AllowedCursersAdded { cursers: cursers_to_add } + ); + } + + /// Remove allowed cursers. Owner only. + /// AllowedCursersV2 must be initialized first. + public entry fun remove_allowed_cursers( + caller: &signer, cursers_to_remove: vector
+ ) acquires AllowedCursersV2 { + auth::assert_only_owner(signer::address_of(caller)); + + assert!( + exists(state_object::object_address()), + error::invalid_state(E_ALLOWED_CURSERS_V2_NOT_INITIALIZED) + ); + + let state = borrow_allowed_cursers_v2_mut(); + + cursers_to_remove.for_each_ref( + |curser| { + assert!( + state.allowed_cursers.contains(curser), + error::not_found(E_CURSER_NOT_ALLOWED) + ); + state.allowed_cursers.remove(curser); + } + ); + + event::emit_event( + &mut state.allowed_cursers_removed_events, + AllowedCursersRemoved { cursers: cursers_to_remove } + ); + } + + // ================================================================ + // | MCMS Entrypoint | + // ================================================================ + struct McmsCallback has drop {} + + public fun mcms_entrypoint( + _metadata: object::Object + ): option::Option acquires RMNRemoteState, AllowedCursersV2 { + let (caller, function, data) = + mcms_registry::get_callback_params(@ccip, McmsCallback {}); + + let function_bytes = *function.bytes(); + let stream = bcs_stream::new(data); + + if (function_bytes == b"initialize") { + let local_chain_selector = bcs_stream::deserialize_u64(&mut stream); + bcs_stream::assert_is_consumed(&stream); + initialize(&caller, local_chain_selector); + } else if (function_bytes == b"set_config") { + let rmn_home_contract_config_digest = + bcs_stream::deserialize_vector_u8(&mut stream); + let signer_onchain_public_keys = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + let node_indexes = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let f_sign = bcs_stream::deserialize_u64(&mut stream); + bcs_stream::assert_is_consumed(&stream); + set_config( + &caller, + rmn_home_contract_config_digest, + signer_onchain_public_keys, + node_indexes, + f_sign + ) + } else if (function_bytes == b"curse") { + let subject = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + curse(&caller, subject) + } else if (function_bytes == b"curse_multiple") { + let subjects = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + bcs_stream::assert_is_consumed(&stream); + curse_multiple(&caller, subjects) + } else if (function_bytes == b"uncurse") { + let subject = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + uncurse(&caller, subject) + } else if (function_bytes == b"uncurse_multiple") { + let subjects = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + bcs_stream::assert_is_consumed(&stream); + uncurse_multiple(&caller, subjects) + } else if (function_bytes == b"initialize_allowed_cursers_v2") { + let initial_cursers = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + initialize_allowed_cursers_v2(&caller, initial_cursers) + } else if (function_bytes == b"add_allowed_cursers") { + let cursers_to_add = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + add_allowed_cursers(&caller, cursers_to_add) + } else if (function_bytes == b"remove_allowed_cursers") { + let cursers_to_remove = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + remove_allowed_cursers(&caller, cursers_to_remove) + } else { + abort error::invalid_argument(E_UNKNOWN_FUNCTION) + }; + + option::none() + } + + /// Callable during upgrades + public(friend) fun register_mcms_entrypoint(publisher: &signer) { + mcms_registry::register_entrypoint( + publisher, string::utf8(b"rmn_remote"), McmsCallback {} + ); + } +} +` + +/** sources/state_object.move */ +export const CCIP_STATE_OBJECT_MOVE = `/// This module creates a single object for storing CCIP state resources in order to: +/// +/// - simplify ownership management +/// - simplify observability: all resources and events can be queried and viewed at a single address +/// - decouple module deployment and initialization: the CCIP module will be deployed using the +/// recommended object code deployment approach, but initialization requires various +/// "constructor" parameters that cannot be passed it at deploy (ie. init_module()) time. +/// Object code deployment only allows for publishing and upgrading modules, with no way to +/// retrieve a signer to store resources (see: 0x1::object_code_deployment), so a different +/// object is necessary. +module ccip::state_object { + use std::account; + use std::error; + use std::object::{Self, ExtendRef, TransferRef}; + use std::signer; + + friend ccip::auth; + friend ccip::fee_quoter; + friend ccip::nonce_manager; + friend ccip::receiver_registry; + friend ccip::rmn_remote; + friend ccip::token_admin_registry; + + struct StateObjectRefs has key { + extend_ref: ExtendRef, + transfer_ref: TransferRef + } + + const E_NOT_OBJECT_DEPLOYMENT: u64 = 1; + + fun init_module(publisher: &signer) { + assert!( + object::is_object(signer::address_of(publisher)), + error::invalid_state(E_NOT_OBJECT_DEPLOYMENT) + ); + + init_module_internal(publisher); + } + + inline fun init_module_internal(publisher: &signer) { + let constructor_ref = object::create_named_object(publisher, b"CCIPStateObject"); + + let extend_ref = object::generate_extend_ref(&constructor_ref); + let transfer_ref = object::generate_transfer_ref(&constructor_ref); + let object_signer = object::generate_signer(&constructor_ref); + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist( + object::address_from_constructor_ref(&constructor_ref) + ); + + move_to(&object_signer, StateObjectRefs { extend_ref, transfer_ref }); + } + + #[view] + public fun get_object_address(): address { + object_address() + } + + public(friend) inline fun object_address(): address { + // hard code the object seed directly in order to keep the function inline. + object::create_object_address(&@ccip, b"CCIPStateObject") + } + + public(friend) fun object_signer(): signer acquires StateObjectRefs { + let store = borrow_global(object_address()); + object::generate_signer_for_extending(&store.extend_ref) + } + + #[test_only] + public fun init_module_for_testing(publisher: &signer) { + init_module_internal(publisher); + } +} +` + +/** sources/token_admin_dispatcher.move */ +export const CCIP_TOKEN_ADMIN_DISPATCHER_MOVE = `module ccip::token_admin_dispatcher { + use std::dispatchable_fungible_asset; + use std::fungible_asset::FungibleAsset; + use std::signer; + + use ccip::auth; + use ccip::token_admin_registry; + + public fun dispatch_lock_or_burn( + caller: &signer, + token_pool_address: address, + fa: FungibleAsset, + sender: address, + remote_chain_selector: u64, + receiver: vector + ): (vector, vector) { + auth::assert_is_allowed_onramp(signer::address_of(caller)); + + if (token_admin_registry::has_token_pool_registration_v2(token_pool_address)) { + token_admin_registry::lock_or_burn_v2( + token_pool_address, + fa, + sender, + remote_chain_selector, + receiver + ) + } else { + let dispatch_fungible_store = + token_admin_registry::start_lock_or_burn( + token_pool_address, + sender, + remote_chain_selector, + receiver + ); + + dispatchable_fungible_asset::deposit(dispatch_fungible_store, fa); + + token_admin_registry::finish_lock_or_burn(token_pool_address) + } + } + + public fun dispatch_release_or_mint( + caller: &signer, + token_pool_address: address, + sender: vector, + receiver: address, + source_amount: u256, + local_token: address, + remote_chain_selector: u64, + source_pool_address: vector, + source_pool_data: vector, + offchain_token_data: vector + ): (FungibleAsset, u64) { + auth::assert_is_allowed_offramp(signer::address_of(caller)); + + if (token_admin_registry::has_token_pool_registration_v2(token_pool_address)) { + token_admin_registry::release_or_mint_v2( + token_pool_address, + sender, + receiver, + source_amount, + local_token, + remote_chain_selector, + source_pool_address, + source_pool_data, + offchain_token_data + ) + } else { + let (dispatch_owner, dispatch_fungible_store) = + token_admin_registry::start_release_or_mint( + token_pool_address, + sender, + receiver, + source_amount, + local_token, + remote_chain_selector, + source_pool_address, + source_pool_data, + offchain_token_data + ); + + let fa = + dispatchable_fungible_asset::withdraw( + &dispatch_owner, dispatch_fungible_store, 0 + ); + + let destination_amount = + token_admin_registry::finish_release_or_mint(token_pool_address); + + (fa, destination_amount) + } + } +} +` + +/** sources/token_admin_registry.move */ +export const CCIP_TOKEN_ADMIN_REGISTRY_MOVE = `module ccip::token_admin_registry { + use std::account; + use std::dispatchable_fungible_asset; + use std::error; + use std::event::{Self, EventHandle}; + use std::function_info::{Self, FunctionInfo}; + use std::fungible_asset::{Self, Metadata, FungibleStore, FungibleAsset}; + use std::object::{Self, Object, ExtendRef, TransferRef}; + use std::option::{Self, Option}; + use std::signer; + use std::big_ordered_map::{Self, BigOrderedMap}; + use std::string::{Self, String}; + use std::type_info::{Self, TypeInfo}; + + use ccip::auth; + use ccip::state_object; + + use mcms::bcs_stream; + use mcms::mcms_registry; + + friend ccip::token_admin_dispatcher; + + enum ExecutionState has store, drop, copy { + IDLE, + LOCK_OR_BURN, + RELEASE_OR_MINT + } + + struct TokenAdminRegistryState has key, store { + extend_ref: ExtendRef, + transfer_ref: TransferRef, + + // fungible asset metadata address -> TokenConfig + token_configs: BigOrderedMap, + pool_set_events: EventHandle, + administrator_transfer_requested_events: EventHandle, + administrator_transferred_events: EventHandle, + token_unregistered_events: EventHandle + } + + struct TokenConfig has store, drop, copy { + token_pool_address: address, + administrator: address, + pending_administrator: address + } + + struct TokenPoolRegistration has key, store { + lock_or_burn_function: FunctionInfo, + release_or_mint_function: FunctionInfo, + proof_typeinfo: TypeInfo, + dispatch_metadata: Object, + dispatch_deposit_fungible_store: Object, + dispatch_extend_ref: ExtendRef, + dispatch_transfer_ref: TransferRef, + dispatch_fa_transfer_ref: fungible_asset::TransferRef, + execution_state: ExecutionState, + executing_lock_or_burn_input_v1: Option, + executing_release_or_mint_input_v1: Option, + executing_lock_or_burn_output_v1: Option, + executing_release_or_mint_output_v1: Option, + local_token: address + } + + struct LockOrBurnInputV1 has store, drop { + sender: address, + remote_chain_selector: u64, + receiver: vector + } + + struct LockOrBurnOutputV1 has store, drop { + dest_token_address: vector, + dest_pool_data: vector + } + + struct ReleaseOrMintInputV1 has store, drop { + sender: vector, + receiver: address, + source_amount: u256, + local_token: address, + remote_chain_selector: u64, + source_pool_address: vector, + source_pool_data: vector, + offchain_token_data: vector + } + + struct ReleaseOrMintOutputV1 has store, drop { + destination_amount: u64 + } + + struct TokenPoolCallbacks has copy, drop, store { + lock_or_burn: |FungibleAsset, LockOrBurnInputV1| (vector, vector), + release_or_mint: |ReleaseOrMintInputV1| (FungibleAsset, u64) + } + + struct TokenPoolRegistrationV2 has key { + callbacks: TokenPoolCallbacks, + local_token: address + } + + #[event] + struct PoolSet has store, drop { + local_token: address, + previous_pool_address: address, + new_pool_address: address + } + + #[event] + struct AdministratorTransferRequested has store, drop { + local_token: address, + current_admin: address, + new_admin: address + } + + #[event] + struct AdministratorTransferred has store, drop { + local_token: address, + new_admin: address + } + + #[event] + struct TokenUnregistered has store, drop { + local_token: address, + previous_pool_address: address + } + + const E_INVALID_FUNGIBLE_ASSET: u64 = 1; + const E_NOT_FUNGIBLE_ASSET_OWNER: u64 = 2; + const E_INVALID_TOKEN_POOL: u64 = 3; + const E_ALREADY_REGISTERED: u64 = 4; + const E_UNKNOWN_FUNCTION: u64 = 5; + const E_PROOF_NOT_IN_TOKEN_POOL_MODULE: u64 = 6; + const E_PROOF_NOT_AT_TOKEN_POOL_ADDRESS: u64 = 7; + const E_UNKNOWN_PROOF_TYPE: u64 = 8; + const E_NOT_IN_IDLE_STATE: u64 = 9; + const E_NOT_IN_LOCK_OR_BURN_STATE: u64 = 10; + const E_NOT_IN_RELEASE_OR_MINT_STATE: u64 = 11; + const E_NON_EMPTY_LOCK_OR_BURN_INPUT: u64 = 12; + const E_NON_EMPTY_LOCK_OR_BURN_OUTPUT: u64 = 13; + const E_NON_EMPTY_RELEASE_OR_MINT_INPUT: u64 = 14; + const E_NON_EMPTY_RELEASE_OR_MINT_OUTPUT: u64 = 15; + const E_MISSING_LOCK_OR_BURN_INPUT: u64 = 16; + const E_MISSING_LOCK_OR_BURN_OUTPUT: u64 = 17; + const E_MISSING_RELEASE_OR_MINT_INPUT: u64 = 18; + const E_MISSING_RELEASE_OR_MINT_OUTPUT: u64 = 19; + const E_TOKEN_POOL_NOT_OBJECT: u64 = 20; + const E_ADMIN_FOR_TOKEN_ALREADY_SET: u64 = 21; + const E_FUNGIBLE_ASSET_NOT_REGISTERED: u64 = 22; + const E_NOT_ADMINISTRATOR: u64 = 23; + const E_NOT_PENDING_ADMINISTRATOR: u64 = 24; + const E_NOT_AUTHORIZED: u64 = 25; + const E_INVALID_TOKEN_FOR_POOL: u64 = 26; + const E_ADMIN_NOT_SET_FOR_TOKEN: u64 = 27; + const E_ADMIN_ALREADY_SET_FOR_TOKEN: u64 = 28; + const E_ZERO_ADDRESS: u64 = 29; + const E_POOL_NOT_REGISTERED: u64 = 30; + const E_TOKEN_MISMATCH: u64 = 31; + + #[view] + public fun type_and_version(): String { + string::utf8(b"TokenAdminRegistry 1.6.0") + } + + fun init_module(publisher: &signer) { + // Register the entrypoint with mcms + if (@mcms_register_entrypoints == @0x1) { + register_mcms_entrypoint(publisher); + }; + + let state_object_signer = state_object::object_signer(); + + let constructor_ref = + object::create_named_object( + &state_object_signer, b"CCIPTokenAdminRegistry" + ); + let extend_ref = object::generate_extend_ref(&constructor_ref); + let transfer_ref = object::generate_transfer_ref(&constructor_ref); + + let state = TokenAdminRegistryState { + extend_ref, + transfer_ref, + token_configs: big_ordered_map::new(), + pool_set_events: account::new_event_handle(&state_object_signer), + administrator_transfer_requested_events: account::new_event_handle( + &state_object_signer + ), + administrator_transferred_events: account::new_event_handle( + &state_object_signer + ), + token_unregistered_events: account::new_event_handle(&state_object_signer) + }; + + move_to(&state_object_signer, state); + } + + #[view] + public fun get_pools( + local_tokens: vector
+ ): vector
acquires TokenAdminRegistryState { + let state = borrow_state(); + + local_tokens.map_ref( + |local_token| { + let local_token: address = *local_token; + if (state.token_configs.contains(&local_token)) { + let token_config = state.token_configs.borrow(&local_token); + token_config.token_pool_address + } else { + // returns @0x0 for assets without token pools. + @0x0 + } + } + ) + } + + #[view] + /// returns the token pool address for the given local token, or @0x0 if the token is not registered. + public fun get_pool(local_token: address): address acquires TokenAdminRegistryState { + let state = borrow_state(); + if (state.token_configs.contains(&local_token)) { + let token_config = state.token_configs.borrow(&local_token); + token_config.token_pool_address + } else { + // returns @0x0 for assets without token pools. + @0x0 + } + } + + #[view] + /// Returns the local token address for the token pool (supports both V1 and V2). + public fun get_pool_local_token( + token_pool_address: address + ): address acquires TokenPoolRegistration, TokenPoolRegistrationV2 { + if (exists(token_pool_address)) { + TokenPoolRegistrationV2[token_pool_address].local_token + } else if (exists(token_pool_address)) { + get_registration(token_pool_address).local_token + } else { + abort error::invalid_argument(E_POOL_NOT_REGISTERED) + } + } + + #[view] + /// Returns the local token address for the token pool. + public fun get_pool_local_token_v2( + token_pool_address: address + ): address acquires TokenPoolRegistrationV2 { + TokenPoolRegistrationV2[token_pool_address].local_token + } + + #[view] + /// Returns true if token pool has TokenPoolRegistrationV2 resource + public fun has_token_pool_registration_v2( + token_pool_address: address + ): bool { + exists(token_pool_address) + } + + #[view] + /// returns (token_pool_address, administrator, pending_administrator) + public fun get_token_config( + local_token: address + ): (address, address, address) acquires TokenAdminRegistryState { + let state = borrow_state(); + if (state.token_configs.contains(&local_token)) { + let token_config = state.token_configs.borrow(&local_token); + ( + token_config.token_pool_address, + token_config.administrator, + token_config.pending_administrator + ) + } else { + (@0x0, @0x0, @0x0) + } + } + + #[view] + /// Get configured tokens paginated using a start key and limit. + /// Caller should call this on a certain block to ensure you the same state for every call. + /// + /// This function retrieves a batch of token addresses from the registry, starting from + /// the token address that comes after the provided start_key. + /// + /// @param start_key - Address to start pagination from (returns tokens AFTER this address) + /// @param max_count - Maximum number of tokens to return + /// + /// @return: + /// - vector
: List of token addresses (up to max_count) + /// - address: Next key to use for pagination (pass this as start_key in next call) + /// - bool: Whether there are more tokens after this batch + public fun get_all_configured_tokens( + start_key: address, max_count: u64 + ): (vector
, address, bool) acquires TokenAdminRegistryState { + let token_configs = &borrow_state().token_configs; + let result = vector[]; + + let current_key_opt = token_configs.next_key(&start_key); + if (max_count == 0 || current_key_opt.is_none()) { + return (result, start_key, current_key_opt.is_some()) + }; + + let current_key = *current_key_opt.borrow(); + + result.push_back(current_key); + + if (max_count == 1) { + let has_more = token_configs.next_key(¤t_key).is_some(); + return (result, current_key, has_more); + }; + + for (i in 1..max_count) { + let next_key_opt = token_configs.next_key(¤t_key); + if (next_key_opt.is_none()) { + return (result, current_key, false) + }; + + current_key = *next_key_opt.borrow(); + result.push_back(current_key); + }; + + // Check if there are more tokens after the last key + let has_more = token_configs.next_key(¤t_key).is_some(); + (result, current_key, has_more) + } + + // ================================================================ + // | Register Pool | + // ================================================================ + #[deprecated] + /// @deprecated: Use \`register_pool_v2()\` instead. + /// + /// Registers pool with \`TokenPoolRegistration\` and sets up dynamic dispatch for a token pool + /// Registry token config mapping must be done separately via \`set_pool()\` + /// by token owner or ccip owner. + public fun register_pool( + token_pool_account: &signer, + token_pool_module_name: vector, + local_token: address, + _proof: ProofType + ) acquires TokenAdminRegistryState { + let token_pool_address = signer::address_of(token_pool_account); + assert!( + !exists(token_pool_address) + && !exists(token_pool_address), + error::invalid_argument(E_ALREADY_REGISTERED) + ); + assert!( + object::object_exists(local_token), + error::invalid_argument(E_INVALID_FUNGIBLE_ASSET) + ); + + let state = borrow_state_mut(); + + let lock_or_burn_function = + function_info::new_function_info( + token_pool_account, + string::utf8(token_pool_module_name), + string::utf8(b"lock_or_burn") + ); + let proof_typeinfo = type_info::type_of(); + assert!( + proof_typeinfo.account_address() == token_pool_address, + error::invalid_argument(E_PROOF_NOT_AT_TOKEN_POOL_ADDRESS) + ); + assert!( + proof_typeinfo.module_name() == token_pool_module_name, + error::invalid_argument(E_PROOF_NOT_IN_TOKEN_POOL_MODULE) + ); + + let release_or_mint_function = + function_info::new_function_info( + token_pool_account, + string::utf8(token_pool_module_name), + string::utf8(b"release_or_mint") + ); + + let dispatch_constructor_ref = + object::create_sticky_object( + object::address_from_extend_ref(&state.extend_ref) + ); + let dispatch_extend_ref = object::generate_extend_ref(&dispatch_constructor_ref); + let dispatch_transfer_ref = + object::generate_transfer_ref(&dispatch_constructor_ref); + + let dispatch_metadata = + fungible_asset::add_fungibility( + &dispatch_constructor_ref, + option::none(), + // max name length is 32 chars + string::utf8(b"CCIPTokenAdminRegistry"), + // max symbol length is 10 chars + string::utf8(b"CCIPTAR"), + 0, + string::utf8(b""), + string::utf8(b"") + ); + + let dispatch_fa_transfer_ref = + fungible_asset::generate_transfer_ref(&dispatch_constructor_ref); + + // create a FungibleStore for dispatchable_deposit(). it's valid for the FungibleStore to be on the same object + // as the fungible asset Metadata itself. + let dispatch_deposit_fungible_store = + fungible_asset::create_store(&dispatch_constructor_ref, dispatch_metadata); + + dispatchable_fungible_asset::register_dispatch_functions( + &dispatch_constructor_ref, + /* withdraw_function= */ option::some(release_or_mint_function), + /* deposit_function= */ option::some(lock_or_burn_function), + /* derived_balance_function= */ option::none() + ); + + move_to( + token_pool_account, + TokenPoolRegistration { + lock_or_burn_function, + release_or_mint_function, + proof_typeinfo, + dispatch_metadata, + dispatch_deposit_fungible_store, + dispatch_extend_ref, + dispatch_transfer_ref, + dispatch_fa_transfer_ref, + execution_state: ExecutionState::IDLE, + executing_lock_or_burn_input_v1: option::none(), + executing_release_or_mint_input_v1: option::none(), + executing_lock_or_burn_output_v1: option::none(), + executing_release_or_mint_output_v1: option::none(), + local_token + } + ); + } + + /// Registers a V2 token pool using function-value callbacks (closures). + /// + /// Upgrade path: existing legacy pools can upgrade to V2 by calling this function, + /// which supersedes the legacy registration without requiring \`unregister_pool()\`. + /// New pools should use V2 directly. Once V2 is registered, legacy registration + /// via \`register_pool()\` is rejected. + public fun register_pool_v2( + token_pool_account: &signer, + local_token: address, + lock_or_burn: |FungibleAsset, LockOrBurnInputV1| (vector, vector) has copy + + drop + store, + release_or_mint: |ReleaseOrMintInputV1| (FungibleAsset, u64) has copy + drop + store + ) { + let token_pool_address = signer::address_of(token_pool_account); + assert!( + !exists(token_pool_address), + error::invalid_argument(E_ALREADY_REGISTERED) + ); + assert!( + object::object_exists(local_token), + error::invalid_argument(E_INVALID_FUNGIBLE_ASSET) + ); + if (exists(token_pool_address)) { + assert!( + get_registration(token_pool_address).local_token == local_token, + error::invalid_argument(E_TOKEN_MISMATCH) + ); + }; + + move_to( + token_pool_account, + TokenPoolRegistrationV2 { + callbacks: TokenPoolCallbacks { lock_or_burn, release_or_mint }, + local_token + } + ); + } + + public entry fun unregister_pool( + caller: &signer, local_token: address + ) acquires TokenAdminRegistryState, TokenPoolRegistration, TokenPoolRegistrationV2 { + let state = borrow_state_mut(); + assert!( + state.token_configs.contains(&local_token), + error::invalid_argument(E_FUNGIBLE_ASSET_NOT_REGISTERED) + ); + + let token_config = state.token_configs.remove(&local_token); + assert!( + token_config.administrator == signer::address_of(caller), + error::permission_denied(E_NOT_ADMINISTRATOR) + ); + + let previous_pool_address = token_config.token_pool_address; + if (exists(previous_pool_address)) { + let TokenPoolRegistration { + lock_or_burn_function: _, + release_or_mint_function: _, + proof_typeinfo: _, + dispatch_metadata: _, + dispatch_deposit_fungible_store: _, + dispatch_extend_ref: _, + dispatch_transfer_ref: _, + dispatch_fa_transfer_ref: _, + execution_state: _, + executing_lock_or_burn_input_v1: _, + executing_release_or_mint_input_v1: _, + executing_lock_or_burn_output_v1: _, + executing_release_or_mint_output_v1: _, + local_token: _ + } = move_from(previous_pool_address); + }; + + if (exists(previous_pool_address)) { + let TokenPoolRegistrationV2 { callbacks: _, local_token: _ } = + move_from(previous_pool_address); + }; + + event::emit_event( + &mut state.token_unregistered_events, + TokenUnregistered { + local_token, + previous_pool_address: token_config.token_pool_address + } + ); + } + + public entry fun set_pool( + caller: &signer, local_token: address, token_pool_address: address + ) acquires TokenAdminRegistryState, TokenPoolRegistration, TokenPoolRegistrationV2 { + assert!( + object::object_exists(local_token), + error::invalid_argument(E_INVALID_FUNGIBLE_ASSET) + ); + + let caller_addr = signer::address_of(caller); + + let pool_local_token = + if (exists(token_pool_address)) { + get_pool_local_token_v2(token_pool_address) + } else if (exists(token_pool_address)) { + get_registration(token_pool_address).local_token + } else { + abort error::invalid_argument(E_POOL_NOT_REGISTERED) + }; + + assert!( + pool_local_token == local_token, + error::invalid_argument(E_INVALID_TOKEN_FOR_POOL) + ); + + let state = borrow_state_mut(); + assert!( + state.token_configs.contains(&local_token), + error::invalid_argument(E_ADMIN_NOT_SET_FOR_TOKEN) + ); + + let config = state.token_configs.borrow_mut(&local_token); + assert!( + config.administrator == caller_addr, + error::permission_denied(E_NOT_ADMINISTRATOR) + ); + + let previous_pool_address = config.token_pool_address; + config.token_pool_address = token_pool_address; + + if (previous_pool_address != token_pool_address) { + event::emit_event( + &mut state.pool_set_events, + PoolSet { + local_token, + previous_pool_address, + new_pool_address: token_pool_address + } + ); + } + } + + public entry fun propose_administrator( + caller: &signer, local_token: address, administrator: address + ) acquires TokenAdminRegistryState { + assert!( + object::object_exists(local_token), + error::invalid_argument(E_INVALID_FUNGIBLE_ASSET) + ); + + let metadata = object::address_to_object(local_token); + let caller_addr = signer::address_of(caller); + + // Allow CCIP owner or token owner to propose administrator + assert!( + object::owns(metadata, caller_addr) || caller_addr == auth::owner(), + error::permission_denied(E_NOT_AUTHORIZED) + ); + + assert!(administrator != @0x0, error::invalid_argument(E_ZERO_ADDRESS)); + + let state = borrow_state_mut(); + if (state.token_configs.contains(&local_token)) { + let config = state.token_configs.borrow_mut(&local_token); + assert!( + config.administrator == @0x0, + error::invalid_argument(E_ADMIN_FOR_TOKEN_ALREADY_SET) + ); + config.pending_administrator = administrator; + } else { + state.token_configs.add( + local_token, + TokenConfig { + token_pool_address: @0x0, + administrator: @0x0, + pending_administrator: administrator + } + ); + }; + + event::emit_event( + &mut state.administrator_transfer_requested_events, + AdministratorTransferRequested { + local_token, + current_admin: @0x0, + new_admin: administrator + } + ); + } + + public entry fun transfer_admin_role( + caller: &signer, local_token: address, new_admin: address + ) acquires TokenAdminRegistryState { + let state = borrow_state_mut(); + + assert!( + state.token_configs.contains(&local_token), + error::invalid_argument(E_FUNGIBLE_ASSET_NOT_REGISTERED) + ); + + let token_config = state.token_configs.borrow_mut(&local_token); + + assert!( + token_config.administrator == signer::address_of(caller), + error::permission_denied(E_NOT_ADMINISTRATOR) + ); + + // can be @0x0 to cancel a pending transfer. + token_config.pending_administrator = new_admin; + + event::emit_event( + &mut state.administrator_transfer_requested_events, + AdministratorTransferRequested { + local_token, + current_admin: token_config.administrator, + new_admin + } + ); + } + + public entry fun accept_admin_role( + caller: &signer, local_token: address + ) acquires TokenAdminRegistryState { + let state = borrow_state_mut(); + + assert!( + state.token_configs.contains(&local_token), + error::invalid_argument(E_FUNGIBLE_ASSET_NOT_REGISTERED) + ); + + let token_config = state.token_configs.borrow_mut(&local_token); + + assert!( + token_config.pending_administrator == signer::address_of(caller), + error::permission_denied(E_NOT_PENDING_ADMINISTRATOR) + ); + + token_config.administrator = token_config.pending_administrator; + token_config.pending_administrator = @0x0; + + event::emit_event( + &mut state.administrator_transferred_events, + AdministratorTransferred { + local_token, + new_admin: token_config.administrator + } + ); + } + + #[view] + public fun is_administrator( + local_token: address, administrator: address + ): bool acquires TokenAdminRegistryState { + let state = borrow_state(); + assert!( + state.token_configs.contains(&local_token), + error::invalid_argument(E_FUNGIBLE_ASSET_NOT_REGISTERED) + ); + + let token_config = state.token_configs.borrow(&local_token); + token_config.administrator == administrator + } + + // ================================================================ + // | Pool I/O V1 | + // ================================================================ + public fun get_lock_or_burn_input_v1( + token_pool_address: address, _proof: ProofType + ): LockOrBurnInputV1 acquires TokenPoolRegistration { + let registration = get_registration_mut(token_pool_address); + + assert!( + type_info::type_of() == registration.proof_typeinfo, + error::permission_denied(E_UNKNOWN_PROOF_TYPE) + ); + + assert!( + registration.execution_state is ExecutionState::LOCK_OR_BURN, + error::invalid_state(E_NOT_IN_LOCK_OR_BURN_STATE) + ); + assert!( + registration.executing_lock_or_burn_input_v1.is_some(), + error::invalid_state(E_MISSING_LOCK_OR_BURN_INPUT) + ); + assert!( + registration.executing_lock_or_burn_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_OUTPUT) + ); + assert!( + registration.executing_release_or_mint_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_INPUT) + ); + assert!( + registration.executing_release_or_mint_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_OUTPUT) + ); + + registration.executing_lock_or_burn_input_v1.extract() + } + + public fun set_lock_or_burn_output_v1( + token_pool_address: address, + _proof: ProofType, + dest_token_address: vector, + dest_pool_data: vector + ) acquires TokenPoolRegistration { + let registration = get_registration_mut(token_pool_address); + + assert!( + type_info::type_of() == registration.proof_typeinfo, + error::permission_denied(E_UNKNOWN_PROOF_TYPE) + ); + + assert!( + registration.execution_state is ExecutionState::LOCK_OR_BURN, + error::invalid_state(E_NOT_IN_LOCK_OR_BURN_STATE) + ); + assert!( + registration.executing_lock_or_burn_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_INPUT) + ); + assert!( + registration.executing_lock_or_burn_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_OUTPUT) + ); + assert!( + registration.executing_release_or_mint_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_INPUT) + ); + assert!( + registration.executing_release_or_mint_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_OUTPUT) + ); + + registration.executing_lock_or_burn_output_v1.fill( + LockOrBurnOutputV1 { dest_token_address, dest_pool_data } + ) + } + + public fun get_release_or_mint_input_v1( + token_pool_address: address, _proof: ProofType + ): ReleaseOrMintInputV1 acquires TokenPoolRegistration { + let registration = get_registration_mut(token_pool_address); + + assert!( + type_info::type_of() == registration.proof_typeinfo, + error::permission_denied(E_UNKNOWN_PROOF_TYPE) + ); + + assert!( + registration.execution_state is ExecutionState::RELEASE_OR_MINT, + error::invalid_state(E_NOT_IN_RELEASE_OR_MINT_STATE) + ); + assert!( + registration.executing_release_or_mint_input_v1.is_some(), + error::invalid_state(E_MISSING_RELEASE_OR_MINT_INPUT) + ); + assert!( + registration.executing_release_or_mint_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_OUTPUT) + ); + assert!( + registration.executing_lock_or_burn_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_INPUT) + ); + assert!( + registration.executing_lock_or_burn_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_OUTPUT) + ); + + registration.executing_release_or_mint_input_v1.extract() + } + + public fun set_release_or_mint_output_v1( + token_pool_address: address, _proof: ProofType, destination_amount: u64 + ) acquires TokenPoolRegistration { + let registration = get_registration_mut(token_pool_address); + + assert!( + type_info::type_of() == registration.proof_typeinfo, + error::permission_denied(E_UNKNOWN_PROOF_TYPE) + ); + + assert!( + registration.execution_state is ExecutionState::RELEASE_OR_MINT, + error::invalid_state(E_NOT_IN_RELEASE_OR_MINT_STATE) + ); + assert!( + registration.executing_release_or_mint_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_INPUT) + ); + assert!( + registration.executing_release_or_mint_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_OUTPUT) + ); + assert!( + registration.executing_lock_or_burn_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_INPUT) + ); + assert!( + registration.executing_lock_or_burn_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_OUTPUT) + ); + + registration.executing_release_or_mint_output_v1.fill( + ReleaseOrMintOutputV1 { destination_amount } + ) + } + + // LockOrBurnInput accessors + public fun get_lock_or_burn_sender(input: &LockOrBurnInputV1): address { + input.sender + } + + public fun get_lock_or_burn_remote_chain_selector( + input: &LockOrBurnInputV1 + ): u64 { + input.remote_chain_selector + } + + public fun get_lock_or_burn_receiver(input: &LockOrBurnInputV1): vector { + input.receiver + } + + // ReleaseOrMintInput accessors + public fun get_release_or_mint_sender(input: &ReleaseOrMintInputV1): vector { + input.sender + } + + public fun get_release_or_mint_receiver( + input: &ReleaseOrMintInputV1 + ): address { + input.receiver + } + + public fun get_release_or_mint_source_amount( + input: &ReleaseOrMintInputV1 + ): u256 { + input.source_amount + } + + public fun get_release_or_mint_local_token( + input: &ReleaseOrMintInputV1 + ): address { + input.local_token + } + + public fun get_release_or_mint_remote_chain_selector( + input: &ReleaseOrMintInputV1 + ): u64 { + input.remote_chain_selector + } + + public fun get_release_or_mint_source_pool_address( + input: &ReleaseOrMintInputV1 + ): vector { + input.source_pool_address + } + + public fun get_release_or_mint_source_pool_data( + input: &ReleaseOrMintInputV1 + ): vector { + input.source_pool_data + } + + public fun get_release_or_mint_offchain_token_data( + input: &ReleaseOrMintInputV1 + ): vector { + input.offchain_token_data + } + + // ================================================================ + // | Lock or Burn | + // ================================================================ + public(friend) fun start_lock_or_burn( + token_pool_address: address, + sender: address, + remote_chain_selector: u64, + receiver: vector + ): Object acquires TokenPoolRegistration { + let registration = get_registration_mut(token_pool_address); + + assert!( + registration.execution_state is ExecutionState::IDLE, + error::invalid_state(E_NOT_IN_IDLE_STATE) + ); + assert!( + registration.executing_lock_or_burn_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_INPUT) + ); + assert!( + registration.executing_lock_or_burn_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_OUTPUT) + ); + assert!( + registration.executing_release_or_mint_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_INPUT) + ); + assert!( + registration.executing_release_or_mint_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_OUTPUT) + ); + + registration.execution_state = ExecutionState::LOCK_OR_BURN; + registration.executing_lock_or_burn_input_v1.fill( + LockOrBurnInputV1 { sender, remote_chain_selector, receiver } + ); + + registration.dispatch_deposit_fungible_store + } + + public(friend) fun finish_lock_or_burn( + token_pool_address: address + ): (vector, vector) acquires TokenPoolRegistration { + let registration = get_registration_mut(token_pool_address); + + assert!( + registration.execution_state is ExecutionState::LOCK_OR_BURN, + error::invalid_state(E_NOT_IN_LOCK_OR_BURN_STATE) + ); + assert!( + registration.executing_lock_or_burn_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_INPUT) + ); + assert!( + registration.executing_lock_or_burn_output_v1.is_some(), + error::invalid_state(E_MISSING_LOCK_OR_BURN_OUTPUT) + ); + assert!( + registration.executing_release_or_mint_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_INPUT) + ); + assert!( + registration.executing_release_or_mint_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_OUTPUT) + ); + + registration.execution_state = ExecutionState::IDLE; + + // the dispatch callback is passed a fungible_asset::TransferRef reference which could allow the store to be frozen, + // causing future deposit/withdraw callbacks to fail. note that this fungible store is only used as part of the dispatch + // mechanism. + // ref: https://github.com/aptos-labs/aptos-core/blob/7fc73792e9db11462c9a42038c4a9eb41cc00192/aptos-move/framework/aptos-framework/sources/fungible_asset.move#L923 + if (fungible_asset::is_frozen(registration.dispatch_deposit_fungible_store)) { + fungible_asset::set_frozen_flag( + ®istration.dispatch_fa_transfer_ref, + registration.dispatch_deposit_fungible_store, + false + ); + }; + + let output = registration.executing_lock_or_burn_output_v1.extract(); + (output.dest_token_address, output.dest_pool_data) + } + + // ================================================================ + // | Release or Mint | + // ================================================================ + public(friend) fun start_release_or_mint( + token_pool_address: address, + sender: vector, + receiver: address, + source_amount: u256, + local_token: address, + remote_chain_selector: u64, + source_pool_address: vector, + source_pool_data: vector, + offchain_token_data: vector + ): (signer, Object) acquires TokenPoolRegistration { + let registration = get_registration_mut(token_pool_address); + + assert!( + registration.execution_state is ExecutionState::IDLE, + error::invalid_state(E_NOT_IN_IDLE_STATE) + ); + assert!( + registration.executing_release_or_mint_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_INPUT) + ); + assert!( + registration.executing_release_or_mint_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_OUTPUT) + ); + assert!( + registration.executing_lock_or_burn_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_INPUT) + ); + assert!( + registration.executing_lock_or_burn_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_OUTPUT) + ); + + registration.execution_state = ExecutionState::RELEASE_OR_MINT; + registration.executing_release_or_mint_input_v1.fill( + ReleaseOrMintInputV1 { + sender, + receiver, + source_amount, + local_token, + remote_chain_selector, + source_pool_address, + source_pool_data, + offchain_token_data + } + ); + + ( + object::generate_signer_for_extending(®istration.dispatch_extend_ref), + registration.dispatch_deposit_fungible_store + ) + } + + public(friend) fun finish_release_or_mint( + token_pool_address: address + ): u64 acquires TokenPoolRegistration { + let registration = get_registration_mut(token_pool_address); + + assert!( + registration.execution_state is ExecutionState::RELEASE_OR_MINT, + error::invalid_state(E_NOT_IN_RELEASE_OR_MINT_STATE) + ); + assert!( + registration.executing_release_or_mint_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_RELEASE_OR_MINT_INPUT) + ); + assert!( + registration.executing_release_or_mint_output_v1.is_some(), + error::invalid_state(E_MISSING_RELEASE_OR_MINT_OUTPUT) + ); + assert!( + registration.executing_lock_or_burn_input_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_INPUT) + ); + assert!( + registration.executing_lock_or_burn_output_v1.is_none(), + error::invalid_state(E_NON_EMPTY_LOCK_OR_BURN_OUTPUT) + ); + + registration.execution_state = ExecutionState::IDLE; + + // the dispatch callback is passed a fungible_asset::TransferRef reference which could allow the store to be frozen, + // causing future deposit/withdraw callbacks to fail. note that this fungible store is only used as part of the dispatch + // mechanism. + // ref: https://github.com/aptos-labs/aptos-core/blob/7fc73792e9db11462c9a42038c4a9eb41cc00192/aptos-move/framework/aptos-framework/sources/fungible_asset.move#L936 + if (fungible_asset::is_frozen(registration.dispatch_deposit_fungible_store)) { + fungible_asset::set_frozen_flag( + ®istration.dispatch_fa_transfer_ref, + registration.dispatch_deposit_fungible_store, + false + ); + }; + + let output = registration.executing_release_or_mint_output_v1.extract(); + + output.destination_amount + } + + public(friend) fun lock_or_burn_v2( + token_pool_address: address, + fa: fungible_asset::FungibleAsset, + sender: address, + remote_chain_selector: u64, + receiver: vector + ): (vector, vector) acquires TokenPoolRegistrationV2 { + let pool_config = &TokenPoolRegistrationV2[token_pool_address]; + let input = LockOrBurnInputV1 { sender, remote_chain_selector, receiver }; + + (pool_config.callbacks.lock_or_burn) + (fa, input) + } + + public(friend) fun release_or_mint_v2( + token_pool_address: address, + sender: vector, + receiver: address, + source_amount: u256, + local_token: address, + remote_chain_selector: u64, + source_pool_address: vector, + source_pool_data: vector, + offchain_token_data: vector + ): (FungibleAsset, u64) acquires TokenPoolRegistrationV2 { + let pool_config = &TokenPoolRegistrationV2[token_pool_address]; + let input = + ReleaseOrMintInputV1 { + sender, + receiver, + source_amount, + local_token, + remote_chain_selector, + source_pool_address, + source_pool_data, + offchain_token_data + }; + + (pool_config.callbacks.release_or_mint) + (input) + } + + inline fun borrow_state(): &TokenAdminRegistryState { + borrow_global(state_object::object_address()) + } + + inline fun borrow_state_mut(): &mut TokenAdminRegistryState { + borrow_global_mut(state_object::object_address()) + } + + inline fun get_registration(token_pool_address: address): &TokenPoolRegistration { + freeze(get_registration_mut(token_pool_address)) + } + + inline fun get_registration_mut(token_pool_address: address) + : &mut TokenPoolRegistration { + assert!( + exists(token_pool_address), + error::invalid_argument(E_INVALID_TOKEN_POOL) + ); + borrow_global_mut(token_pool_address) + } + + // ================================================================ + // | MCMS Entrypoint | + // ================================================================ + struct McmsCallback has drop {} + + public fun mcms_entrypoint( + _metadata: Object + ): option::Option acquires TokenAdminRegistryState, TokenPoolRegistration, TokenPoolRegistrationV2 { + let (caller, function, data) = + mcms_registry::get_callback_params(@ccip, McmsCallback {}); + + let function_bytes = *function.bytes(); + let stream = bcs_stream::new(data); + + if (function_bytes == b"set_pool") { + let local_token = bcs_stream::deserialize_address(&mut stream); + let token_pool_address = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + set_pool(&caller, local_token, token_pool_address) + } else if (function_bytes == b"propose_administrator") { + let local_token = bcs_stream::deserialize_address(&mut stream); + let administrator = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + propose_administrator(&caller, local_token, administrator) + } else if (function_bytes == b"transfer_admin_role") { + let local_token = bcs_stream::deserialize_address(&mut stream); + let new_admin = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + transfer_admin_role(&caller, local_token, new_admin) + } else if (function_bytes == b"accept_admin_role") { + let local_token = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + accept_admin_role(&caller, local_token) + } else { + abort error::invalid_argument(E_UNKNOWN_FUNCTION) + }; + + option::none() + } + + /// Callable during upgrades + public(friend) fun register_mcms_entrypoint(publisher: &signer) { + mcms_registry::register_entrypoint( + publisher, string::utf8(b"token_admin_registry"), McmsCallback {} + ); + } + + #[test_only] + public fun init_module_for_testing(publisher: &signer) { + init_module(publisher); + } + + #[test_only] + public fun get_token_unregistered_events(): vector acquires TokenAdminRegistryState { + event::emitted_events_by_handle( + &borrow_state().token_unregistered_events + ) + } + + #[test_only] + fun insert_token_addresses_for_test( + token_addresses: vector
+ ) acquires TokenAdminRegistryState { + let state = borrow_state_mut(); + + token_addresses.for_each( + |token_address| { + state.token_configs.add( + token_address, + TokenConfig { + token_pool_address: @0x0, + administrator: @0x0, + pending_administrator: @0x0 + } + ); + } + ); + } + + #[test(publisher = @ccip)] + fun test_get_all_configured_tokens(publisher: &signer) acquires TokenAdminRegistryState { + state_object::init_module_for_testing(publisher); + init_module_for_testing(publisher); + + insert_token_addresses_for_test(vector[@0x1, @0x2, @0x3]); + + let (res, next_key, has_more) = get_all_configured_tokens(@0x0, 0); + assert!(res.length() == 0); + assert!(next_key == @0x0); + assert!(has_more); + + let (res, next_key, has_more) = get_all_configured_tokens(@0x0, 3); + assert!(res.length() == 3); + assert!(vector[@0x1, @0x2, @0x3] == res); + assert!(next_key == @0x3); + assert!(!has_more); + } + + #[test(publisher = @ccip)] + fun test_get_all_configured_tokens_edge_cases( + publisher: &signer + ) acquires TokenAdminRegistryState { + state_object::init_module_for_testing(publisher); + init_module_for_testing(publisher); + + // Test case 1: Empty state + let (res, next_key, has_more) = get_all_configured_tokens(@0x0, 1); + assert!(res.length() == 0); + assert!(next_key == @0x0); + assert!(!has_more); + + // Test case 2: Single token + insert_token_addresses_for_test(vector[@0x1]); + let (res, _next_key, has_more) = get_all_configured_tokens(@0x0, 1); + assert!(res.length() == 1); + assert!(res[0] == @0x1); + assert!(!has_more); + + // Test case 3: Start from middle + insert_token_addresses_for_test(vector[@0x2, @0x3]); + let (res, _next_key, has_more) = get_all_configured_tokens(@0x1, 2); + assert!(res.length() == 2); + assert!(res[0] == @0x2); + assert!(res[1] == @0x3); + assert!(!has_more); + + // Test case 4: Request more than available + let (res, _next_key, has_more) = get_all_configured_tokens(@0x0, 5); + assert!(res.length() == 3); + assert!(res[0] == @0x1); + assert!(res[1] == @0x2); + assert!(res[2] == @0x3); + assert!(!has_more); + } + + #[test(publisher = @ccip)] + fun test_get_all_configured_tokens_pagination( + publisher: &signer + ) acquires TokenAdminRegistryState { + state_object::init_module_for_testing(publisher); + init_module_for_testing(publisher); + + insert_token_addresses_for_test(vector[@0x1, @0x2, @0x3, @0x4, @0x5]); + + // Test pagination with different chunk sizes + let current_key = @0x0; + let total_tokens = vector[]; + + // First page: get 2 tokens + let (res, next_key, more) = get_all_configured_tokens(current_key, 2); + assert!(res.length() == 2); + assert!(res[0] == @0x1); + assert!(res[1] == @0x2); + assert!(more); + current_key = next_key; + total_tokens.append(res); + + // Second page: get 2 more tokens + let (res, next_key, more) = get_all_configured_tokens(current_key, 2); + assert!(res.length() == 2); + assert!(res[0] == @0x3); + assert!(res[1] == @0x4); + assert!(more); + current_key = next_key; + total_tokens.append(res); + + // Last page: get remaining token + let (res, _next_key, more) = get_all_configured_tokens(current_key, 2); + assert!(res.length() == 1); + assert!(res[0] == @0x5); + assert!(!more); + total_tokens.append(res); + + // Verify we got all tokens in order + assert!(total_tokens.length() == 5); + assert!(total_tokens[0] == @0x1); + assert!(total_tokens[1] == @0x2); + assert!(total_tokens[2] == @0x3); + assert!(total_tokens[3] == @0x4); + assert!(total_tokens[4] == @0x5); + } + + #[test(publisher = @ccip)] + fun test_get_all_configured_tokens_non_existent( + publisher: &signer + ) acquires TokenAdminRegistryState { + state_object::init_module_for_testing(publisher); + init_module_for_testing(publisher); + + insert_token_addresses_for_test(vector[@0x1, @0x2, @0x3]); + + // Test starting from non-existent key + let (res, next_key, has_more) = get_all_configured_tokens(@0x4, 1); + assert!(res.length() == 0); + assert!(next_key == @0x4); + assert!(!has_more); + + // Test starting from key between existing tokens + let (res, _next_key, has_more) = get_all_configured_tokens(@0x1, 1); + assert!(res.length() == 1); + assert!(res[0] == @0x2); + assert!(has_more); + } +} +` + +/** sources/util/address.move */ +export const CCIP_UTIL_ADDRESS_MOVE = `module ccip::address { + + const E_ZERO_ADDRESS_NOT_ALLOWED: u64 = 1; + + public fun assert_non_zero_address_vector(addr: &vector) { + assert!(!addr.is_empty(), E_ZERO_ADDRESS_NOT_ALLOWED); + + let is_zero_address = addr.all(|byte| *byte == 0); + assert!(!is_zero_address, E_ZERO_ADDRESS_NOT_ALLOWED); + } + + public fun assert_non_zero_address(addr: address) { + assert!(addr != @0x0, E_ZERO_ADDRESS_NOT_ALLOWED); + } +} +` diff --git a/ccip-sdk/src/cct/aptos/bytecodes/lock_release_token_pool.ts b/ccip-sdk/src/cct/aptos/bytecodes/lock_release_token_pool.ts new file mode 100644 index 00000000..bfd8bcbe --- /dev/null +++ b/ccip-sdk/src/cct/aptos/bytecodes/lock_release_token_pool.ts @@ -0,0 +1,994 @@ +/** + * LockReleaseTokenPool Move package source files. + * + * Source: chainlink-aptos contracts/ccip/ccip_token_pools/lock_release_token_pool + * AptosFramework rev: 16beac69835f3a71564c96164a606a23f259099a + * ChainlinkCCIP + MCMS: embedded as local dependencies + * + * For standard Aptos Fungible Asset tokens using lock/release (custody-based) mechanism. + * Tokens are locked in the pool on outbound and released on inbound. + * + * Vendored as source (not compiled bytecodes) because Aptos Move modules + * must be compiled with the deployer's address at deploy time. + * + * Lazy-loaded via dynamic import() — same pattern as EVM BurnMintERC20 bytecode. + */ + +export const LOCK_RELEASE_POOL_MOVE_TOML = `[package] +name = "LockReleaseTokenPool" +version = "1.0.0" +authors = [] + +[addresses] +ccip = "_" +ccip_token_pool = "_" +lock_release_token_pool = "_" +mcms = "_" +mcms_register_entrypoints = "_" +lock_release_local_token = "_" + +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "16beac69835f3a71564c96164a606a23f259099a", subdir = "aptos-move/framework/aptos-framework" } +ChainlinkCCIP = { local = "../ccip" } +CCIPTokenPool = { local = "../token_pool" } +` + +export const LOCK_RELEASE_TOKEN_POOL_MOVE = `module lock_release_token_pool::lock_release_token_pool { + use std::account::{Self, SignerCapability}; + use std::error; + use std::fungible_asset::{ + Self, + FungibleAsset, + Metadata, + TransferRef, + FungibleStore + }; + use std::dispatchable_fungible_asset; + use std::primary_fungible_store; + use std::object::{Self, Object, ObjectCore}; + use std::option::{Self, Option}; + use std::signer; + use std::string::{Self, String}; + + use ccip::token_admin_registry::{Self, LockOrBurnInputV1, ReleaseOrMintInputV1}; + use ccip_token_pool::ownable; + use ccip_token_pool::rate_limiter; + use ccip_token_pool::token_pool; + + use mcms::mcms_registry; + use mcms::bcs_stream; + + const STORE_OBJECT_SEED: vector = b"CcipLockReleaseTokenPool"; + + struct LockReleaseTokenPoolDeployment has key { + store_signer_cap: SignerCapability, + ownable_state: ownable::OwnableState, + token_pool_state: token_pool::TokenPoolState + } + + struct LockReleaseTokenPoolState has key, store { + store_signer_cap: SignerCapability, + ownable_state: ownable::OwnableState, + token_pool_state: token_pool::TokenPoolState, + store_signer_address: address, + transfer_ref: Option, + rebalancer: address + } + + const E_NOT_PUBLISHER: u64 = 1; + const E_ALREADY_INITIALIZED: u64 = 2; + const E_INVALID_FUNGIBLE_ASSET: u64 = 3; + const E_INVALID_ARGUMENTS: u64 = 4; + const E_UNKNOWN_FUNCTION: u64 = 5; + const E_LOCAL_TOKEN_MISMATCH: u64 = 6; + const E_DISPATCHABLE_TOKEN_WITHOUT_TRANSFER_REF: u64 = 7; + const E_UNAUTHORIZED: u64 = 8; + const E_INSUFFICIENT_LIQUIDITY: u64 = 9; + const E_TRANSFER_REF_NOT_SET: u64 = 10; + + // ================================================================ + // | Init | + // ================================================================ + #[view] + public fun type_and_version(): String { + string::utf8(b"LockReleaseTokenPool 1.6.0") + } + + fun init_module(publisher: &signer) { + // register the pool on deployment, because in the case of object code deployment, + // this is the only time we have a signer ref to @ccip_lock_release_pool. + assert!( + object::object_exists(@lock_release_local_token), + error::invalid_argument(E_INVALID_FUNGIBLE_ASSET) + ); + let metadata = object::address_to_object(@lock_release_local_token); + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist(@lock_release_token_pool); + + // the name of this module. if incorrect, callbacks will fail to be registered and + // register_pool will revert. + let token_pool_module_name = b"lock_release_token_pool"; + + // Register the entrypoint with mcms + if (@mcms_register_entrypoints == @0x1) { + register_mcms_entrypoint(publisher, token_pool_module_name); + }; + + // Register V2 pool with closure-based callbacks + register_v2_callbacks(publisher); + + // create a resource account to be the owner of the primary FungibleStore we will use. + let (store_signer, store_signer_cap) = + account::create_resource_account(publisher, STORE_OBJECT_SEED); + + // make sure this is a valid fungible asset that is primary fungible store enabled, + // ie. created with primary_fungible_store::create_primary_store_enabled_fungible_asset + primary_fungible_store::ensure_primary_store_exists( + signer::address_of(&store_signer), metadata + ); + + move_to( + publisher, + LockReleaseTokenPoolDeployment { + store_signer_cap, + ownable_state: ownable::new(&store_signer, @lock_release_token_pool), + token_pool_state: token_pool::initialize( + &store_signer, @lock_release_local_token, vector[] + ) + } + ); + } + + /// Tokens that have dynamic dispatch enabled must provide a \`TransferRef\` + /// Tokens that do not have dynamic dispatch enabled can provide \`option::none()\` + /// You can still provide a transfer ref for tokens that don't have dynamic dispatch enabled + /// if you choose to do so. + public fun initialize( + caller: &signer, transfer_ref: Option, rebalancer: address + ) acquires LockReleaseTokenPoolDeployment { + assert_can_initialize(signer::address_of(caller)); + + assert!( + exists(@lock_release_token_pool), + error::invalid_argument(E_ALREADY_INITIALIZED) + ); + + let LockReleaseTokenPoolDeployment { + store_signer_cap, + ownable_state, + token_pool_state + } = move_from(@lock_release_token_pool); + + let store_signer = account::create_signer_with_capability(&store_signer_cap); + let store_signer_address = signer::address_of(&store_signer); + + // If transfer ref is not provided, tokens with dynamic dispatch on deposit and withdraw + // are not allowed for this pool + if (transfer_ref.is_none()) { + let store = + primary_fungible_store::primary_store( + store_signer_address, + token_pool::get_fa_metadata(&token_pool_state) + ); + assert!( + fungible_asset::deposit_dispatch_function(store).is_none() + && fungible_asset::withdraw_dispatch_function(store).is_none(), + E_DISPATCHABLE_TOKEN_WITHOUT_TRANSFER_REF + ); + } else { + let metadata = object::address_to_object(@lock_release_local_token); + let transfer_ref_metadata = + fungible_asset::transfer_ref_metadata(transfer_ref.borrow()); + assert!(metadata == transfer_ref_metadata, E_LOCAL_TOKEN_MISMATCH); + }; + + let pool = LockReleaseTokenPoolState { + store_signer_cap, + ownable_state, + token_pool_state, + store_signer_address, + transfer_ref, + rebalancer + }; + move_to(&store_signer, pool); + } + + public fun register_v2_callbacks(publisher: &signer) { + assert!( + signer::address_of(publisher) == @lock_release_token_pool, + error::permission_denied(E_NOT_PUBLISHER) + ); + token_admin_registry::register_pool_v2( + publisher, + @lock_release_local_token, + lock_or_burn_v2, + release_or_mint_v2 + ); + } + + // ================================================================ + // | Exposing token_pool functions | + // ================================================================ + #[view] + public fun get_token(): address acquires LockReleaseTokenPoolState { + token_pool::get_token(&borrow_pool().token_pool_state) + } + + #[view] + public fun get_router(): address { + token_pool::get_router() + } + + #[view] + public fun get_token_decimals(): u8 acquires LockReleaseTokenPoolState { + token_pool::get_token_decimals(&borrow_pool().token_pool_state) + } + + #[view] + public fun get_remote_pools( + remote_chain_selector: u64 + ): vector> acquires LockReleaseTokenPoolState { + token_pool::get_remote_pools( + &borrow_pool().token_pool_state, remote_chain_selector + ) + } + + #[view] + public fun is_remote_pool( + remote_chain_selector: u64, remote_pool_address: vector + ): bool acquires LockReleaseTokenPoolState { + token_pool::is_remote_pool( + &borrow_pool().token_pool_state, + remote_chain_selector, + remote_pool_address + ) + } + + #[view] + public fun get_remote_token( + remote_chain_selector: u64 + ): vector acquires LockReleaseTokenPoolState { + let pool = borrow_pool(); + token_pool::get_remote_token(&pool.token_pool_state, remote_chain_selector) + } + + public entry fun add_remote_pool( + caller: &signer, remote_chain_selector: u64, remote_pool_address: vector + ) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::add_remote_pool( + &mut pool.token_pool_state, + remote_chain_selector, + remote_pool_address + ); + } + + public entry fun remove_remote_pool( + caller: &signer, remote_chain_selector: u64, remote_pool_address: vector + ) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::remove_remote_pool( + &mut pool.token_pool_state, + remote_chain_selector, + remote_pool_address + ); + } + + inline fun has_transfer_ref(pool: &LockReleaseTokenPoolState): bool { + pool.transfer_ref.is_some() + } + + #[view] + public fun pool_primary_store(): Object acquires LockReleaseTokenPoolState { + let pool = borrow_pool(); + primary_fungible_store::primary_store( + pool.store_signer_address, + token_pool::get_fa_metadata(&pool.token_pool_state) + ) + } + + inline fun pool_primary_store_inlined( + pool: &LockReleaseTokenPoolState + ): Object { + primary_fungible_store::primary_store( + pool.store_signer_address, + token_pool::get_fa_metadata(&pool.token_pool_state) + ) + } + + #[view] + public fun balance(): u64 acquires LockReleaseTokenPoolState { + fungible_asset::balance(pool_primary_store()) + } + + #[view] + public fun derived_balance(): u64 acquires LockReleaseTokenPoolState { + dispatchable_fungible_asset::derived_balance(pool_primary_store()) + } + + #[view] + public fun is_supported_chain( + remote_chain_selector: u64 + ): bool acquires LockReleaseTokenPoolState { + let pool = borrow_pool(); + token_pool::is_supported_chain(&pool.token_pool_state, remote_chain_selector) + } + + #[view] + public fun get_supported_chains(): vector acquires LockReleaseTokenPoolState { + let pool = borrow_pool(); + token_pool::get_supported_chains(&pool.token_pool_state) + } + + public entry fun apply_chain_updates( + caller: &signer, + remote_chain_selectors_to_remove: vector, + remote_chain_selectors_to_add: vector, + remote_pool_addresses_to_add: vector>>, + remote_token_addresses_to_add: vector> + ) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::apply_chain_updates( + &mut pool.token_pool_state, + remote_chain_selectors_to_remove, + remote_chain_selectors_to_add, + remote_pool_addresses_to_add, + remote_token_addresses_to_add + ); + } + + #[view] + public fun get_allowlist_enabled(): bool acquires LockReleaseTokenPoolState { + let pool = borrow_pool(); + token_pool::get_allowlist_enabled(&pool.token_pool_state) + } + + public entry fun set_allowlist_enabled( + caller: &signer, enabled: bool + ) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + token_pool::set_allowlist_enabled(&mut pool.token_pool_state, enabled); + } + + #[view] + public fun get_allowlist(): vector
acquires LockReleaseTokenPoolState { + let pool = borrow_pool(); + token_pool::get_allowlist(&pool.token_pool_state) + } + + public entry fun apply_allowlist_updates( + caller: &signer, removes: vector
, adds: vector
+ ) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + token_pool::apply_allowlist_updates(&mut pool.token_pool_state, removes, adds); + } + + // ================================================================ + // | Lock/Release | + // ================================================================ + + // the callback proof type used as authentication to retrieve and set input and output arguments. + struct CallbackProof has drop {} + + public fun lock_or_burn( + _store: Object, fa: FungibleAsset, _transfer_ref: &TransferRef + ) acquires LockReleaseTokenPoolState { + // retrieve the input for this lock or burn operation. if this function is invoked + // outside of ccip::token_admin_registry, the transaction will abort. + let input = + token_admin_registry::get_lock_or_burn_input_v1( + @lock_release_token_pool, CallbackProof {} + ); + + let pool = borrow_pool_mut(); + let fa_amount = fungible_asset::amount(&fa); + + // This method validates various aspects of the lock or burn operation. If any of the + // validations fail, the transaction will abort. + let dest_token_address = + token_pool::validate_lock_or_burn( + &mut pool.token_pool_state, + &fa, + &input, + fa_amount + ); + + // Construct lock_or_burn output before we lose access to fa + let dest_pool_data = token_pool::encode_local_decimals(&pool.token_pool_state); + let metadata = token_pool::get_fa_metadata(&pool.token_pool_state); + let store = + primary_fungible_store::primary_store(pool.store_signer_address, metadata); + + // Lock the funds in the pool + if (has_transfer_ref(pool)) { + let transfer_ref = pool.transfer_ref.borrow(); + fungible_asset::deposit_with_ref(transfer_ref, store, fa); + } else { + fungible_asset::deposit(store, fa); + }; + + // set the output for this lock or burn operation. + token_admin_registry::set_lock_or_burn_output_v1( + @lock_release_token_pool, + CallbackProof {}, + dest_token_address, + dest_pool_data + ); + + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(&input); + + token_pool::emit_locked_or_burned( + &mut pool.token_pool_state, fa_amount, remote_chain_selector + ); + } + + public fun release_or_mint( + _store: Object, _amount: u64, _transfer_ref: &TransferRef + ): FungibleAsset acquires LockReleaseTokenPoolState { + // retrieve the input for this release or mint operation. if this function is invoked + // outside of ccip::token_admin_registry, the transaction will abort. + let input = + token_admin_registry::get_release_or_mint_input_v1( + @lock_release_token_pool, CallbackProof {} + ); + let pool = borrow_pool_mut(); + let local_amount = + token_pool::calculate_release_or_mint_amount(&pool.token_pool_state, &input); + + token_pool::validate_release_or_mint( + &mut pool.token_pool_state, &input, local_amount + ); + + let store_signer = account::create_signer_with_capability(&pool.store_signer_cap); + let metadata = token_pool::get_fa_metadata(&pool.token_pool_state); + let store = + primary_fungible_store::primary_store(pool.store_signer_address, metadata); + + // Withdraw the amount from the store for release. this will revert if the store has insufficient balance. + let fa = + if (has_transfer_ref(pool)) { + let transfer_ref = pool.transfer_ref.borrow(); + fungible_asset::withdraw_with_ref(transfer_ref, store, local_amount) + } else { + fungible_asset::withdraw(&store_signer, store, local_amount) + }; + + // set the output for this release or mint operation. + token_admin_registry::set_release_or_mint_output_v1( + @lock_release_token_pool, CallbackProof {}, local_amount + ); + + let recipient = token_admin_registry::get_release_or_mint_receiver(&input); + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(&input); + + token_pool::emit_released_or_minted( + &mut pool.token_pool_state, + recipient, + local_amount, + remote_chain_selector + ); + + // return the withdrawn fungible asset. + fa + } + + #[persistent] + fun lock_or_burn_v2( + fa: FungibleAsset, input: LockOrBurnInputV1 + ): (vector, vector) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + let fa_amount = fungible_asset::amount(&fa); + + let dest_token_address = + token_pool::validate_lock_or_burn( + &mut pool.token_pool_state, + &fa, + &input, + fa_amount + ); + + // Lock the funds in the pool + primary_fungible_store::deposit(pool.store_signer_address, fa); + + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(&input); + + token_pool::emit_locked_or_burned( + &mut pool.token_pool_state, fa_amount, remote_chain_selector + ); + + (dest_token_address, token_pool::encode_local_decimals(&pool.token_pool_state)) + } + + #[persistent] + fun release_or_mint_v2( + input: ReleaseOrMintInputV1 + ): (FungibleAsset, u64) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + let local_amount = + token_pool::calculate_release_or_mint_amount(&pool.token_pool_state, &input); + + token_pool::validate_release_or_mint( + &mut pool.token_pool_state, &input, local_amount + ); + + let store_signer = account::create_signer_with_capability(&pool.store_signer_cap); + let metadata = token_pool::get_fa_metadata(&pool.token_pool_state); + + // Withdraw the amount from the store for release + let fa = primary_fungible_store::withdraw(&store_signer, metadata, local_amount); + + let recipient = token_admin_registry::get_release_or_mint_receiver(&input); + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(&input); + + token_pool::emit_released_or_minted( + &mut pool.token_pool_state, + recipient, + local_amount, + remote_chain_selector + ); + + (fa, local_amount) + } + + // ================================================================ + // | Rate limit config | + // ================================================================ + public entry fun set_chain_rate_limiter_configs( + caller: &signer, + remote_chain_selectors: vector, + outbound_is_enableds: vector, + outbound_capacities: vector, + outbound_rates: vector, + inbound_is_enableds: vector, + inbound_capacities: vector, + inbound_rates: vector + ) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + let number_of_chains = remote_chain_selectors.length(); + + assert!( + number_of_chains == outbound_is_enableds.length() + && number_of_chains == outbound_capacities.length() + && number_of_chains == outbound_rates.length() + && number_of_chains == inbound_is_enableds.length() + && number_of_chains == inbound_capacities.length() + && number_of_chains == inbound_rates.length(), + error::invalid_argument(E_INVALID_ARGUMENTS) + ); + + for (i in 0..number_of_chains) { + token_pool::set_chain_rate_limiter_config( + &mut pool.token_pool_state, + remote_chain_selectors[i], + outbound_is_enableds[i], + outbound_capacities[i], + outbound_rates[i], + inbound_is_enableds[i], + inbound_capacities[i], + inbound_rates[i] + ); + }; + } + + public entry fun set_chain_rate_limiter_config( + caller: &signer, + remote_chain_selector: u64, + outbound_is_enabled: bool, + outbound_capacity: u64, + outbound_rate: u64, + inbound_is_enabled: bool, + inbound_capacity: u64, + inbound_rate: u64 + ) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::set_chain_rate_limiter_config( + &mut pool.token_pool_state, + remote_chain_selector, + outbound_is_enabled, + outbound_capacity, + outbound_rate, + inbound_is_enabled, + inbound_capacity, + inbound_rate + ); + } + + #[view] + public fun get_current_inbound_rate_limiter_state( + remote_chain_selector: u64 + ): rate_limiter::TokenBucket acquires LockReleaseTokenPoolState { + token_pool::get_current_inbound_rate_limiter_state( + &borrow_pool().token_pool_state, remote_chain_selector + ) + } + + #[view] + public fun get_current_outbound_rate_limiter_state( + remote_chain_selector: u64 + ): rate_limiter::TokenBucket acquires LockReleaseTokenPoolState { + token_pool::get_current_outbound_rate_limiter_state( + &borrow_pool().token_pool_state, remote_chain_selector + ) + } + + // ================================================================ + // | Liquidity Management | + // ================================================================ + + /// @notice Adds liquidity to the pool. The tokens should be sent before calling this function + /// @param amount The amount of liquidity to add + public entry fun provide_liquidity( + caller: &signer, amount: u64 + ) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + let caller_address = assert_is_rebalancer(caller, pool); + + let (caller_store, pool_store) = get_caller_and_pool_stores( + caller_address, pool + ); + + transfer_tokens(pool, caller, caller_store, pool_store, amount); + + token_pool::emit_liquidity_added( + &mut pool.token_pool_state, caller_address, amount + ); + } + + /// @notice Removes liquidity from the pool + /// @param amount The amount of liquidity to remove + public entry fun withdraw_liquidity( + caller: &signer, amount: u64 + ) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + let caller_address = assert_is_rebalancer(caller, pool); + + let (caller_store, pool_store) = get_caller_and_pool_stores( + caller_address, pool + ); + assert!(fungible_asset::balance(pool_store) >= amount, E_INSUFFICIENT_LIQUIDITY); + + let store_signer = account::create_signer_with_capability(&pool.store_signer_cap); + + transfer_tokens( + pool, + &store_signer, + pool_store, + caller_store, + amount + ); + + token_pool::emit_liquidity_removed( + &mut pool.token_pool_state, caller_address, amount + ); + } + + inline fun assert_is_rebalancer( + caller: &signer, pool: &LockReleaseTokenPoolState + ): address { + let caller_address = signer::address_of(caller); + assert!(caller_address == pool.rebalancer, E_UNAUTHORIZED); + caller_address + } + + inline fun get_caller_and_pool_stores( + caller_address: address, pool: &LockReleaseTokenPoolState + ): (Object, Object) { + let metadata = token_pool::get_fa_metadata(&pool.token_pool_state); + let caller_store = + primary_fungible_store::ensure_primary_store_exists( + caller_address, metadata + ); + let pool_store = pool_primary_store_inlined(pool); + (caller_store, pool_store) + } + + inline fun transfer_tokens( + pool: &LockReleaseTokenPoolState, + from: &signer, + from_store: Object, + to_store: Object, + amount: u64 + ) { + if (has_transfer_ref(pool)) { + let transfer_ref = pool.transfer_ref.borrow(); + fungible_asset::transfer_with_ref(transfer_ref, from_store, to_store, amount); + } else { + fungible_asset::transfer(from, from_store, to_store, amount); + }; + } + + public entry fun set_rebalancer( + caller: &signer, rebalancer: address + ) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + let old_rebalancer = pool.rebalancer; + pool.rebalancer = rebalancer; + + token_pool::emit_rebalancer_set( + &mut pool.token_pool_state, old_rebalancer, rebalancer + ); + } + + #[view] + public fun get_rebalancer(): address acquires LockReleaseTokenPoolState { + borrow_pool().rebalancer + } + + // ================================================================ + // | Ref Migration | + // ================================================================ + public fun migrate_transfer_ref(caller: &signer): TransferRef acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + assert!(pool.transfer_ref.is_some(), E_TRANSFER_REF_NOT_SET); + + pool.transfer_ref.extract() + } + + // ================================================================ + // | Storage helpers | + // ================================================================ + #[view] + public fun get_store_address(): address { + store_address() + } + + inline fun store_address(): address { + account::create_resource_address(&@lock_release_token_pool, STORE_OBJECT_SEED) + } + + fun assert_can_initialize(caller_address: address) { + if (caller_address == @lock_release_token_pool) { return }; + + if (object::is_object(@lock_release_token_pool)) { + let ccip_lock_release_pool_object = + object::address_to_object(@lock_release_token_pool); + if (caller_address == object::owner(ccip_lock_release_pool_object) + || caller_address == object::root_owner(ccip_lock_release_pool_object)) { + return + }; + }; + + abort error::permission_denied(E_NOT_PUBLISHER) + } + + inline fun borrow_pool(): &LockReleaseTokenPoolState { + borrow_global(store_address()) + } + + inline fun borrow_pool_mut(): &mut LockReleaseTokenPoolState { + borrow_global_mut(store_address()) + } + + // ================================================================ + // | Expose ownable | + // ================================================================ + #[view] + public fun owner(): address acquires LockReleaseTokenPoolState { + ownable::owner(&borrow_pool().ownable_state) + } + + #[view] + public fun has_pending_transfer(): bool acquires LockReleaseTokenPoolState { + ownable::has_pending_transfer(&borrow_pool().ownable_state) + } + + #[view] + public fun pending_transfer_from(): Option
acquires LockReleaseTokenPoolState { + ownable::pending_transfer_from(&borrow_pool().ownable_state) + } + + #[view] + public fun pending_transfer_to(): Option
acquires LockReleaseTokenPoolState { + ownable::pending_transfer_to(&borrow_pool().ownable_state) + } + + #[view] + public fun pending_transfer_accepted(): Option acquires LockReleaseTokenPoolState { + ownable::pending_transfer_accepted(&borrow_pool().ownable_state) + } + + public entry fun transfer_ownership( + caller: &signer, to: address + ) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + ownable::transfer_ownership(caller, &mut pool.ownable_state, to) + } + + public entry fun accept_ownership(caller: &signer) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + ownable::accept_ownership(caller, &mut pool.ownable_state) + } + + public entry fun execute_ownership_transfer( + caller: &signer, to: address + ) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + ownable::execute_ownership_transfer(caller, &mut pool.ownable_state, to) + } + + // ================================================================ + // | MCMS entrypoint | + // ================================================================ + struct McmsCallback has drop {} + + public fun mcms_entrypoint( + _metadata: object::Object + ): option::Option acquires LockReleaseTokenPoolState { + let (caller, function, data) = + mcms_registry::get_callback_params(@lock_release_token_pool, McmsCallback {}); + + let function_bytes = *function.bytes(); + let stream = bcs_stream::new(data); + + if (function_bytes == b"add_remote_pool") { + let remote_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let remote_pool_address = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + add_remote_pool(&caller, remote_chain_selector, remote_pool_address); + } else if (function_bytes == b"remove_remote_pool") { + let remote_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let remote_pool_address = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + remove_remote_pool(&caller, remote_chain_selector, remote_pool_address); + } else if (function_bytes == b"apply_chain_updates") { + let remote_chain_selectors_to_remove = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let remote_chain_selectors_to_add = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let remote_pool_addresses_to_add = + bcs_stream::deserialize_vector( + &mut stream, + |stream| bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ) + ); + let remote_token_addresses_to_add = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + bcs_stream::assert_is_consumed(&stream); + apply_chain_updates( + &caller, + remote_chain_selectors_to_remove, + remote_chain_selectors_to_add, + remote_pool_addresses_to_add, + remote_token_addresses_to_add + ); + } else if (function_bytes == b"set_allowlist_enabled") { + let enabled = bcs_stream::deserialize_bool(&mut stream); + bcs_stream::assert_is_consumed(&stream); + set_allowlist_enabled(&caller, enabled); + } else if (function_bytes == b"apply_allowlist_updates") { + let removes = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + let adds = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + apply_allowlist_updates(&caller, removes, adds); + } else if (function_bytes == b"set_chain_rate_limiter_configs") { + let remote_chain_selectors = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let outbound_is_enableds = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_bool(stream) + ); + let outbound_capacities = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let outbound_rates = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let inbound_is_enableds = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_bool(stream) + ); + let inbound_capacities = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let inbound_rates = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + bcs_stream::assert_is_consumed(&stream); + set_chain_rate_limiter_configs( + &caller, + remote_chain_selectors, + outbound_is_enableds, + outbound_capacities, + outbound_rates, + inbound_is_enableds, + inbound_capacities, + inbound_rates + ); + } else if (function_bytes == b"set_chain_rate_limiter_config") { + let remote_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let outbound_is_enabled = bcs_stream::deserialize_bool(&mut stream); + let outbound_capacity = bcs_stream::deserialize_u64(&mut stream); + let outbound_rate = bcs_stream::deserialize_u64(&mut stream); + let inbound_is_enabled = bcs_stream::deserialize_bool(&mut stream); + let inbound_capacity = bcs_stream::deserialize_u64(&mut stream); + let inbound_rate = bcs_stream::deserialize_u64(&mut stream); + bcs_stream::assert_is_consumed(&stream); + set_chain_rate_limiter_config( + &caller, + remote_chain_selector, + outbound_is_enabled, + outbound_capacity, + outbound_rate, + inbound_is_enabled, + inbound_capacity, + inbound_rate + ); + } else if (function_bytes == b"transfer_ownership") { + let to = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + transfer_ownership(&caller, to); + } else if (function_bytes == b"accept_ownership") { + bcs_stream::assert_is_consumed(&stream); + accept_ownership(&caller); + } else if (function_bytes == b"execute_ownership_transfer") { + let to = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + execute_ownership_transfer(&caller, to) + } else if (function_bytes == b"set_rebalancer") { + let rebalancer = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + set_rebalancer(&caller, rebalancer); + } else if (function_bytes == b"provide_liquidity") { + let amount = bcs_stream::deserialize_u64(&mut stream); + bcs_stream::assert_is_consumed(&stream); + provide_liquidity(&caller, amount); + } else if (function_bytes == b"withdraw_liquidity") { + let amount = bcs_stream::deserialize_u64(&mut stream); + bcs_stream::assert_is_consumed(&stream); + withdraw_liquidity(&caller, amount); + } else { + abort error::invalid_argument(E_UNKNOWN_FUNCTION) + }; + + option::none() + } + + /// Callable during upgrades + public(friend) fun register_mcms_entrypoint( + publisher: &signer, module_name: vector + ) { + mcms_registry::register_entrypoint( + publisher, string::utf8(module_name), McmsCallback {} + ); + } +} +` diff --git a/ccip-sdk/src/cct/aptos/bytecodes/managed_token.ts b/ccip-sdk/src/cct/aptos/bytecodes/managed_token.ts new file mode 100644 index 00000000..17a6edbe --- /dev/null +++ b/ccip-sdk/src/cct/aptos/bytecodes/managed_token.ts @@ -0,0 +1,1020 @@ +/** + * ManagedToken Move package source files. + * + * Source: chainlink-aptos contracts/managed_token + * AptosFramework rev: 16beac69835f3a71564c96164a606a23f259099a + * + * Vendored as source (not compiled bytecodes) because Aptos Move modules + * must be compiled with the deployer's address at deploy time. + * + * Lazy-loaded via dynamic import() — same pattern as EVM BurnMintERC20 bytecode. + */ + +/** Move.toml package manifest. */ +export const MOVE_TOML = `[package] +name = "ManagedToken" +version = "1.0.0" +authors = [] + +[addresses] +managed_token = "_" + +[dev-addresses] +# Calculated with object::create_named_object() +managed_token = "0x121dfbc38157d675d96eef0bcc54e70e9801714138ce54028b5655459c6376ee" + +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "16beac69835f3a71564c96164a606a23f259099a", subdir = "aptos-move/framework/aptos-framework" } + +[dev-dependencies] +` + +/** sources/allowlist.move */ +export const ALLOWLIST_MOVE = `module managed_token::allowlist { + use std::account; + use std::event::{Self, EventHandle}; + use std::error; + use std::string::{Self, String}; + + struct AllowlistState has store { + allowlist_name: String, + allowlist_enabled: bool, + allowlist: vector
, + allowlist_add_events: EventHandle, + allowlist_remove_events: EventHandle + } + + #[event] + struct AllowlistRemove has store, drop { + allowlist_name: String, + sender: address + } + + #[event] + struct AllowlistAdd has store, drop { + allowlist_name: String, + sender: address + } + + const E_ALLOWLIST_NOT_ENABLED: u64 = 1; + + public fun new(event_account: &signer, allowlist: vector
): AllowlistState { + new_with_name(event_account, allowlist, string::utf8(b"default")) + } + + public fun new_with_name( + event_account: &signer, allowlist: vector
, allowlist_name: String + ): AllowlistState { + AllowlistState { + allowlist_name, + allowlist_enabled: !allowlist.is_empty(), + allowlist, + allowlist_add_events: account::new_event_handle(event_account), + allowlist_remove_events: account::new_event_handle(event_account) + } + } + + public fun get_allowlist_enabled(state: &AllowlistState): bool { + state.allowlist_enabled + } + + public fun set_allowlist_enabled( + state: &mut AllowlistState, enabled: bool + ) { + state.allowlist_enabled = enabled; + } + + public fun get_allowlist(state: &AllowlistState): vector
{ + state.allowlist + } + + public fun is_allowed(state: &AllowlistState, sender: address): bool { + if (!state.allowlist_enabled) { + return true + }; + + state.allowlist.contains(&sender) + } + + public fun apply_allowlist_updates( + state: &mut AllowlistState, removes: vector
, adds: vector
+ ) { + removes.for_each_ref( + |remove_address| { + let (found, i) = state.allowlist.index_of(remove_address); + if (found) { + state.allowlist.swap_remove(i); + event::emit_event( + &mut state.allowlist_remove_events, + AllowlistRemove { + allowlist_name: state.allowlist_name, + sender: *remove_address + } + ); + } + } + ); + + if (!adds.is_empty()) { + assert!( + state.allowlist_enabled, + error::invalid_state(E_ALLOWLIST_NOT_ENABLED) + ); + + adds.for_each_ref( + |add_address| { + let add_address: address = *add_address; + let (found, _) = state.allowlist.index_of(&add_address); + if (add_address != @0x0 && !found) { + state.allowlist.push_back(add_address); + event::emit_event( + &mut state.allowlist_add_events, + AllowlistAdd { + allowlist_name: state.allowlist_name, + sender: add_address + } + ); + } + } + ); + } + } + + public fun destroy_allowlist(state: AllowlistState) { + let AllowlistState { + allowlist_name: _, + allowlist_enabled: _, + allowlist: _, + allowlist_add_events: add_events, + allowlist_remove_events: remove_events + } = state; + + event::destroy_handle(add_events); + event::destroy_handle(remove_events); + } + + #[test_only] + public fun new_add_event(add: address): AllowlistAdd { + AllowlistAdd { sender: add, allowlist_name: string::utf8(b"default") } + } + + #[test_only] + public fun new_remove_event(remove: address): AllowlistRemove { + AllowlistRemove { sender: remove, allowlist_name: string::utf8(b"default") } + } + + #[test_only] + public fun get_allowlist_add_events(state: &AllowlistState): &EventHandle { + &state.allowlist_add_events + } + + #[test_only] + public fun get_allowlist_remove_events(state: &AllowlistState): + &EventHandle { + &state.allowlist_remove_events + } +} + +#[test_only] +module managed_token::allowlist_test { + use std::account; + use std::event; + use std::signer; + use std::vector; + + use managed_token::allowlist::{Self, AllowlistAdd, AllowlistRemove}; + + #[test(owner = @0x0)] + fun init_empty_is_empty_and_disabled(owner: &signer) { + let state = set_up_test(owner, vector::empty()); + + assert!(!allowlist::get_allowlist_enabled(&state)); + assert!(allowlist::get_allowlist(&state).is_empty()); + + // Any address is allowed when the allowlist is disabled + assert!(allowlist::is_allowed(&state, @0x1111111111111)); + + allowlist::destroy_allowlist(state); + } + + #[test(owner = @0x0)] + fun init_non_empty_is_non_empty_and_enabled(owner: &signer) { + let init_allowlist = vector[@0x1, @0x2]; + + let state = set_up_test(owner, init_allowlist); + + assert!(allowlist::get_allowlist_enabled(&state)); + assert!(allowlist::get_allowlist(&state).length() == 2); + + // The given addresses are allowed + assert!(allowlist::is_allowed(&state, init_allowlist[0])); + assert!(allowlist::is_allowed(&state, init_allowlist[1])); + + // Other addresses are not allowed + assert!(!allowlist::is_allowed(&state, @0x3)); + + allowlist::destroy_allowlist(state); + } + + #[test(owner = @0x0)] + #[expected_failure(abort_code = 0x30001, location = allowlist)] + fun cannot_add_to_disabled_allowlist(owner: &signer) { + let state = set_up_test(owner, vector::empty()); + + let adds = vector[@0x1]; + + allowlist::apply_allowlist_updates(&mut state, vector::empty(), adds); + + allowlist::destroy_allowlist(state); + } + + #[test(owner = @0x0)] + fun apply_allowlist_updates_mutates_state(owner: &signer) { + let state = set_up_test(owner, vector::empty()); + allowlist::set_allowlist_enabled(&mut state, true); + + assert!(allowlist::get_allowlist(&state).is_empty()); + + allowlist::apply_allowlist_updates(&mut state, vector::empty(), vector::empty()); + + assert!(allowlist::get_allowlist(&state).is_empty()); + + let adds = vector[@0x1, @0x2]; + + allowlist::apply_allowlist_updates(&mut state, vector::empty(), adds); + + assert_add_events_emitted(adds, &state); + + let removes = vector[@0x1]; + + allowlist::apply_allowlist_updates(&mut state, removes, vector::empty()); + + assert_remove_events_emitted(removes, &state); + + assert!(allowlist::get_allowlist(&state).length() == 1); + assert!(allowlist::is_allowed(&state, @0x2)); + assert!(!allowlist::is_allowed(&state, @0x1)); + + allowlist::destroy_allowlist(state); + } + + #[test(owner = @0x0)] + fun apply_allowlist_updates_removes_before_adds(owner: &signer) { + let account_to_allow = @0x1; + let state = set_up_test(owner, vector::empty()); + allowlist::set_allowlist_enabled(&mut state, true); + + let adds_and_removes = vector[account_to_allow]; + + allowlist::apply_allowlist_updates(&mut state, vector::empty(), adds_and_removes); + + assert!(allowlist::get_allowlist(&state).length() == 1); + assert!(allowlist::is_allowed(&state, account_to_allow)); + + allowlist::apply_allowlist_updates(&mut state, adds_and_removes, adds_and_removes); + + // Since removes happen before adds, the account should still be allowed + assert!(allowlist::is_allowed(&state, account_to_allow)); + + assert_remove_events_emitted(adds_and_removes, &state); + // Events don't get purged after calling event::emitted_events so we'll have + // both the first and the second add event in the emitted events + adds_and_removes.push_back(account_to_allow); + assert_add_events_emitted(adds_and_removes, &state); + + allowlist::destroy_allowlist(state); + } + + inline fun assert_add_events_emitted( + added_addresses: vector
, state: &allowlist::AllowlistState + ) { + let expected = + added_addresses.map:: (|add| allowlist::new_add_event(add)); + let got = + event::emitted_events_by_handle( + allowlist::get_allowlist_add_events(state) + ); + let number_of_adds = expected.length(); + + // Assert that exactly one event was emitted for each add + assert!(got.length() == number_of_adds); + + // Assert that the emitted events match the expected events + for (i in 0..number_of_adds) { + assert!(expected.borrow(i) == got.borrow(i)); + } + } + + inline fun assert_remove_events_emitted( + added_addresses: vector
, state: &allowlist::AllowlistState + ) { + let expected = + added_addresses.map:: (|add| allowlist::new_remove_event( + add + )); + let got = + event::emitted_events_by_handle( + allowlist::get_allowlist_remove_events(state) + ); + let number_of_adds = expected.length(); + + // Assert that exactly one event was emitted for each add + assert!(got.length() == number_of_adds); + + // Assert that the emitted events match the expected events + for (i in 0..number_of_adds) { + assert!(expected.borrow(i) == got.borrow(i)); + } + } + + inline fun set_up_test(owner: &signer, allowlist: vector
): + allowlist::AllowlistState { + account::create_account_for_test(signer::address_of(owner)); + + allowlist::new(owner, allowlist) + } +} +` + +/** sources/ownable.move */ +export const OWNABLE_MOVE = `/// This module implements an Ownable component similar to Ownable2Step.sol for managing +/// object ownership. +/// +/// Due to Aptos's security model requiring the original owner's signer for 0x1::object::transfer, +/// this implementation uses a 3-step ownership transfer flow: +/// +/// 1. Initial owner calls transfer_ownership with the new owner's address +/// 2. Pending owner calls accept_ownership to confirm the transfer +/// 3. Initial owner calls execute_ownership_transfer to complete the transfer +/// +/// The execute_ownership_transfer function requires a signer in order to perform the +/// object transfer, while other operations only require the caller address to maintain the +/// principle of least privilege. +/// +/// Note that direct ownership transfers via 0x1::object::transfer are still possible. +/// This module handles such cases gracefully by reading the current owner directly +/// from the object. +module managed_token::ownable { + use std::account; + use std::error; + use std::event::{Self, EventHandle}; + use std::object::{Self, Object, ObjectCore}; + use std::option::{Self, Option}; + use std::signer; + + struct OwnableState has store { + target_object: Object, + pending_transfer: Option, + ownership_transfer_requested_events: EventHandle, + ownership_transfer_accepted_events: EventHandle, + ownership_transferred_events: EventHandle + } + + struct PendingTransfer has store, drop { + from: address, + to: address, + accepted: bool + } + + const E_MUST_BE_PROPOSED_OWNER: u64 = 1; + const E_CANNOT_TRANSFER_TO_SELF: u64 = 2; + const E_ONLY_CALLABLE_BY_OWNER: u64 = 3; + const E_PROPOSED_OWNER_MISMATCH: u64 = 4; + const E_OWNER_CHANGED: u64 = 5; + const E_NO_PENDING_TRANSFER: u64 = 6; + const E_TRANSFER_NOT_ACCEPTED: u64 = 7; + const E_TRANSFER_ALREADY_ACCEPTED: u64 = 8; + + #[event] + struct OwnershipTransferRequested has store, drop { + from: address, + to: address + } + + #[event] + struct OwnershipTransferAccepted has store, drop { + from: address, + to: address + } + + #[event] + struct OwnershipTransferred has store, drop { + from: address, + to: address + } + + public fun new(event_account: &signer, object_address: address): OwnableState { + let new_state = OwnableState { + target_object: object::address_to_object(object_address), + pending_transfer: option::none(), + ownership_transfer_requested_events: account::new_event_handle(event_account), + ownership_transfer_accepted_events: account::new_event_handle(event_account), + ownership_transferred_events: account::new_event_handle(event_account) + }; + + new_state + } + + public fun owner(state: &OwnableState): address { + owner_internal(state) + } + + public fun has_pending_transfer(state: &OwnableState): bool { + state.pending_transfer.is_some() + } + + public fun pending_transfer_from(state: &OwnableState): Option
{ + state.pending_transfer.map_ref(|pending_transfer| pending_transfer.from) + } + + public fun pending_transfer_to(state: &OwnableState): Option
{ + state.pending_transfer.map_ref(|pending_transfer| pending_transfer.to) + } + + public fun pending_transfer_accepted(state: &OwnableState): Option { + state.pending_transfer.map_ref(|pending_transfer| pending_transfer.accepted) + } + + inline fun owner_internal(state: &OwnableState): address { + object::owner(state.target_object) + } + + public fun transfer_ownership( + caller: &signer, state: &mut OwnableState, to: address + ) { + let caller_address = signer::address_of(caller); + assert_only_owner_internal(caller_address, state); + assert!(caller_address != to, error::invalid_argument(E_CANNOT_TRANSFER_TO_SELF)); + + state.pending_transfer = option::some( + PendingTransfer { from: caller_address, to, accepted: false } + ); + + event::emit_event( + &mut state.ownership_transfer_requested_events, + OwnershipTransferRequested { from: caller_address, to } + ); + } + + public fun accept_ownership( + caller: &signer, state: &mut OwnableState + ) { + let caller_address = signer::address_of(caller); + assert!( + state.pending_transfer.is_some(), + error::permission_denied(E_NO_PENDING_TRANSFER) + ); + + let current_owner = owner_internal(state); + let pending_transfer = state.pending_transfer.borrow_mut(); + + // check that the owner has not changed from a direct call to 0x1::object::transfer, + // in which case the transfer flow should be restarted. + assert!( + pending_transfer.from == current_owner, + error::permission_denied(E_OWNER_CHANGED) + ); + assert!( + pending_transfer.to == caller_address, + error::permission_denied(E_MUST_BE_PROPOSED_OWNER) + ); + assert!( + !pending_transfer.accepted, + error::invalid_state(E_TRANSFER_ALREADY_ACCEPTED) + ); + + pending_transfer.accepted = true; + + event::emit_event( + &mut state.ownership_transfer_accepted_events, + OwnershipTransferAccepted { from: pending_transfer.from, to: caller_address } + ); + } + + public fun execute_ownership_transfer( + caller: &signer, state: &mut OwnableState, to: address + ) { + let caller_address = signer::address_of(caller); + assert_only_owner_internal(caller_address, state); + + let current_owner = owner_internal(state); + let pending_transfer = state.pending_transfer.extract(); + + // check that the owner has not changed from a direct call to 0x1::object::transfer, + // in which case the transfer flow should be restarted. + assert!( + pending_transfer.from == current_owner, + error::permission_denied(E_OWNER_CHANGED) + ); + assert!( + pending_transfer.to == to, + error::permission_denied(E_PROPOSED_OWNER_MISMATCH) + ); + assert!( + pending_transfer.accepted, error::invalid_state(E_TRANSFER_NOT_ACCEPTED) + ); + + object::transfer(caller, state.target_object, pending_transfer.to); + state.pending_transfer = option::none(); + + event::emit_event( + &mut state.ownership_transferred_events, + OwnershipTransferred { from: caller_address, to } + ); + } + + public fun assert_only_owner(caller: address, state: &OwnableState) { + assert_only_owner_internal(caller, state) + } + + inline fun assert_only_owner_internal( + caller: address, state: &OwnableState + ) { + assert!( + caller == owner_internal(state), + error::permission_denied(E_ONLY_CALLABLE_BY_OWNER) + ); + } + + public fun destroy(state: OwnableState) { + let OwnableState { + target_object: _, + pending_transfer: _, + ownership_transfer_requested_events, + ownership_transfer_accepted_events, + ownership_transferred_events + } = state; + + event::destroy_handle(ownership_transfer_requested_events); + event::destroy_handle(ownership_transfer_accepted_events); + event::destroy_handle(ownership_transferred_events); + } + + #[test_only] + public fun get_ownership_transfer_requested_events( + state: &OwnableState + ): &EventHandle { + &state.ownership_transfer_requested_events + } + + #[test_only] + public fun get_ownership_transfer_accepted_events( + state: &OwnableState + ): &EventHandle { + &state.ownership_transfer_accepted_events + } + + #[test_only] + public fun get_ownership_transferred_events( + state: &OwnableState + ): &EventHandle { + &state.ownership_transferred_events + } +} +` + +/** sources/managed_token.move */ +export const MANAGED_TOKEN_MOVE = `module managed_token::managed_token { + use std::account; + use std::event::{Self, EventHandle}; + use std::fungible_asset::{Self, BurnRef, Metadata, MintRef, TransferRef}; + use std::object::{Self, ExtendRef, Object, TransferRef as ObjectTransferRef}; + use std::option::{Option}; + use std::primary_fungible_store; + use std::signer; + use std::string::{Self, String}; + + use managed_token::allowlist::{Self, AllowlistState}; + use managed_token::ownable::{Self, OwnableState}; + + const TOKEN_STATE_SEED: vector = b"managed_token::managed_token::token_state"; + + struct TokenStateDeployment has key { + extend_ref: ExtendRef, + transfer_ref: ObjectTransferRef, + ownable_state: OwnableState, + allowed_minters: AllowlistState, + allowed_burners: AllowlistState, + initialize_events: EventHandle, + mint_events: EventHandle, + burn_events: EventHandle + } + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + struct TokenState has key { + extend_ref: ExtendRef, + transfer_ref: ObjectTransferRef, + ownable_state: OwnableState, + allowed_minters: AllowlistState, + allowed_burners: AllowlistState, + token: Object, + initialize_events: EventHandle, + mint_events: EventHandle, + burn_events: EventHandle + } + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + struct TokenMetadataRefs has key { + extend_ref: ExtendRef, + mint_ref: MintRef, + burn_ref: BurnRef, + transfer_ref: TransferRef + } + + #[event] + struct Initialize has drop, store { + publisher: address, + token: Object, + max_supply: Option, + decimals: u8, + icon: String, + project: String + } + + #[event] + struct Mint has drop, store { + minter: address, + to: address, + amount: u64 + } + + #[event] + struct Burn has drop, store { + burner: address, + from: address, + amount: u64 + } + + const E_NOT_PUBLISHER: u64 = 1; + const E_NOT_ALLOWED_MINTER: u64 = 2; + const E_NOT_ALLOWED_BURNER: u64 = 3; + const E_TOKEN_NOT_INITIALIZED: u64 = 4; + const E_TOKEN_ALREADY_INITIALIZED: u64 = 5; + const E_TOKEN_STATE_DEPLOYMENT_ALREADY_INITIALIZED: u64 = 6; + + #[view] + public fun type_and_version(): String { + string::utf8(b"ManagedToken 1.0.0") + } + + #[view] + public fun token_state_address(): address { + token_state_address_internal() + } + + inline fun token_state_address_internal(): address { + object::create_object_address(&@managed_token, TOKEN_STATE_SEED) + } + + #[view] + public fun token_metadata(): address acquires TokenState { + assert!( + exists(token_state_address_internal()), + E_TOKEN_NOT_INITIALIZED + ); + token_metadata_internal(&TokenState[token_state_address_internal()]) + } + + inline fun token_metadata_internal(state: &TokenState): address { + object::object_address(&state.token) + } + + #[view] + public fun get_allowed_minters(): vector
acquires TokenState { + allowlist::get_allowlist( + &TokenState[token_state_address_internal()].allowed_minters + ) + } + + #[view] + public fun get_allowed_burners(): vector
acquires TokenState { + allowlist::get_allowlist( + &TokenState[token_state_address_internal()].allowed_burners + ) + } + + #[view] + public fun is_minter_allowed(minter: address): bool acquires TokenState { + allowlist::is_allowed( + &TokenState[token_state_address_internal()].allowed_minters, + minter + ) + } + + #[view] + public fun is_burner_allowed(burner: address): bool acquires TokenState { + allowlist::is_allowed( + &TokenState[token_state_address_internal()].allowed_burners, + burner + ) + } + + /// \`publisher\` is the code object, deployed through object_code_deployment + fun init_module(publisher: &signer) { + assert!(object::is_object(@managed_token), E_NOT_PUBLISHER); + + // Create object owned by code object + let constructor_ref = &object::create_named_object(publisher, TOKEN_STATE_SEED); + let extend_ref = object::generate_extend_ref(constructor_ref); + let token_state_signer = &object::generate_signer(constructor_ref); + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist(signer::address_of(token_state_signer)); + + let allowed_minters = + allowlist::new_with_name( + token_state_signer, vector[], string::utf8(b"minters") + ); + allowlist::set_allowlist_enabled(&mut allowed_minters, true); + + let allowed_burners = + allowlist::new_with_name( + token_state_signer, vector[], string::utf8(b"burners") + ); + allowlist::set_allowlist_enabled(&mut allowed_burners, true); + + move_to( + token_state_signer, + TokenStateDeployment { + extend_ref, + transfer_ref: object::generate_transfer_ref(constructor_ref), + ownable_state: ownable::new(token_state_signer, @managed_token), + allowed_minters, + allowed_burners, + initialize_events: account::new_event_handle(token_state_signer), + mint_events: account::new_event_handle(token_state_signer), + burn_events: account::new_event_handle(token_state_signer) + } + ); + } + + // ================================================================ + // | Only Owner Functions | + // ================================================================ + + /// Only owner of this code object can initialize a token once + public entry fun initialize( + publisher: &signer, + max_supply: Option, + name: String, + symbol: String, + decimals: u8, + icon: String, + project: String + ) acquires TokenStateDeployment { + let publisher_addr = signer::address_of(publisher); + let token_state_address = token_state_address_internal(); + + assert!( + exists(token_state_address), + E_TOKEN_STATE_DEPLOYMENT_ALREADY_INITIALIZED + ); + + let TokenStateDeployment { + extend_ref, + transfer_ref, + ownable_state, + allowed_minters, + allowed_burners, + initialize_events, + mint_events, + burn_events + } = move_from(token_state_address); + + assert_only_owner(signer::address_of(publisher), &ownable_state); + + let token_state_signer = &object::generate_signer_for_extending(&extend_ref); + + // Code object owns token state, which owns the fungible asset + // Code object => token state => fungible asset + let constructor_ref = + &object::create_named_object(token_state_signer, *symbol.bytes()); + primary_fungible_store::create_primary_store_enabled_fungible_asset( + constructor_ref, + max_supply, + name, + symbol, + decimals, + icon, + project + ); + + let metadata_object_signer = &object::generate_signer(constructor_ref); + move_to( + metadata_object_signer, + TokenMetadataRefs { + extend_ref: object::generate_extend_ref(constructor_ref), + mint_ref: fungible_asset::generate_mint_ref(constructor_ref), + burn_ref: fungible_asset::generate_burn_ref(constructor_ref), + transfer_ref: fungible_asset::generate_transfer_ref(constructor_ref) + } + ); + + let token = object::object_from_constructor_ref(constructor_ref); + + event::emit_event( + &mut initialize_events, + Initialize { + publisher: publisher_addr, + token, + max_supply, + decimals, + icon, + project + } + ); + + move_to( + token_state_signer, + TokenState { + extend_ref, + transfer_ref, + ownable_state, + allowed_minters, + allowed_burners, + token, + initialize_events, + mint_events, + burn_events + } + ); + } + + public entry fun apply_allowed_minter_updates( + caller: &signer, + minters_to_remove: vector
, + minters_to_add: vector
+ ) acquires TokenState { + let token_state = &mut TokenState[token_state_address_internal()]; + assert_only_owner(signer::address_of(caller), &token_state.ownable_state); + + allowlist::apply_allowlist_updates( + &mut token_state.allowed_minters, + minters_to_remove, + minters_to_add + ); + } + + public entry fun apply_allowed_burner_updates( + caller: &signer, + burners_to_remove: vector
, + burners_to_add: vector
+ ) acquires TokenState { + let token_state = &mut TokenState[token_state_address_internal()]; + assert_only_owner(signer::address_of(caller), &token_state.ownable_state); + + allowlist::apply_allowlist_updates( + &mut token_state.allowed_burners, + burners_to_remove, + burners_to_add + ); + } + + // ================================================================ + // | Mint/Burn Functions | + // ================================================================ + + public entry fun mint( + minter: &signer, to: address, amount: u64 + ) acquires TokenMetadataRefs, TokenState { + let minter_addr = signer::address_of(minter); + let state = &mut TokenState[token_state_address_internal()]; + assert_is_allowed_minter(minter_addr, state); + + if (amount == 0) { return }; + + primary_fungible_store::mint( + &borrow_token_metadata_refs(state).mint_ref, to, amount + ); + + event::emit_event( + &mut state.mint_events, + Mint { minter: minter_addr, to, amount } + ); + } + + public entry fun burn( + burner: &signer, from: address, amount: u64 + ) acquires TokenMetadataRefs, TokenState { + let burner_addr = signer::address_of(burner); + let state = &mut TokenState[token_state_address_internal()]; + assert_is_allowed_burner(burner_addr, state); + + if (amount == 0) { return }; + + primary_fungible_store::burn( + &borrow_token_metadata_refs(state).burn_ref, from, amount + ); + + event::emit_event( + &mut state.burn_events, + Burn { burner: burner_addr, from, amount } + ); + } + + inline fun assert_is_allowed_minter( + caller: address, state: &TokenState + ) { + assert!( + caller == owner_internal(state) + || allowlist::is_allowed(&state.allowed_minters, caller), + E_NOT_ALLOWED_MINTER + ); + } + + inline fun assert_is_allowed_burner( + caller: address, state: &TokenState + ) { + assert!( + caller == owner_internal(state) + || allowlist::is_allowed(&state.allowed_burners, caller), + E_NOT_ALLOWED_BURNER + ); + } + + inline fun borrow_token_metadata_refs(state: &TokenState): &TokenMetadataRefs { + &TokenMetadataRefs[token_metadata_internal(state)] + } + + // ================================================================ + // | Ownable State | + // ================================================================ + + #[view] + public fun owner(): address acquires TokenState { + owner_internal(&TokenState[token_state_address_internal()]) + } + + #[view] + public fun has_pending_transfer(): bool acquires TokenState { + ownable::has_pending_transfer( + &TokenState[token_state_address_internal()].ownable_state + ) + } + + #[view] + public fun pending_transfer_from(): Option
acquires TokenState { + ownable::pending_transfer_from( + &TokenState[token_state_address_internal()].ownable_state + ) + } + + #[view] + public fun pending_transfer_to(): Option
acquires TokenState { + ownable::pending_transfer_to( + &TokenState[token_state_address_internal()].ownable_state + ) + } + + #[view] + public fun pending_transfer_accepted(): Option acquires TokenState { + ownable::pending_transfer_accepted( + &TokenState[token_state_address_internal()].ownable_state + ) + } + + inline fun owner_internal(state: &TokenState): address { + ownable::owner(&state.ownable_state) + } + + fun assert_only_owner(caller: address, ownable_state: &OwnableState) { + ownable::assert_only_owner(caller, ownable_state) + } + + /// ownable::transfer_ownership checks if the caller is the owner + /// So we only extract the ownable state from the token state + public entry fun transfer_ownership(caller: &signer, to: address) acquires TokenState { + ownable::transfer_ownership( + caller, + &mut TokenState[token_state_address_internal()].ownable_state, + to + ) + } + + /// Anyone can call this as \`ownable::accept_ownership\` verifies + /// that the caller is the pending owner + public entry fun accept_ownership(caller: &signer) acquires TokenState { + ownable::accept_ownership( + caller, + &mut TokenState[token_state_address_internal()].ownable_state + ) + } + + /// ownable::execute_ownership_transfer checks if the caller is the owner + /// So we only extract the ownable state from the token state + public entry fun execute_ownership_transfer( + caller: &signer, to: address + ) acquires TokenState { + ownable::execute_ownership_transfer( + caller, + &mut TokenState[token_state_address_internal()].ownable_state, + to + ) + } + + #[test_only] + public fun init_module_for_testing(publisher: &signer) { + init_module(publisher); + } +} +` diff --git a/ccip-sdk/src/cct/aptos/bytecodes/managed_token_pool.ts b/ccip-sdk/src/cct/aptos/bytecodes/managed_token_pool.ts new file mode 100644 index 00000000..e8b7d1ba --- /dev/null +++ b/ccip-sdk/src/cct/aptos/bytecodes/managed_token_pool.ts @@ -0,0 +1,1976 @@ +/** + * ManagedTokenPool Move package source files. + * + * Source: chainlink-aptos contracts/ccip/ccip_token_pools/managed_token_pool + * AptosFramework rev: 16beac69835f3a71564c96164a606a23f259099a + * ChainlinkCCIP + MCMS: embedded as local dependencies + * + * Vendored as source (not compiled bytecodes) because Aptos Move modules + * must be compiled with the deployer's address at deploy time. + * + * Lazy-loaded via dynamic import() — same pattern as EVM BurnMintERC20 bytecode. + */ + +/** Move.toml for the ManagedTokenPool package. */ +export const POOL_MOVE_TOML = `[package] +name = "ManagedTokenPool" +version = "1.0.0" +authors = [] + +[addresses] +ccip = "_" +ccip_token_pool = "_" +managed_token_pool = "_" +mcms = "_" +mcms_register_entrypoints = "_" +managed_token = "_" + +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "16beac69835f3a71564c96164a606a23f259099a", subdir = "aptos-move/framework/aptos-framework" } +ChainlinkCCIP = { local = "../ccip" } +CCIPTokenPool = { local = "../token_pool" } +ManagedToken = { local = "../managed_token" } +` + +/** sources/managed_token_pool.move */ +export const MANAGED_TOKEN_POOL_MOVE = `module managed_token_pool::managed_token_pool { + use std::account::{Self, SignerCapability}; + use std::error; + use std::fungible_asset::{Self, FungibleAsset, Metadata, TransferRef}; + use std::primary_fungible_store; + use std::object::{Self, Object}; + use std::option::{Self, Option}; + use std::signer; + use std::string::{Self, String}; + + use managed_token::managed_token; + + use ccip::token_admin_registry::{Self, LockOrBurnInputV1, ReleaseOrMintInputV1}; + use ccip_token_pool::ownable; + use ccip_token_pool::rate_limiter; + use ccip_token_pool::token_pool; + + use mcms::mcms_registry; + use mcms::bcs_stream; + + const STORE_OBJECT_SEED: vector = b"CcipManagedTokenPool"; + + struct ManagedTokenPoolState has key, store { + store_signer_cap: SignerCapability, + ownable_state: ownable::OwnableState, + token_pool_state: token_pool::TokenPoolState, + store_signer_address: address + } + + const E_INVALID_ARGUMENTS: u64 = 1; + const E_UNKNOWN_FUNCTION: u64 = 2; + const E_NOT_PUBLISHER: u64 = 3; + + // ================================================================ + // | Init | + // ================================================================ + #[view] + public fun type_and_version(): String { + string::utf8(b"ManagedTokenPool 1.6.0") + } + + fun init_module(publisher: &signer) { + // register the pool on deployment, because in the case of object code deployment, + // this is the only time we have a signer ref to @ccip_managed_pool. + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist(@managed_token_pool); + + // the name of this module. if incorrect, callbacks will fail to be registered and + // register_pool will revert. + let token_pool_module_name = b"managed_token_pool"; + + // Register the entrypoint with mcms + if (@mcms_register_entrypoints == @0x1) { + register_mcms_entrypoint(publisher, token_pool_module_name); + }; + + // Register V2 pool with closure-based callbacks + register_v2_callbacks(publisher); + + // create a resource account to be the owner of the primary FungibleStore we will use. + let (store_signer, store_signer_cap) = + account::create_resource_account(publisher, STORE_OBJECT_SEED); + + let managed_token_address = managed_token::token_metadata(); + let metadata = object::address_to_object(managed_token_address); + + // make sure this is a valid fungible asset that is primary fungible store enabled, + // ie. created with primary_fungible_store::create_primary_store_enabled_fungible_asset + primary_fungible_store::ensure_primary_store_exists( + signer::address_of(&store_signer), metadata + ); + + let store_signer = account::create_signer_with_capability(&store_signer_cap); + + let pool = ManagedTokenPoolState { + ownable_state: ownable::new(&store_signer, @managed_token_pool), + store_signer_address: signer::address_of(&store_signer), + store_signer_cap, + token_pool_state: token_pool::initialize( + &store_signer, managed_token_address, vector[] + ) + }; + + move_to(&store_signer, pool); + } + + public fun register_v2_callbacks(publisher: &signer) { + assert!( + signer::address_of(publisher) == @managed_token_pool, + error::permission_denied(E_NOT_PUBLISHER) + ); + let managed_token_address = managed_token::token_metadata(); + token_admin_registry::register_pool_v2( + publisher, + managed_token_address, + lock_or_burn_v2, + release_or_mint_v2 + ); + } + + // ================================================================ + // | Exposing token_pool functions | + // ================================================================ + #[view] + public fun get_token(): address acquires ManagedTokenPoolState { + token_pool::get_token(&borrow_pool().token_pool_state) + } + + #[view] + public fun get_router(): address { + token_pool::get_router() + } + + #[view] + public fun get_token_decimals(): u8 acquires ManagedTokenPoolState { + token_pool::get_token_decimals(&borrow_pool().token_pool_state) + } + + #[view] + public fun get_remote_pools( + remote_chain_selector: u64 + ): vector> acquires ManagedTokenPoolState { + token_pool::get_remote_pools( + &borrow_pool().token_pool_state, remote_chain_selector + ) + } + + #[view] + public fun is_remote_pool( + remote_chain_selector: u64, remote_pool_address: vector + ): bool acquires ManagedTokenPoolState { + token_pool::is_remote_pool( + &borrow_pool().token_pool_state, + remote_chain_selector, + remote_pool_address + ) + } + + #[view] + public fun get_remote_token( + remote_chain_selector: u64 + ): vector acquires ManagedTokenPoolState { + let pool = borrow_pool(); + token_pool::get_remote_token(&pool.token_pool_state, remote_chain_selector) + } + + public entry fun add_remote_pool( + caller: &signer, remote_chain_selector: u64, remote_pool_address: vector + ) acquires ManagedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::add_remote_pool( + &mut pool.token_pool_state, + remote_chain_selector, + remote_pool_address + ); + } + + public entry fun remove_remote_pool( + caller: &signer, remote_chain_selector: u64, remote_pool_address: vector + ) acquires ManagedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::remove_remote_pool( + &mut pool.token_pool_state, + remote_chain_selector, + remote_pool_address + ); + } + + #[view] + public fun is_supported_chain(remote_chain_selector: u64): bool acquires ManagedTokenPoolState { + let pool = borrow_pool(); + token_pool::is_supported_chain(&pool.token_pool_state, remote_chain_selector) + } + + #[view] + public fun get_supported_chains(): vector acquires ManagedTokenPoolState { + let pool = borrow_pool(); + token_pool::get_supported_chains(&pool.token_pool_state) + } + + public entry fun apply_chain_updates( + caller: &signer, + remote_chain_selectors_to_remove: vector, + remote_chain_selectors_to_add: vector, + remote_pool_addresses_to_add: vector>>, + remote_token_addresses_to_add: vector> + ) acquires ManagedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::apply_chain_updates( + &mut pool.token_pool_state, + remote_chain_selectors_to_remove, + remote_chain_selectors_to_add, + remote_pool_addresses_to_add, + remote_token_addresses_to_add + ); + } + + #[view] + public fun get_allowlist_enabled(): bool acquires ManagedTokenPoolState { + let pool = borrow_pool(); + token_pool::get_allowlist_enabled(&pool.token_pool_state) + } + + public entry fun set_allowlist_enabled( + caller: &signer, enabled: bool + ) acquires ManagedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + token_pool::set_allowlist_enabled(&mut pool.token_pool_state, enabled); + } + + #[view] + public fun get_allowlist(): vector
acquires ManagedTokenPoolState { + let pool = borrow_pool(); + token_pool::get_allowlist(&pool.token_pool_state) + } + + public entry fun apply_allowlist_updates( + caller: &signer, removes: vector
, adds: vector
+ ) acquires ManagedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + token_pool::apply_allowlist_updates(&mut pool.token_pool_state, removes, adds); + } + + // ================================================================ + // | Burn/Mint | + // ================================================================ + + // the callback proof type used as authentication to retrieve and set input and output arguments. + struct CallbackProof has drop {} + + public fun lock_or_burn( + _store: Object, fa: FungibleAsset, _transfer_ref: &TransferRef + ) acquires ManagedTokenPoolState { + // retrieve the input for this lock or burn operation. if this function is invoked + // outside of ccip::token_admin_registry, the transaction will abort. + let input = + token_admin_registry::get_lock_or_burn_input_v1( + @managed_token_pool, CallbackProof {} + ); + + let pool = borrow_pool_mut(); + let fa_amount = fungible_asset::amount(&fa); + + // This method validates various aspects of the lock or burn operation. If any of the + // validations fail, the transaction will abort. + let dest_token_address = + token_pool::validate_lock_or_burn( + &mut pool.token_pool_state, + &fa, + &input, + fa_amount + ); + + // Construct lock_or_burn output before we lose access to fa + let dest_pool_data = token_pool::encode_local_decimals(&pool.token_pool_state); + + // Burn the funds + let store = + primary_fungible_store::ensure_primary_store_exists( + pool.store_signer_address, fungible_asset::asset_metadata(&fa) + ); + let signer = &account::create_signer_with_capability(&pool.store_signer_cap); + fungible_asset::deposit(store, fa); + managed_token::burn(signer, pool.store_signer_address, fa_amount); + + // set the output for this lock or burn operation. + token_admin_registry::set_lock_or_burn_output_v1( + @managed_token_pool, + CallbackProof {}, + dest_token_address, + dest_pool_data + ); + + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(&input); + + token_pool::emit_locked_or_burned( + &mut pool.token_pool_state, fa_amount, remote_chain_selector + ); + } + + public fun release_or_mint( + _store: Object, _amount: u64, _transfer_ref: &TransferRef + ): FungibleAsset acquires ManagedTokenPoolState { + // retrieve the input for this release or mint operation. if this function is invoked + // outside of ccip::token_admin_registry, the transaction will abort. + let input = + token_admin_registry::get_release_or_mint_input_v1( + @managed_token_pool, CallbackProof {} + ); + let pool = borrow_pool_mut(); + let local_amount = + token_pool::calculate_release_or_mint_amount(&pool.token_pool_state, &input); + + token_pool::validate_release_or_mint( + &mut pool.token_pool_state, &input, local_amount + ); + + // Mint the amount for release. + let local_token = token_admin_registry::get_release_or_mint_local_token(&input); + let metadata = object::address_to_object(local_token); + let store = + primary_fungible_store::ensure_primary_store_exists( + pool.store_signer_address, metadata + ); + let signer = &account::create_signer_with_capability(&pool.store_signer_cap); + managed_token::mint(signer, pool.store_signer_address, local_amount); + let fa = fungible_asset::withdraw(signer, store, local_amount); + + // set the output for this release or mint operation. + token_admin_registry::set_release_or_mint_output_v1( + @managed_token_pool, CallbackProof {}, local_amount + ); + + let recipient = token_admin_registry::get_release_or_mint_receiver(&input); + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(&input); + + token_pool::emit_released_or_minted( + &mut pool.token_pool_state, + recipient, + local_amount, + remote_chain_selector + ); + + // return the withdrawn fungible asset. + fa + } + + #[persistent] + fun lock_or_burn_v2(fa: FungibleAsset, input: LockOrBurnInputV1) + : (vector, vector) { + let pool = borrow_pool_mut(); + let fa_amount = fungible_asset::amount(&fa); + + // This method validates various aspects of the lock or burn operation. If any of the + // validations fail, the transaction will abort. + let dest_token_address = + token_pool::validate_lock_or_burn( + &mut pool.token_pool_state, + &fa, + &input, + fa_amount + ); + + // Burn the funds + let store = + primary_fungible_store::ensure_primary_store_exists( + pool.store_signer_address, fungible_asset::asset_metadata(&fa) + ); + let signer = &account::create_signer_with_capability(&pool.store_signer_cap); + fungible_asset::deposit(store, fa); + managed_token::burn(signer, pool.store_signer_address, fa_amount); + + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(&input); + + token_pool::emit_locked_or_burned( + &mut pool.token_pool_state, fa_amount, remote_chain_selector + ); + + (dest_token_address, token_pool::encode_local_decimals(&pool.token_pool_state)) + } + + #[persistent] + fun release_or_mint_v2(input: ReleaseOrMintInputV1): (FungibleAsset, u64) { + let pool = borrow_pool_mut(); + let local_amount = + token_pool::calculate_release_or_mint_amount(&pool.token_pool_state, &input); + + token_pool::validate_release_or_mint( + &mut pool.token_pool_state, &input, local_amount + ); + + // Mint the amount for release. + let local_token = token_admin_registry::get_release_or_mint_local_token(&input); + let metadata = object::address_to_object(local_token); + let store = + primary_fungible_store::ensure_primary_store_exists( + pool.store_signer_address, metadata + ); + let signer = &account::create_signer_with_capability(&pool.store_signer_cap); + managed_token::mint(signer, pool.store_signer_address, local_amount); + + // Calling into \`fungible_asset::withdraw\` works as managed token is not dispatchable + let fa = fungible_asset::withdraw(signer, store, local_amount); + let recipient = token_admin_registry::get_release_or_mint_receiver(&input); + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(&input); + + token_pool::emit_released_or_minted( + &mut pool.token_pool_state, + recipient, + local_amount, + remote_chain_selector + ); + + (fa, local_amount) + } + + // ================================================================ + // | Rate limit config | + // ================================================================ + public entry fun set_chain_rate_limiter_configs( + caller: &signer, + remote_chain_selectors: vector, + outbound_is_enableds: vector, + outbound_capacities: vector, + outbound_rates: vector, + inbound_is_enableds: vector, + inbound_capacities: vector, + inbound_rates: vector + ) acquires ManagedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + let number_of_chains = remote_chain_selectors.length(); + + assert!( + number_of_chains == outbound_is_enableds.length() + && number_of_chains == outbound_capacities.length() + && number_of_chains == outbound_rates.length() + && number_of_chains == inbound_is_enableds.length() + && number_of_chains == inbound_capacities.length() + && number_of_chains == inbound_rates.length(), + error::invalid_argument(E_INVALID_ARGUMENTS) + ); + + for (i in 0..number_of_chains) { + token_pool::set_chain_rate_limiter_config( + &mut pool.token_pool_state, + remote_chain_selectors[i], + outbound_is_enableds[i], + outbound_capacities[i], + outbound_rates[i], + inbound_is_enableds[i], + inbound_capacities[i], + inbound_rates[i] + ); + }; + } + + public entry fun set_chain_rate_limiter_config( + caller: &signer, + remote_chain_selector: u64, + outbound_is_enabled: bool, + outbound_capacity: u64, + outbound_rate: u64, + inbound_is_enabled: bool, + inbound_capacity: u64, + inbound_rate: u64 + ) acquires ManagedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::set_chain_rate_limiter_config( + &mut pool.token_pool_state, + remote_chain_selector, + outbound_is_enabled, + outbound_capacity, + outbound_rate, + inbound_is_enabled, + inbound_capacity, + inbound_rate + ); + } + + #[view] + public fun get_current_inbound_rate_limiter_state( + remote_chain_selector: u64 + ): rate_limiter::TokenBucket acquires ManagedTokenPoolState { + token_pool::get_current_inbound_rate_limiter_state( + &borrow_pool().token_pool_state, remote_chain_selector + ) + } + + #[view] + public fun get_current_outbound_rate_limiter_state( + remote_chain_selector: u64 + ): rate_limiter::TokenBucket acquires ManagedTokenPoolState { + token_pool::get_current_outbound_rate_limiter_state( + &borrow_pool().token_pool_state, remote_chain_selector + ) + } + + // ================================================================ + // | Storage helpers | + // ================================================================ + #[view] + public fun get_store_address(): address { + store_address() + } + + inline fun store_address(): address { + account::create_resource_address(&@managed_token_pool, STORE_OBJECT_SEED) + } + + inline fun borrow_pool(): &ManagedTokenPoolState { + borrow_global(store_address()) + } + + inline fun borrow_pool_mut(): &mut ManagedTokenPoolState { + borrow_global_mut(store_address()) + } + + // ================================================================ + // | Expose ownable | + // ================================================================ + #[view] + public fun owner(): address acquires ManagedTokenPoolState { + ownable::owner(&borrow_pool().ownable_state) + } + + #[view] + public fun has_pending_transfer(): bool acquires ManagedTokenPoolState { + ownable::has_pending_transfer(&borrow_pool().ownable_state) + } + + #[view] + public fun pending_transfer_from(): Option
acquires ManagedTokenPoolState { + ownable::pending_transfer_from(&borrow_pool().ownable_state) + } + + #[view] + public fun pending_transfer_to(): Option
acquires ManagedTokenPoolState { + ownable::pending_transfer_to(&borrow_pool().ownable_state) + } + + #[view] + public fun pending_transfer_accepted(): Option acquires ManagedTokenPoolState { + ownable::pending_transfer_accepted(&borrow_pool().ownable_state) + } + + public entry fun transfer_ownership(caller: &signer, to: address) acquires ManagedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::transfer_ownership(caller, &mut pool.ownable_state, to) + } + + public entry fun accept_ownership(caller: &signer) acquires ManagedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::accept_ownership(caller, &mut pool.ownable_state) + } + + public entry fun execute_ownership_transfer( + caller: &signer, to: address + ) acquires ManagedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::execute_ownership_transfer(caller, &mut pool.ownable_state, to) + } + + // ================================================================ + // | MCMS entrypoint | + // ================================================================ + struct McmsCallback has drop {} + + public fun mcms_entrypoint( + _metadata: object::Object + ): option::Option acquires ManagedTokenPoolState { + let (caller, function, data) = + mcms_registry::get_callback_params(@managed_token_pool, McmsCallback {}); + + let function_bytes = *function.bytes(); + let stream = bcs_stream::new(data); + + if (function_bytes == b"add_remote_pool") { + let remote_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let remote_pool_address = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + add_remote_pool(&caller, remote_chain_selector, remote_pool_address); + } else if (function_bytes == b"remove_remote_pool") { + let remote_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let remote_pool_address = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + remove_remote_pool(&caller, remote_chain_selector, remote_pool_address); + } else if (function_bytes == b"apply_chain_updates") { + let remote_chain_selectors_to_remove = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let remote_chain_selectors_to_add = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let remote_pool_addresses_to_add = + bcs_stream::deserialize_vector( + &mut stream, + |stream| bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ) + ); + let remote_token_addresses_to_add = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + bcs_stream::assert_is_consumed(&stream); + apply_chain_updates( + &caller, + remote_chain_selectors_to_remove, + remote_chain_selectors_to_add, + remote_pool_addresses_to_add, + remote_token_addresses_to_add + ); + } else if (function_bytes == b"set_allowlist_enabled") { + let enabled = bcs_stream::deserialize_bool(&mut stream); + bcs_stream::assert_is_consumed(&stream); + set_allowlist_enabled(&caller, enabled); + } else if (function_bytes == b"apply_allowlist_updates") { + let removes = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + let adds = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + apply_allowlist_updates(&caller, removes, adds); + } else if (function_bytes == b"set_chain_rate_limiter_configs") { + let remote_chain_selectors = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let outbound_is_enableds = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_bool(stream) + ); + let outbound_capacities = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let outbound_rates = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let inbound_is_enableds = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_bool(stream) + ); + let inbound_capacities = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let inbound_rates = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + bcs_stream::assert_is_consumed(&stream); + set_chain_rate_limiter_configs( + &caller, + remote_chain_selectors, + outbound_is_enableds, + outbound_capacities, + outbound_rates, + inbound_is_enableds, + inbound_capacities, + inbound_rates + ); + } else if (function_bytes == b"set_chain_rate_limiter_config") { + let remote_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let outbound_is_enabled = bcs_stream::deserialize_bool(&mut stream); + let outbound_capacity = bcs_stream::deserialize_u64(&mut stream); + let outbound_rate = bcs_stream::deserialize_u64(&mut stream); + let inbound_is_enabled = bcs_stream::deserialize_bool(&mut stream); + let inbound_capacity = bcs_stream::deserialize_u64(&mut stream); + let inbound_rate = bcs_stream::deserialize_u64(&mut stream); + bcs_stream::assert_is_consumed(&stream); + set_chain_rate_limiter_config( + &caller, + remote_chain_selector, + outbound_is_enabled, + outbound_capacity, + outbound_rate, + inbound_is_enabled, + inbound_capacity, + inbound_rate + ); + } else if (function_bytes == b"transfer_ownership") { + let to = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + transfer_ownership(&caller, to); + } else if (function_bytes == b"accept_ownership") { + bcs_stream::assert_is_consumed(&stream); + accept_ownership(&caller); + } else if (function_bytes == b"execute_ownership_transfer") { + let to = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + execute_ownership_transfer(&caller, to) + } else { + abort error::invalid_argument(E_UNKNOWN_FUNCTION) + }; + + option::none() + } + + /// Callable during upgrades + public(friend) fun register_mcms_entrypoint( + publisher: &signer, module_name: vector + ) { + mcms_registry::register_entrypoint( + publisher, string::utf8(module_name), McmsCallback {} + ); + } + + // ================================================================ + // | Test functions | + // ================================================================ + #[test_only] + public entry fun test_init_module(owner: &signer) { + init_module(owner); + } + + #[test_only] + /// Used for registering the pool with V2 closure-based callbacks. + public fun create_callback_proof(): CallbackProof { + CallbackProof {} + } +} +` + +/** Move.toml for the token_pool dependency package. */ +export const TOKEN_POOL_MOVE_TOML = `[package] +name = "CCIPTokenPool" +version = "1.0.0" +authors = [] + +[addresses] +ccip = "_" +ccip_token_pool = "_" +mcms = "_" +mcms_register_entrypoints = "_" + +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "16beac69835f3a71564c96164a606a23f259099a", subdir = "aptos-move/framework/aptos-framework" } +ChainlinkCCIP = { local = "../ccip" } +` + +/** token_pool/sources/token_pool.move */ +export const TOKEN_POOL_MOVE = `module ccip_token_pool::token_pool { + use std::account::{Self}; + use std::error; + use std::event::{Self, EventHandle}; + use std::fungible_asset::{Self, FungibleAsset, Metadata}; + use std::object::{Self, Object}; + use std::smart_table::{Self, SmartTable}; + + use ccip::address; + use ccip::eth_abi; + use ccip::token_admin_registry; + use ccip::rmn_remote; + use ccip::allowlist; + + use ccip_token_pool::rate_limiter; + use ccip_token_pool::token_pool_rate_limiter; + + const MAX_U256: u256 = + 115792089237316195423570985008687907853269984665640564039457584007913129639935; + const MAX_U64: u256 = 18446744073709551615; + + struct TokenPoolState has key, store { + allowlist_state: allowlist::AllowlistState, + fa_metadata: Object, + remote_chain_configs: SmartTable, + rate_limiter_config: token_pool_rate_limiter::RateLimitState, + locked_events: EventHandle, + released_events: EventHandle, + remote_pool_added_events: EventHandle, + remote_pool_removed_events: EventHandle, + chain_added_events: EventHandle, + chain_removed_events: EventHandle, + liquidity_added_events: EventHandle, + liquidity_removed_events: EventHandle, + rebalancer_set_events: EventHandle + } + + struct RemoteChainConfig has store, drop, copy { + remote_token_address: vector, + remote_pools: vector> + } + + #[event] + struct LockedOrBurned has store, drop { + remote_chain_selector: u64, + local_token: address, + amount: u64 + } + + #[event] + struct ReleasedOrMinted has store, drop { + remote_chain_selector: u64, + local_token: address, + recipient: address, + amount: u64 + } + + #[event] + struct AllowlistRemove has store, drop { + sender: address + } + + #[event] + struct AllowlistAdd has store, drop { + sender: address + } + + #[event] + struct RemotePoolAdded has store, drop { + remote_chain_selector: u64, + remote_pool_address: vector + } + + #[event] + struct RemotePoolRemoved has store, drop { + remote_chain_selector: u64, + remote_pool_address: vector + } + + #[event] + struct ChainAdded has store, drop { + remote_chain_selector: u64, + remote_token_address: vector + } + + #[event] + struct ChainRemoved has store, drop { + remote_chain_selector: u64 + } + + #[event] + struct LiquidityAdded has store, drop { + local_token: address, + provider: address, + amount: u64 + } + + #[event] + struct LiquidityRemoved has store, drop { + local_token: address, + provider: address, + amount: u64 + } + + #[event] + struct RebalancerSet has store, drop { + old_rebalancer: address, + new_rebalancer: address + } + + const E_NOT_ALLOWED_CALLER: u64 = 1; + const E_UNKNOWN_FUNGIBLE_ASSET: u64 = 2; + const E_UNKNOWN_REMOTE_CHAIN_SELECTOR: u64 = 3; + const E_ZERO_ADDRESS_NOT_ALLOWED: u64 = 4; + const E_REMOTE_POOL_ALREADY_ADDED: u64 = 5; + const E_UNKNOWN_REMOTE_POOL: u64 = 6; + const E_REMOTE_CHAIN_TO_ADD_MISMATCH: u64 = 7; + const E_REMOTE_CHAIN_ALREADY_EXISTS: u64 = 8; + const E_INVALID_REMOTE_CHAIN_DECIMALS: u64 = 9; + const E_INVALID_ENCODED_AMOUNT: u64 = 10; + const E_DECIMAL_OVERFLOW: u64 = 11; + const E_CURSED_CHAIN: u64 = 12; + + // ================================================================ + // | Initialize and state | + // ================================================================ + + /// This function should be called from the init_module function to ensure the events + /// are created on the correct object. + public fun initialize( + event_account: &signer, local_token: address, allowlist: vector
+ ): TokenPoolState { + let fa_metadata = object::address_to_object(local_token); + + TokenPoolState { + allowlist_state: allowlist::new(event_account, allowlist), + fa_metadata, + remote_chain_configs: smart_table::new(), + rate_limiter_config: token_pool_rate_limiter::new(event_account), + locked_events: account::new_event_handle(event_account), + released_events: account::new_event_handle(event_account), + remote_pool_added_events: account::new_event_handle(event_account), + remote_pool_removed_events: account::new_event_handle(event_account), + chain_added_events: account::new_event_handle(event_account), + chain_removed_events: account::new_event_handle(event_account), + liquidity_added_events: account::new_event_handle(event_account), + liquidity_removed_events: account::new_event_handle(event_account), + rebalancer_set_events: account::new_event_handle(event_account) + } + } + + #[view] + public fun get_router(): address { + @ccip + } + + public fun get_token(state: &TokenPoolState): address { + object::object_address(&state.fa_metadata) + } + + public fun get_token_decimals(state: &TokenPoolState): u8 { + fungible_asset::decimals(state.fa_metadata) + } + + public fun get_fa_metadata(state: &TokenPoolState): Object { + state.fa_metadata + } + + // ================================================================ + // | Remote Chains | + // ================================================================ + public fun get_supported_chains(state: &TokenPoolState): vector { + state.remote_chain_configs.keys() + } + + public fun is_supported_chain( + state: &TokenPoolState, remote_chain_selector: u64 + ): bool { + state.remote_chain_configs.contains(remote_chain_selector) + } + + public fun apply_chain_updates( + state: &mut TokenPoolState, + remote_chain_selectors_to_remove: vector, + remote_chain_selectors_to_add: vector, + remote_pool_addresses_to_add: vector>>, + remote_token_addresses_to_add: vector> + ) { + remote_chain_selectors_to_remove.for_each_ref( + |remote_chain_selector| { + let remote_chain_selector: u64 = *remote_chain_selector; + assert!( + state.remote_chain_configs.contains(remote_chain_selector), + error::invalid_argument(E_UNKNOWN_REMOTE_CHAIN_SELECTOR) + ); + state.remote_chain_configs.remove(remote_chain_selector); + + event::emit_event( + &mut state.chain_removed_events, + ChainRemoved { remote_chain_selector } + ); + } + ); + + let add_len = remote_chain_selectors_to_add.length(); + assert!( + add_len == remote_pool_addresses_to_add.length(), + error::invalid_argument(E_REMOTE_CHAIN_TO_ADD_MISMATCH) + ); + assert!( + add_len == remote_token_addresses_to_add.length(), + error::invalid_argument(E_REMOTE_CHAIN_TO_ADD_MISMATCH) + ); + + for (i in 0..add_len) { + let remote_chain_selector = remote_chain_selectors_to_add[i]; + assert!( + !state.remote_chain_configs.contains(remote_chain_selector), + error::invalid_argument(E_REMOTE_CHAIN_ALREADY_EXISTS) + ); + let remote_pool_addresses = remote_pool_addresses_to_add[i]; + let remote_token_address = remote_token_addresses_to_add[i]; + address::assert_non_zero_address_vector(&remote_token_address); + + let remote_chain_config = RemoteChainConfig { + remote_token_address, + remote_pools: vector[] + }; + + remote_pool_addresses.for_each( + |remote_pool_address| { + let remote_pool_address: vector = remote_pool_address; + address::assert_non_zero_address_vector(&remote_pool_address); + + let (found, _) = + remote_chain_config.remote_pools.index_of(&remote_pool_address); + assert!( + !found, error::invalid_argument(E_REMOTE_POOL_ALREADY_ADDED) + ); + + remote_chain_config.remote_pools.push_back(remote_pool_address); + + event::emit_event( + &mut state.remote_pool_added_events, + RemotePoolAdded { remote_chain_selector, remote_pool_address } + ); + } + ); + + state.remote_chain_configs.add(remote_chain_selector, remote_chain_config); + + event::emit_event( + &mut state.chain_added_events, + ChainAdded { remote_chain_selector, remote_token_address } + ); + }; + } + + // ================================================================ + // | Remote Pools | + // ================================================================ + public fun get_remote_pools( + state: &TokenPoolState, remote_chain_selector: u64 + ): vector> { + assert!( + state.remote_chain_configs.contains(remote_chain_selector), + error::invalid_argument(E_UNKNOWN_REMOTE_CHAIN_SELECTOR) + ); + let remote_chain_config = + state.remote_chain_configs.borrow(remote_chain_selector); + remote_chain_config.remote_pools + } + + public fun is_remote_pool( + state: &TokenPoolState, remote_chain_selector: u64, remote_pool_address: vector + ): bool { + let remote_pools = get_remote_pools(state, remote_chain_selector); + let (found, _) = remote_pools.index_of(&remote_pool_address); + found + } + + public fun get_remote_token( + state: &TokenPoolState, remote_chain_selector: u64 + ): vector { + assert!( + state.remote_chain_configs.contains(remote_chain_selector), + error::invalid_argument(E_UNKNOWN_REMOTE_CHAIN_SELECTOR) + ); + let remote_chain_config = + state.remote_chain_configs.borrow(remote_chain_selector); + remote_chain_config.remote_token_address + } + + public fun add_remote_pool( + state: &mut TokenPoolState, + remote_chain_selector: u64, + remote_pool_address: vector + ) { + address::assert_non_zero_address_vector(&remote_pool_address); + + assert!( + state.remote_chain_configs.contains(remote_chain_selector), + error::invalid_argument(E_UNKNOWN_REMOTE_CHAIN_SELECTOR) + ); + let remote_chain_config = + state.remote_chain_configs.borrow_mut(remote_chain_selector); + + let (found, _) = remote_chain_config.remote_pools.index_of(&remote_pool_address); + assert!(!found, error::invalid_argument(E_REMOTE_POOL_ALREADY_ADDED)); + + remote_chain_config.remote_pools.push_back(remote_pool_address); + + event::emit_event( + &mut state.remote_pool_added_events, + RemotePoolAdded { remote_chain_selector, remote_pool_address } + ); + } + + public fun remove_remote_pool( + state: &mut TokenPoolState, + remote_chain_selector: u64, + remote_pool_address: vector + ) { + assert!( + state.remote_chain_configs.contains(remote_chain_selector), + error::invalid_argument(E_UNKNOWN_REMOTE_CHAIN_SELECTOR) + ); + let remote_chain_config = + state.remote_chain_configs.borrow_mut(remote_chain_selector); + + let (found, i) = remote_chain_config.remote_pools.index_of(&remote_pool_address); + assert!(found, error::invalid_argument(E_UNKNOWN_REMOTE_POOL)); + + // remove instead of swap_remove for readability, so the newest added pool is always at the end. + remote_chain_config.remote_pools.remove(i); + + event::emit_event( + &mut state.remote_pool_removed_events, + RemotePoolRemoved { remote_chain_selector, remote_pool_address } + ); + } + + // ================================================================ + // | Validation | + // ================================================================ + + // Returns the remote token as bytes + public fun validate_lock_or_burn( + state: &mut TokenPoolState, + fa: &FungibleAsset, + input: &token_admin_registry::LockOrBurnInputV1, + local_amount: u64 + ): vector { + // Validate the fungible asset + let fa_metadata = fungible_asset::metadata_from_asset(fa); + let configured_token = get_token(state); + + // make sure the caller is requesting this pool's fungible asset. + assert!( + configured_token == object::object_address(&fa_metadata), + error::invalid_argument(E_UNKNOWN_FUNGIBLE_ASSET) + ); + + // Check RMN curse status + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(input); + assert!( + !rmn_remote::is_cursed_u128((remote_chain_selector as u128)), + error::invalid_state(E_CURSED_CHAIN) + ); + + let sender = token_admin_registry::get_lock_or_burn_sender(input); + // Allowlist check + assert!( + allowlist::is_allowed(&state.allowlist_state, sender), + error::permission_denied(E_NOT_ALLOWED_CALLER) + ); + + if (!is_supported_chain(state, remote_chain_selector)) { + abort error::invalid_argument(E_UNKNOWN_REMOTE_CHAIN_SELECTOR) + }; + + token_pool_rate_limiter::consume_outbound( + &mut state.rate_limiter_config, + remote_chain_selector, + local_amount + ); + + get_remote_token(state, remote_chain_selector) + } + + public fun validate_release_or_mint( + state: &mut TokenPoolState, + input: &token_admin_registry::ReleaseOrMintInputV1, + local_amount: u64 + ) { + // Validate the fungible asset + let local_token = token_admin_registry::get_release_or_mint_local_token(input); + let configured_token = get_token(state); + + // make sure the caller is requesting this pool's fungible asset. + assert!( + configured_token == local_token, + error::invalid_argument(E_UNKNOWN_FUNGIBLE_ASSET) + ); + + // Check RMN curse status + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(input); + assert!( + !rmn_remote::is_cursed_u128((remote_chain_selector as u128)), + error::invalid_state(E_CURSED_CHAIN) + ); + + let source_pool_address = + token_admin_registry::get_release_or_mint_source_pool_address(input); + + // This checks if the remote chain selector and the source pool are valid. + assert!( + is_remote_pool(state, remote_chain_selector, source_pool_address), + error::invalid_argument(E_UNKNOWN_REMOTE_POOL) + ); + + token_pool_rate_limiter::consume_inbound( + &mut state.rate_limiter_config, + remote_chain_selector, + local_amount + ); + } + + // ================================================================ + // | Events | + // ================================================================ + public fun emit_released_or_minted( + state: &mut TokenPoolState, + recipient: address, + amount: u64, + remote_chain_selector: u64 + ) { + let local_token = object::object_address(&state.fa_metadata); + + event::emit_event( + &mut state.released_events, + ReleasedOrMinted { + remote_chain_selector, + local_token, + recipient, + amount + } + ); + } + + public fun emit_locked_or_burned( + state: &mut TokenPoolState, amount: u64, remote_chain_selector: u64 + ) { + let local_token = object::object_address(&state.fa_metadata); + + event::emit_event( + &mut state.locked_events, + LockedOrBurned { remote_chain_selector, local_token, amount } + ); + } + + public fun emit_liquidity_added( + state: &mut TokenPoolState, provider: address, amount: u64 + ) { + let local_token = object::object_address(&state.fa_metadata); + + event::emit_event( + &mut state.liquidity_added_events, + LiquidityAdded { local_token, provider, amount } + ); + } + + public fun emit_liquidity_removed( + state: &mut TokenPoolState, provider: address, amount: u64 + ) { + let local_token = object::object_address(&state.fa_metadata); + + event::emit_event( + &mut state.liquidity_removed_events, + LiquidityRemoved { local_token, provider, amount } + ); + } + + public fun emit_rebalancer_set( + state: &mut TokenPoolState, old_rebalancer: address, new_rebalancer: address + ) { + event::emit_event( + &mut state.rebalancer_set_events, + RebalancerSet { old_rebalancer, new_rebalancer } + ); + } + + // ================================================================ + // | Decimals | + // ================================================================ + public fun encode_local_decimals(state: &TokenPoolState): vector { + let fa_decimals = fungible_asset::decimals(state.fa_metadata); + let ret = vector[]; + eth_abi::encode_u8(&mut ret, fa_decimals); + ret + } + + #[view] + public fun parse_remote_decimals( + source_pool_data: vector, local_decimals: u8 + ): u8 { + let data_len = source_pool_data.length(); + if (data_len == 0) { + // Fallback to the local value. + return local_decimals + }; + + assert!(data_len == 32, error::invalid_state(E_INVALID_REMOTE_CHAIN_DECIMALS)); + + let remote_decimals = eth_abi::decode_u256_value(source_pool_data); + assert!( + remote_decimals <= 255, + error::invalid_state(E_INVALID_REMOTE_CHAIN_DECIMALS) + ); + + remote_decimals as u8 + } + + #[view] + public fun calculate_local_amount( + remote_amount: u256, remote_decimals: u8, local_decimals: u8 + ): u64 { + let local_amount = + calculate_local_amount_internal( + remote_amount, remote_decimals, local_decimals + ); + assert!(local_amount <= MAX_U64, error::invalid_state(E_INVALID_ENCODED_AMOUNT)); + local_amount as u64 + } + + #[view] + fun calculate_local_amount_internal( + remote_amount: u256, remote_decimals: u8, local_decimals: u8 + ): u256 { + if (remote_decimals == local_decimals) { + return remote_amount + } else if (remote_decimals > local_decimals) { + let decimals_diff = remote_decimals - local_decimals; + let current_amount = remote_amount; + for (i in 0..decimals_diff) { + current_amount /= 10; + }; + return current_amount + } else { + let decimals_diff = local_decimals - remote_decimals; + // This is a safety check to prevent overflow in the next calculation. + // More than 77 would never fit in a uint256 and would cause an overflow. We also check if the resulting amount + // would overflow. + assert!(decimals_diff <= 77, error::invalid_state(E_DECIMAL_OVERFLOW)); + + let multiplier: u256 = 1; + let base: u256 = 10; + for (i in 0..decimals_diff) { + multiplier = multiplier * base; + }; + + assert!( + remote_amount <= (MAX_U256 / multiplier), + error::invalid_state(E_DECIMAL_OVERFLOW) + ); + + return remote_amount * multiplier + } + } + + public fun calculate_release_or_mint_amount( + state: &TokenPoolState, input: &token_admin_registry::ReleaseOrMintInputV1 + ): u64 { + let local_decimals = get_token_decimals(state); + let source_amount = + token_admin_registry::get_release_or_mint_source_amount(input); + let source_pool_data = + token_admin_registry::get_release_or_mint_source_pool_data(input); + let remote_decimals = parse_remote_decimals(source_pool_data, local_decimals); + let local_amount = + calculate_local_amount(source_amount, remote_decimals, local_decimals); + local_amount + } + + // ================================================================ + // | Rate limit config | + // ================================================================ + public fun set_chain_rate_limiter_config( + state: &mut TokenPoolState, + remote_chain_selector: u64, + outbound_is_enabled: bool, + outbound_capacity: u64, + outbound_rate: u64, + inbound_is_enabled: bool, + inbound_capacity: u64, + inbound_rate: u64 + ) { + token_pool_rate_limiter::set_chain_rate_limiter_config( + &mut state.rate_limiter_config, + remote_chain_selector, + outbound_is_enabled, + outbound_capacity, + outbound_rate, + inbound_is_enabled, + inbound_capacity, + inbound_rate + ); + } + + public fun get_current_inbound_rate_limiter_state( + state: &TokenPoolState, remote_chain_selector: u64 + ): rate_limiter::TokenBucket { + token_pool_rate_limiter::get_current_inbound_rate_limiter_state( + &state.rate_limiter_config, remote_chain_selector + ) + } + + public fun get_current_outbound_rate_limiter_state( + state: &TokenPoolState, remote_chain_selector: u64 + ): rate_limiter::TokenBucket { + token_pool_rate_limiter::get_current_outbound_rate_limiter_state( + &state.rate_limiter_config, remote_chain_selector + ) + } + + // ================================================================ + // | Allowlist | + // ================================================================ + public fun get_allowlist_enabled(state: &TokenPoolState): bool { + allowlist::get_allowlist_enabled(&state.allowlist_state) + } + + public fun set_allowlist_enabled( + state: &mut TokenPoolState, enabled: bool + ) { + allowlist::set_allowlist_enabled(&mut state.allowlist_state, enabled); + } + + public fun get_allowlist(state: &TokenPoolState): vector
{ + allowlist::get_allowlist(&state.allowlist_state) + } + + public fun apply_allowlist_updates( + state: &mut TokenPoolState, removes: vector
, adds: vector
+ ) { + allowlist::apply_allowlist_updates(&mut state.allowlist_state, removes, adds); + } + + // ================================================================ + // | Test functions | + // ================================================================ + #[test_only] + public fun destroy_token_pool(state: TokenPoolState) { + let TokenPoolState { + allowlist_state, + fa_metadata: _fa_metadata, + remote_chain_configs, + rate_limiter_config, + locked_events, + released_events, + remote_pool_added_events, + remote_pool_removed_events, + chain_added_events, + chain_removed_events, + liquidity_added_events, + liquidity_removed_events, + rebalancer_set_events + } = state; + + allowlist::destroy_allowlist(allowlist_state); + remote_chain_configs.destroy(); + event::destroy_handle(locked_events); + event::destroy_handle(released_events); + event::destroy_handle(remote_pool_added_events); + event::destroy_handle(remote_pool_removed_events); + event::destroy_handle(chain_added_events); + event::destroy_handle(chain_removed_events); + event::destroy_handle(liquidity_added_events); + event::destroy_handle(liquidity_removed_events); + event::destroy_handle(rebalancer_set_events); + + token_pool_rate_limiter::destroy_rate_limiter(rate_limiter_config); + } + + #[test_only] + public fun get_locked_or_burned_events(state: &TokenPoolState): vector { + event::emitted_events_by_handle(&state.locked_events) + } + + #[test_only] + public fun get_released_or_minted_events(state: &TokenPoolState) + : vector { + event::emitted_events_by_handle(&state.released_events) + } +} +` + +/** token_pool/sources/ownable.move */ +export const TOKEN_POOL_OWNABLE_MOVE = `/// This module implements an Ownable component similar to Ownable2Step.sol for managing +/// object ownership. +/// +/// Due to Aptos's security model requiring the original owner's signer for 0x1::object::transfer, +/// this implementation uses a 3-step ownership transfer flow: +/// +/// 1. Initial owner calls transfer_ownership with the new owner's address +/// 2. Pending owner calls accept_ownership to confirm the transfer +/// 3. Initial owner calls execute_ownership_transfer to complete the transfer +/// +/// The execute_ownership_transfer function requires a signer in order to perform the +/// object transfer, while other operations only require the caller address to maintain the +/// principle of least privilege. +/// +/// Note that direct ownership transfers via 0x1::object::transfer are still possible. +/// This module handles such cases gracefully by reading the current owner directly +/// from the object. +module ccip_token_pool::ownable { + use std::account; + use std::error; + use std::event::{Self, EventHandle}; + use std::object::{Self, Object, ObjectCore}; + use std::option::{Self, Option}; + use std::signer; + + struct OwnableState has store { + target_object: Object, + pending_transfer: Option, + ownership_transfer_requested_events: EventHandle, + ownership_transfer_accepted_events: EventHandle, + ownership_transferred_events: EventHandle + } + + struct PendingTransfer has store, drop { + from: address, + to: address, + accepted: bool + } + + const E_MUST_BE_PROPOSED_OWNER: u64 = 1; + const E_CANNOT_TRANSFER_TO_SELF: u64 = 2; + const E_ONLY_CALLABLE_BY_OWNER: u64 = 3; + const E_PROPOSED_OWNER_MISMATCH: u64 = 4; + const E_OWNER_CHANGED: u64 = 5; + const E_NO_PENDING_TRANSFER: u64 = 6; + const E_TRANSFER_NOT_ACCEPTED: u64 = 7; + const E_TRANSFER_ALREADY_ACCEPTED: u64 = 8; + + #[event] + struct OwnershipTransferRequested has store, drop { + from: address, + to: address + } + + #[event] + struct OwnershipTransferAccepted has store, drop { + from: address, + to: address + } + + #[event] + struct OwnershipTransferred has store, drop { + from: address, + to: address + } + + public fun new(event_account: &signer, object_address: address): OwnableState { + let new_state = OwnableState { + target_object: object::address_to_object(object_address), + pending_transfer: option::none(), + ownership_transfer_requested_events: account::new_event_handle(event_account), + ownership_transfer_accepted_events: account::new_event_handle(event_account), + ownership_transferred_events: account::new_event_handle(event_account) + }; + + new_state + } + + public fun owner(state: &OwnableState): address { + owner_internal(state) + } + + public fun has_pending_transfer(state: &OwnableState): bool { + state.pending_transfer.is_some() + } + + public fun pending_transfer_from(state: &OwnableState): Option
{ + state.pending_transfer.map_ref(|pending_transfer| pending_transfer.from) + } + + public fun pending_transfer_to(state: &OwnableState): Option
{ + state.pending_transfer.map_ref(|pending_transfer| pending_transfer.to) + } + + public fun pending_transfer_accepted(state: &OwnableState): Option { + state.pending_transfer.map_ref(|pending_transfer| pending_transfer.accepted) + } + + inline fun owner_internal(state: &OwnableState): address { + object::owner(state.target_object) + } + + public fun transfer_ownership( + caller: &signer, state: &mut OwnableState, to: address + ) { + let caller_address = signer::address_of(caller); + assert_only_owner_internal(caller_address, state); + assert!(caller_address != to, error::invalid_argument(E_CANNOT_TRANSFER_TO_SELF)); + + state.pending_transfer = option::some( + PendingTransfer { from: caller_address, to, accepted: false } + ); + + event::emit_event( + &mut state.ownership_transfer_requested_events, + OwnershipTransferRequested { from: caller_address, to } + ); + } + + public fun accept_ownership(caller: &signer, state: &mut OwnableState) { + let caller_address = signer::address_of(caller); + assert!( + state.pending_transfer.is_some(), + error::permission_denied(E_NO_PENDING_TRANSFER) + ); + + let current_owner = owner_internal(state); + let pending_transfer = state.pending_transfer.borrow_mut(); + + // check that the owner has not changed from a direct call to 0x1::object::transfer, + // in which case the transfer flow should be restarted. + assert!( + pending_transfer.from == current_owner, + error::permission_denied(E_OWNER_CHANGED) + ); + assert!( + pending_transfer.to == caller_address, + error::permission_denied(E_MUST_BE_PROPOSED_OWNER) + ); + assert!( + !pending_transfer.accepted, + error::invalid_state(E_TRANSFER_ALREADY_ACCEPTED) + ); + + pending_transfer.accepted = true; + + event::emit_event( + &mut state.ownership_transfer_accepted_events, + OwnershipTransferAccepted { from: pending_transfer.from, to: caller_address } + ); + } + + public fun execute_ownership_transfer( + caller: &signer, state: &mut OwnableState, to: address + ) { + let caller_address = signer::address_of(caller); + assert_only_owner_internal(caller_address, state); + + let current_owner = owner_internal(state); + let pending_transfer = state.pending_transfer.extract(); + + // check that the owner has not changed from a direct call to 0x1::object::transfer, + // in which case the transfer flow should be restarted. + assert!( + pending_transfer.from == current_owner, + error::permission_denied(E_OWNER_CHANGED) + ); + assert!( + pending_transfer.to == to, + error::permission_denied(E_PROPOSED_OWNER_MISMATCH) + ); + assert!( + pending_transfer.accepted, + error::invalid_state(E_TRANSFER_NOT_ACCEPTED) + ); + + object::transfer(caller, state.target_object, pending_transfer.to); + state.pending_transfer = option::none(); + + event::emit_event( + &mut state.ownership_transferred_events, + OwnershipTransferred { from: caller_address, to } + ); + } + + public fun assert_only_owner(caller: address, state: &OwnableState) { + assert_only_owner_internal(caller, state) + } + + inline fun assert_only_owner_internal( + caller: address, state: &OwnableState + ) { + assert!( + caller == owner_internal(state), + error::permission_denied(E_ONLY_CALLABLE_BY_OWNER) + ); + } + + public fun destroy(state: OwnableState) { + let OwnableState { + target_object: _, + pending_transfer: _, + ownership_transfer_requested_events, + ownership_transfer_accepted_events, + ownership_transferred_events + } = state; + + event::destroy_handle(ownership_transfer_requested_events); + event::destroy_handle(ownership_transfer_accepted_events); + event::destroy_handle(ownership_transferred_events); + } + + #[test_only] + public fun get_ownership_transfer_requested_events( + state: &OwnableState + ): &EventHandle { + &state.ownership_transfer_requested_events + } + + #[test_only] + public fun get_ownership_transfer_accepted_events( + state: &OwnableState + ): &EventHandle { + &state.ownership_transfer_accepted_events + } + + #[test_only] + public fun get_ownership_transferred_events( + state: &OwnableState + ): &EventHandle { + &state.ownership_transferred_events + } +} +` + +/** token_pool/sources/rate_limiter.move */ +export const RATE_LIMITER_MOVE = `module ccip_token_pool::rate_limiter { + use std::error; + use std::timestamp; + + struct TokenBucket has store, drop { + tokens: u64, + last_updated: u64, + is_enabled: bool, + capacity: u64, + rate: u64 + } + + const E_TOKEN_MAX_CAPACITY_EXCEEDED: u64 = 1; + const E_TOKEN_RATE_LIMIT_REACHED: u64 = 2; + + public fun new(is_enabled: bool, capacity: u64, rate: u64): TokenBucket { + TokenBucket { + tokens: 0, + last_updated: timestamp::now_seconds(), + is_enabled, + capacity, + rate + } + } + + public fun get_current_token_bucket_state(state: &TokenBucket): TokenBucket { + TokenBucket { + tokens: calculate_refill( + state, timestamp::now_seconds() - state.last_updated + ), + last_updated: timestamp::now_seconds(), + is_enabled: state.is_enabled, + capacity: state.capacity, + rate: state.rate + } + } + + public fun consume(bucket: &mut TokenBucket, requested_tokens: u64) { + if (!bucket.is_enabled || requested_tokens == 0) { return }; + + update_bucket(bucket); + + assert!( + requested_tokens <= bucket.capacity, + error::invalid_argument(E_TOKEN_MAX_CAPACITY_EXCEEDED) + ); + + assert!( + requested_tokens <= bucket.tokens, + error::invalid_argument(E_TOKEN_RATE_LIMIT_REACHED) + ); + + bucket.tokens -= requested_tokens; + } + + /// We allow 0 rate and/or 0 capacity rate limits to effectively disable value transfer. + public fun set_token_bucket_config( + bucket: &mut TokenBucket, is_enabled: bool, capacity: u64, rate: u64 + ) { + update_bucket(bucket); + + bucket.tokens = min(bucket.tokens, capacity); + bucket.capacity = capacity; + bucket.rate = rate; + bucket.is_enabled = is_enabled; + } + + inline fun update_bucket(bucket: &mut TokenBucket) { + let time_now_seconds = timestamp::now_seconds(); + let time_diff = time_now_seconds - bucket.last_updated; + + if (time_diff > 0) { + bucket.tokens = calculate_refill(bucket, time_diff); + bucket.last_updated = time_now_seconds; + }; + } + + inline fun calculate_refill(bucket: &TokenBucket, time_diff: u64): u64 { + min( + bucket.capacity, bucket.tokens + time_diff * bucket.rate + ) + } + + inline fun min(a: u64, b: u64): u64 { + if (a > b) b else a + } +} +` + +/** token_pool/sources/token_pool_rate_limiter.move */ +export const TOKEN_POOL_RATE_LIMITER_MOVE = `module ccip_token_pool::token_pool_rate_limiter { + use std::smart_table; + use std::smart_table::SmartTable; + use std::account; + use std::error; + use std::event; + use std::event::EventHandle; + + use ccip_token_pool::rate_limiter; + + struct RateLimitState has store { + outbound_rate_limiter_config: SmartTable, + inbound_rate_limiter_config: SmartTable, + tokens_consumed_events: EventHandle, + config_changed_events: EventHandle + } + + #[event] + struct TokensConsumed has store, drop { + remote_chain_selector: u64, + tokens: u64 + } + + #[event] + struct ConfigChanged has store, drop { + remote_chain_selector: u64, + outbound_is_enabled: bool, + outbound_capacity: u64, + outbound_rate: u64, + inbound_is_enabled: bool, + inbound_capacity: u64, + inbound_rate: u64 + } + + const E_BUCKET_NOT_FOUND: u64 = 1; + + public fun new(event_account: &signer): RateLimitState { + RateLimitState { + outbound_rate_limiter_config: smart_table::new(), + inbound_rate_limiter_config: smart_table::new(), + tokens_consumed_events: account::new_event_handle(event_account), + config_changed_events: account::new_event_handle(event_account) + } + } + + public fun consume_inbound( + state: &mut RateLimitState, dest_chain_selector: u64, requested_tokens: u64 + ) { + consume_from_bucket( + &mut state.tokens_consumed_events, + &mut state.inbound_rate_limiter_config, + dest_chain_selector, + requested_tokens + ); + } + + public fun consume_outbound( + state: &mut RateLimitState, dest_chain_selector: u64, requested_tokens: u64 + ) { + consume_from_bucket( + &mut state.tokens_consumed_events, + &mut state.outbound_rate_limiter_config, + dest_chain_selector, + requested_tokens + ); + } + + inline fun consume_from_bucket( + tokens_consumed_events: &mut EventHandle, + rate_limiter: &mut SmartTable, + dest_chain_selector: u64, + requested_tokens: u64 + ) { + assert!( + rate_limiter.contains(dest_chain_selector), + error::invalid_argument(E_BUCKET_NOT_FOUND) + ); + + let bucket = rate_limiter.borrow_mut(dest_chain_selector); + rate_limiter::consume(bucket, requested_tokens); + + event::emit_event( + tokens_consumed_events, + TokensConsumed { + remote_chain_selector: dest_chain_selector, + tokens: requested_tokens + } + ); + } + + public fun set_chain_rate_limiter_config( + state: &mut RateLimitState, + remote_chain_selector: u64, + outbound_is_enabled: bool, + outbound_capacity: u64, + outbound_rate: u64, + inbound_is_enabled: bool, + inbound_capacity: u64, + inbound_rate: u64 + ) { + let outbound_config = + state.outbound_rate_limiter_config.borrow_mut_with_default( + remote_chain_selector, + rate_limiter::new(false, 0, 0) + ); + rate_limiter::set_token_bucket_config( + outbound_config, + outbound_is_enabled, + outbound_capacity, + outbound_rate + ); + + let inbound_config = + state.inbound_rate_limiter_config.borrow_mut_with_default( + remote_chain_selector, + rate_limiter::new(false, 0, 0) + ); + rate_limiter::set_token_bucket_config( + inbound_config, + inbound_is_enabled, + inbound_capacity, + inbound_rate + ); + + event::emit_event( + &mut state.config_changed_events, + ConfigChanged { + remote_chain_selector, + outbound_is_enabled, + outbound_capacity, + outbound_rate, + inbound_is_enabled, + inbound_capacity, + inbound_rate + } + ); + } + + public fun get_current_inbound_rate_limiter_state( + state: &RateLimitState, remote_chain_selector: u64 + ): rate_limiter::TokenBucket { + rate_limiter::get_current_token_bucket_state( + state.inbound_rate_limiter_config.borrow(remote_chain_selector) + ) + } + + public fun get_current_outbound_rate_limiter_state( + state: &RateLimitState, remote_chain_selector: u64 + ): rate_limiter::TokenBucket { + rate_limiter::get_current_token_bucket_state( + state.outbound_rate_limiter_config.borrow(remote_chain_selector) + ) + } + + public fun destroy_rate_limiter(state: RateLimitState) { + let RateLimitState { + outbound_rate_limiter_config, + inbound_rate_limiter_config, + tokens_consumed_events, + config_changed_events + } = state; + + outbound_rate_limiter_config.destroy(); + inbound_rate_limiter_config.destroy(); + event::destroy_handle(tokens_consumed_events); + event::destroy_handle(config_changed_events); + } +} +` diff --git a/ccip-sdk/src/cct/aptos/bytecodes/mcms.ts b/ccip-sdk/src/cct/aptos/bytecodes/mcms.ts new file mode 100644 index 00000000..e484cbcf --- /dev/null +++ b/ccip-sdk/src/cct/aptos/bytecodes/mcms.ts @@ -0,0 +1,3362 @@ +/** + * ChainlinkManyChainMultisig (MCMS) Move sources — embedded from chainlink-aptos. + * + * These sources are compiled locally alongside pool packages so that + * the compiled bytecode matches the on-chain modules exactly. + * + * @packageDocumentation + */ + +/** Move.toml for ChainlinkManyChainMultisig. */ +export const MCMS_MOVE_TOML = `[package] +name = "ChainlinkManyChainMultisig" +version = "1.0.0" +upgrade_policy = "compatible" + +[addresses] +mcms = "_" +mcms_owner = "_" + +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "16beac69835f3a71564c96164a606a23f259099a", subdir = "aptos-move/framework/aptos-framework" } +` + +/** sources/mcms_account.move */ +export const MCMS_MCMS_ACCOUNT_MOVE = `/// This module manages the ownership of the MCMS package. +module mcms::mcms_account { + use std::account::{Self, SignerCapability}; + use std::error; + use std::event; + use std::resource_account; + use std::signer; + + friend mcms::mcms; + friend mcms::mcms_deployer; + friend mcms::mcms_registry; + + struct AccountState has key, store { + signer_cap: SignerCapability, + owner: address, + pending_owner: address + } + + #[event] + struct OwnershipTransferRequested has store, drop { + from: address, + to: address + } + + #[event] + struct OwnershipTransferred has store, drop { + from: address, + to: address + } + + const E_CANNOT_TRANSFER_TO_SELF: u64 = 1; + const E_MUST_BE_PROPOSED_OWNER: u64 = 2; + const E_UNAUTHORIZED: u64 = 3; + + fun init_module(publisher: &signer) { + let signer_cap = + resource_account::retrieve_resource_account_cap(publisher, @mcms_owner); + init_module_internal(publisher, signer_cap); + } + + inline fun init_module_internal( + publisher: &signer, signer_cap: SignerCapability + ) { + move_to( + publisher, + AccountState { + signer_cap, + owner: @mcms_owner, + pending_owner: @0x0 + } + ); + } + + /// Transfers ownership to the specified address. + public entry fun transfer_ownership(caller: &signer, to: address) acquires AccountState { + let state = borrow_state_mut(); + + assert_is_owner_internal(state, caller); + + assert!( + signer::address_of(caller) != to, + error::invalid_argument(E_CANNOT_TRANSFER_TO_SELF) + ); + + state.pending_owner = to; + + event::emit(OwnershipTransferRequested { from: state.owner, to }); + } + + /// Transfers ownership back to the \`@mcms\` address. + public entry fun transfer_ownership_to_self(caller: &signer) acquires AccountState { + transfer_ownership(caller, @mcms); + } + + /// Accepts ownership transfer. Can only be called by the pending owner. + public entry fun accept_ownership(caller: &signer) acquires AccountState { + let state = borrow_state_mut(); + + let caller_address = signer::address_of(caller); + assert!( + caller_address == state.pending_owner, + error::permission_denied(E_MUST_BE_PROPOSED_OWNER) + ); + + let previous_owner = state.owner; + state.owner = caller_address; + state.pending_owner = @0x0; + + event::emit(OwnershipTransferred { from: previous_owner, to: state.owner }); + } + + #[view] + /// Returns the current owner. + public fun owner(): address acquires AccountState { + borrow_state().owner + } + + #[view] + /// Returns \`true\` if the module is self-owned (owned by \`@mcms\`). + public fun is_self_owned(): bool acquires AccountState { + owner() == @mcms + } + + public(friend) fun get_signer(): signer acquires AccountState { + account::create_signer_with_capability(&borrow_state().signer_cap) + } + + public(friend) fun assert_is_owner(caller: &signer) acquires AccountState { + assert_is_owner_internal(borrow_state(), caller); + } + + inline fun assert_is_owner_internal( + state: &AccountState, caller: &signer + ) { + assert!( + state.owner == signer::address_of(caller), + error::permission_denied(E_UNAUTHORIZED) + ); + } + + inline fun borrow_state(): &AccountState { + borrow_global(@mcms) + } + + inline fun borrow_state_mut(): &mut AccountState { + borrow_global_mut(@mcms) + } + + #[test_only] + public fun init_module_for_testing(publisher: &signer) { + let test_signer_cap = account::create_test_signer_cap(@mcms); + init_module_internal(publisher, test_signer_cap); + } +} +` + +/** sources/mcms_deployer.move */ +export const MCMS_MCMS_DEPLOYER_MOVE = `/// This module is a modified version of Aptos' large_packages package, providing functions for publishing and upgrading +/// MCMS-owned modules of arbitrary sizes via object code deployment. +module mcms::mcms_deployer { + use std::code::PackageRegistry; + use std::error; + use std::smart_table::{Self, SmartTable}; + use std::object; + use std::object_code_deployment; + + use mcms::mcms_account; + use mcms::mcms_registry; + + const E_CODE_MISMATCH: u64 = 1; + + struct StagingArea has key { + metadata_serialized: vector, + code: SmartTable>, + last_module_idx: u64 + } + + /// Stages a chunk of code in the StagingArea. + /// This function allows for incremental building of a large package. + public entry fun stage_code_chunk( + caller: &signer, + metadata_chunk: vector, + code_indices: vector, + code_chunks: vector> + ) acquires StagingArea { + mcms_account::assert_is_owner(caller); + + stage_code_chunk_internal(metadata_chunk, code_indices, code_chunks); + } + + /// Stages a code chunk and immediately publishes it to a new object. + public entry fun stage_code_chunk_and_publish_to_object( + caller: &signer, + metadata_chunk: vector, + code_indices: vector, + code_chunks: vector>, + new_owner_seed: vector + ) acquires StagingArea { + mcms_account::assert_is_owner(caller); + + let staging_area = + stage_code_chunk_internal(metadata_chunk, code_indices, code_chunks); + let code = assemble_module_code(staging_area); + + let owner_signer = + &mcms_registry::create_owner_for_new_code_object(new_owner_seed); + + object_code_deployment::publish( + owner_signer, staging_area.metadata_serialized, code + ); + + cleanup_staging_area_internal(); + } + + /// Stages a code chunk and immediately upgrades an existing code object. + public entry fun stage_code_chunk_and_upgrade_object_code( + caller: &signer, + metadata_chunk: vector, + code_indices: vector, + code_chunks: vector>, + code_object_address: address + ) acquires StagingArea { + mcms_account::assert_is_owner(caller); + + let staging_area = + stage_code_chunk_internal(metadata_chunk, code_indices, code_chunks); + let code = assemble_module_code(staging_area); + + let owner_signer = + &mcms_registry::get_signer_for_code_object_upgrade(code_object_address); + + object_code_deployment::upgrade( + owner_signer, + staging_area.metadata_serialized, + code, + object::address_to_object(code_object_address) + ); + + cleanup_staging_area_internal(); + } + + /// Cleans up the staging area, removing any staged code chunks. + /// This function can be called to reset the staging area without publishing or upgrading. + public entry fun cleanup_staging_area(caller: &signer) acquires StagingArea { + mcms_account::assert_is_owner(caller); + + cleanup_staging_area_internal(); + } + + inline fun stage_code_chunk_internal( + metadata_chunk: vector, + code_indices: vector, + code_chunks: vector> + ): &mut StagingArea { + assert!( + code_indices.length() == code_chunks.length(), + error::invalid_argument(E_CODE_MISMATCH) + ); + + if (!exists(@mcms)) { + move_to( + &mcms_account::get_signer(), + StagingArea { + metadata_serialized: vector[], + code: smart_table::new(), + last_module_idx: 0 + } + ); + }; + + let staging_area = borrow_global_mut(@mcms); + + if (!metadata_chunk.is_empty()) { + staging_area.metadata_serialized.append(metadata_chunk); + }; + + for (i in 0..code_chunks.length()) { + let inner_code = code_chunks[i]; + let idx = (code_indices[i] as u64); + + if (staging_area.code.contains(idx)) { + staging_area.code.borrow_mut(idx).append(inner_code); + } else { + staging_area.code.add(idx, inner_code); + if (idx > staging_area.last_module_idx) { + staging_area.last_module_idx = idx; + } + }; + }; + + staging_area + } + + inline fun assemble_module_code(staging_area: &mut StagingArea): vector> { + let last_module_idx = staging_area.last_module_idx; + let code = vector[]; + for (i in 0..(last_module_idx + 1)) { + code.push_back(*staging_area.code.borrow(i)); + }; + code + } + + inline fun cleanup_staging_area_internal() { + let StagingArea { metadata_serialized: _, code, last_module_idx: _ } = + move_from(@mcms); + code.destroy(); + } +} +` + +/** sources/mcms_executor.move */ +export const MCMS_MCMS_EXECUTOR_MOVE = `/// This module helps to stage large mcms::execute invocations, that cannot be done in a single +/// transaction due to the transaction size limit. +module mcms::mcms_executor { + use std::signer; + use std::string::String; + + use mcms::mcms; + + struct PendingExecute has key, store { + data: vector, + proofs: vector> + } + + public entry fun stage_data( + caller: &signer, data_chunk: vector, partial_proofs: vector> + ) acquires PendingExecute { + let caller_address = signer::address_of(caller); + if (!exists(caller_address)) { + move_to( + caller, + PendingExecute { data: vector[], proofs: vector[] } + ); + }; + let pending_execute = borrow_global_mut(caller_address); + if (!data_chunk.is_empty()) { + pending_execute.data.append(data_chunk); + }; + if (!partial_proofs.is_empty()) { + pending_execute.proofs.append(partial_proofs); + }; + } + + public entry fun stage_data_and_execute( + caller: &signer, + role: u8, + chain_id: u256, + multisig: address, + nonce: u64, + to: address, + module_name: String, + function: String, + data_chunk: vector, + partial_proofs: vector> + ) acquires PendingExecute { + if (!exists(signer::address_of(caller))) { + move_to( + caller, + PendingExecute { data: vector[], proofs: vector[] } + ); + }; + let PendingExecute { data, proofs } = + move_from(signer::address_of(caller)); + if (!data_chunk.is_empty()) { + data.append(data_chunk); + }; + if (!partial_proofs.is_empty()) { + proofs.append(partial_proofs); + }; + mcms::execute( + role, + chain_id, + multisig, + nonce, + to, + module_name, + function, + data, + proofs + ); + } + + public entry fun clear_staged_data(caller: &signer) acquires PendingExecute { + let PendingExecute { data: _, proofs: _ } = + move_from(signer::address_of(caller)); + } +} +` + +/** sources/mcms_registry.move */ +export const MCMS_MCMS_REGISTRY_MOVE = `/// This module handles registration and management of code object owners and callbacks. +module mcms::mcms_registry { + use std::account::{Self, SignerCapability}; + use std::bcs; + use std::code::PackageRegistry; + use std::dispatchable_fungible_asset; + use std::error; + use std::event; + use std::fungible_asset::{Self, Metadata}; + use std::function_info::{Self, FunctionInfo}; + use std::object::{Self, ExtendRef, Object}; + use std::option; + use std::signer; + use std::big_ordered_map::{Self, BigOrderedMap}; + use std::string::{Self, String}; + use std::type_info::{Self, TypeInfo}; + + use mcms::mcms_account; + + friend mcms::mcms; + friend mcms::mcms_deployer; + + const EXISTING_OBJECT_REGISTRATION_SEED: vector = b"CHAINLINK_MCMS_EXISTING_OBJECT_REGISTRATION"; + const NEW_OBJECT_REGISTRATION_SEED: vector = b"CHAINLINK_MCMS_NEW_OBJECT_REGISTRATION"; + const DISPATCH_OBJECT_SEED: vector = b"CHAINLINK_MCMS_DISPATCH_OBJECT"; + + // https://github.com/aptos-labs/aptos-core/blob/7fc73792e9db11462c9a42038c4a9eb41cc00192/aptos-move/framework/aptos-framework/sources/object_code_deployment.move#L53 + const OBJECT_CODE_DEPLOYMENT_DOMAIN_SEPARATOR: vector = b"aptos_framework::object_code_deployment"; + + struct RegistryState has key { + // preregistered code object and/or registered callback address -> owner/signer address + registered_addresses: BigOrderedMap + } + + struct OwnerRegistration has key { + owner_seed: vector, + owner_cap: SignerCapability, + is_preregistered: bool, + + // module name -> registered module + callback_modules: BigOrderedMap, RegisteredModule> + } + + struct OwnerTransfers has key { + // object address -> pending transfer + pending_transfers: BigOrderedMap + } + + struct RegisteredModule has store, drop { + callback_function_info: FunctionInfo, + proof_type_info: TypeInfo, + dispatch_metadata: Object, + dispatch_extend_ref: ExtendRef + } + + struct PendingCodeObjectTransfer has store, drop { + to: address, + accepted: bool + } + + struct ExecutingCallbackParams has key { + expected_type_info: TypeInfo, + function: String, + data: vector + } + + #[event] + struct EntrypointRegistered has store, drop { + owner_address: address, + account_address: address, + module_name: String + } + + #[event] + struct CodeObjectTransferRequested has store, drop { + object_address: address, + mcms_owner_address: address, + new_owner_address: address + } + + #[event] + struct CodeObjectTransferAccepted has store, drop { + object_address: address, + mcms_owner_address: address, + new_owner_address: address + } + + #[event] + struct CodeObjectTransferred has store, drop { + object_address: address, + mcms_owner_address: address, + new_owner_address: address + } + + #[event] + struct OwnerCreatedForPreexistingObject has store, drop { + owner_address: address, + object_address: address + } + + #[event] + struct OwnerCreatedForNewObject has store, drop { + owner_address: address, + expected_object_address: address + } + + #[event] + struct OwnerCreatedForEntrypoint has store, drop { + owner_address: address, + account_or_object_address: address + } + + const E_CALLBACK_PARAMS_ALREADY_EXISTS: u64 = 1; + const E_MISSING_CALLBACK_PARAMS: u64 = 2; + const E_WRONG_PROOF_TYPE: u64 = 3; + const E_CALLBACK_PARAMS_NOT_CONSUMED: u64 = 4; + const E_PROOF_NOT_AT_ACCOUNT_ADDRESS: u64 = 5; + const E_PROOF_NOT_IN_MODULE: u64 = 6; + const E_MODULE_ALREADY_REGISTERED: u64 = 7; + const E_EMPTY_MODULE_NAME: u64 = 8; + const E_MODULE_NAME_TOO_LONG: u64 = 9; + const E_ADDRESS_NOT_REGISTERED: u64 = 10; + const E_INVALID_CODE_OBJECT: u64 = 11; + const E_OWNER_ALREADY_REGISTERED: u64 = 12; + const E_NOT_CODE_OBJECT_OWNER: u64 = 13; + const E_UNGATED_TRANSFER_DISABLED: u64 = 14; + const E_NO_PENDING_TRANSFER: u64 = 15; + const E_TRANSFER_ALREADY_ACCEPTED: u64 = 16; + const E_NEW_OWNER_MISMATCH: u64 = 17; + const E_TRANSFER_NOT_ACCEPTED: u64 = 18; + const E_NOT_PROPOSED_OWNER: u64 = 19; + const E_MODULE_NOT_REGISTERED: u64 = 20; + + fun init_module(publisher: &signer) { + move_to( + publisher, + RegistryState { + registered_addresses: big_ordered_map::new_with_config(0, 0, false) + } + ); + } + + #[view] + /// Returns the resource address for a new code object owner using the provided seed. + public fun get_new_code_object_owner_address( + new_owner_seed: vector + ): address { + let owner_seed = NEW_OBJECT_REGISTRATION_SEED; + owner_seed.append(new_owner_seed); + account::create_resource_address(&@mcms, owner_seed) + } + + #[view] + /// Computes and returns the new code object's address using the new_owner_seed. + public fun get_new_code_object_address(new_owner_seed: vector): address { + let object_owner_address = get_new_code_object_owner_address(new_owner_seed); + let object_code_deployment_seed = + bcs::to_bytes(&OBJECT_CODE_DEPLOYMENT_DOMAIN_SEPARATOR); + object_code_deployment_seed.append(bcs::to_bytes(&1u64)); + object::create_object_address( + &object_owner_address, object_code_deployment_seed + ) + } + + #[view] + /// Derives the resource address for an preexisting code object's owner using the given object_address. + public fun get_preexisting_code_object_owner_address( + object_address: address + ): address { + let owner_seed = EXISTING_OBJECT_REGISTRATION_SEED; + owner_seed.append(bcs::to_bytes(&object_address)); + account::create_resource_address(&@mcms, owner_seed) + } + + #[view] + /// Returns the registered owner address for a given account address. The account address + /// can be either a code object address or a callback address. + /// Aborts if the address is not registered. + public fun get_registered_owner_address( + account_address: address + ): address acquires RegistryState { + let state = borrow_state(); + assert!( + state.registered_addresses.contains(&account_address), + error::invalid_argument(E_ADDRESS_NOT_REGISTERED) + ); + *state.registered_addresses.borrow(&account_address) + } + + #[view] + /// Returns true if the given address is a code object and is owned by MCMS. + /// Aborts if the address is not a valid code object. + public fun is_owned_code_object(object_address: address): bool acquires RegistryState { + assert!( + object::object_exists(object_address), + error::invalid_argument(E_INVALID_CODE_OBJECT) + ); + let code_object = object::address_to_object(object_address); + + let owner_address = get_registered_owner_address(object_address); + object::owner(code_object) == owner_address + } + + /// Imports a code object (ie. managed by 0x1::code_object_deployment) that was not deployed + /// using mcms_deployer, and has not registered for a callback, to be owned by MCMS. + /// If either of these conditions has already occurred, then an object owner was already + /// created and there is no need to call this function - however, the below flow can still + /// be followed to transfer ownership to MCMS, omitting the final step. + /// + /// Ownership transfer flow: + /// - if it was deployed using mcms_deployer, call get_new_code_object_owner_address() with + /// the same new_owner_seed used when publishing to get the MCMS object owner address. + /// - otherwise, call get_preexisting_code_object_owner_address() to get the MCMS object owner + /// address. + /// - call 0x1::object::transfer, transfering ownership to the MCMS object owner address. + /// - call create_owner_for_preexisting_code_object() with the object address. + /// + /// After these steps, MCMS will be the code object owner, and will be able to deploy and upgrade + /// the code object using proposals with mcms_deployer ops. + public entry fun create_owner_for_preexisting_code_object( + caller: &signer, object_address: address + ) acquires RegistryState { + mcms_account::assert_is_owner(caller); + assert!( + object::object_exists(object_address), + error::invalid_argument(E_INVALID_CODE_OBJECT) + ); + + let state = borrow_state_mut(); + let owner_signer = + &create_owner_for_preexisting_code_object_internal(state, object_address); + + event::emit( + OwnerCreatedForPreexistingObject { + owner_address: signer::address_of(owner_signer), + object_address + } + ); + } + + /// Transfers ownership of a code object to a new owner. Note that this does not unregister + /// the entrypoint or remove the previous owner from the registry. + /// + /// Due to Aptos's security model requiring the original owner's signer for 0x1::object::transfer, + /// we use the same 3-step ownership transfer flow as our ownable.move implementation: + /// + /// 1. MCMS owner calls transfer_code_object with the new owner's address + /// 2. Pending owner calls accept_code_object to confirm the transfer + /// 3. MCMS owner calls execute_code_object_transfer to complete the transfer + public entry fun transfer_code_object( + caller: &signer, object_address: address, new_owner_address: address + ) acquires RegistryState, OwnerRegistration, OwnerTransfers { + mcms_account::assert_is_owner(caller); + + assert!( + object::object_exists(object_address), + error::invalid_argument(E_INVALID_CODE_OBJECT) + ); + + let code_object = object::address_to_object(object_address); + + // this could occur if the code object was pre-existing and the original creator kept the TransferRef, + // transferred the object to MCMS by generating a LinearTransferRef. + assert!( + object::ungated_transfer_allowed(code_object), + error::permission_denied(E_UNGATED_TRANSFER_DISABLED) + ); + + let state = borrow_state(); + assert!( + state.registered_addresses.contains(&object_address), + error::invalid_argument(E_ADDRESS_NOT_REGISTERED) + ); + + let owner_address = *state.registered_addresses.borrow(&object_address); + // this could occur if the code object has already been transferred away either through this process + // or through a TransferRef if the object was pre-existing. + assert!( + object::owner(code_object) == owner_address, + error::invalid_state(E_NOT_CODE_OBJECT_OWNER) + ); + + if (!exists(owner_address)) { + let owner_registration = borrow_owner_registration(owner_address); + let owner_signer = + &account::create_signer_with_capability(&owner_registration.owner_cap); + move_to( + owner_signer, + OwnerTransfers { + pending_transfers: big_ordered_map::new_with_config(0, 0, false) + } + ); + }; + + let pending_transfers = borrow_global_mut(owner_address); + + // override any pending transfers if a new transfer has been requested. + pending_transfers.pending_transfers.upsert( + object_address, + PendingCodeObjectTransfer { to: new_owner_address, accepted: false } + ); + + event::emit( + CodeObjectTransferRequested { + object_address, + mcms_owner_address: owner_address, + new_owner_address + } + ); + } + + public entry fun accept_code_object( + caller: &signer, object_address: address + ) acquires RegistryState, OwnerTransfers { + assert!( + object::object_exists(object_address), + error::invalid_argument(E_INVALID_CODE_OBJECT) + ); + + let code_object = object::address_to_object(object_address); + + let state = borrow_state(); + assert!( + state.registered_addresses.contains(&object_address), + error::invalid_argument(E_ADDRESS_NOT_REGISTERED) + ); + + let owner_address = *state.registered_addresses.borrow(&object_address); + // these conditions could occur if the code object was pre-existing and the owner transferred object ownership or disabled + // ungated transfers using the TransferRef after this transfer process was initiated. + assert!( + object::owner(code_object) == owner_address, + error::invalid_state(E_NOT_CODE_OBJECT_OWNER) + ); + assert!( + object::ungated_transfer_allowed(code_object), + error::permission_denied(E_UNGATED_TRANSFER_DISABLED) + ); + + assert!( + exists(owner_address), + error::invalid_state(E_NO_PENDING_TRANSFER) + ); + let pending_transfers = borrow_global_mut(owner_address); + + assert!( + pending_transfers.pending_transfers.contains(&object_address), + error::invalid_state(E_NO_PENDING_TRANSFER) + ); + + let pending_transfer = + pending_transfers.pending_transfers.borrow_mut(&object_address); + assert!( + pending_transfer.to == signer::address_of(caller), + error::permission_denied(E_NOT_PROPOSED_OWNER) + ); + assert!( + !pending_transfer.accepted, + error::invalid_state(E_TRANSFER_ALREADY_ACCEPTED) + ); + + pending_transfer.accepted = true; + + event::emit( + CodeObjectTransferAccepted { + object_address, + mcms_owner_address: owner_address, + new_owner_address: pending_transfer.to + } + ); + } + + public entry fun execute_code_object_transfer( + caller: &signer, object_address: address, new_owner_address: address + ) acquires RegistryState, OwnerRegistration, OwnerTransfers { + mcms_account::assert_is_owner(caller); + + assert!( + object::object_exists(object_address), + error::invalid_argument(E_INVALID_CODE_OBJECT) + ); + + let code_object = object::address_to_object(object_address); + + let state = borrow_state(); + assert!( + state.registered_addresses.contains(&object_address), + error::invalid_argument(E_ADDRESS_NOT_REGISTERED) + ); + + let owner_address = *state.registered_addresses.borrow(&object_address); + // these conditions could occur if the code object was pre-existing and the owner transferred object ownership or disabled + // ungated transfers using the TransferRef after this transfer process was initiated. + assert!( + object::owner(code_object) == owner_address, + error::invalid_state(E_NOT_CODE_OBJECT_OWNER) + ); + assert!( + object::ungated_transfer_allowed(code_object), + error::permission_denied(E_UNGATED_TRANSFER_DISABLED) + ); + + assert!( + exists(owner_address), + error::invalid_state(E_NO_PENDING_TRANSFER) + ); + let pending_transfers = borrow_global_mut(owner_address); + + assert!( + pending_transfers.pending_transfers.contains(&object_address), + error::invalid_state(E_NO_PENDING_TRANSFER) + ); + let pending_transfer = + pending_transfers.pending_transfers.borrow_mut(&object_address); + assert!( + pending_transfer.to == new_owner_address, + error::invalid_state(E_NEW_OWNER_MISMATCH) + ); + assert!( + pending_transfer.accepted, + error::invalid_state(E_TRANSFER_NOT_ACCEPTED) + ); + + let owner_registration = borrow_owner_registration(owner_address); + let owner_signer = + &account::create_signer_with_capability(&owner_registration.owner_cap); + + object::transfer(owner_signer, code_object, new_owner_address); + + event::emit( + CodeObjectTransferred { + object_address, + mcms_owner_address: owner_address, + new_owner_address + } + ); + + pending_transfers.pending_transfers.remove(&object_address); + if (pending_transfers.pending_transfers.is_empty()) { + let OwnerTransfers { pending_transfers } = + move_from(owner_address); + pending_transfers.destroy_empty(); + } + } + + public(friend) fun create_owner_for_new_code_object( + new_owner_seed: vector + ): signer acquires RegistryState { + let owner_seed = NEW_OBJECT_REGISTRATION_SEED; + owner_seed.append(new_owner_seed); + let new_code_object_address = get_new_code_object_address(new_owner_seed); + let owner_signer = + create_owner_internal( + borrow_state_mut(), + owner_seed, + new_code_object_address, + true + ); + + event::emit( + OwnerCreatedForNewObject { + owner_address: signer::address_of(&owner_signer), + expected_object_address: new_code_object_address + } + ); + + owner_signer + } + + public(friend) fun get_signer_for_code_object_upgrade( + object_address: address + ): signer acquires RegistryState, OwnerRegistration { + assert!( + object::object_exists(object_address), + error::invalid_argument(E_INVALID_CODE_OBJECT) + ); + + let state = borrow_state(); + assert!( + state.registered_addresses.contains(&object_address), + error::invalid_argument(E_ADDRESS_NOT_REGISTERED) + ); + let owner_address = *state.registered_addresses.borrow(&object_address); + + let owner_registration = borrow_owner_registration(owner_address); + account::create_signer_with_capability(&owner_registration.owner_cap) + } + + inline fun create_owner_for_preexisting_code_object_internal( + state: &mut RegistryState, object_address: address + ): signer { + let owner_seed = EXISTING_OBJECT_REGISTRATION_SEED; + owner_seed.append(bcs::to_bytes(&object_address)); + create_owner_internal(state, owner_seed, object_address, false) + } + + inline fun create_owner_internal( + state: &mut RegistryState, + owner_seed: vector, + code_object_address: address, + is_preregistered: bool + ): signer { + let mcms_signer = &mcms_account::get_signer(); + + let owner_address = account::create_resource_address(&@mcms, owner_seed); + assert!( + !exists(owner_address), + error::invalid_state(E_OWNER_ALREADY_REGISTERED) + ); + + let (owner_signer, owner_cap) = + account::create_resource_account(mcms_signer, owner_seed); + move_to( + &owner_signer, + OwnerRegistration { + owner_seed, + owner_cap, + is_preregistered, + callback_modules: big_ordered_map::new_with_config(0, 0, false) + } + ); + + state.registered_addresses.add( + code_object_address, signer::address_of(&owner_signer) + ); + owner_signer + } + + /// Registers a callback to mcms_entrypoint to enable dynamic dispatch. + public fun register_entrypoint( + account: &signer, module_name: String, _proof: T + ): address acquires RegistryState, OwnerRegistration { + let account_address = signer::address_of(account); + let account_address_bytes = bcs::to_bytes(&account_address); + + let module_name_bytes = *module_name.bytes(); + let module_name_len = module_name_bytes.length(); + assert!(module_name_len > 0, error::invalid_argument(E_EMPTY_MODULE_NAME)); + assert!(module_name_len <= 64, error::invalid_argument(E_MODULE_NAME_TOO_LONG)); + + let state = borrow_state_mut(); + + let owner_address = + if (!state.registered_addresses.contains(&account_address)) { + let owner_signer = + create_owner_for_preexisting_code_object_internal( + state, account_address + ); + + let owner_address = signer::address_of(&owner_signer); + + event::emit( + OwnerCreatedForEntrypoint { + owner_address, + account_or_object_address: account_address + } + ); + + owner_address + } else { + *state.registered_addresses.borrow(&account_address) + }; + + let registration = borrow_owner_registration_mut(owner_address); + + assert!( + !registration.callback_modules.contains(&module_name_bytes), + error::invalid_argument(E_MODULE_ALREADY_REGISTERED) + ); + + let proof_type_info = type_info::type_of(); + + assert!( + proof_type_info.account_address() == account_address, + error::invalid_argument(E_PROOF_NOT_AT_ACCOUNT_ADDRESS) + ); + + let owner_signer = + account::create_signer_with_capability(®istration.owner_cap); + + let object_seed = DISPATCH_OBJECT_SEED; + object_seed.append(account_address_bytes); + object_seed.append(module_name_bytes); + + let dispatch_constructor_ref = + object::create_named_object(&owner_signer, object_seed); + let dispatch_extend_ref = object::generate_extend_ref(&dispatch_constructor_ref); + let dispatch_metadata = + fungible_asset::add_fungibility( + &dispatch_constructor_ref, + option::none(), + string::utf8(b"mcms"), + string::utf8(b"mcms"), + 0, + string::utf8(b""), + string::utf8(b"") + ); + + let callback_function_info = + function_info::new_function_info( + account, + string::utf8(proof_type_info.module_name()), + string::utf8(b"mcms_entrypoint") + ); + + dispatchable_fungible_asset::register_derive_supply_dispatch_function( + &dispatch_constructor_ref, option::some(callback_function_info) + ); + + let registered_module = RegisteredModule { + callback_function_info, + proof_type_info, + dispatch_metadata, + dispatch_extend_ref + }; + + registration.callback_modules.add(module_name_bytes, registered_module); + + event::emit(EntrypointRegistered { owner_address, account_address, module_name }); + + owner_address + } + + public(friend) fun start_dispatch( + callback_address: address, + callback_module_name: String, + callback_function: String, + data: vector + ): Object acquires RegistryState, OwnerRegistration { + let state = borrow_state(); + + assert!( + state.registered_addresses.contains(&callback_address), + error::invalid_argument(E_ADDRESS_NOT_REGISTERED) + ); + + let owner_address = *state.registered_addresses.borrow(&callback_address); + assert!( + !exists(owner_address), + error::invalid_state(E_CALLBACK_PARAMS_ALREADY_EXISTS) + ); + + let registration = borrow_owner_registration(owner_address); + + let callback_module_name_bytes = *callback_module_name.bytes(); + assert!( + registration.callback_modules.contains(&callback_module_name_bytes), + error::invalid_state(E_MODULE_NOT_REGISTERED) + ); + + let registered_module = + registration.callback_modules.borrow(&callback_module_name_bytes); + + let owner_signer = + account::create_signer_with_capability(®istration.owner_cap); + + move_to( + &owner_signer, + ExecutingCallbackParams { + expected_type_info: registered_module.proof_type_info, + function: callback_function, + data + } + ); + + registered_module.dispatch_metadata + } + + public(friend) fun finish_dispatch(callback_address: address) acquires RegistryState { + let state = borrow_state(); + + assert!( + state.registered_addresses.contains(&callback_address), + error::invalid_state(E_ADDRESS_NOT_REGISTERED) + ); + + let owner_address = *state.registered_addresses.borrow(&callback_address); + assert!( + !exists(owner_address), + error::invalid_argument(E_CALLBACK_PARAMS_NOT_CONSUMED) + ); + } + + public fun get_callback_params( + callback_address: address, _proof: T + ): (signer, String, vector) acquires RegistryState, OwnerRegistration, ExecutingCallbackParams { + let state = borrow_state(); + + assert!( + state.registered_addresses.contains(&callback_address), + error::invalid_argument(E_ADDRESS_NOT_REGISTERED) + ); + + let owner_address = *state.registered_addresses.borrow(&callback_address); + assert!( + exists(owner_address), + error::invalid_state(E_MISSING_CALLBACK_PARAMS) + ); + + let ExecutingCallbackParams { expected_type_info, function, data } = + move_from(owner_address); + + let proof_type_info = type_info::type_of(); + assert!( + expected_type_info == proof_type_info, + error::invalid_argument(E_WRONG_PROOF_TYPE) + ); + + let registration = borrow_owner_registration(owner_address); + let owner_signer = + account::create_signer_with_capability(®istration.owner_cap); + + (owner_signer, function, data) + } + + inline fun borrow_state(): &RegistryState { + borrow_global(@mcms) + } + + inline fun borrow_state_mut(): &mut RegistryState { + borrow_global_mut(@mcms) + } + + inline fun borrow_owner_registration(account_address: address): &OwnerRegistration { + assert!( + exists(account_address), + error::invalid_argument(E_ADDRESS_NOT_REGISTERED) + ); + borrow_global(account_address) + } + + inline fun borrow_owner_registration_mut(account_address: address) + : &mut OwnerRegistration { + assert!( + exists(account_address), + error::invalid_argument(E_ADDRESS_NOT_REGISTERED) + ); + borrow_global_mut(account_address) + } + + #[test_only] + public fun init_module_for_testing(publisher: &signer) { + init_module(publisher); + } + + #[test_only] + public fun test_start_dispatch( + callback_address: address, + callback_module_name: String, + callback_function: String, + data: vector + ): Object acquires RegistryState, OwnerRegistration { + start_dispatch( + callback_address, + callback_module_name, + callback_function, + data + ) + } + + #[test_only] + public fun test_finish_dispatch(callback_address: address) acquires RegistryState { + finish_dispatch(callback_address) + } + + #[test_only] + public fun move_from_owner_transfers(owner_address: address) acquires OwnerTransfers { + let OwnerTransfers { pending_transfers } = + move_from(owner_address); + pending_transfers.destroy({ |_dv| {} }); + } +} +` + +/** sources/mcms.move */ +export const MCMS_MCMS_MOVE = `/// This module is the Aptos implementation of Chainlink's MultiChainMultiSig contract. +module mcms::mcms { + use std::aptos_hash::keccak256; + use std::bcs; + use std::event; + use std::signer; + use std::simple_map::{Self, SimpleMap}; + use std::string::{String}; + use aptos_std::smart_table::{Self, SmartTable}; + use aptos_std::smart_vector::{Self, SmartVector}; + use aptos_framework::chain_id; + use aptos_framework::object::{Self, ExtendRef, Object}; + use aptos_framework::timestamp; + use aptos_std::secp256k1; + use mcms::bcs_stream::{Self, BCSStream}; + use mcms::mcms_account; + use mcms::mcms_deployer; + use mcms::mcms_registry; + use mcms::params::{Self}; + + const BYPASSER_ROLE: u8 = 0; + const CANCELLER_ROLE: u8 = 1; + const PROPOSER_ROLE: u8 = 2; + const TIMELOCK_ROLE: u8 = 3; + const MAX_ROLE: u8 = 4; + + const NUM_GROUPS: u64 = 32; + const MAX_NUM_SIGNERS: u64 = 200; + + // equivalent to initializing empty uint8[NUM_GROUPS] in Solidity + const VEC_NUM_GROUPS: vector = vector[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + // keccak256("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA_APTOS") + const MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA: vector = x"a71d47b6c00b64ee21af96a1d424cb2dcbbed12becdcd3b4e6c7fc4c2f80a697"; + + // keccak256("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP_APTOS") + const MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP: vector = x"e5a6d1256b00d7ec22512b6b60a3f4d75c559745d2dbf309f77b8b756caabe14"; + + /// Special timestamp value indicating an operation is done + const DONE_TIMESTAMP: u64 = 1; + + const ZERO_HASH: vector = vector[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + struct MultisigState has key { + bypasser: Object, + canceller: Object, + proposer: Object + } + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + struct Multisig has key { + extend_ref: ExtendRef, + + /// signers is used to easily validate the existence of the signer by its address. We still + /// have signers stored in config in order to easily deactivate them when a new config is set. + signers: SimpleMap, Signer>, + config: Config, + + /// Remember signed hashes that this contract has seen. Each signed hash can only be set once. + seen_signed_hashes: SimpleMap, bool>, + expiring_root_and_op_count: ExpiringRootAndOpCount, + root_metadata: RootMetadata + } + + struct Op has copy, drop { + role: u8, + chain_id: u256, + multisig: address, + nonce: u64, + to: address, + module_name: String, + function_name: String, + data: vector + } + + struct RootMetadata has copy, drop, store { + role: u8, + chain_id: u256, + multisig: address, + pre_op_count: u64, + post_op_count: u64, + override_previous_root: bool + } + + struct Signer has store, copy, drop { + addr: vector, + index: u8, // index of signer in config.signers + group: u8 // 0 <= group < NUM_GROUPS. Each signer can only be in one group. + } + + struct Config has store, copy, drop { + signers: vector, + + // group_quorums[i] stores the quorum for the i-th signer group. Any group with + // group_quorums[i] = 0 is considered disabled. The i-th group is successful if + // it is enabled and at least group_quorums[i] of its children are successful. + group_quorums: vector, + + // group_parents[i] stores the parent group of the i-th signer group. We ensure that the + // groups form a tree structure (where the root/0-th signer group points to itself as + // parent) by enforcing + // - (i != 0) implies (group_parents[i] < i) + // - group_parents[0] == 0 + group_parents: vector + } + + struct ExpiringRootAndOpCount has store, drop { + root: vector, + valid_until: u64, + op_count: u64 + } + + #[event] + struct MultisigStateInitialized has drop, store { + bypasser: Object, + canceller: Object, + proposer: Object + } + + #[event] + struct ConfigSet has drop, store { + role: u8, + config: Config, + is_root_cleared: bool + } + + #[event] + struct NewRoot has drop, store { + role: u8, + root: vector, + valid_until: u64, + metadata: RootMetadata + } + + #[event] + struct OpExecuted has drop, store { + role: u8, + chain_id: u256, + multisig: address, + nonce: u64, + to: address, + module_name: String, + function_name: String, + data: vector + } + + const E_ALREADY_SEEN_HASH: u64 = 1; + const E_POST_OP_COUNT_REACHED: u64 = 2; + const E_WRONG_CHAIN_ID: u64 = 3; + const E_WRONG_MULTISIG: u64 = 4; + const E_ROOT_EXPIRED: u64 = 5; + const E_WRONG_NONCE: u64 = 6; + const E_VALID_UNTIL_EXPIRED: u64 = 7; + const E_INVALID_SIGNER: u64 = 8; + const E_MISSING_CONFIG: u64 = 9; + const E_INSUFFICIENT_SIGNERS: u64 = 10; + const E_PROOF_CANNOT_BE_VERIFIED: u64 = 11; + const E_PENDING_OPS: u64 = 12; + const E_WRONG_PRE_OP_COUNT: u64 = 13; + const E_WRONG_POST_OP_COUNT: u64 = 14; + const E_INVALID_NUM_SIGNERS: u64 = 15; + const E_SIGNER_GROUPS_LEN_MISMATCH: u64 = 16; + const E_INVALID_GROUP_QUORUM_LEN: u64 = 17; + const E_INVALID_GROUP_PARENTS_LEN: u64 = 18; + const E_OUT_OF_BOUNDS_GROUP: u64 = 19; + const E_GROUP_TREE_NOT_WELL_FORMED: u64 = 20; + const E_SIGNER_IN_DISABLED_GROUP: u64 = 21; + const E_OUT_OF_BOUNDS_GROUP_QUORUM: u64 = 22; + const E_SIGNER_ADDR_MUST_BE_INCREASING: u64 = 23; + const E_INVALID_SIGNER_ADDR_LEN: u64 = 24; + const E_UNKNOWN_MCMS_MODULE_FUNCTION: u64 = 25; + const E_UNKNOWN_FRAMEWORK_MODULE_FUNCTION: u64 = 26; + const E_UNKNOWN_FRAMEWORK_MODULE: u64 = 27; + const E_SELF_CALL_ROLE_MISMATCH: u64 = 28; + const E_NOT_BYPASSER_ROLE: u64 = 29; + const E_INVALID_ROLE: u64 = 30; + const E_NOT_AUTHORIZED_ROLE: u64 = 31; + const E_NOT_AUTHORIZED: u64 = 32; + const E_OPERATION_ALREADY_SCHEDULED: u64 = 33; + const E_INSUFFICIENT_DELAY: u64 = 34; + const E_OPERATION_NOT_READY: u64 = 35; + const E_MISSING_DEPENDENCY: u64 = 36; + const E_OPERATION_CANNOT_BE_CANCELLED: u64 = 37; + const E_FUNCTION_BLOCKED: u64 = 38; + const E_INVALID_INDEX: u64 = 39; + const E_UNKNOWN_MCMS_ACCOUNT_MODULE_FUNCTION: u64 = 40; + const E_UNKNOWN_MCMS_DEPLOYER_MODULE_FUNCTION: u64 = 41; + const E_UNKNOWN_MCMS_REGISTRY_MODULE_FUNCTION: u64 = 42; + const E_INVALID_PARAMETERS: u64 = 43; + const E_INVALID_SIGNATURE_LEN: u64 = 44; + const E_INVALID_V_SIGNATURE: u64 = 45; + const E_FAILED_ECDSA_RECOVER: u64 = 46; + const E_INVALID_MODULE_NAME: u64 = 47; + const E_UNKNOWN_MCMS_TIMELOCK_FUNCTION: u64 = 48; + const E_INVALID_ROOT_LEN: u64 = 49; + const E_NOT_CANCELLER_ROLE: u64 = 50; + const E_NOT_TIMELOCK_ROLE: u64 = 51; + const E_UNKNOWN_MCMS_MODULE: u64 = 52; + + fun init_module(publisher: &signer) { + let bypasser = create_multisig(publisher, BYPASSER_ROLE); + let canceller = create_multisig(publisher, CANCELLER_ROLE); + let proposer = create_multisig(publisher, PROPOSER_ROLE); + + move_to( + publisher, + MultisigState { bypasser, canceller, proposer } + ); + + event::emit(MultisigStateInitialized { bypasser, canceller, proposer }); + + move_to( + publisher, + Timelock { + min_delay: 0, + timestamps: smart_table::new(), + blocked_functions: smart_vector::new() + } + ); + + event::emit(TimelockInitialized { min_delay: 0 }); + } + + inline fun create_multisig(publisher: &signer, role: u8): Object { + let constructor_ref = &object::create_object(signer::address_of(publisher)); + let object_signer = object::generate_signer(constructor_ref); + let extend_ref = object::generate_extend_ref(constructor_ref); + + move_to( + &object_signer, + Multisig { + extend_ref, + signers: simple_map::new(), + config: Config { + signers: vector[], + group_quorums: VEC_NUM_GROUPS, + group_parents: VEC_NUM_GROUPS + }, + seen_signed_hashes: simple_map::new(), + expiring_root_and_op_count: ExpiringRootAndOpCount { + root: vector[], + valid_until: 0, + op_count: 0 + }, + root_metadata: RootMetadata { + role, + chain_id: 0, + multisig: signer::address_of(&object_signer), + pre_op_count: 0, + post_op_count: 0, + override_previous_root: false + } + } + ); + + object::object_from_constructor_ref(constructor_ref) + } + + /// @notice set_root Sets a new expiring root. + /// + /// @param root is the new expiring root. + /// @param valid_until is the time by which root is valid + /// @param chain_id is the chain id of the chain on which the root is valid + /// @param multisig is the address of the multisig to set the root for + /// @param pre_op_count is the number of operations that have been executed before this root was set + /// @param post_op_count is the number of operations that have been executed after this root was set + /// @param override_previous_root is a boolean that indicates whether to override the previous root + /// @param metadata_proof is the MerkleProof of inclusion of the metadata in the Merkle tree. + /// @param signatures the ECDSA signatures on (root, valid_until). + /// + /// @dev the message (root, valid_until) should be signed by a sufficient set of signers. + /// This signature authenticates also the metadata. + /// + /// @dev this method can be executed by anyone who has the root and valid signatures. + /// as we validate the correctness of signatures, this imposes no risk. + public entry fun set_root( + role: u8, + root: vector, + valid_until: u64, + chain_id: u256, + multisig_addr: address, + pre_op_count: u64, + post_op_count: u64, + override_previous_root: bool, + metadata_proof: vector>, + signatures: vector> + ) acquires Multisig, MultisigState { + assert!(is_valid_role(role), E_INVALID_ROLE); + + let metadata = RootMetadata { + role, + chain_id, + multisig: multisig_addr, + pre_op_count, + post_op_count, + override_previous_root + }; + + let signed_hash = compute_eth_message_hash(root, valid_until); + + // Validate that \`multisig\` is a registered multisig for \`role\`. + let multisig = borrow_multisig_mut(multisig_object(role)); + + assert!( + !multisig.seen_signed_hashes.contains_key(&signed_hash), + E_ALREADY_SEEN_HASH + ); + assert!(timestamp::now_seconds() <= valid_until, E_VALID_UNTIL_EXPIRED); + assert!(metadata.chain_id == (chain_id::get() as u256), E_WRONG_CHAIN_ID); + assert!(metadata.multisig == @mcms, E_WRONG_MULTISIG); + + let op_count = multisig.expiring_root_and_op_count.op_count; + assert!( + override_previous_root || op_count == multisig.root_metadata.post_op_count, + E_PENDING_OPS + ); + + assert!(op_count == metadata.pre_op_count, E_WRONG_PRE_OP_COUNT); + assert!(metadata.pre_op_count <= metadata.post_op_count, E_WRONG_POST_OP_COUNT); + + let metadata_leaf_hash = hash_metadata_leaf(metadata); + assert!( + verify_merkle_proof(metadata_proof, root, metadata_leaf_hash), + E_PROOF_CANNOT_BE_VERIFIED + ); + + let prev_address = vector[]; + let group_vote_counts: vector = vector[]; + params::right_pad_vec(&mut group_vote_counts, NUM_GROUPS); + + let signatures_len = signatures.length(); + for (i in 0..signatures_len) { + let signature = signatures[i]; + let signer_addr = ecdsa_recover_evm_addr(signed_hash, signature); + // the off-chain system is required to sort the signatures by the + // signer address in an increasing order + if (i > 0) { + assert!( + params::vector_u8_gt(&signer_addr, &prev_address), + E_SIGNER_ADDR_MUST_BE_INCREASING + ); + }; + prev_address = signer_addr; + + assert!(multisig.signers.contains_key(&signer_addr), E_INVALID_SIGNER); + let signer = *multisig.signers.borrow(&signer_addr); + + // check group quorums + let group: u8 = signer.group; + while (true) { + let group_vote_count = group_vote_counts.borrow_mut((group as u64)); + *group_vote_count += 1; + + let quorum = multisig.config.group_quorums.borrow((group as u64)); + if (*group_vote_count != *quorum) { + // bail out unless we just hit the quorum. we only hit each quorum once, + // so we never move on to the parent of a group more than once. + break + }; + + if (group == 0) { + // root group reached + break + }; + + // group quorum reached, restart loop and check parent group + group = multisig.config.group_parents[(group as u64)]; + }; + }; + + // the group at the root of the tree (with index 0) determines whether the vote passed, + // we cannot proceed if it isn't configured with a valid (non-zero) quorum + let root_group_quorum = multisig.config.group_quorums[0]; + assert!(root_group_quorum != 0, E_MISSING_CONFIG); + + // check root group reached quorum + let root_group_vote_count = group_vote_counts[0]; + assert!(root_group_vote_count >= root_group_quorum, E_INSUFFICIENT_SIGNERS); + + multisig.seen_signed_hashes.add(signed_hash, true); + multisig.expiring_root_and_op_count = ExpiringRootAndOpCount { + root, + valid_until, + op_count: metadata.pre_op_count + }; + multisig.root_metadata = metadata; + + event::emit( + NewRoot { + role, + root, + valid_until, + metadata: RootMetadata { + role, + chain_id, + multisig: multisig_addr, + pre_op_count: metadata.pre_op_count, + post_op_count: metadata.post_op_count, + override_previous_root: metadata.override_previous_root + } + } + ); + } + + inline fun ecdsa_recover_evm_addr( + eth_signed_message_hash: vector, signature: vector + ): vector { + // ensure signature has correct length - (r,s,v) concatenated = 65 bytes + assert!(signature.length() == 65, E_INVALID_SIGNATURE_LEN); + // extract v from signature + let v = signature.pop_back(); + // convert 64 byte signature into ECDSASignature struct + let sig = secp256k1::ecdsa_signature_from_bytes(signature); + // Aptos uses the rust libsecp256k1 parse() under the hood which has a different numbering scheme + // see: https://docs.rs/libsecp256k1/latest/libsecp256k1/struct.RecoveryId.html#method.parse_rpc + assert!(v >= 27 && v < 27 + 4, E_INVALID_V_SIGNATURE); + let v = v - 27; + + // retrieve signer public key + let public_key = secp256k1::ecdsa_recover(eth_signed_message_hash, v, &sig); + assert!(public_key.is_some(), E_FAILED_ECDSA_RECOVER); + + // return last 20 bytes of hashed public key as the recovered ethereum address + let public_key_bytes = + secp256k1::ecdsa_raw_public_key_to_bytes(&public_key.extract()); + keccak256(public_key_bytes).trim(12) // trims publicKeyBytes to 12 bytes, returns trimmed last 20 bytes + } + + /// Execute an operation after verifying its inclusion in the merkle tree + public entry fun execute( + role: u8, + chain_id: u256, + multisig_addr: address, + nonce: u64, + to: address, + module_name: String, + function_name: String, + data: vector, + proof: vector> + ) acquires Multisig, MultisigState, Timelock { + assert!(is_valid_role(role), E_INVALID_ROLE); + + let op = Op { + role, + chain_id, + multisig: multisig_addr, + nonce, + to, + module_name, + function_name, + data + }; + let multisig = borrow_multisig_mut(multisig_object(role)); + + assert!( + multisig.root_metadata.post_op_count + > multisig.expiring_root_and_op_count.op_count, + E_POST_OP_COUNT_REACHED + ); + assert!(chain_id == (chain_id::get() as u256), E_WRONG_CHAIN_ID); + assert!( + timestamp::now_seconds() <= multisig.expiring_root_and_op_count.valid_until, + E_ROOT_EXPIRED + ); + assert!(op.multisig == @mcms, E_WRONG_MULTISIG); + assert!(nonce == multisig.expiring_root_and_op_count.op_count, E_WRONG_NONCE); + + // computes keccak256(abi.encode(MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP, op)) + let hashed_leaf = hash_op_leaf(MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP, op); + assert!( + verify_merkle_proof( + proof, multisig.expiring_root_and_op_count.root, hashed_leaf + ), + E_PROOF_CANNOT_BE_VERIFIED + ); + + multisig.expiring_root_and_op_count.op_count += 1; + + // Only allow dispatching to timelock functions + assert!( + op.to == @mcms && *op.module_name.bytes() == b"mcms", + E_INVALID_MODULE_NAME + ); + + dispatch_to_timelock(role, op.function_name, op.data); + + event::emit( + OpExecuted { + role, + chain_id, + multisig: multisig_addr, + nonce, + to, + module_name, + function_name, + data + } + ); + } + + /// Only callable from \`execute\`, the role that was validated is passed down to the timelock functions + inline fun dispatch_to_timelock( + role: u8, function_name: String, data: vector + ) { + let function_name_bytes = *function_name.bytes(); + let stream = bcs_stream::new(data); + + if (function_name_bytes == b"timelock_schedule_batch") { + dispatch_timelock_schedule_batch(role, &mut stream) + } else if (function_name_bytes == b"timelock_bypasser_execute_batch") { + dispatch_timelock_bypasser_execute_batch(role, &mut stream) + } else if (function_name_bytes == b"timelock_execute_batch") { + dispatch_timelock_execute_batch(&mut stream) + } else if (function_name_bytes == b"timelock_cancel") { + dispatch_timelock_cancel(role, &mut stream) + } else if (function_name_bytes == b"timelock_update_min_delay") { + dispatch_timelock_update_min_delay(role, &mut stream) + } else if (function_name_bytes == b"timelock_block_function") { + dispatch_timelock_block_function(role, &mut stream) + } else if (function_name_bytes == b"timelock_unblock_function") { + dispatch_timelock_unblock_function(role, &mut stream) + } else { + abort E_UNKNOWN_MCMS_TIMELOCK_FUNCTION + } + } + + /// \`dispatch_timelock_\` functions should only be called from dispatch functions + inline fun dispatch_timelock_schedule_batch( + role: u8, stream: &mut BCSStream + ) { + assert!( + role == PROPOSER_ROLE || role == TIMELOCK_ROLE, E_NOT_AUTHORIZED_ROLE + ); + + let targets = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_address(stream) + ); + let module_names = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_string(stream) + ); + let function_names = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_string(stream) + ); + let datas = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + let predecessor = bcs_stream::deserialize_vector_u8(stream); + let salt = bcs_stream::deserialize_vector_u8(stream); + let delay = bcs_stream::deserialize_u64(stream); + bcs_stream::assert_is_consumed(stream); + + timelock_schedule_batch( + targets, + module_names, + function_names, + datas, + predecessor, + salt, + delay + ) + } + + inline fun dispatch_timelock_bypasser_execute_batch( + role: u8, stream: &mut BCSStream + ) { + assert!( + role == BYPASSER_ROLE || role == TIMELOCK_ROLE, E_NOT_AUTHORIZED_ROLE + ); + + let targets = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_address(stream) + ); + let module_names = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_string(stream) + ); + let function_names = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_string(stream) + ); + let datas = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + bcs_stream::assert_is_consumed(stream); + + timelock_bypasser_execute_batch(targets, module_names, function_names, datas) + } + + inline fun dispatch_timelock_execute_batch(stream: &mut BCSStream) { + let targets = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_address(stream) + ); + let module_names = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_string(stream) + ); + let function_names = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_string(stream) + ); + let datas = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + let predecessor = bcs_stream::deserialize_vector_u8(stream); + let salt = bcs_stream::deserialize_vector_u8(stream); + bcs_stream::assert_is_consumed(stream); + + timelock_execute_batch( + targets, + module_names, + function_names, + datas, + predecessor, + salt + ) + } + + inline fun dispatch_timelock_cancel(role: u8, stream: &mut BCSStream) { + assert!( + role == CANCELLER_ROLE || role == TIMELOCK_ROLE, E_NOT_AUTHORIZED_ROLE + ); + + let id = bcs_stream::deserialize_vector_u8(stream); + bcs_stream::assert_is_consumed(stream); + + timelock_cancel(id) + } + + inline fun dispatch_timelock_update_min_delay( + role: u8, stream: &mut BCSStream + ) { + assert!(role == TIMELOCK_ROLE, E_NOT_TIMELOCK_ROLE); + + let new_min_delay = bcs_stream::deserialize_u64(stream); + bcs_stream::assert_is_consumed(stream); + + timelock_update_min_delay(new_min_delay) + } + + inline fun dispatch_timelock_block_function( + role: u8, stream: &mut BCSStream + ) { + assert!(role == TIMELOCK_ROLE, E_NOT_TIMELOCK_ROLE); + + let target = bcs_stream::deserialize_address(stream); + let module_name = bcs_stream::deserialize_string(stream); + let function_name = bcs_stream::deserialize_string(stream); + bcs_stream::assert_is_consumed(stream); + + timelock_block_function(target, module_name, function_name) + } + + inline fun dispatch_timelock_unblock_function( + role: u8, stream: &mut BCSStream + ) { + assert!(role == TIMELOCK_ROLE, E_NOT_TIMELOCK_ROLE); + + let target = bcs_stream::deserialize_address(stream); + let module_name = bcs_stream::deserialize_string(stream); + let function_name = bcs_stream::deserialize_string(stream); + bcs_stream::assert_is_consumed(stream); + + timelock_unblock_function(target, module_name, function_name) + } + + /// Updates the multisig configuration, including signer addresses and group settings. + public entry fun set_config( + caller: &signer, + role: u8, + signer_addresses: vector>, + signer_groups: vector, + group_quorums: vector, + group_parents: vector, + clear_root: bool + ) acquires Multisig, MultisigState { + mcms_account::assert_is_owner(caller); + + assert!( + signer_addresses.length() != 0 + && signer_addresses.length() <= MAX_NUM_SIGNERS, + E_INVALID_NUM_SIGNERS + ); + assert!( + signer_addresses.length() == signer_groups.length(), + E_SIGNER_GROUPS_LEN_MISMATCH + ); + assert!(group_quorums.length() == NUM_GROUPS, E_INVALID_GROUP_QUORUM_LEN); + assert!(group_parents.length() == NUM_GROUPS, E_INVALID_GROUP_PARENTS_LEN); + + // validate group structure + // counts number of children of each group + let group_children_counts = vector[]; + params::right_pad_vec(&mut group_children_counts, NUM_GROUPS); + // first, we count the signers as children + signer_groups.for_each_ref( + |group| { + let group: u64 = *group as u64; + assert!(group < NUM_GROUPS, E_OUT_OF_BOUNDS_GROUP); + let count = group_children_counts.borrow_mut(group); + *count += 1; + } + ); + + // second, we iterate backwards so as to check each group and propagate counts from + // child group to parent groups up the tree to the root + for (j in 0..NUM_GROUPS) { + let i = NUM_GROUPS - j - 1; + // ensure we have a well-formed group tree: + // - the root should have itself as parent + // - all other groups should have a parent group with a lower index + let group_parent = group_parents[i] as u64; + assert!( + i == 0 || group_parent < i, E_GROUP_TREE_NOT_WELL_FORMED + ); + assert!( + i != 0 || group_parent == 0, E_GROUP_TREE_NOT_WELL_FORMED + ); + + let group_quorum = group_quorums[i]; + let disabled = group_quorum == 0; + let group_children_count = group_children_counts[i]; + if (disabled) { + // if group is disabled, ensure it has no children + assert!(group_children_count == 0, E_SIGNER_IN_DISABLED_GROUP); + } else { + // if group is enabled, ensure group quorum can be met + assert!( + group_children_count >= group_quorum, E_OUT_OF_BOUNDS_GROUP_QUORUM + ); + + // propagate children counts to parent group + let count = group_children_counts.borrow_mut(group_parent); + *count += 1; + }; + }; + + let multisig = borrow_multisig_mut(multisig_object(role)); + + // remove old signer addresses + multisig.signers = simple_map::new(); + multisig.config.signers = vector[]; + + // save group quorums and parents to timelock + multisig.config.group_quorums = group_quorums; + multisig.config.group_parents = group_parents; + + // check signer addresses are in increasing order and save signers to timelock + // evm zero address (20 bytes of 0) is the smallest address possible + let prev_signer_addr = vector[]; + for (i in 0..signer_addresses.length()) { + let signer_addr = signer_addresses[i]; + assert!(signer_addr.length() == 20, E_INVALID_SIGNER_ADDR_LEN); + + if (i > 0) { + assert!( + params::vector_u8_gt(&signer_addr, &prev_signer_addr), + E_SIGNER_ADDR_MUST_BE_INCREASING + ); + }; + + let signer = Signer { + addr: signer_addr, + index: (i as u8), + group: signer_groups[i] + }; + multisig.signers.add(signer_addr, signer); + multisig.config.signers.push_back(signer); + prev_signer_addr = signer_addr; + }; + + if (clear_root) { + // clearRoot is equivalent to overriding with a completely empty root + let op_count = multisig.expiring_root_and_op_count.op_count; + multisig.expiring_root_and_op_count = ExpiringRootAndOpCount { + root: vector[], + valid_until: 0, + op_count + }; + multisig.root_metadata = RootMetadata { + role, + chain_id: (chain_id::get() as u256), + multisig: @mcms, + pre_op_count: op_count, + post_op_count: op_count, + override_previous_root: true + }; + }; + + event::emit(ConfigSet { + role, + config: multisig.config, + is_root_cleared: clear_root + }); + } + + public fun verify_merkle_proof( + proof: vector>, root: vector, leaf: vector + ): bool { + let computed_hash = leaf; + proof.for_each_ref( + |proof_element| { + let (left, right) = + if (params::vector_u8_gt(&computed_hash, proof_element)) { + (*proof_element, computed_hash) + } else { + (computed_hash, *proof_element) + }; + let hash_input: vector = left; + hash_input.append(right); + computed_hash = keccak256(hash_input); + } + ); + computed_hash == root + } + + public fun compute_eth_message_hash( + root: vector, valid_until: u64 + ): vector { + // abi.encode(root (bytes32), valid_until) + let valid_until_bytes = params::encode_uint(valid_until, 32); + assert!(root.length() == 32, E_INVALID_ROOT_LEN); // root should be 32 bytes + let abi_encoded_params = &mut root; + abi_encoded_params.append(valid_until_bytes); + + // keccak256(abi_encoded_params) + let hashed_encoded_params = keccak256(*abi_encoded_params); + + // ECDSA.toEthSignedMessageHash() + let eth_msg_prefix = b"\\x19Ethereum Signed Message:\\n32"; + let hash = &mut eth_msg_prefix; + hash.append(hashed_encoded_params); + keccak256(*hash) + } + + public fun hash_op_leaf(domain_separator: vector, op: Op): vector { + let packed = vector[]; + packed.append(domain_separator); + packed.append(bcs::to_bytes(&op.role)); + packed.append(bcs::to_bytes(&op.chain_id)); + packed.append(bcs::to_bytes(&op.multisig)); + packed.append(bcs::to_bytes(&op.nonce)); + packed.append(bcs::to_bytes(&op.to)); + packed.append(bcs::to_bytes(&op.module_name)); + packed.append(bcs::to_bytes(&op.function_name)); + packed.append(bcs::to_bytes(&op.data)); + keccak256(packed) + } + + #[view] + public fun seen_signed_hashes( + multisig: Object + ): SimpleMap, bool> acquires Multisig { + borrow_multisig(multisig).seen_signed_hashes + } + + #[view] + /// Returns the current Merkle root along with its expiration timestamp and op count. + public fun expiring_root_and_op_count( + multisig: Object + ): (vector, u64, u64) acquires Multisig { + let multisig = borrow_multisig(multisig); + ( + multisig.expiring_root_and_op_count.root, + multisig.expiring_root_and_op_count.valid_until, + multisig.expiring_root_and_op_count.op_count + ) + } + + #[view] + public fun root_metadata(multisig: Object): RootMetadata acquires Multisig { + borrow_multisig(multisig).root_metadata + } + + #[view] + public fun get_root_metadata(role: u8): RootMetadata acquires MultisigState, Multisig { + let multisig = multisig_object(role); + borrow_multisig(multisig).root_metadata + } + + #[view] + public fun get_op_count(role: u8): u64 acquires MultisigState, Multisig { + let multisig = multisig_object(role); + borrow_multisig(multisig).expiring_root_and_op_count.op_count + } + + #[view] + public fun get_root(role: u8): (vector, u64) acquires MultisigState, Multisig { + let multisig = borrow_multisig(multisig_object(role)); + ( + multisig.expiring_root_and_op_count.root, + multisig.expiring_root_and_op_count.valid_until + ) + } + + #[view] + public fun get_config(role: u8): Config acquires MultisigState, Multisig { + let multisig = multisig_object(role); + borrow_multisig(multisig).config + } + + #[view] + public fun signers(multisig: Object): SimpleMap, Signer> acquires Multisig { + borrow_multisig(multisig).signers + } + + #[view] + /// Returns the registered multisig objects for the given role. + public fun multisig_object(role: u8): Object acquires MultisigState { + let state = borrow(); + if (role == BYPASSER_ROLE) { + state.bypasser + } else if (role == CANCELLER_ROLE) { + state.canceller + } else if (role == PROPOSER_ROLE) { + state.proposer + } else { + abort E_INVALID_ROLE + } + } + + #[view] + public fun num_groups(): u64 { + NUM_GROUPS + } + + #[view] + public fun max_num_signers(): u64 { + MAX_NUM_SIGNERS + } + + #[view] + public fun bypasser_role(): u8 { + BYPASSER_ROLE + } + + #[view] + public fun canceller_role(): u8 { + CANCELLER_ROLE + } + + #[view] + public fun proposer_role(): u8 { + PROPOSER_ROLE + } + + #[view] + public fun timelock_role(): u8 { + TIMELOCK_ROLE + } + + #[view] + public fun is_valid_role(role: u8): bool { + role < MAX_ROLE + } + + #[view] + public fun zero_hash(): vector { + ZERO_HASH + } + + fun hash_metadata_leaf(metadata: RootMetadata): vector { + let packed = vector[]; + packed.append(MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA); + packed.append(bcs::to_bytes(&metadata.role)); + packed.append(bcs::to_bytes(&metadata.chain_id)); + packed.append(bcs::to_bytes(&metadata.multisig)); + packed.append(bcs::to_bytes(&metadata.pre_op_count)); + packed.append(bcs::to_bytes(&metadata.post_op_count)); + packed.append(bcs::to_bytes(&metadata.override_previous_root)); + keccak256(packed) + } + + inline fun borrow_multisig(obj: Object): &Multisig acquires Multisig { + borrow_global(object::object_address(&obj)) + } + + inline fun borrow_multisig_mut(multisig: Object): &mut Multisig acquires Multisig { + borrow_global_mut(object::object_address(&multisig)) + } + + inline fun borrow(): &MultisigState acquires MultisigState { + borrow_global(@mcms) + } + + inline fun borrow_mut(): &mut MultisigState acquires MultisigState { + borrow_global_mut(@mcms) + } + + public fun role(root_metadata: RootMetadata): u8 { + root_metadata.role + } + + public fun chain_id(root_metadata: RootMetadata): u256 { + root_metadata.chain_id + } + + public fun root_metadata_multisig(root_metadata: RootMetadata): address { + root_metadata.multisig + } + + public fun pre_op_count(root_metadata: RootMetadata): u64 { + root_metadata.pre_op_count + } + + public fun post_op_count(root_metadata: RootMetadata): u64 { + root_metadata.post_op_count + } + + public fun override_previous_root(root_metadata: RootMetadata): bool { + root_metadata.override_previous_root + } + + public fun config_signers(config: &Config): vector { + config.signers + } + + public fun config_group_quorums(config: &Config): vector { + config.group_quorums + } + + public fun config_group_parents(config: &Config): vector { + config.group_parents + } + + // ======================================================================================= + // | Timelock Implementation | + // ======================================================================================= + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + struct Timelock has key { + min_delay: u64, + /// hashed batch of hashed calls -> timestamp + timestamps: SmartTable, u64>, + /// blocked functions + blocked_functions: SmartVector + } + + struct Call has copy, drop, store { + function: Function, + data: vector + } + + struct Function has copy, drop, store { + target: address, + module_name: String, + function_name: String + } + + #[event] + struct TimelockInitialized has drop, store { + min_delay: u64 + } + + #[event] + struct BypasserCallExecuted has drop, store { + index: u64, + target: address, + module_name: String, + function_name: String, + data: vector + } + + #[event] + struct Cancelled has drop, store { + id: vector + } + + #[event] + struct CallScheduled has drop, store { + id: vector, + index: u64, + target: address, + module_name: String, + function_name: String, + data: vector, + predecessor: vector, + salt: vector, + delay: u64 + } + + #[event] + struct CallExecuted has drop, store { + id: vector, + index: u64, + target: address, + module_name: String, + function_name: String, + data: vector + } + + #[event] + struct UpdateMinDelay has drop, store { + old_min_delay: u64, + new_min_delay: u64 + } + + #[event] + struct FunctionBlocked has drop, store { + target: address, + module_name: String, + function_name: String + } + + #[event] + struct FunctionUnblocked has drop, store { + target: address, + module_name: String, + function_name: String + } + + /// Schedule a batch of calls to be executed after a delay. + /// This function can only be called by PROPOSER or ADMIN role. + inline fun timelock_schedule_batch( + targets: vector
, + module_names: vector, + function_names: vector, + datas: vector>, + predecessor: vector, + salt: vector, + delay: u64 + ) { + let calls = create_calls(targets, module_names, function_names, datas); + let id = hash_operation_batch(calls, predecessor, salt); + let timelock = borrow_mut_timelock(); + + timelock_schedule(timelock, id, delay); + + for (i in 0..calls.length()) { + assert_not_blocked(timelock, &calls[i].function); + event::emit( + CallScheduled { + id, + index: i, + target: calls[i].function.target, + module_name: calls[i].function.module_name, + function_name: calls[i].function.function_name, + data: calls[i].data, + predecessor, + salt, + delay + } + ); + }; + } + + inline fun timelock_schedule( + timelock: &mut Timelock, id: vector, delay: u64 + ) { + assert!( + !timelock_is_operation_internal(timelock, id), + E_OPERATION_ALREADY_SCHEDULED + ); + assert!(delay >= timelock.min_delay, E_INSUFFICIENT_DELAY); + + let timestamp = timestamp::now_seconds() + delay; + timelock.timestamps.add(id, timestamp); + + } + + inline fun timelock_before_call( + id: vector, predecessor: vector + ) { + assert!(timelock_is_operation_ready(id), E_OPERATION_NOT_READY); + assert!( + predecessor == ZERO_HASH || timelock_is_operation_done(predecessor), + E_MISSING_DEPENDENCY + ); + } + + inline fun timelock_after_call(id: vector) { + assert!(timelock_is_operation_ready(id), E_OPERATION_NOT_READY); + *borrow_mut_timelock().timestamps.borrow_mut(id) = DONE_TIMESTAMP; + } + + /// Anyone can call this as it checks if the operation was scheduled by a bypasser or proposer. + public entry fun timelock_execute_batch( + targets: vector
, + module_names: vector, + function_names: vector, + datas: vector>, + predecessor: vector, + salt: vector + ) acquires Multisig, MultisigState, Timelock { + let calls = create_calls(targets, module_names, function_names, datas); + let id = hash_operation_batch(calls, predecessor, salt); + + timelock_before_call(id, predecessor); + + for (i in 0..calls.length()) { + let function = calls[i].function; + let target = function.target; + let module_name = function.module_name; + let function_name = function.function_name; + let data = calls[i].data; + + timelock_dispatch(target, module_name, function_name, data); + + event::emit( + CallExecuted { + id, + index: i, + target, + module_name, + function_name, + data + } + ); + }; + + timelock_after_call(id); + } + + fun timelock_bypasser_execute_batch( + targets: vector
, + module_names: vector, + function_names: vector, + datas: vector> + ) acquires Multisig, MultisigState, Timelock { + let len = targets.length(); + assert!( + len == module_names.length() + && len == function_names.length() + && len == datas.length(), + E_INVALID_PARAMETERS + ); + + for (i in 0..len) { + let target = targets[i]; + let module_name = module_names[i]; + let function_name = function_names[i]; + let data = datas[i]; + + timelock_dispatch(target, module_name, function_name, data); + + event::emit( + BypasserCallExecuted { index: i, target, module_name, function_name, data } + ); + }; + } + + /// If we reach here, we know that the call was scheduled and is ready to be executed. + /// Only callable from \`timelock_execute_batch\` or \`timelock_bypasser_execute_batch\` + inline fun timelock_dispatch( + target: address, + module_name: String, + function_name: String, + data: vector + ) { + let module_name_bytes = *module_name.bytes(); + let function_name_bytes = *function_name.bytes(); + + if (target == @mcms) { + if (module_name_bytes == b"mcms") { + // dispatch to the mcms module's functions for setting config, scheduling, executing, and canceling operations. + timelock_dispatch_to_self(function_name, data); + } else if (module_name_bytes == b"mcms_account") { + // dispatch to the account module's functions for ownership transfers. + timelock_dispatch_to_account(function_name_bytes, data); + } else if (module_name_bytes == b"mcms_deployer") { + // dispatch to the deployer module's functions for deploying and upgrading contracts. + timelock_dispatch_to_deployer(function_name_bytes, data); + } else if (module_name_bytes == b"mcms_registry") { + // dispatch to the registry module's functions for code object management. + timelock_dispatch_to_registry(function_name_bytes, data); + } else { + abort E_UNKNOWN_MCMS_MODULE; + } + } else { + // If role is present, it must be a bypasser (calling from \`execute\`). + let object_meta = + mcms_registry::start_dispatch(target, module_name, function_name, data); + aptos_framework::dispatchable_fungible_asset::derived_supply(object_meta); + mcms_registry::finish_dispatch(target); + } + } + + inline fun timelock_dispatch_to_self( + function_name: String, data: vector + ) { + let stream = bcs_stream::new(data); + let fn_bytes = *function_name.bytes(); + let prefix = b"timelock"; + + if (fn_bytes.length() >= prefix.length() + && fn_bytes.slice(0, prefix.length()) == prefix) { + // Pass \`TIMELOCK_ROLE\` as the function call has already been validated + dispatch_to_timelock(TIMELOCK_ROLE, function_name, data); + } else if (fn_bytes == b"set_config") { + let role_param = bcs_stream::deserialize_u8(&mut stream); + let signer_addresses = + bcs_stream::deserialize_vector( + &mut stream, + |stream| { bcs_stream::deserialize_vector_u8(stream) } + ); + let signer_groups = bcs_stream::deserialize_vector_u8(&mut stream); + let group_quorums = bcs_stream::deserialize_vector_u8(&mut stream); + let group_parents = bcs_stream::deserialize_vector_u8(&mut stream); + let clear_root = bcs_stream::deserialize_bool(&mut stream); + bcs_stream::assert_is_consumed(&stream); + + set_config( + &mcms_account::get_signer(), // Must get MCMS signer for \`set_config\` + role_param, + signer_addresses, + signer_groups, + group_quorums, + group_parents, + clear_root + ); + } else { + abort E_UNKNOWN_MCMS_MODULE_FUNCTION + } + } + + inline fun timelock_dispatch_to_account( + function_name_bytes: vector, data: vector + ) { + let stream = bcs_stream::new(data); + let self_signer = &mcms_account::get_signer(); + + if (function_name_bytes == b"transfer_ownership") { + let target = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + mcms_account::transfer_ownership(self_signer, target); + } else if (function_name_bytes == b"accept_ownership") { + bcs_stream::assert_is_consumed(&stream); + mcms_account::accept_ownership(self_signer); + } else { + abort E_UNKNOWN_MCMS_ACCOUNT_MODULE_FUNCTION; + } + } + + inline fun timelock_dispatch_to_deployer( + function_name_bytes: vector, data: vector + ) { + let self_signer = &mcms_account::get_signer(); + let stream = bcs_stream::new(data); + + if (function_name_bytes == b"stage_code_chunk") { + let metadata_chunk = bcs_stream::deserialize_vector_u8(&mut stream); + let code_indices = + bcs_stream::deserialize_vector( + &mut stream, + |stream| { bcs_stream::deserialize_u16(stream) } + ); + let code_chunks = + bcs_stream::deserialize_vector( + &mut stream, + |stream| { bcs_stream::deserialize_vector_u8(stream) } + ); + bcs_stream::assert_is_consumed(&stream); + + mcms_deployer::stage_code_chunk( + self_signer, + metadata_chunk, + code_indices, + code_chunks + ); + } else if (function_name_bytes == b"stage_code_chunk_and_publish_to_object") { + let metadata_chunk = bcs_stream::deserialize_vector_u8(&mut stream); + let code_indices = + bcs_stream::deserialize_vector( + &mut stream, + |stream| { bcs_stream::deserialize_u16(stream) } + ); + let code_chunks = + bcs_stream::deserialize_vector( + &mut stream, + |stream| { bcs_stream::deserialize_vector_u8(stream) } + ); + let new_owner_seed = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + + mcms_deployer::stage_code_chunk_and_publish_to_object( + self_signer, + metadata_chunk, + code_indices, + code_chunks, + new_owner_seed + ); + } else if (function_name_bytes == b"stage_code_chunk_and_upgrade_object_code") { + let metadata_chunk = bcs_stream::deserialize_vector_u8(&mut stream); + let code_indices = + bcs_stream::deserialize_vector( + &mut stream, + |stream| { bcs_stream::deserialize_u16(stream) } + ); + let code_chunks = + bcs_stream::deserialize_vector( + &mut stream, + |stream| { bcs_stream::deserialize_vector_u8(stream) } + ); + let code_object_address = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + + mcms_deployer::stage_code_chunk_and_upgrade_object_code( + self_signer, + metadata_chunk, + code_indices, + code_chunks, + code_object_address + ); + } else if (function_name_bytes == b"cleanup_staging_area") { + bcs_stream::assert_is_consumed(&stream); + mcms_deployer::cleanup_staging_area(self_signer); + } else { + abort E_UNKNOWN_MCMS_DEPLOYER_MODULE_FUNCTION; + } + } + + inline fun timelock_dispatch_to_registry( + function_name_bytes: vector, data: vector + ) { + let stream = bcs_stream::new(data); + let self_signer = &mcms_account::get_signer(); + + if (function_name_bytes == b"create_owner_for_preexisting_code_object") { + let object_address = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + mcms_registry::create_owner_for_preexisting_code_object( + self_signer, object_address + ); + } else if (function_name_bytes == b"transfer_code_object") { + let object_address = bcs_stream::deserialize_address(&mut stream); + let new_owner_address = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + mcms_registry::transfer_code_object( + self_signer, object_address, new_owner_address + ); + } else if (function_name_bytes == b"execute_code_object_transfer") { + let object_address = bcs_stream::deserialize_address(&mut stream); + let new_owner_address = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + mcms_registry::execute_code_object_transfer( + self_signer, object_address, new_owner_address + ); + } else { + abort E_UNKNOWN_MCMS_REGISTRY_MODULE_FUNCTION; + } + } + + inline fun timelock_cancel(id: vector) { + assert!(timelock_is_operation_pending(id), E_OPERATION_CANNOT_BE_CANCELLED); + + borrow_mut_timelock().timestamps.remove(id); + event::emit(Cancelled { id }); + } + + inline fun timelock_update_min_delay(new_min_delay: u64) { + let timelock = borrow_mut_timelock(); + let old_min_delay = timelock.min_delay; + timelock.min_delay = new_min_delay; + + event::emit(UpdateMinDelay { old_min_delay, new_min_delay }); + } + + inline fun timelock_block_function( + target: address, module_name: String, function_name: String + ) { + let already_blocked = false; + let new_function = Function { target, module_name, function_name }; + let timelock = borrow_mut_timelock(); + + for (i in 0..timelock.blocked_functions.length()) { + let blocked_function = timelock.blocked_functions.borrow(i); + if (equals(&new_function, blocked_function)) { + already_blocked = true; + break + }; + }; + + if (!already_blocked) { + timelock.blocked_functions.push_back(new_function); + event::emit(FunctionBlocked { target, module_name, function_name }); + }; + } + + inline fun timelock_unblock_function( + target: address, module_name: String, function_name: String + ) { + let function_to_unblock = Function { target, module_name, function_name }; + let timelock = borrow_mut_timelock(); + + for (i in 0..timelock.blocked_functions.length()) { + let blocked_function = timelock.blocked_functions.borrow(i); + if (equals(&function_to_unblock, blocked_function)) { + timelock.blocked_functions.swap_remove(i); + event::emit(FunctionUnblocked { target, module_name, function_name }); + break + }; + }; + } + + inline fun assert_not_blocked( + timelock: &Timelock, function: &Function + ) { + for (i in 0..timelock.blocked_functions.length()) { + let blocked_function = timelock.blocked_functions.borrow(i); + if (equals(function, blocked_function)) { + abort E_FUNCTION_BLOCKED; + }; + }; + } + + #[view] + public fun timelock_get_blocked_function(index: u64): Function acquires Timelock { + let timelock = borrow_timelock(); + assert!(index < timelock.blocked_functions.length(), E_INVALID_INDEX); + *timelock.blocked_functions.borrow(index) + } + + #[view] + public fun timelock_is_operation(id: vector): bool acquires Timelock { + timelock_is_operation_internal(borrow_timelock(), id) + } + + inline fun timelock_is_operation_internal( + timelock: &Timelock, id: vector + ): bool { + timelock.timestamps.contains(id) && *timelock.timestamps.borrow(id) > 0 + } + + #[view] + public fun timelock_is_operation_pending(id: vector): bool acquires Timelock { + let timelock = borrow_timelock(); + timelock.timestamps.contains(id) + && *timelock.timestamps.borrow(id) > DONE_TIMESTAMP + } + + #[view] + public fun timelock_is_operation_ready(id: vector): bool acquires Timelock { + let timelock = borrow_timelock(); + if (!timelock.timestamps.contains(id)) { + return false + }; + + let timestamp_value = *timelock.timestamps.borrow(id); + timestamp_value > DONE_TIMESTAMP && timestamp_value <= timestamp::now_seconds() + } + + #[view] + public fun timelock_is_operation_done(id: vector): bool acquires Timelock { + let timelock = borrow_timelock(); + timelock.timestamps.contains(id) + && *timelock.timestamps.borrow(id) == DONE_TIMESTAMP + } + + #[view] + public fun timelock_get_timestamp(id: vector): u64 acquires Timelock { + let timelock = borrow_timelock(); + if (timelock.timestamps.contains(id)) { + *timelock.timestamps.borrow(id) + } else { 0 } + } + + #[view] + public fun timelock_min_delay(): u64 acquires Timelock { + borrow_timelock().min_delay + } + + #[view] + public fun timelock_get_blocked_functions(): vector acquires Timelock { + let timelock = borrow_timelock(); + let blocked_functions = vector[]; + for (i in 0..timelock.blocked_functions.length()) { + blocked_functions.push_back(*timelock.blocked_functions.borrow(i)); + }; + blocked_functions + } + + #[view] + public fun timelock_get_blocked_functions_count(): u64 acquires Timelock { + borrow_timelock().blocked_functions.length() + } + + public fun create_calls( + targets: vector
, + module_names: vector, + function_names: vector, + datas: vector> + ): vector { + let len = targets.length(); + assert!( + len == module_names.length() + && len == function_names.length() + && len == datas.length(), + E_INVALID_PARAMETERS + ); + + let calls = vector[]; + for (i in 0..len) { + let target = targets[i]; + let module_name = module_names[i]; + let function_name = function_names[i]; + let data = datas[i]; + let function = Function { target, module_name, function_name }; + let call = Call { function, data }; + calls.push_back(call); + }; + + calls + } + + public fun hash_operation_batch( + calls: vector, predecessor: vector, salt: vector + ): vector { + let packed = vector[]; + packed.append(bcs::to_bytes(&calls)); + packed.append(predecessor); + packed.append(salt); + keccak256(packed) + } + + fun equals(fn1: &Function, fn2: &Function): bool { + fn1.target == fn2.target + && fn1.module_name.bytes() == fn2.module_name.bytes() + && fn1.function_name.bytes() == fn2.function_name.bytes() + } + + inline fun borrow_timelock(): &Timelock acquires Timelock { + borrow_global(@mcms) + } + + inline fun borrow_mut_timelock(): &mut Timelock acquires Timelock { + borrow_global_mut(@mcms) + } + + public fun signer_view(signer_: &Signer): (vector, u8, u8) { + (signer_.addr, signer_.index, signer_.group) + } + + public fun function_name(function: Function): String { + function.function_name + } + + public fun module_name(function: Function): String { + function.module_name + } + + public fun target(function: Function): address { + function.target + } + + public fun data(call: Call): vector { + call.data + } + + // ======================= TEST ONLY FUNCTIONS ======================= // + #[test_only] + public fun init_module_for_testing(publisher: &signer) { + init_module(publisher); + } + + #[test_only] + public fun test_hash_metadata_leaf( + role: u8, + chain_id: u256, + multisig: address, + pre_op_count: u64, + post_op_count: u64, + override_previous_root: bool + ): vector { + let metadata = RootMetadata { + role, + chain_id, + multisig, + pre_op_count, + post_op_count, + override_previous_root + }; + hash_metadata_leaf(metadata) + } + + #[test_only] + public fun test_set_expiring_root_and_op_count( + multisig: Object, + root: vector, + valid_until: u64, + op_count: u64 + ) acquires Multisig { + let multisig = borrow_multisig_mut(multisig); + multisig.expiring_root_and_op_count.root = root; + multisig.expiring_root_and_op_count.valid_until = valid_until; + multisig.expiring_root_and_op_count.op_count = op_count; + } + + #[test_only] + public fun test_set_root_metadata( + multisig: Object, + role: u8, + chain_id: u256, + multisig_addr: address, + pre_op_count: u64, + post_op_count: u64, + override_previous_root: bool + ) acquires Multisig { + let multisig = borrow_multisig_mut(multisig); + multisig.root_metadata.role = role; + multisig.root_metadata.chain_id = chain_id; + multisig.root_metadata.multisig = multisig_addr; + multisig.root_metadata.pre_op_count = pre_op_count; + multisig.root_metadata.post_op_count = post_op_count; + multisig.root_metadata.override_previous_root = override_previous_root; + } + + #[test_only] + public fun test_ecdsa_recover_evm_addr( + eth_signed_message_hash: vector, signature: vector + ): vector { + ecdsa_recover_evm_addr(eth_signed_message_hash, signature) + } + + #[test_only] + public fun test_timelock_schedule_batch( + targets: vector
, + module_names: vector, + function_names: vector, + datas: vector>, + predecessor: vector, + salt: vector, + delay: u64 + ) acquires Timelock { + timelock_schedule_batch( + targets, + module_names, + function_names, + datas, + predecessor, + salt, + delay + ); + } + + #[test_only] + public fun test_timelock_update_min_delay(delay: u64) acquires Timelock { + timelock_update_min_delay(delay); + } + + #[test_only] + public fun test_timelock_cancel(id: vector) acquires Timelock { + timelock_cancel(id); + } + + #[test_only] + public fun test_timelock_bypasser_execute_batch( + targets: vector
, + module_names: vector, + function_names: vector, + datas: vector> + ) acquires Multisig, MultisigState, Timelock { + timelock_bypasser_execute_batch(targets, module_names, function_names, datas); + } + + #[test_only] + public fun test_timelock_block_function( + target: address, module_name: String, function_name: String + ) acquires Timelock { + timelock_block_function(target, module_name, function_name); + } + + #[test_only] + public fun test_timelock_unblock_function( + target: address, module_name: String, function_name: String + ) acquires Timelock { + timelock_unblock_function(target, module_name, function_name); + } + + #[test_only] + public fun create_op( + role: u8, + chain_id: u256, + multisig: address, + nonce: u64, + to: address, + module_name: String, + function_name: String, + data: vector + ): Op { + Op { + role, + chain_id, + multisig, + nonce, + to, + module_name, + function_name, + data + } + } + + #[test_only] + public fun test_timelock_dispatch( + target: address, + module_name: String, + function_name: String, + data: vector + ) acquires Multisig, MultisigState, Timelock { + timelock_dispatch(target, module_name, function_name, data) + } +} +` + +/** sources/utils/bcs_stream.move */ +export const MCMS_UTILS_BCS_STREAM_MOVE = `/// Copied and modified from: https://github.com/aptos-labs/aptos-core/blob/9baf39b6fba7812f09238c91973f61fd0955057c/aptos-move/move-examples/bcs-stream/sources/stream.move +/// +/// This module enables the deserialization of BCS-formatted byte arrays into Move primitive types. +/// Deserialization Strategies: +/// - Per-Byte Deserialization: Employed for most types to ensure lower gas consumption, this method processes each byte +/// individually to match the length and type requirements of target Move types. +/// - Exception: For the \`deserialize_address\` function, the function-based approach from \`aptos_std::from_bcs\` is used +/// due to type constraints, even though it is generally more gas-intensive. +/// - This can be optimized further by introducing native vector slices. +/// Application: +/// - This deserializer is particularly valuable for processing BCS serialized data within Move modules, +/// especially useful for systems requiring cross-chain message interpretation or off-chain data verification. +module mcms::bcs_stream { + use std::error; + use std::vector; + use std::option::{Self, Option}; + use std::string::{Self, String}; + + use aptos_std::from_bcs; + + /// The data does not fit the expected format. + const E_MALFORMED_DATA: u64 = 1; + /// There are not enough bytes to deserialize for the given type. + const E_OUT_OF_BYTES: u64 = 2; + /// The stream has not been consumed. + const E_NOT_CONSUMED: u64 = 3; + + struct BCSStream has drop { + /// Byte buffer containing the serialized data. + data: vector, + /// Cursor indicating the current position in the byte buffer. + cur: u64 + } + + /// Constructs a new BCSStream instance from the provided byte array. + public fun new(data: vector): BCSStream { + BCSStream { data, cur: 0 } + } + + /// Asserts that the stream has been fully consumed. + public fun assert_is_consumed(stream: &BCSStream) { + assert!(stream.cur == stream.data.length(), error::invalid_state(E_NOT_CONSUMED)); + } + + /// Deserializes a ULEB128-encoded integer from the stream. + /// In the BCS format, lengths of vectors are represented using ULEB128 encoding. + public fun deserialize_uleb128(stream: &mut BCSStream): u64 { + let res = 0; + let shift = 0; + + while (stream.cur < stream.data.length()) { + let byte = stream.data[stream.cur]; + stream.cur += 1; + + let val = ((byte & 0x7f) as u64); + if (((val << shift) >> shift) != val) { + abort error::invalid_argument(E_MALFORMED_DATA) + }; + res |=(val << shift); + + if ((byte & 0x80) == 0) { + if (shift > 0 && val == 0) { + abort error::invalid_argument(E_MALFORMED_DATA) + }; + return res + }; + + shift += 7; + if (shift > 64) { + abort error::invalid_argument(E_MALFORMED_DATA) + }; + }; + + abort error::out_of_range(E_OUT_OF_BYTES) + } + + /// Deserializes a \`bool\` value from the stream. + public fun deserialize_bool(stream: &mut BCSStream): bool { + assert!(stream.cur < stream.data.length(), error::out_of_range(E_OUT_OF_BYTES)); + let byte = stream.data[stream.cur]; + stream.cur += 1; + if (byte == 0) { false } + else if (byte == 1) { true } + else { + abort error::invalid_argument(E_MALFORMED_DATA) + } + } + + /// Deserializes an \`address\` value from the stream. + /// 32-byte \`address\` values are serialized using little-endian byte order. + /// This function utilizes the \`to_address\` function from the \`aptos_std::from_bcs\` module, + /// because the Move type system does not permit per-byte referencing of addresses. + public fun deserialize_address(stream: &mut BCSStream): address { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 32 <= data.length(), error::out_of_range(E_OUT_OF_BYTES) + ); + let res = from_bcs::to_address(data.slice(cur, cur + 32)); + + stream.cur = cur + 32; + res + } + + /// Deserializes a \`u8\` value from the stream. + /// 1-byte \`u8\` values are serialized using little-endian byte order. + public fun deserialize_u8(stream: &mut BCSStream): u8 { + let data = &stream.data; + let cur = stream.cur; + + assert!(cur < data.length(), error::out_of_range(E_OUT_OF_BYTES)); + + let res = data[cur]; + + stream.cur = cur + 1; + res + } + + /// Deserializes a \`u16\` value from the stream. + /// 2-byte \`u16\` values are serialized using little-endian byte order. + public fun deserialize_u16(stream: &mut BCSStream): u16 { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 2 <= data.length(), error::out_of_range(E_OUT_OF_BYTES) + ); + let res = (data[cur] as u16) | ((data[cur + 1] as u16) << 8); + + stream.cur += 2; + res + } + + /// Deserializes a \`u32\` value from the stream. + /// 4-byte \`u32\` values are serialized using little-endian byte order. + public fun deserialize_u32(stream: &mut BCSStream): u32 { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 4 <= data.length(), error::out_of_range(E_OUT_OF_BYTES) + ); + let res = + (data[cur] as u32) | ((data[cur + 1] as u32) << 8) | ((data[cur + 2] as u32) + << 16) | ((data[cur + 3] as u32) << 24); + + stream.cur += 4; + res + } + + /// Deserializes a \`u64\` value from the stream. + /// 8-byte \`u64\` values are serialized using little-endian byte order. + public fun deserialize_u64(stream: &mut BCSStream): u64 { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 8 <= data.length(), error::out_of_range(E_OUT_OF_BYTES) + ); + let res = + (data[cur] as u64) | ((data[cur + 1] as u64) << 8) | ((data[cur + 2] as u64) + << 16) | ((data[cur + 3] as u64) << 24) | ((data[cur + 4] as u64) << 32) + | ((data[cur + 5] as u64) << 40) | ((data[cur + 6] as u64) << 48) + | ((data[cur + 7] as u64) << 56); + + stream.cur += 8; + res + } + + /// Deserializes a \`u128\` value from the stream. + /// 16-byte \`u128\` values are serialized using little-endian byte order. + public fun deserialize_u128(stream: &mut BCSStream): u128 { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 16 <= data.length(), error::out_of_range(E_OUT_OF_BYTES) + ); + let res = + (data[cur] as u128) | ((data[cur + 1] as u128) << 8) + | ((data[cur + 2] as u128) << 16) | ((data[cur + 3] as u128) << 24) + | ((data[cur + 4] as u128) << 32) | ((data[cur + 5] as u128) << 40) + | ((data[cur + 6] as u128) << 48) | ((data[cur + 7] as u128) << 56) + | ((data[cur + 8] as u128) << 64) | ((data[cur + 9] as u128) << 72) + | ((data[cur + 10] as u128) << 80) | ((data[cur + 11] as u128) << 88) + | ((data[cur + 12] as u128) << 96) | ((data[cur + 13] as u128) << 104) + | ((data[cur + 14] as u128) << 112) | ((data[cur + 15] as u128) << 120); + + stream.cur += 16; + res + } + + /// Deserializes a \`u256\` value from the stream. + /// 32-byte \`u256\` values are serialized using little-endian byte order. + public fun deserialize_u256(stream: &mut BCSStream): u256 { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 32 <= data.length(), error::out_of_range(E_OUT_OF_BYTES) + ); + let res = + (data[cur] as u256) | ((data[cur + 1] as u256) << 8) + | ((data[cur + 2] as u256) << 16) | ((data[cur + 3] as u256) << 24) + | ((data[cur + 4] as u256) << 32) | ((data[cur + 5] as u256) << 40) + | ((data[cur + 6] as u256) << 48) | ((data[cur + 7] as u256) << 56) + | ((data[cur + 8] as u256) << 64) | ((data[cur + 9] as u256) << 72) + | ((data[cur + 10] as u256) << 80) | ((data[cur + 11] as u256) << 88) + | ((data[cur + 12] as u256) << 96) | ((data[cur + 13] as u256) << 104) + | ((data[cur + 14] as u256) << 112) | ((data[cur + 15] as u256) << 120) + | ((data[cur + 16] as u256) << 128) | ((data[cur + 17] as u256) << 136) + | ((data[cur + 18] as u256) << 144) | ((data[cur + 19] as u256) << 152) + | ((data[cur + 20] as u256) << 160) | ((data[cur + 21] as u256) << 168) + | ((data[cur + 22] as u256) << 176) | ((data[cur + 23] as u256) << 184) + | ((data[cur + 24] as u256) << 192) | ((data[cur + 25] as u256) << 200) + | ((data[cur + 26] as u256) << 208) | ((data[cur + 27] as u256) << 216) + | ((data[cur + 28] as u256) << 224) | ((data[cur + 29] as u256) << 232) + | ((data[cur + 30] as u256) << 240) | ((data[cur + 31] as u256) << 248); + + stream.cur += 32; + res + } + + /// Deserializes a \`u256\` value from the stream. + public entry fun deserialize_u256_entry(data: vector, cursor: u64) { + let stream = BCSStream { data, cur: cursor }; + deserialize_u256(&mut stream); + } + + /// Deserializes an array of BCS deserializable elements from the stream. + /// First, reads the length of the vector, which is in uleb128 format. + /// After determining the length, it then reads the contents of the vector. + /// The \`elem_deserializer\` lambda expression is used sequentially to deserialize each element of the vector. + public inline fun deserialize_vector( + stream: &mut BCSStream, elem_deserializer: |&mut BCSStream| E + ): vector { + let len = deserialize_uleb128(stream); + let v = vector::empty(); + + for (i in 0..len) { + v.push_back(elem_deserializer(stream)); + }; + + v + } + + public fun deserialize_vector_u8(stream: &mut BCSStream): vector { + let len = deserialize_uleb128(stream); + let data = &mut stream.data; + let cur = stream.cur; + + assert!( + cur + len <= data.length(), error::out_of_range(E_OUT_OF_BYTES) + ); + + // AIP-105 introduces vector::move_range to efficiently move a range of elements from one vector to another. + let res = data.trim(cur); + stream.data = res.trim(len); + stream.cur = 0; + + res + } + + public fun deserialize_fixed_vector_u8( + stream: &mut BCSStream, len: u64 + ): vector { + let data = &mut stream.data; + let cur = stream.cur; + + assert!( + cur + len <= data.length(), error::out_of_range(E_OUT_OF_BYTES) + ); + + // AIP-105 introduces vector::move_range to efficiently move a range of elements from one vector to another. + let res = data.trim(cur); + stream.data = res.trim(len); + stream.cur = 0; + + res + } + + /// Deserializes utf-8 \`String\` from the stream. + /// First, reads the length of the String, which is in uleb128 format. + /// After determining the length, it then reads the contents of the String. + public fun deserialize_string(stream: &mut BCSStream): String { + let len = deserialize_uleb128(stream); + let data = &mut stream.data; + let cur = stream.cur; + + assert!( + cur + len <= data.length(), error::out_of_range(E_OUT_OF_BYTES) + ); + + // AIP-105 introduces vector::move_range to efficiently move a range of elements from one vector to another. + let res = data.trim(cur); + stream.data = res.trim(len); + stream.cur = 0; + + string::utf8(res) + } + + /// Deserializes \`Option\` from the stream. + /// First, reads a single byte representing the presence (0x01) or absence (0x00) of data. + /// After determining the presence of data, it then reads the actual data if present. + /// The \`elem_deserializer\` lambda expression is used to deserialize the element contained within the \`Option\`. + public inline fun deserialize_option( + stream: &mut BCSStream, elem_deserializer: |&mut BCSStream| E + ): Option { + let is_data = deserialize_bool(stream); + if (is_data) { + option::some(elem_deserializer(stream)) + } else { + option::none() + } + } +} +` + +/** sources/utils/params.move */ +export const MCMS_UTILS_PARAMS_MOVE = `module mcms::params { + use std::bcs; + + const E_CMP_VECTORS_DIFF_LEN: u64 = 1; + const E_INPUT_TOO_LARGE_FOR_NUM_BYTES: u64 = 2; + + public inline fun encode_uint(input: T, num_bytes: u64): vector { + let bcs_bytes = bcs::to_bytes(&input); + + let len = bcs_bytes.length(); + assert!(len <= num_bytes, E_INPUT_TOO_LARGE_FOR_NUM_BYTES); + + if (len < num_bytes) { + let bytes_to_pad = num_bytes - len; + for (i in 0..bytes_to_pad) { + bcs_bytes.push_back(0); + }; + }; + + // little endian to big endian + bcs_bytes.reverse(); + + bcs_bytes + } + + public inline fun right_pad_vec(v: &mut vector, num_bytes: u64) { + let len = v.length(); + if (len < num_bytes) { + let bytes_to_pad = num_bytes - len; + for (i in 0..bytes_to_pad) { + v.push_back(0); + }; + }; + } + + /// compares two vectors of equal length, returns true if a > b, false otherwise. + public fun vector_u8_gt(a: &vector, b: &vector): bool { + let len = a.length(); + assert!(len == b.length(), E_CMP_VECTORS_DIFF_LEN); + + if (len == 0) { + return false + }; + + // compare each byte until not equal + for (i in 0..len) { + let byte_a = a[i]; + let byte_b = b[i]; + if (byte_a > byte_b) { + return true + } else if (byte_a < byte_b) { + return false + }; + }; + + // vectors are equal, a == b + false + } +} +` diff --git a/ccip-sdk/src/cct/aptos/bytecodes/regulated_token_pool.ts b/ccip-sdk/src/cct/aptos/bytecodes/regulated_token_pool.ts new file mode 100644 index 00000000..706f9088 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/bytecodes/regulated_token_pool.ts @@ -0,0 +1,1252 @@ +/** + * RegulatedTokenPool Move package source files. + * + * Source: chainlink-aptos contracts/ccip/ccip_token_pools/regulated_token_pool + * + contracts/regulated_token + * AptosFramework rev: 16beac69835f3a71564c96164a606a23f259099a + * ChainlinkCCIP + MCMS: embedded as local dependencies + * + * For regulated tokens with pause/freeze/role-based access control. + * The regulated_token package provides dynamic dispatch deposit/withdraw + * functions that enforce compliance controls. + * + * Vendored as source (not compiled bytecodes) because Aptos Move modules + * must be compiled with the deployer's address at deploy time. + * + * Lazy-loaded via dynamic import() — same pattern as EVM BurnMintERC20 bytecode. + */ + +export const REGULATED_POOL_MOVE_TOML = `[package] +name = "RegulatedTokenPool" +version = "1.0.0" +authors = [] + +[addresses] +ccip = "_" +ccip_token_pool = "_" +regulated_token_pool = "_" +mcms = "_" +mcms_register_entrypoints = "_" +regulated_token = "_" +admin = "_" + +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "16beac69835f3a71564c96164a606a23f259099a", subdir = "aptos-move/framework/aptos-framework" } +ChainlinkCCIP = { local = "../ccip" } +CCIPTokenPool = { local = "../token_pool" } +RegulatedToken = { local = "../regulated_token" } +` + +export const REGULATED_TOKEN_POOL_MOVE = `module regulated_token_pool::regulated_token_pool { + use std::account::{Self, SignerCapability}; + use std::error; + use std::fungible_asset::{Self, FungibleAsset, Metadata, TransferRef}; + use std::primary_fungible_store; + use std::object::{Self, Object}; + use std::option::{Self, Option}; + use std::signer; + use std::string::{Self, String}; + + use regulated_token::regulated_token::{Self}; + + use ccip::token_admin_registry::{Self, LockOrBurnInputV1, ReleaseOrMintInputV1}; + use ccip_token_pool::ownable; + use ccip_token_pool::rate_limiter; + use ccip_token_pool::token_pool; + + use mcms::mcms_registry; + use mcms::bcs_stream; + + const STORE_OBJECT_SEED: vector = b"CcipRegulatedTokenPool"; + + struct RegulatedTokenPoolState has key, store { + store_signer_cap: SignerCapability, + ownable_state: ownable::OwnableState, + token_pool_state: token_pool::TokenPoolState, + store_signer_address: address + } + + const E_INVALID_ARGUMENTS: u64 = 1; + const E_UNKNOWN_FUNCTION: u64 = 2; + const E_NOT_PUBLISHER: u64 = 3; + + // ================================================================ + // | Init | + // ================================================================ + #[view] + public fun type_and_version(): String { + string::utf8(b"RegulatedTokenPool 1.6.0") + } + + fun init_module(publisher: &signer) { + // register the pool on deployment, because in the case of object code deployment, + // this is the only time we have a signer ref to @regulated_token_pool. + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist(@regulated_token_pool); + + // the name of this module. if incorrect, callbacks will fail to be registered and + // register_pool will revert. + let token_pool_module_name = b"regulated_token_pool"; + + // Register the entrypoint with mcms + if (@mcms_register_entrypoints == @0x1) { + register_mcms_entrypoint(publisher, token_pool_module_name); + }; + + // Register V2 pool with closure-based callbacks + register_v2_callbacks(publisher); + + // create a resource account to be the owner of the primary FungibleStore we will use. + let (store_signer, store_signer_cap) = + account::create_resource_account(publisher, STORE_OBJECT_SEED); + + let regulated_token_address = regulated_token::token_address(); + let metadata = object::address_to_object(regulated_token_address); + + // make sure this is a valid fungible asset that is primary fungible store enabled, + // ie. created with primary_fungible_store::create_primary_store_enabled_fungible_asset + primary_fungible_store::ensure_primary_store_exists( + signer::address_of(&store_signer), metadata + ); + + let pool = RegulatedTokenPoolState { + ownable_state: ownable::new(&store_signer, @regulated_token_pool), + store_signer_address: signer::address_of(&store_signer), + store_signer_cap, + token_pool_state: token_pool::initialize( + &store_signer, regulated_token_address, vector[] + ) + }; + + move_to(&store_signer, pool); + } + + public fun register_v2_callbacks(publisher: &signer) { + assert!( + signer::address_of(publisher) == @regulated_token_pool, + error::permission_denied(E_NOT_PUBLISHER) + ); + let regulated_token_address = regulated_token::token_address(); + token_admin_registry::register_pool_v2( + publisher, + regulated_token_address, + lock_or_burn_v2, + release_or_mint_v2 + ); + } + + // ================================================================ + // | Exposing token_pool functions | + // ================================================================ + #[view] + public fun get_token(): address acquires RegulatedTokenPoolState { + token_pool::get_token(&borrow_pool().token_pool_state) + } + + #[view] + public fun get_router(): address { + token_pool::get_router() + } + + #[view] + public fun get_token_decimals(): u8 acquires RegulatedTokenPoolState { + token_pool::get_token_decimals(&borrow_pool().token_pool_state) + } + + #[view] + public fun get_remote_pools( + remote_chain_selector: u64 + ): vector> acquires RegulatedTokenPoolState { + token_pool::get_remote_pools( + &borrow_pool().token_pool_state, remote_chain_selector + ) + } + + #[view] + public fun is_remote_pool( + remote_chain_selector: u64, remote_pool_address: vector + ): bool acquires RegulatedTokenPoolState { + token_pool::is_remote_pool( + &borrow_pool().token_pool_state, + remote_chain_selector, + remote_pool_address + ) + } + + #[view] + public fun get_remote_token( + remote_chain_selector: u64 + ): vector acquires RegulatedTokenPoolState { + let pool = borrow_pool(); + token_pool::get_remote_token(&pool.token_pool_state, remote_chain_selector) + } + + public entry fun add_remote_pool( + caller: &signer, remote_chain_selector: u64, remote_pool_address: vector + ) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::add_remote_pool( + &mut pool.token_pool_state, + remote_chain_selector, + remote_pool_address + ); + } + + public entry fun remove_remote_pool( + caller: &signer, remote_chain_selector: u64, remote_pool_address: vector + ) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::remove_remote_pool( + &mut pool.token_pool_state, + remote_chain_selector, + remote_pool_address + ); + } + + #[view] + public fun is_supported_chain(remote_chain_selector: u64): bool acquires RegulatedTokenPoolState { + let pool = borrow_pool(); + token_pool::is_supported_chain(&pool.token_pool_state, remote_chain_selector) + } + + #[view] + public fun get_supported_chains(): vector acquires RegulatedTokenPoolState { + let pool = borrow_pool(); + token_pool::get_supported_chains(&pool.token_pool_state) + } + + public entry fun apply_chain_updates( + caller: &signer, + remote_chain_selectors_to_remove: vector, + remote_chain_selectors_to_add: vector, + remote_pool_addresses_to_add: vector>>, + remote_token_addresses_to_add: vector> + ) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::apply_chain_updates( + &mut pool.token_pool_state, + remote_chain_selectors_to_remove, + remote_chain_selectors_to_add, + remote_pool_addresses_to_add, + remote_token_addresses_to_add + ); + } + + #[view] + public fun get_allowlist_enabled(): bool acquires RegulatedTokenPoolState { + let pool = borrow_pool(); + token_pool::get_allowlist_enabled(&pool.token_pool_state) + } + + public entry fun set_allowlist_enabled( + caller: &signer, enabled: bool + ) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + token_pool::set_allowlist_enabled(&mut pool.token_pool_state, enabled); + } + + #[view] + public fun get_allowlist(): vector
acquires RegulatedTokenPoolState { + let pool = borrow_pool(); + token_pool::get_allowlist(&pool.token_pool_state) + } + + public entry fun apply_allowlist_updates( + caller: &signer, removes: vector
, adds: vector
+ ) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + token_pool::apply_allowlist_updates(&mut pool.token_pool_state, removes, adds); + } + + // ================================================================ + // | Burn/Mint | + // ================================================================ + + // the callback proof type used as authentication to retrieve and set input and output arguments. + struct CallbackProof has drop {} + + public fun lock_or_burn( + _store: Object, fa: FungibleAsset, _transfer_ref: &TransferRef + ) acquires RegulatedTokenPoolState { + // retrieve the input for this lock or burn operation. if this function is invoked + // outside of ccip::token_admin_registry, the transaction will abort. + let input = + token_admin_registry::get_lock_or_burn_input_v1( + @regulated_token_pool, CallbackProof {} + ); + + let pool = borrow_pool_mut(); + let fa_amount = fungible_asset::amount(&fa); + + // This method validates various aspects of the lock or burn operation. If any of the + // validations fail, the transaction will abort. + let dest_token_address = + token_pool::validate_lock_or_burn( + &mut pool.token_pool_state, + &fa, + &input, + fa_amount + ); + + // Construct lock_or_burn output before we lose access to fa + let dest_pool_data = token_pool::encode_local_decimals(&pool.token_pool_state); + + // Burn the funds using regulated token's bridge burn function + // The pool store signer must have BRIDGE_MINTER_OR_BURNER role + let pool_signer = &account::create_signer_with_capability(&pool.store_signer_cap); + let sender = token_admin_registry::get_lock_or_burn_sender(&input); + regulated_token::bridge_burn(pool_signer, sender, fa); + + // set the output for this lock or burn operation. + token_admin_registry::set_lock_or_burn_output_v1( + @regulated_token_pool, + CallbackProof {}, + dest_token_address, + dest_pool_data + ); + + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(&input); + + token_pool::emit_locked_or_burned( + &mut pool.token_pool_state, fa_amount, remote_chain_selector + ); + } + + public fun release_or_mint( + _store: Object, _amount: u64, _transfer_ref: &TransferRef + ): FungibleAsset acquires RegulatedTokenPoolState { + // retrieve the input for this release or mint operation. if this function is invoked + // outside of ccip::token_admin_registry, the transaction will abort. + let input = + token_admin_registry::get_release_or_mint_input_v1( + @regulated_token_pool, CallbackProof {} + ); + let pool = borrow_pool_mut(); + let local_amount = + token_pool::calculate_release_or_mint_amount(&pool.token_pool_state, &input); + + token_pool::validate_release_or_mint( + &mut pool.token_pool_state, &input, local_amount + ); + + // Mint the amount for release using regulated token's bridge mint function + // The pool store signer must have BRIDGE_MINTER_OR_BURNER role + let pool_signer = &account::create_signer_with_capability(&pool.store_signer_cap); + let receiver = token_admin_registry::get_release_or_mint_receiver(&input); + let fa = regulated_token::bridge_mint(pool_signer, receiver, local_amount); + + // set the output for this release or mint operation. + token_admin_registry::set_release_or_mint_output_v1( + @regulated_token_pool, CallbackProof {}, local_amount + ); + + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(&input); + + token_pool::emit_released_or_minted( + &mut pool.token_pool_state, + receiver, + local_amount, + remote_chain_selector + ); + + // return the withdrawn fungible asset. + fa + } + + #[persistent] + fun lock_or_burn_v2( + fa: FungibleAsset, input: LockOrBurnInputV1 + ): (vector, vector) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + let fa_amount = fungible_asset::amount(&fa); + + let dest_token_address = + token_pool::validate_lock_or_burn( + &mut pool.token_pool_state, + &fa, + &input, + fa_amount + ); + + let pool_signer = &account::create_signer_with_capability(&pool.store_signer_cap); + let sender = token_admin_registry::get_lock_or_burn_sender(&input); + regulated_token::bridge_burn(pool_signer, sender, fa); + + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(&input); + + token_pool::emit_locked_or_burned( + &mut pool.token_pool_state, fa_amount, remote_chain_selector + ); + + (dest_token_address, token_pool::encode_local_decimals(&pool.token_pool_state)) + } + + #[persistent] + fun release_or_mint_v2( + input: ReleaseOrMintInputV1 + ): (FungibleAsset, u64) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + let local_amount = + token_pool::calculate_release_or_mint_amount(&pool.token_pool_state, &input); + + token_pool::validate_release_or_mint( + &mut pool.token_pool_state, &input, local_amount + ); + + // Mint the amount for release using regulated token's bridge mint function + let pool_signer = &account::create_signer_with_capability(&pool.store_signer_cap); + let receiver = token_admin_registry::get_release_or_mint_receiver(&input); + let fa = regulated_token::bridge_mint(pool_signer, receiver, local_amount); + + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(&input); + + token_pool::emit_released_or_minted( + &mut pool.token_pool_state, + receiver, + local_amount, + remote_chain_selector + ); + + (fa, local_amount) + } + + // ================================================================ + // | Rate limit config | + // ================================================================ + public entry fun set_chain_rate_limiter_configs( + caller: &signer, + remote_chain_selectors: vector, + outbound_is_enableds: vector, + outbound_capacities: vector, + outbound_rates: vector, + inbound_is_enableds: vector, + inbound_capacities: vector, + inbound_rates: vector + ) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + let number_of_chains = remote_chain_selectors.length(); + + assert!( + number_of_chains == outbound_is_enableds.length() + && number_of_chains == outbound_capacities.length() + && number_of_chains == outbound_rates.length() + && number_of_chains == inbound_is_enableds.length() + && number_of_chains == inbound_capacities.length() + && number_of_chains == inbound_rates.length(), + error::invalid_argument(E_INVALID_ARGUMENTS) + ); + + for (i in 0..number_of_chains) { + token_pool::set_chain_rate_limiter_config( + &mut pool.token_pool_state, + remote_chain_selectors[i], + outbound_is_enableds[i], + outbound_capacities[i], + outbound_rates[i], + inbound_is_enableds[i], + inbound_capacities[i], + inbound_rates[i] + ); + }; + } + + public entry fun set_chain_rate_limiter_config( + caller: &signer, + remote_chain_selector: u64, + outbound_is_enabled: bool, + outbound_capacity: u64, + outbound_rate: u64, + inbound_is_enabled: bool, + inbound_capacity: u64, + inbound_rate: u64 + ) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::assert_only_owner(signer::address_of(caller), &pool.ownable_state); + + token_pool::set_chain_rate_limiter_config( + &mut pool.token_pool_state, + remote_chain_selector, + outbound_is_enabled, + outbound_capacity, + outbound_rate, + inbound_is_enabled, + inbound_capacity, + inbound_rate + ); + } + + #[view] + public fun get_current_inbound_rate_limiter_state( + remote_chain_selector: u64 + ): rate_limiter::TokenBucket acquires RegulatedTokenPoolState { + token_pool::get_current_inbound_rate_limiter_state( + &borrow_pool().token_pool_state, remote_chain_selector + ) + } + + #[view] + public fun get_current_outbound_rate_limiter_state( + remote_chain_selector: u64 + ): rate_limiter::TokenBucket acquires RegulatedTokenPoolState { + token_pool::get_current_outbound_rate_limiter_state( + &borrow_pool().token_pool_state, remote_chain_selector + ) + } + + // ================================================================ + // | Storage helpers | + // ================================================================ + #[view] + public fun get_store_address(): address { + store_address() + } + + inline fun store_address(): address { + account::create_resource_address(&@regulated_token_pool, STORE_OBJECT_SEED) + } + + inline fun borrow_pool(): &RegulatedTokenPoolState { + borrow_global(store_address()) + } + + inline fun borrow_pool_mut(): &mut RegulatedTokenPoolState { + borrow_global_mut(store_address()) + } + + // ================================================================ + // | Expose ownable | + // ================================================================ + #[view] + public fun owner(): address acquires RegulatedTokenPoolState { + ownable::owner(&borrow_pool().ownable_state) + } + + #[view] + public fun has_pending_transfer(): bool acquires RegulatedTokenPoolState { + ownable::has_pending_transfer(&borrow_pool().ownable_state) + } + + #[view] + public fun pending_transfer_from(): Option
acquires RegulatedTokenPoolState { + ownable::pending_transfer_from(&borrow_pool().ownable_state) + } + + #[view] + public fun pending_transfer_to(): Option
acquires RegulatedTokenPoolState { + ownable::pending_transfer_to(&borrow_pool().ownable_state) + } + + #[view] + public fun pending_transfer_accepted(): Option acquires RegulatedTokenPoolState { + ownable::pending_transfer_accepted(&borrow_pool().ownable_state) + } + + public entry fun transfer_ownership(caller: &signer, to: address) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::transfer_ownership(caller, &mut pool.ownable_state, to) + } + + public entry fun accept_ownership(caller: &signer) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::accept_ownership(caller, &mut pool.ownable_state) + } + + public entry fun execute_ownership_transfer( + caller: &signer, to: address + ) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + ownable::execute_ownership_transfer(caller, &mut pool.ownable_state, to) + } + + // ================================================================ + // | MCMS entrypoint | + // ================================================================ + struct McmsCallback has drop {} + + public fun mcms_entrypoint( + _metadata: object::Object + ): option::Option acquires RegulatedTokenPoolState { + let (caller, function, data) = + mcms_registry::get_callback_params(@regulated_token_pool, McmsCallback {}); + + let function_bytes = *function.bytes(); + let stream = bcs_stream::new(data); + + if (function_bytes == b"add_remote_pool") { + let remote_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let remote_pool_address = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + add_remote_pool(&caller, remote_chain_selector, remote_pool_address); + } else if (function_bytes == b"remove_remote_pool") { + let remote_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let remote_pool_address = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + remove_remote_pool(&caller, remote_chain_selector, remote_pool_address); + } else if (function_bytes == b"apply_chain_updates") { + let remote_chain_selectors_to_remove = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let remote_chain_selectors_to_add = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let remote_pool_addresses_to_add = + bcs_stream::deserialize_vector( + &mut stream, + |stream| bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ) + ); + let remote_token_addresses_to_add = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + bcs_stream::assert_is_consumed(&stream); + apply_chain_updates( + &caller, + remote_chain_selectors_to_remove, + remote_chain_selectors_to_add, + remote_pool_addresses_to_add, + remote_token_addresses_to_add + ); + } else if (function_bytes == b"set_allowlist_enabled") { + let enabled = bcs_stream::deserialize_bool(&mut stream); + bcs_stream::assert_is_consumed(&stream); + set_allowlist_enabled(&caller, enabled); + } else if (function_bytes == b"apply_allowlist_updates") { + let removes = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + let adds = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + apply_allowlist_updates(&caller, removes, adds); + } else if (function_bytes == b"set_chain_rate_limiter_configs") { + let remote_chain_selectors = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let outbound_is_enableds = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_bool(stream) + ); + let outbound_capacities = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let outbound_rates = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let inbound_is_enableds = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_bool(stream) + ); + let inbound_capacities = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + let inbound_rates = + bcs_stream::deserialize_vector( + &mut stream, |stream| bcs_stream::deserialize_u64(stream) + ); + bcs_stream::assert_is_consumed(&stream); + set_chain_rate_limiter_configs( + &caller, + remote_chain_selectors, + outbound_is_enableds, + outbound_capacities, + outbound_rates, + inbound_is_enableds, + inbound_capacities, + inbound_rates + ); + } else if (function_bytes == b"set_chain_rate_limiter_config") { + let remote_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let outbound_is_enabled = bcs_stream::deserialize_bool(&mut stream); + let outbound_capacity = bcs_stream::deserialize_u64(&mut stream); + let outbound_rate = bcs_stream::deserialize_u64(&mut stream); + let inbound_is_enabled = bcs_stream::deserialize_bool(&mut stream); + let inbound_capacity = bcs_stream::deserialize_u64(&mut stream); + let inbound_rate = bcs_stream::deserialize_u64(&mut stream); + bcs_stream::assert_is_consumed(&stream); + set_chain_rate_limiter_config( + &caller, + remote_chain_selector, + outbound_is_enabled, + outbound_capacity, + outbound_rate, + inbound_is_enabled, + inbound_capacity, + inbound_rate + ); + } else if (function_bytes == b"transfer_ownership") { + let to = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + transfer_ownership(&caller, to); + } else if (function_bytes == b"accept_ownership") { + bcs_stream::assert_is_consumed(&stream); + accept_ownership(&caller); + } else if (function_bytes == b"execute_ownership_transfer") { + let to = bcs_stream::deserialize_address(&mut stream); + bcs_stream::assert_is_consumed(&stream); + execute_ownership_transfer(&caller, to) + } else { + abort error::invalid_argument(E_UNKNOWN_FUNCTION) + }; + + option::none() + } + + /// Callable during upgrades + public(friend) fun register_mcms_entrypoint( + publisher: &signer, module_name: vector + ) { + mcms_registry::register_entrypoint( + publisher, string::utf8(module_name), McmsCallback {} + ); + } +} +` + +export const REGULATED_TOKEN_MOVE_TOML = `[package] +name = "RegulatedToken" +version = "1.0.0" +authors = [] + +[addresses] +regulated_token = "_" +admin = "_" + +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "16beac69835f3a71564c96164a606a23f259099a", subdir = "aptos-move/framework/aptos-framework" } +` + +// prettier-ignore +export const REGULATED_TOKEN_MOVE = "module regulated_token::regulated_token {\n use std::event;\n use std::fungible_asset::{\n Self,\n BurnRef,\n FungibleAsset,\n Metadata,\n MintRef,\n TransferRef,\n RawBalanceRef,\n RawSupplyRef,\n MutateMetadataRef\n };\n use std::object::{\n Self,\n ExtendRef,\n Object,\n TransferRef as ObjectTransferRef\n };\n use std::option::{Self, Option};\n use std::primary_fungible_store;\n use std::account;\n use std::signer;\n use std::string::{Self, String};\n use std::dispatchable_fungible_asset;\n use std::function_info;\n use std::big_ordered_map::{Self, BigOrderedMap};\n\n use regulated_token::access_control::{Self};\n use regulated_token::ownable::{Self, OwnableState};\n\n const TOKEN_STATE_SEED: vector = b\"regulated_token::regulated_token::token_state\";\n\n const PAUSER_ROLE: u8 = 0;\n const UNPAUSER_ROLE: u8 = 1;\n const FREEZER_ROLE: u8 = 2;\n const UNFREEZER_ROLE: u8 = 3;\n const MINTER_ROLE: u8 = 4;\n const BURNER_ROLE: u8 = 5;\n const BRIDGE_MINTER_OR_BURNER_ROLE: u8 = 6;\n const RECOVERY_ROLE: u8 = 7;\n\n enum Role has copy, drop, store {\n PAUSER_ROLE,\n UNPAUSER_ROLE,\n FREEZER_ROLE,\n UNFREEZER_ROLE,\n MINTER_ROLE,\n BURNER_ROLE,\n BRIDGE_MINTER_OR_BURNER_ROLE,\n RECOVERY_ROLE\n }\n\n #[resource_group_member(group = aptos_framework::object::ObjectGroup)]\n struct TokenStateDeployment has key {\n extend_ref: ExtendRef,\n transfer_ref: ObjectTransferRef,\n paused: bool,\n frozen_accounts: BigOrderedMap,\n ownable_state: OwnableState\n }\n\n #[resource_group_member(group = aptos_framework::object::ObjectGroup)]\n struct TokenState has key {\n extend_ref: ExtendRef,\n transfer_ref: ObjectTransferRef,\n paused: bool,\n frozen_accounts: BigOrderedMap,\n ownable_state: OwnableState,\n token: Object\n }\n\n #[resource_group_member(group = aptos_framework::object::ObjectGroup)]\n struct TokenMetadataRefs has key {\n extend_ref: ExtendRef,\n mint_ref: MintRef,\n burn_ref: BurnRef,\n transfer_ref: TransferRef,\n raw_balance_ref: RawBalanceRef,\n raw_supply_ref: RawSupplyRef,\n mutate_metadata_ref: MutateMetadataRef\n }\n\n #[event]\n struct InitializeToken has drop, store {\n publisher: address,\n token: Object,\n max_supply: Option,\n decimals: u8,\n icon: String,\n project: String\n }\n\n #[event]\n struct NativeMint has drop, store {\n minter: address,\n to: address,\n amount: u64\n }\n\n #[event]\n struct BridgeMint has drop, store {\n minter: address,\n to: address,\n amount: u64\n }\n\n #[event]\n struct NativeBurn has drop, store {\n burner: address,\n from: address,\n amount: u64\n }\n\n #[event]\n struct BridgeBurn has drop, store {\n burner: address,\n from: address,\n amount: u64\n }\n\n #[event]\n struct MinterAdded has drop, store {\n admin: address,\n minter: address,\n role: R,\n operation_type: u8\n }\n\n #[event]\n struct Paused has drop, store {\n pauser: address\n }\n\n #[event]\n struct Unpaused has drop, store {\n unpauser: address\n }\n\n #[event]\n struct AccountFrozen has drop, store {\n freezer: address,\n account: address\n }\n\n #[event]\n struct AccountUnfrozen has drop, store {\n unfreezer: address,\n account: address\n }\n\n #[event]\n struct TokensRecovered has drop, store {\n caller: address,\n token_metadata: Object,\n from: address,\n to: address,\n amount: u64\n }\n\n /// The caller is not the signer of this contract\n const E_NOT_PUBLISHER: u64 = 1;\n /// TokenState has not been initialized yet\n const E_TOKEN_NOT_INITIALIZED: u64 = 2;\n /// Caller must have either BURNER_ROLE or BRIDGE_MINTER_OR_BURNER_ROLE\n const E_ONLY_BURNER_OR_BRIDGE: u64 = 3;\n /// Caller must have either MINTER_ROLE or BRIDGE_MINTER_OR_BURNER_ROLE\n const E_ONLY_MINTER_OR_BRIDGE: u64 = 4;\n /// Invalid fungible asset for transfer ref\n const E_INVALID_ASSET: u64 = 5;\n /// Zero address (0x0) is not allowed\n const E_ZERO_ADDRESS_NOT_ALLOWED: u64 = 6;\n /// Cannot transfer tokens to the regulated token contract address\n const E_CANNOT_TRANSFER_TO_REGULATED_TOKEN: u64 = 7;\n /// Contract is paused\n const E_PAUSED: u64 = 8;\n /// Account is frozen and cannot perform token operations\n const E_ACCOUNT_FROZEN: u64 = 9;\n /// Contract is already paused\n const E_ALREADY_PAUSED: u64 = 14;\n /// Contract is not paused\n const E_NOT_PAUSED: u64 = 15;\n /// Invalid role number provided\n const E_INVALID_ROLE_NUMBER: u64 = 10;\n /// Invalid fungible store provided for token metadata\n const E_INVALID_STORE: u64 = 11;\n /// Fungible store does not exist for this account\n const E_STORE_DOES_NOT_EXIST: u64 = 12;\n /// TokenState deployment has already been initialized\n const E_TOKEN_STATE_DEPLOYMENT_ALREADY_INITIALIZED: u64 = 13;\n /// Account msut be frozen for recovery\n const E_ACCOUNT_MUST_BE_FROZEN_FOR_RECOVERY: u64 = 14;\n\n #[view]\n public fun type_and_version(): String {\n string::utf8(b\"RegulatedToken 1.0.0\")\n }\n\n #[view]\n public fun token_state_address(): address {\n token_state_address_internal()\n }\n\n #[view]\n public fun token_state_object(): Object {\n token_state_object_internal()\n }\n\n #[view]\n public fun admin(): address {\n access_control::admin(token_state_object_internal())\n }\n\n #[view]\n public fun pending_admin(): address {\n access_control::pending_admin(token_state_object_internal())\n }\n\n inline fun token_state_object_internal(): Object {\n let token_state_address = token_state_address_internal();\n assert!(exists(token_state_address), E_TOKEN_NOT_INITIALIZED);\n object::address_to_object(token_state_address)\n }\n\n inline fun token_state_address_internal(): address {\n object::create_object_address(&@regulated_token, TOKEN_STATE_SEED)\n }\n\n #[view]\n public fun token_address(): address acquires TokenState {\n object::object_address(&token_metadata_internal())\n }\n\n #[view]\n public fun token_metadata(): Object acquires TokenState {\n token_metadata_internal()\n }\n\n inline fun token_metadata_from_state_obj(\n state_obj: Object\n ): Object {\n TokenState[object::object_address(&state_obj)].token\n }\n\n inline fun token_metadata_internal(): Object {\n let state_address = token_state_address_internal();\n assert!(exists(state_address), E_TOKEN_NOT_INITIALIZED);\n TokenState[state_address].token\n }\n\n #[view]\n public fun is_paused(): bool acquires TokenState {\n TokenState[token_state_address_internal()].paused\n }\n\n #[view]\n public fun get_role_members(role_number: u8): vector
{\n let role = get_role(role_number);\n access_control::get_role_members(token_state_object_internal(), role)\n }\n\n #[view]\n public fun get_role_member_count(role_number: u8): u64 {\n let role = get_role(role_number);\n access_control::get_role_member_count(token_state_object_internal(), role)\n }\n\n #[view]\n public fun get_role_member(role_number: u8, index: u64): address {\n let role = get_role(role_number);\n access_control::get_role_member(token_state_object_internal(), role, index)\n }\n\n #[view]\n public fun get_admin(): address {\n access_control::admin(token_state_object_internal())\n }\n\n #[view]\n public fun get_minters(): vector
{\n access_control::get_role_members(token_state_object_internal(), minter_role())\n }\n\n #[view]\n public fun get_bridge_minters_or_burners(): vector
{\n access_control::get_role_members(\n token_state_object_internal(), bridge_minter_or_burner_role()\n )\n }\n\n #[view]\n public fun get_burners(): vector
{\n access_control::get_role_members(token_state_object_internal(), burner_role())\n }\n\n #[view]\n public fun get_freezers(): vector
{\n access_control::get_role_members(token_state_object_internal(), freezer_role())\n }\n\n #[view]\n public fun get_unfreezers(): vector
{\n access_control::get_role_members(\n token_state_object_internal(), unfreezer_role()\n )\n }\n\n #[view]\n public fun get_pausers(): vector
{\n access_control::get_role_members(token_state_object_internal(), pauser_role())\n }\n\n #[view]\n public fun get_unpausers(): vector
{\n access_control::get_role_members(\n token_state_object_internal(), unpauser_role()\n )\n }\n\n #[view]\n public fun get_recovery_managers(): vector
{\n access_control::get_role_members(\n token_state_object_internal(), recovery_role()\n )\n }\n\n #[view]\n public fun get_pending_admin(): address {\n access_control::pending_admin(token_state_object_internal())\n }\n\n #[view]\n public fun is_frozen(account: address): bool acquires TokenState {\n TokenState[token_state_address_internal()].frozen_accounts.contains(&account)\n }\n\n #[view]\n /// Get frozen accounts paginated using a start key and limit.\n /// Caller should call this on a certain block to ensure you the same state for every call.\n ///\n /// This function retrieves a batch of frozen account addresses from the registry, starting from\n /// the account address that comes after the provided start_key.\n ///\n /// @param start_key - Address to start pagination from (returns accounts AFTER this address)\n /// @param max_count - Maximum number of accounts to return\n ///\n /// @return:\n /// - vector
: List of frozen account addresses (up to max_count)\n /// - address: Next key to use for pagination (pass this as start_key in next call)\n /// - bool: Whether there are more accounts after this batch\n public fun get_all_frozen_accounts(\n start_key: address, max_count: u64\n ): (vector
, address, bool) acquires TokenState {\n let frozen_accounts = &TokenState[token_state_address_internal()].frozen_accounts;\n let result = vector[];\n\n let current_key_opt = frozen_accounts.next_key(&start_key);\n if (max_count == 0 || current_key_opt.is_none()) {\n return (result, start_key, current_key_opt.is_some())\n };\n\n let current_key = *current_key_opt.borrow();\n\n result.push_back(current_key);\n\n for (_i in 1..max_count) {\n let next_key_opt = frozen_accounts.next_key(¤t_key);\n if (next_key_opt.is_none()) {\n return (result, current_key, false)\n };\n\n current_key = *next_key_opt.borrow();\n result.push_back(current_key);\n };\n\n // Check if there are more accounts after the last key\n let has_more = frozen_accounts.next_key(¤t_key).is_some();\n (result, current_key, has_more)\n }\n\n #[view]\n public fun has_role(account: address, role: u8): bool {\n access_control::has_role(token_state_object_internal(), account, get_role(role))\n }\n\n public fun deposit(\n store: Object, fa: FungibleAsset, transfer_ref: &TransferRef\n ) acquires TokenState {\n let state_obj = token_state_object_internal();\n let token_metadata = token_metadata_from_state_obj(state_obj);\n let token_state = &TokenState[object::object_address(&state_obj)];\n\n assert_not_paused(token_state);\n assert_not_frozen(object::owner(store), token_state);\n assert_correct_asset(transfer_ref, token_metadata, store);\n\n fungible_asset::deposit_with_ref(transfer_ref, store, fa);\n }\n\n public fun withdraw(\n store: Object, amount: u64, transfer_ref: &TransferRef\n ): FungibleAsset acquires TokenState {\n let state_obj = token_state_object_internal();\n let token_metadata = token_metadata_from_state_obj(state_obj);\n let token_state = &TokenState[object::object_address(&state_obj)];\n\n assert_not_paused(token_state);\n assert_not_frozen(object::owner(store), token_state);\n assert_correct_asset(transfer_ref, token_metadata, store);\n\n fungible_asset::withdraw_with_ref(transfer_ref, store, amount)\n }\n\n /// `publisher` is the code object, deployed through object_code_deployment\n fun init_module(publisher: &signer) {\n assert!(object::is_object(@regulated_token), E_NOT_PUBLISHER);\n\n // Create object owned by code object\n let constructor_ref = &object::create_named_object(publisher, TOKEN_STATE_SEED);\n let token_state_signer = &object::generate_signer(constructor_ref);\n\n // Create an Account on the object for event handles.\n account::create_account_if_does_not_exist(signer::address_of(token_state_signer));\n\n move_to(\n token_state_signer,\n TokenStateDeployment {\n extend_ref: object::generate_extend_ref(constructor_ref),\n transfer_ref: object::generate_transfer_ref(constructor_ref),\n paused: false,\n frozen_accounts: big_ordered_map::new_with_config(0, 0, false),\n ownable_state: ownable::new(token_state_signer, @regulated_token)\n }\n );\n\n // Initialize the access control module with `@admin` as the admin\n access_control::init(constructor_ref, @admin);\n }\n\n /// Only owner of this code object can initialize a token once\n public entry fun initialize(\n publisher: &signer,\n max_supply: Option,\n name: String,\n symbol: String,\n decimals: u8,\n icon: String,\n project: String\n ) acquires TokenStateDeployment {\n let publisher_addr = signer::address_of(publisher);\n let token_state_address = token_state_address_internal();\n\n assert!(\n exists(token_state_address),\n E_TOKEN_STATE_DEPLOYMENT_ALREADY_INITIALIZED\n );\n\n let TokenStateDeployment {\n extend_ref,\n transfer_ref,\n paused,\n frozen_accounts,\n ownable_state\n } = move_from(token_state_address);\n\n ownable::assert_only_owner(publisher_addr, &ownable_state);\n\n let token_state_signer = &object::generate_signer_for_extending(&extend_ref);\n\n // Code object owns token state, which owns the fungible asset\n // Code object => token state => fungible asset\n let constructor_ref =\n &object::create_named_object(token_state_signer, *symbol.bytes());\n primary_fungible_store::create_primary_store_enabled_fungible_asset(\n constructor_ref,\n max_supply,\n name,\n symbol,\n decimals,\n icon,\n project\n );\n\n fungible_asset::set_untransferable(constructor_ref);\n\n move_to(\n &object::generate_signer(constructor_ref),\n TokenMetadataRefs {\n extend_ref: object::generate_extend_ref(constructor_ref),\n mint_ref: fungible_asset::generate_mint_ref(constructor_ref),\n burn_ref: fungible_asset::generate_burn_ref(constructor_ref),\n transfer_ref: fungible_asset::generate_transfer_ref(constructor_ref),\n raw_balance_ref: fungible_asset::generate_raw_balance_ref(constructor_ref),\n raw_supply_ref: fungible_asset::generate_raw_supply_ref(constructor_ref),\n mutate_metadata_ref: fungible_asset::generate_mutate_metadata_ref(\n constructor_ref\n )\n }\n );\n\n // Set up dynamic dispatch functions\n let deposit =\n function_info::new_function_info_from_address(\n @regulated_token,\n string::utf8(b\"regulated_token\"),\n string::utf8(b\"deposit\")\n );\n let withdraw =\n function_info::new_function_info_from_address(\n @regulated_token,\n string::utf8(b\"regulated_token\"),\n string::utf8(b\"withdraw\")\n );\n dispatchable_fungible_asset::register_dispatch_functions(\n constructor_ref,\n option::some(withdraw),\n option::some(deposit),\n option::none()\n );\n\n let token = object::object_from_constructor_ref(constructor_ref);\n event::emit(\n InitializeToken {\n publisher: publisher_addr,\n token,\n max_supply,\n decimals,\n icon,\n project\n }\n );\n\n move_to(\n token_state_signer,\n TokenState {\n extend_ref,\n transfer_ref,\n paused,\n frozen_accounts,\n ownable_state,\n token\n }\n );\n }\n\n public entry fun mint(\n caller: &signer, to: address, amount: u64\n ) acquires TokenMetadataRefs, TokenState {\n let state_obj = token_state_object_internal();\n let token_state = &TokenState[object::object_address(&state_obj)];\n\n assert_not_paused(token_state);\n assert_not_frozen(to, token_state);\n\n let minter = signer::address_of(caller);\n let is_bridge_minter =\n access_control::has_role(state_obj, minter, bridge_minter_or_burner_role());\n let is_native_minter = access_control::has_role(state_obj, minter, minter_role());\n\n assert!(is_bridge_minter || is_native_minter, E_ONLY_MINTER_OR_BRIDGE);\n\n primary_fungible_store::mint(&borrow_token_metadata_refs().mint_ref, to, amount);\n\n if (is_bridge_minter) {\n event::emit(BridgeMint { minter, to, amount });\n } else {\n event::emit(NativeMint { minter, to, amount });\n };\n }\n\n public entry fun burn(\n caller: &signer, from: address, amount: u64\n ) acquires TokenMetadataRefs, TokenState {\n let state_obj = token_state_object_internal();\n let token_state = &TokenState[object::object_address(&state_obj)];\n\n assert_not_paused(token_state);\n assert_not_frozen(from, token_state);\n\n let burner = signer::address_of(caller);\n let (is_bridge_burner, _) = assert_burner_and_get_type(burner, state_obj);\n\n primary_fungible_store::burn(\n &borrow_token_metadata_refs().burn_ref, from, amount\n );\n\n if (is_bridge_burner) {\n event::emit(BridgeBurn { burner, from, amount });\n } else {\n event::emit(NativeBurn { burner, from, amount });\n }\n }\n\n /// Bridge-specific function to mint tokens directly as `FungibleAsset`.\n /// Required because this token has dynamic dispatch enabled\n /// as minting to pool and calling `fungible_asset::withdraw()` reverts.\n /// Only callable by accounts with BRIDGE_MINTER_OR_BURNER_ROLE.\n public fun bridge_mint(\n caller: &signer, to: address, amount: u64\n ): FungibleAsset acquires TokenMetadataRefs, TokenState {\n let state_obj = token_state_object_internal();\n let token_state = &TokenState[object::object_address(&state_obj)];\n\n assert_not_paused(token_state);\n assert_bridge_minter_or_burner(caller, state_obj);\n assert_not_frozen(to, token_state);\n\n let fa = fungible_asset::mint(&borrow_token_metadata_refs().mint_ref, amount);\n\n event::emit(BridgeMint { minter: signer::address_of(caller), to, amount });\n\n fa\n }\n\n /// Bridge-specific function to burn `FungibleAsset` directly.\n /// Required because this token has dynamic dispatch enabled\n /// as depositing to pool and calling `fungible_asset::deposit()` reverts.\n /// Only callable by accounts with BRIDGE_MINTER_OR_BURNER_ROLE.\n public fun bridge_burn(\n caller: &signer, from: address, fa: FungibleAsset\n ) acquires TokenMetadataRefs, TokenState {\n let state_obj = token_state_object_internal();\n let token_state = &TokenState[object::object_address(&state_obj)];\n\n assert_not_paused(token_state);\n assert_bridge_minter_or_burner(caller, state_obj);\n assert_not_frozen(from, token_state);\n\n let amount = fungible_asset::amount(&fa);\n fungible_asset::burn(&borrow_token_metadata_refs().burn_ref, fa);\n\n event::emit(BridgeBurn { burner: signer::address_of(caller), from, amount });\n }\n\n fun freeze_account_internal(\n caller_addr: address,\n account: address,\n transfer_ref: &TransferRef,\n token_state: &mut TokenState\n ) {\n // Ensure the account is frozen at the primary store level\n primary_fungible_store::set_frozen_flag(transfer_ref, account, true);\n\n if (!token_state.frozen_accounts.contains(&account)) {\n token_state.frozen_accounts.add(account, true);\n };\n\n event::emit(AccountFrozen { freezer: caller_addr, account });\n }\n\n fun unfreeze_account_internal(\n caller_addr: address,\n account: address,\n transfer_ref: &TransferRef,\n token_state: &mut TokenState\n ) {\n // Ensure the account is unfrozen at the primary store level\n primary_fungible_store::set_frozen_flag(transfer_ref, account, false);\n\n if (token_state.frozen_accounts.contains(&account)) {\n token_state.frozen_accounts.remove(&account);\n };\n\n event::emit(AccountUnfrozen { unfreezer: caller_addr, account });\n }\n\n fun burn_frozen_funds_internal(\n burner: address,\n account: address,\n burn_ref: &BurnRef,\n token_metadata: Object,\n is_frozen: bool,\n is_bridge_burner: bool\n ) {\n if (is_frozen) {\n let balance = primary_fungible_store::balance(account, token_metadata);\n if (balance > 0) {\n primary_fungible_store::burn(burn_ref, account, balance);\n if (is_bridge_burner) {\n event::emit(BridgeBurn { burner, from: account, amount: balance });\n } else {\n event::emit(NativeBurn { burner, from: account, amount: balance });\n };\n };\n };\n }\n\n fun recover_frozen_funds_internal(\n caller: address,\n from: address,\n to: address,\n transfer_ref: &TransferRef,\n token_state: &TokenState\n ) {\n assert!(\n token_state.frozen_accounts.contains(&from),\n E_ACCOUNT_MUST_BE_FROZEN_FOR_RECOVERY\n );\n\n let balance = primary_fungible_store::balance(from, token_state.token);\n if (balance > 0) {\n primary_fungible_store::transfer_with_ref(transfer_ref, from, to, balance);\n event::emit(\n TokensRecovered {\n caller,\n token_metadata: token_state.token,\n from,\n to,\n amount: balance\n }\n );\n };\n }\n\n /// Periphery function to apply roles to accounts\n public entry fun grant_role(\n caller: &signer, role_number: u8, account: address\n ) {\n let role = get_role(role_number);\n\n access_control::grant_role(\n caller,\n token_state_object_internal(),\n role,\n account\n );\n\n if (role == minter_role() || role == bridge_minter_or_burner_role()) {\n event::emit(\n MinterAdded {\n admin: signer::address_of(caller),\n minter: account,\n role,\n operation_type: role_number\n }\n );\n }\n }\n\n public entry fun revoke_role(\n caller: &signer, role_number: u8, account: address\n ) {\n let role = get_role(role_number);\n access_control::revoke_role(\n caller,\n token_state_object_internal(),\n role,\n account\n );\n }\n\n public entry fun freeze_accounts(\n caller: &signer, accounts: vector
\n ) acquires TokenMetadataRefs, TokenState {\n let state_obj = token_state_object_internal();\n assert_freezer(caller, state_obj);\n\n let caller_addr = signer::address_of(caller);\n let transfer_ref = &borrow_token_metadata_refs().transfer_ref;\n for (i in 0..accounts.length()) {\n freeze_account_internal(\n caller_addr,\n accounts[i],\n transfer_ref,\n &mut TokenState[object::object_address(&state_obj)]\n );\n };\n }\n\n public entry fun freeze_account(\n caller: &signer, account: address\n ) acquires TokenMetadataRefs, TokenState {\n let state_obj = token_state_object_internal();\n assert_freezer(caller, state_obj);\n\n let transfer_ref = &borrow_token_metadata_refs().transfer_ref;\n freeze_account_internal(\n signer::address_of(caller),\n account,\n transfer_ref,\n &mut TokenState[object::object_address(&state_obj)]\n );\n }\n\n public entry fun unfreeze_accounts(\n caller: &signer, accounts: vector
\n ) acquires TokenMetadataRefs, TokenState {\n let state_obj = token_state_object_internal();\n assert_unfreezer(caller, state_obj);\n\n let caller_addr = signer::address_of(caller);\n let transfer_ref = &borrow_token_metadata_refs().transfer_ref;\n for (i in 0..accounts.length()) {\n unfreeze_account_internal(\n caller_addr,\n accounts[i],\n transfer_ref,\n &mut TokenState[object::object_address(&state_obj)]\n );\n };\n }\n\n public entry fun unfreeze_account(\n caller: &signer, account: address\n ) acquires TokenMetadataRefs, TokenState {\n let state_obj = token_state_object_internal();\n assert_unfreezer(caller, state_obj);\n\n let transfer_ref = &borrow_token_metadata_refs().transfer_ref;\n unfreeze_account_internal(\n signer::address_of(caller),\n account,\n transfer_ref,\n &mut TokenState[object::object_address(&state_obj)]\n );\n }\n\n /// Batch revoke and grant roles by role number\n /// `batch_revoke_role` and `batch_grant_role` assert that the caller is the admin\n public entry fun apply_role_updates(\n caller: &signer,\n role_number: u8,\n addresses_to_remove: vector
,\n addresses_to_add: vector
\n ) {\n let role = get_role(role_number);\n let state_obj = token_state_object_internal();\n\n if (addresses_to_remove.length() > 0) {\n access_control::batch_revoke_role(\n caller,\n state_obj,\n role,\n addresses_to_remove\n );\n };\n\n if (addresses_to_add.length() > 0) {\n access_control::batch_grant_role(caller, state_obj, role, addresses_to_add);\n };\n }\n\n public entry fun pause(caller: &signer) acquires TokenState {\n let state_obj = token_state_object_internal();\n assert_pauser(caller, state_obj);\n\n let state = &mut TokenState[object::object_address(&state_obj)];\n assert!(!state.paused, E_ALREADY_PAUSED);\n\n state.paused = true;\n event::emit(Paused { pauser: signer::address_of(caller) });\n }\n\n public entry fun unpause(caller: &signer) acquires TokenState {\n let state_obj = token_state_object_internal();\n assert_unpauser(caller, state_obj);\n\n let state = &mut TokenState[object::object_address(&state_obj)];\n assert!(state.paused, E_NOT_PAUSED);\n\n state.paused = false;\n event::emit(Unpaused { unpauser: signer::address_of(caller) });\n }\n\n /// Validates and sets up burn frozen funds operation.\n inline fun validate_burn_frozen_funds(\n caller: &signer\n ): (\n address, &BurnRef, Object, &TokenState, bool\n ) {\n let state_obj = token_state_object_internal();\n let token_state = &TokenState[object::object_address(&state_obj)];\n assert_not_paused(token_state);\n\n let burner = signer::address_of(caller);\n let (is_bridge_burner, _) = assert_burner_and_get_type(burner, state_obj);\n let token_metadata = token_metadata_from_state_obj(state_obj);\n let burn_ref = &borrow_token_metadata_refs().burn_ref;\n\n (\n burner, burn_ref, token_metadata, token_state, is_bridge_burner\n )\n }\n\n public entry fun batch_burn_frozen_funds(\n caller: &signer, accounts: vector
\n ) acquires TokenMetadataRefs, TokenState {\n let (\n burner, burn_ref, token_metadata, token_state, is_bridge_burner\n ) = validate_burn_frozen_funds(caller);\n\n for (i in 0..accounts.length()) {\n burn_frozen_funds_internal(\n burner,\n accounts[i],\n burn_ref,\n token_metadata,\n token_state.frozen_accounts.contains(&accounts[i]),\n is_bridge_burner\n );\n };\n }\n\n public entry fun burn_frozen_funds(\n caller: &signer, from: address\n ) acquires TokenMetadataRefs, TokenState {\n let (\n burner, burn_ref, token_metadata, token_state, is_bridge_burner\n ) = validate_burn_frozen_funds(caller);\n\n burn_frozen_funds_internal(\n burner,\n from,\n burn_ref,\n token_metadata,\n token_state.frozen_accounts.contains(&from),\n is_bridge_burner\n );\n }\n\n /// Recovers funds from frozen accounts by transferring them to a specified account.\n /// Only callable by accounts with RECOVERY_ROLE.\n public entry fun recover_frozen_funds(\n caller: &signer, from: address, to: address\n ) acquires TokenMetadataRefs, TokenState {\n let (transfer_ref, token_state) = validate_recovery_procedure(caller, to);\n recover_frozen_funds_internal(\n signer::address_of(caller),\n from,\n to,\n transfer_ref,\n token_state\n );\n }\n\n /// Batch version of recover_frozen_funds for processing multiple frozen accounts.\n /// Only callable by accounts with RECOVERY_ROLE.\n public entry fun batch_recover_frozen_funds(\n caller: &signer, accounts: vector
, to: address\n ) acquires TokenMetadataRefs, TokenState {\n let caller_addr = signer::address_of(caller);\n let (transfer_ref, token_state) = validate_recovery_procedure(caller, to);\n\n for (i in 0..accounts.length()) {\n recover_frozen_funds_internal(\n caller_addr,\n accounts[i],\n to,\n transfer_ref,\n token_state\n );\n };\n }\n\n inline fun assert_valid_recovery_recipient(\n to: address, token_state: &TokenState\n ) {\n assert!(to != @0x0, E_ZERO_ADDRESS_NOT_ALLOWED);\n assert!(\n to != @regulated_token && to != token_state_address_internal(),\n E_CANNOT_TRANSFER_TO_REGULATED_TOKEN\n );\n assert_not_frozen(to, token_state);\n }\n\n inline fun validate_recovery_procedure(caller: &signer, to: address)\n : (&TransferRef, &TokenState) {\n let state_obj = token_state_object_internal();\n let token_state = &TokenState[object::object_address(&state_obj)];\n\n assert_not_paused(token_state);\n assert_recovery_role(caller, state_obj);\n assert_valid_recovery_recipient(to, token_state);\n\n (&borrow_token_metadata_refs().transfer_ref, token_state)\n }\n\n public entry fun transfer_admin(caller: &signer, new_admin: address) {\n access_control::transfer_admin(\n caller, token_state_object_internal(), new_admin\n );\n }\n\n public entry fun accept_admin(caller: &signer) {\n access_control::accept_admin(\n caller, token_state_object_internal()\n );\n }\n\n /// Helper function to recover tokens from a specific address\n fun recover_tokens_from_address(\n caller_addr: address,\n from: address,\n to: address,\n transfer_ref: &TransferRef\n ) {\n let token_metadata = fungible_asset::transfer_ref_metadata(transfer_ref);\n let balance = primary_fungible_store::balance(from, token_metadata);\n if (balance > 0) {\n primary_fungible_store::transfer_with_ref(transfer_ref, from, to, balance);\n event::emit(\n TokensRecovered {\n caller: caller_addr,\n token_metadata,\n from,\n to,\n amount: balance\n }\n );\n }\n }\n\n /// In case regulated tokens get stuck in the contract or token state, this function can be used to recover them\n /// This function can only be called by the recovery role\n public entry fun recover_tokens(\n caller: &signer, to: address\n ) acquires TokenMetadataRefs, TokenState {\n let (transfer_ref, _token_state) = validate_recovery_procedure(caller, to);\n let caller_addr = signer::address_of(caller);\n\n // Recover regulated tokens sent to contract\n recover_tokens_from_address(\n caller_addr,\n @regulated_token,\n to,\n transfer_ref\n );\n\n // Recover regulated tokens sent to token state address\n recover_tokens_from_address(\n caller_addr,\n token_state_address_internal(),\n to,\n transfer_ref\n );\n }\n\n fun assert_not_paused(token_state: &TokenState) {\n assert!(!token_state.paused, E_PAUSED);\n }\n\n inline fun assert_pauser(\n caller: &signer, state_obj: Object\n ) {\n access_control::assert_role(\n state_obj, signer::address_of(caller), pauser_role()\n );\n }\n\n inline fun assert_unpauser(\n caller: &signer, state_obj: Object\n ) {\n access_control::assert_role(\n state_obj, signer::address_of(caller), unpauser_role()\n );\n }\n\n inline fun assert_freezer(\n caller: &signer, state_obj: Object\n ) {\n access_control::assert_role(\n state_obj, signer::address_of(caller), freezer_role()\n );\n }\n\n inline fun assert_unfreezer(\n caller: &signer, state_obj: Object\n ) {\n access_control::assert_role(\n state_obj, signer::address_of(caller), unfreezer_role()\n );\n }\n\n inline fun assert_recovery_role(\n caller: &signer, state_obj: Object\n ) {\n access_control::assert_role(\n state_obj, signer::address_of(caller), recovery_role()\n );\n }\n\n fun assert_bridge_minter_or_burner(\n caller: &signer, state_obj: Object\n ) {\n access_control::assert_role(\n state_obj,\n signer::address_of(caller),\n bridge_minter_or_burner_role()\n );\n }\n\n inline fun assert_burner_and_get_type(\n burner: address, state_obj: Object\n ): (bool, bool) {\n let is_bridge_burner =\n access_control::has_role(state_obj, burner, bridge_minter_or_burner_role());\n let is_native_burner = access_control::has_role(state_obj, burner, burner_role());\n\n assert!(is_bridge_burner || is_native_burner, E_ONLY_BURNER_OR_BRIDGE);\n\n (is_bridge_burner, is_native_burner)\n }\n\n fun assert_not_frozen(account: address, token_state: &TokenState) {\n assert!(!token_state.frozen_accounts.contains(&account), E_ACCOUNT_FROZEN);\n }\n\n fun assert_correct_asset(\n transfer_ref: &TransferRef, token_metadata: Object, store: Object\n ) {\n assert!(\n fungible_asset::transfer_ref_metadata(transfer_ref) == token_metadata,\n E_INVALID_ASSET\n );\n assert!(fungible_asset::store_metadata(store) == token_metadata, E_INVALID_STORE);\n }\n\n fun get_role(role_number: u8): Role {\n if (role_number == PAUSER_ROLE) {\n pauser_role()\n } else if (role_number == UNPAUSER_ROLE) {\n unpauser_role()\n } else if (role_number == FREEZER_ROLE) {\n freezer_role()\n } else if (role_number == UNFREEZER_ROLE) {\n unfreezer_role()\n } else if (role_number == MINTER_ROLE) {\n minter_role()\n } else if (role_number == BURNER_ROLE) {\n burner_role()\n } else if (role_number == BRIDGE_MINTER_OR_BURNER_ROLE) {\n bridge_minter_or_burner_role()\n } else if (role_number == RECOVERY_ROLE) {\n recovery_role()\n } else {\n abort E_INVALID_ROLE_NUMBER\n }\n }\n\n inline fun borrow_token_metadata_refs(): &TokenMetadataRefs {\n let token_metadata = token_metadata_internal();\n &TokenMetadataRefs[object::object_address(&token_metadata)]\n }\n\n public fun pauser_role(): Role {\n Role::PAUSER_ROLE\n }\n\n public fun unpauser_role(): Role {\n Role::UNPAUSER_ROLE\n }\n\n public fun freezer_role(): Role {\n Role::FREEZER_ROLE\n }\n\n public fun unfreezer_role(): Role {\n Role::UNFREEZER_ROLE\n }\n\n public fun minter_role(): Role {\n Role::MINTER_ROLE\n }\n\n public fun burner_role(): Role {\n Role::BURNER_ROLE\n }\n\n public fun bridge_minter_or_burner_role(): Role {\n Role::BRIDGE_MINTER_OR_BURNER_ROLE\n }\n\n public fun recovery_role(): Role {\n Role::RECOVERY_ROLE\n }\n\n // ====================== Ownable Functions ======================\n #[view]\n public fun owner(): address acquires TokenState {\n ownable::owner(&TokenState[token_state_address_internal()].ownable_state)\n }\n\n #[view]\n public fun has_pending_transfer(): bool acquires TokenState {\n ownable::has_pending_transfer(\n &TokenState[token_state_address_internal()].ownable_state\n )\n }\n\n #[view]\n public fun pending_transfer_from(): Option
acquires TokenState {\n ownable::pending_transfer_from(\n &TokenState[token_state_address_internal()].ownable_state\n )\n }\n\n #[view]\n public fun pending_transfer_to(): Option
acquires TokenState {\n ownable::pending_transfer_to(\n &TokenState[token_state_address_internal()].ownable_state\n )\n }\n\n #[view]\n public fun pending_transfer_accepted(): Option acquires TokenState {\n ownable::pending_transfer_accepted(\n &TokenState[token_state_address_internal()].ownable_state\n )\n }\n\n public entry fun transfer_ownership(caller: &signer, to: address) acquires TokenState {\n let state = &mut TokenState[token_state_address_internal()];\n ownable::transfer_ownership(caller, &mut state.ownable_state, to)\n }\n\n public entry fun accept_ownership(caller: &signer) acquires TokenState {\n let state = &mut TokenState[token_state_address_internal()];\n ownable::accept_ownership(caller, &mut state.ownable_state)\n }\n\n public entry fun execute_ownership_transfer(\n caller: &signer, to: address\n ) acquires TokenState {\n let state = &mut TokenState[token_state_address_internal()];\n ownable::execute_ownership_transfer(caller, &mut state.ownable_state, to)\n }\n}\n"; + +export const REGULATED_ACCESS_CONTROL_MOVE = `module regulated_token::access_control { + use std::event; + use std::ordered_map::{Self, OrderedMap}; + use std::object::{Self, Object}; + use std::signer; + use std::object::ConstructorRef; + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + struct AccessControlState has key, store { + /// Mapping from role to list of addresses that have the role + roles: OrderedMap>, + /// The admin address who can manage all roles + admin: address, + /// Pending admin for two-step admin transfer + pending_admin: address + } + + #[event] + struct RoleGranted has drop, store { + role: Role, + account: address, + sender: address + } + + #[event] + struct RoleRevoked has drop, store { + role: Role, + account: address, + sender: address + } + + #[event] + struct TransferAdmin has drop, store { + admin: address, + pending_admin: address + } + + #[event] + struct AcceptAdmin has drop, store { + old_admin: address, + new_admin: address + } + + /// Role state not initialized + const E_ROLE_STATE_NOT_INITIALIZED: u64 = 1; + /// Caller does not have the required role + const E_MISSING_ROLE: u64 = 2; + /// Caller is not the admin + const E_NOT_ADMIN: u64 = 3; + /// Cannot transfer admin to same address + const E_SAME_ADMIN: u64 = 4; + /// Index out of bounds + const E_INDEX_OUT_OF_BOUNDS: u64 = 5; + + public fun init( + constructor_ref: &ConstructorRef, admin: address + ) { + let obj_signer = object::generate_signer(constructor_ref); + move_to( + &obj_signer, + AccessControlState { + admin, + pending_admin: @0x0, + roles: ordered_map::new() + } + ); + } + + #[view] + public fun has_role( + state_obj: Object, account: address, role: Role + ): bool acquires AccessControlState { + let roles = &borrow(state_obj).roles; + roles.contains(&role) && roles.borrow(&role).contains(&account) + } + + #[view] + public fun get_role_members( + state_obj: Object, role: Role + ): vector
acquires AccessControlState { + let state = borrow(state_obj); + if (state.roles.contains(&role)) { + *state.roles.borrow(&role) + } else { + vector[] + } + } + + #[view] + public fun get_role_member_count( + state_obj: Object, role: Role + ): u64 acquires AccessControlState { + let roles = &borrow(state_obj).roles; + if (roles.contains(&role)) { + roles.borrow(&role).length() + } else { 0 } + } + + #[view] + public fun get_role_member( + state_obj: Object, role: Role, index: u64 + ): address acquires AccessControlState { + let roles = &borrow(state_obj).roles; + assert!(roles.contains(&role), E_MISSING_ROLE); + + let addresses = roles.borrow(&role); + assert!(index < addresses.length(), E_INDEX_OUT_OF_BOUNDS); + addresses[index] + } + + #[view] + public fun admin( + state_obj: Object + ): address acquires AccessControlState { + borrow(state_obj).admin + } + + #[view] + public fun pending_admin( + state_obj: Object + ): address acquires AccessControlState { + borrow(state_obj).pending_admin + } + + public entry fun batch_grant_role( + caller: &signer, + state_obj: Object, + role: Role, + accounts: vector
+ ) acquires AccessControlState { + if (accounts.length() == 0) return; + + let state = authorized_borrow_mut(caller, state_obj); + let sender = signer::address_of(caller); + + for (i in 0..accounts.length()) { + grant_role_internal(state, role, accounts[i], sender); + }; + } + + public entry fun grant_role( + caller: &signer, state_obj: Object, role: Role, account: address + ) acquires AccessControlState { + let state = authorized_borrow_mut(caller, state_obj); + let sender = signer::address_of(caller); + + grant_role_internal(state, role, account, sender); + } + + fun grant_role_internal( + state: &mut AccessControlState, + role: Role, + account: address, + sender: address + ) { + if (state.roles.contains(&role)) { + let addresses = state.roles.borrow_mut(&role); + if (!addresses.contains(&account)) { + addresses.push_back(account); + event::emit(RoleGranted { role, account, sender }); + } + } else { + state.roles.add(role, vector[account]); + event::emit(RoleGranted { role, account, sender }); + } + } + + public entry fun batch_revoke_role( + caller: &signer, + state_obj: Object, + role: Role, + accounts: vector
+ ) acquires AccessControlState { + if (accounts.length() == 0) return; + + let state = authorized_borrow_mut(caller, state_obj); + let sender = signer::address_of(caller); + + for (i in 0..accounts.length()) { + revoke_role_internal(state, role, accounts[i], sender); + }; + } + + public entry fun revoke_role( + caller: &signer, state_obj: Object, role: Role, account: address + ) acquires AccessControlState { + let state = authorized_borrow_mut(caller, state_obj); + let sender = signer::address_of(caller); + + revoke_role_internal(state, role, account, sender); + } + + fun revoke_role_internal( + state: &mut AccessControlState, + role: Role, + account: address, + sender: address + ) { + if (state.roles.contains(&role)) { + let addresses = state.roles.borrow_mut(&role); + let (found, index) = addresses.index_of(&account); + if (found) { + addresses.remove(index); + event::emit(RoleRevoked { role, account, sender }); + } + } + } + + public entry fun renounce_role( + caller: &signer, state_obj: Object, role: Role + ) acquires AccessControlState { + let state = borrow_mut(state_obj); + let caller_addr = signer::address_of(caller); + + if (state.roles.contains(&role)) { + let addresses = state.roles.borrow_mut(&role); + let (found, index) = addresses.index_of(&caller_addr); + if (found) { + addresses.remove(index); + event::emit(RoleRevoked { role, account: caller_addr, sender: caller_addr }); + }; + }; + } + + public fun assert_role( + state_obj: Object, caller: address, role: Role + ) acquires AccessControlState { + assert!( + has_role(state_obj, caller, role), + E_MISSING_ROLE + ); + } + + public entry fun transfer_admin( + admin: &signer, state_obj: Object, new_admin: address + ) acquires AccessControlState { + let state = authorized_borrow_mut(admin, state_obj); + assert!(signer::address_of(admin) != new_admin, E_SAME_ADMIN); + + state.pending_admin = new_admin; + + event::emit(TransferAdmin { admin: state.admin, pending_admin: new_admin }); + } + + public entry fun accept_admin( + pending_admin: &signer, state_obj: Object + ) acquires AccessControlState { + let state = borrow_mut(state_obj); + let pending_admin_addr = signer::address_of(pending_admin); + + assert!(pending_admin_addr == state.pending_admin, E_NOT_ADMIN); + + let old_admin = state.admin; + state.admin = state.pending_admin; + state.pending_admin = @0x0; + + event::emit(AcceptAdmin { old_admin, new_admin: state.admin }); + } + + inline fun authorized_borrow_mut( + caller: &signer, state_obj: Object + ): &mut AccessControlState { + let state = borrow_mut(state_obj); + assert!(state.admin == signer::address_of(caller), E_NOT_ADMIN); + state + } + + inline fun borrow_mut( + state_obj: Object + ): &mut AccessControlState { + let obj_addr = assert_exists(state_obj); + &mut AccessControlState[obj_addr] + } + + inline fun borrow(state_obj: Object) + : &AccessControlState { + let obj_addr = assert_exists(state_obj); + &AccessControlState[obj_addr] + } + + inline fun assert_exists( + state_obj: Object + ): address { + let obj_addr = object::object_address(&state_obj); + assert!( + exists>(obj_addr), + E_ROLE_STATE_NOT_INITIALIZED + ); + obj_addr + } +} +` + +export const REGULATED_OWNABLE_MOVE = `/// This module implements an Ownable component similar to Ownable2Step.sol for managing +/// object ownership. +/// +/// Due to Aptos's security model requiring the original owner's signer for 0x1::object::transfer, +/// this implementation uses a 3-step ownership transfer flow: +/// +/// 1. Initial owner calls transfer_ownership with the new owner's address +/// 2. Pending owner calls accept_ownership to confirm the transfer +/// 3. Initial owner calls execute_ownership_transfer to complete the transfer +/// +/// The execute_ownership_transfer function requires a signer in order to perform the +/// object transfer, while other operations only require the caller address to maintain the +/// principle of least privilege. +/// +/// Note that direct ownership transfers via 0x1::object::transfer are still possible. +/// This module handles such cases gracefully by reading the current owner directly +/// from the object. +module regulated_token::ownable { + use std::account; + use std::error; + use std::event::{Self, EventHandle}; + use std::object::{Self, Object, ObjectCore}; + use std::option::{Self, Option}; + use std::signer; + + struct OwnableState has store { + target_object: Object, + pending_transfer: Option, + ownership_transfer_requested_events: EventHandle, + ownership_transfer_accepted_events: EventHandle, + ownership_transferred_events: EventHandle + } + + struct PendingTransfer has store, drop { + from: address, + to: address, + accepted: bool + } + + const E_MUST_BE_PROPOSED_OWNER: u64 = 1; + const E_CANNOT_TRANSFER_TO_SELF: u64 = 2; + const E_ONLY_CALLABLE_BY_OWNER: u64 = 3; + const E_PROPOSED_OWNER_MISMATCH: u64 = 4; + const E_OWNER_CHANGED: u64 = 5; + const E_NO_PENDING_TRANSFER: u64 = 6; + const E_TRANSFER_NOT_ACCEPTED: u64 = 7; + const E_TRANSFER_ALREADY_ACCEPTED: u64 = 8; + + #[event] + struct OwnershipTransferRequested has store, drop { + from: address, + to: address + } + + #[event] + struct OwnershipTransferAccepted has store, drop { + from: address, + to: address + } + + #[event] + struct OwnershipTransferred has store, drop { + from: address, + to: address + } + + public fun new(event_account: &signer, object_address: address): OwnableState { + let new_state = OwnableState { + target_object: object::address_to_object(object_address), + pending_transfer: option::none(), + ownership_transfer_requested_events: account::new_event_handle(event_account), + ownership_transfer_accepted_events: account::new_event_handle(event_account), + ownership_transferred_events: account::new_event_handle(event_account) + }; + + new_state + } + + public fun owner(state: &OwnableState): address { + owner_internal(state) + } + + public fun has_pending_transfer(state: &OwnableState): bool { + state.pending_transfer.is_some() + } + + public fun pending_transfer_from(state: &OwnableState): Option
{ + state.pending_transfer.map_ref(|pending_transfer| pending_transfer.from) + } + + public fun pending_transfer_to(state: &OwnableState): Option
{ + state.pending_transfer.map_ref(|pending_transfer| pending_transfer.to) + } + + public fun pending_transfer_accepted(state: &OwnableState): Option { + state.pending_transfer.map_ref(|pending_transfer| pending_transfer.accepted) + } + + inline fun owner_internal(state: &OwnableState): address { + object::owner(state.target_object) + } + + public fun transfer_ownership( + caller: &signer, state: &mut OwnableState, to: address + ) { + let caller_address = signer::address_of(caller); + assert_only_owner_internal(caller_address, state); + assert!(caller_address != to, error::invalid_argument(E_CANNOT_TRANSFER_TO_SELF)); + + state.pending_transfer = option::some( + PendingTransfer { from: caller_address, to, accepted: false } + ); + + event::emit_event( + &mut state.ownership_transfer_requested_events, + OwnershipTransferRequested { from: caller_address, to } + ); + } + + public fun accept_ownership(caller: &signer, state: &mut OwnableState) { + let caller_address = signer::address_of(caller); + assert!( + state.pending_transfer.is_some(), + error::permission_denied(E_NO_PENDING_TRANSFER) + ); + + let current_owner = owner_internal(state); + let pending_transfer = state.pending_transfer.borrow_mut(); + + // check that the owner has not changed from a direct call to 0x1::object::transfer, + // in which case the transfer flow should be restarted. + assert!( + pending_transfer.from == current_owner, + error::permission_denied(E_OWNER_CHANGED) + ); + assert!( + pending_transfer.to == caller_address, + error::permission_denied(E_MUST_BE_PROPOSED_OWNER) + ); + assert!( + !pending_transfer.accepted, + error::invalid_state(E_TRANSFER_ALREADY_ACCEPTED) + ); + + pending_transfer.accepted = true; + + event::emit_event( + &mut state.ownership_transfer_accepted_events, + OwnershipTransferAccepted { from: pending_transfer.from, to: caller_address } + ); + } + + public fun execute_ownership_transfer( + caller: &signer, state: &mut OwnableState, to: address + ) { + let caller_address = signer::address_of(caller); + assert_only_owner_internal(caller_address, state); + + let current_owner = owner_internal(state); + let pending_transfer = state.pending_transfer.extract(); + + // check that the owner has not changed from a direct call to 0x1::object::transfer, + // in which case the transfer flow should be restarted. + assert!( + pending_transfer.from == current_owner, + error::permission_denied(E_OWNER_CHANGED) + ); + assert!( + pending_transfer.to == to, + error::permission_denied(E_PROPOSED_OWNER_MISMATCH) + ); + assert!( + pending_transfer.accepted, + error::invalid_state(E_TRANSFER_NOT_ACCEPTED) + ); + + object::transfer(caller, state.target_object, pending_transfer.to); + state.pending_transfer = option::none(); + + event::emit_event( + &mut state.ownership_transferred_events, + OwnershipTransferred { from: caller_address, to } + ); + } + + public fun assert_only_owner(caller: address, state: &OwnableState) { + assert_only_owner_internal(caller, state) + } + + inline fun assert_only_owner_internal( + caller: address, state: &OwnableState + ) { + assert!( + caller == owner_internal(state), + error::permission_denied(E_ONLY_CALLABLE_BY_OWNER) + ); + } + + public fun destroy(state: OwnableState) { + let OwnableState { + target_object: _, + pending_transfer: _, + ownership_transfer_requested_events, + ownership_transfer_accepted_events, + ownership_transferred_events + } = state; + + event::destroy_handle(ownership_transfer_requested_events); + event::destroy_handle(ownership_transfer_accepted_events); + event::destroy_handle(ownership_transferred_events); + } +} +` diff --git a/ccip-sdk/src/cct/aptos/common.ts b/ccip-sdk/src/cct/aptos/common.ts new file mode 100644 index 00000000..6b4998d0 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/common.ts @@ -0,0 +1,849 @@ +/** + * Shared helpers for Aptos CCT operations: Move compilation (deploy-time), + * pool-module discovery, initialization guards, pool-type detection, and + * object-address derivation. + * + * These are free functions operating over an {@link AptosChain} facade + * (`chain.provider` / `chain.logger`) rather than methods on a chain subclass. + * + * **Deploy helpers are Node.js/CLI-only** — Move compilation requires the + * `aptos` CLI plus filesystem and child-process access, so it cannot run in a + * browser. See the token/pool deploy ops for the backend-relay pattern. + * + * @packageDocumentation + */ + +/* eslint-disable import-x/no-nodejs-modules -- Node.js-only module: requires CLI compilation */ +import { Buffer } from 'buffer' +import { execSync } from 'node:child_process' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +/* eslint-enable import-x/no-nodejs-modules */ + +import { type Aptos, AccountAddress, createObjectAddress } from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../aptos/index.ts' +import { + CCIPGrantMintBurnAccessParamsInvalidError, + CCIPPoolDeployFailedError, + CCIPPoolDeployParamsInvalidError, + CCIPPoolNotInitializedError, + CCIPTokenDeployFailedError, + CCIPTokenDeployParamsInvalidError, + CCIPTokenPoolInfoNotFoundError, +} from '../../errors/index.ts' +import type { + AptosDeployPoolParams, + AptosDeployTokenParams, + AptosTokenModule, +} from '../../token-admin/types.ts' +import type { Logger } from '../../types.ts' + +/** Domain separator used by object_code_deployment::publish to derive object addresses. */ +const OBJECT_CODE_DEPLOYMENT_DOMAIN = 'aptos_framework::object_code_deployment' + +/** Seed used by init_module to create the token state named object. */ +const TOKEN_STATE_SEED = 'managed_token::managed_token::token_state' + +/** + * Computes the deterministic object address that `object_code_deployment::publish` + * will create for a given sender and their current sequence number. + * + * Uses the Aptos SDK's `createObjectAddress` with the same seed derivation as + * `object_code_deployment::object_seed`: `bcs(domain_separator) || bcs(seq + 1)`. + */ +export async function computeObjectAddress( + provider: Aptos, + sender: string, +): Promise<{ objectAddress: string; sequenceNumber: bigint }> { + const { sequence_number } = await provider.getAccountInfo({ accountAddress: sender }) + const sequenceNumber = BigInt(sequence_number) + + const domainBytes = Buffer.from(OBJECT_CODE_DEPLOYMENT_DOMAIN, 'utf8') + // BCS vector: ULEB128(length) + bytes + const uleb = Buffer.from([domainBytes.length]) + // BCS u64: 8 bytes little-endian; object_seed uses sequence_number + 1 + const seqBuf = Buffer.alloc(8) + seqBuf.writeBigUInt64LE(sequenceNumber + 1n) + + const seed = new Uint8Array(Buffer.concat([uleb, domainBytes, seqBuf])) + const objectAddress = createObjectAddress(AccountAddress.from(sender), seed).toString() + + return { objectAddress, sequenceNumber } +} + +/** + * Derives the fungible asset metadata address from the code object address and token symbol. + * + * Object hierarchy: code object → token state (TOKEN_STATE_SEED) → FA (symbol bytes). + */ +export function deriveFungibleAssetAddress(objectAddress: string, symbol: string): string { + // token state = createObjectAddress(code_object, TOKEN_STATE_SEED) + const tokenStateAddress = createObjectAddress( + AccountAddress.from(objectAddress), + new Uint8Array(Buffer.from(TOKEN_STATE_SEED, 'utf8')), + ) + + // FA metadata = createObjectAddress(token_state, symbol_bytes) + const faAddress = createObjectAddress( + tokenStateAddress, + new Uint8Array(Buffer.from(symbol, 'utf8')), + ) + + return faAddress.toString() +} + +/** + * Resolves the code object address for a managed or regulated token by walking + * the Aptos object ownership chain: FA metadata → owner (TokenState) → owner (code object). + * + * Uses the generic `0x1::object::ObjectCore` resource which stores the `owner` field + * for every Aptos object — no dependency on specific module view functions. + * + * @param provider - Aptos provider instance + * @param faMetadataAddress - Fungible asset metadata address (the user-facing token address) + * @returns Code object address (grandparent of the FA metadata) + * @throws {@link CCIPPoolDeployParamsInvalidError} if ownership chain cannot be resolved + */ +export async function resolveCodeObjectAddress( + provider: Aptos, + faMetadataAddress: string, +): Promise { + const resourceType = '0x1::object::ObjectCore' + + // Step 1: FA metadata → owner (TokenState) + let tokenStateOwner: string + try { + const faResource = await provider.getAccountResource<{ owner: string }>({ + accountAddress: faMetadataAddress, + resourceType, + }) + tokenStateOwner = faResource.owner + } catch { + throw new CCIPPoolDeployParamsInvalidError( + 'tokenAddress', + `cannot resolve object owner for FA metadata at ${faMetadataAddress} — is this a valid Aptos fungible asset?`, + ) + } + + // Step 2: TokenState → owner (code object) + let codeObjectAddress: string + try { + const stateResource = await provider.getAccountResource<{ owner: string }>({ + accountAddress: tokenStateOwner, + resourceType, + }) + codeObjectAddress = stateResource.owner + } catch { + throw new CCIPPoolDeployParamsInvalidError( + 'tokenAddress', + `cannot resolve code object from token state at ${tokenStateOwner} — unexpected object hierarchy`, + ) + } + + // Normalize to full 0x-prefixed 64-char hex (API may return short form) + return AccountAddress.from(codeObjectAddress).toString() +} + +/** + * Validates deploy parameters for Aptos ManagedToken. + * @throws {@link CCIPTokenDeployParamsInvalidError} on invalid params + */ +export function validateParams(params: AptosDeployTokenParams): void { + if (!params.name || params.name.trim().length === 0) { + throw new CCIPTokenDeployParamsInvalidError('name', 'must be non-empty') + } + if (!params.symbol || params.symbol.trim().length === 0) { + throw new CCIPTokenDeployParamsInvalidError('symbol', 'must be non-empty') + } + if (params.maxSupply !== undefined && params.maxSupply < 0n) { + throw new CCIPTokenDeployParamsInvalidError('maxSupply', 'must be non-negative') + } + if (params.initialSupply !== undefined && params.initialSupply < 0n) { + throw new CCIPTokenDeployParamsInvalidError('initialSupply', 'must be non-negative') + } + if ( + params.maxSupply !== undefined && + params.maxSupply > 0n && + params.initialSupply !== undefined && + params.initialSupply > params.maxSupply + ) { + throw new CCIPTokenDeployParamsInvalidError('initialSupply', 'exceeds maxSupply') + } +} + +/** + * Checks that the `aptos` CLI is available. + * @throws {@link CCIPTokenDeployFailedError} if not installed + */ +export function ensureAptosCli(): void { + try { + execSync('aptos --version', { stdio: 'ignore' }) + } catch { + throw new CCIPTokenDeployFailedError( + 'aptos CLI is not installed. Install from https://aptos.dev/tools/aptos-cli/', + ) + } +} + +/** + * Writes Move source files to a temp directory and compiles them + * with the object address as the named address. + * + * @param objectAddress - The deterministic object address where the module will be published + * @param logger - Logger instance + * @returns metadataBytes and byteCode extracted from the compiled JSON payload + */ +export async function compilePackage( + objectAddress: string, + logger: Logger, +): Promise<{ metadataBytes: string; byteCode: string[] }> { + const { MOVE_TOML, ALLOWLIST_MOVE, OWNABLE_MOVE, MANAGED_TOKEN_MOVE } = + await import('./bytecodes/managed_token.ts') + + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'managed-token-')) + const sourcesDir = path.join(tmpDir, 'sources') + fs.mkdirSync(sourcesDir, { recursive: true }) + + try { + // Write Move source files + fs.writeFileSync(path.join(tmpDir, 'Move.toml'), MOVE_TOML) + fs.writeFileSync(path.join(sourcesDir, 'allowlist.move'), ALLOWLIST_MOVE) + fs.writeFileSync(path.join(sourcesDir, 'ownable.move'), OWNABLE_MOVE) + fs.writeFileSync(path.join(sourcesDir, 'managed_token.move'), MANAGED_TOKEN_MOVE) + + const outputFile = path.join(tmpDir, 'compiled.json') + + const cmd = [ + 'aptos move build-publish-payload', + `--json-output-file ${outputFile}`, + `--package-dir ${tmpDir}`, + `--named-addresses managed_token=${objectAddress}`, + '--skip-fetch-latest-git-deps', + '--assume-yes', + ].join(' ') + + logger.debug('compilePackage: compiling ManagedToken Move package...') + execSync(cmd, { stdio: 'pipe' }) + + const compiled = JSON.parse(fs.readFileSync(outputFile, 'utf8')) as { + args: [{ value: string }, { value: string[] }] + } + const metadataBytes = compiled.args[0].value + const byteCode = compiled.args[1].value + + logger.debug('compilePackage: compiled', byteCode.length, 'modules') + return { metadataBytes, byteCode } + } finally { + // Clean up temp dir + fs.rmSync(tmpDir, { recursive: true, force: true }) + } +} + +/** + * Validates deploy parameters for Aptos pool. + * @throws {@link CCIPPoolDeployParamsInvalidError} on invalid params + */ +export function validatePoolParams(params: AptosDeployPoolParams): AptosTokenModule { + const poolType: string = params.poolType + if (poolType !== 'burn-mint' && poolType !== 'lock-release') { + throw new CCIPPoolDeployParamsInvalidError('poolType', "must be 'burn-mint' or 'lock-release'") + } + + const tokenModule: AptosTokenModule = params.tokenModule ?? 'managed' + const tokenModuleStr: string = tokenModule + if ( + tokenModuleStr !== 'managed' && + tokenModuleStr !== 'generic' && + tokenModuleStr !== 'regulated' + ) { + throw new CCIPPoolDeployParamsInvalidError( + 'tokenModule', + "must be 'managed', 'generic', or 'regulated'", + ) + } + + // managed and regulated only support burn-mint + if (tokenModule === 'managed' && poolType !== 'burn-mint') { + throw new CCIPPoolDeployParamsInvalidError( + 'poolType', + "managed tokens only support 'burn-mint' pools (managed_token_pool is inherently burn-mint)", + ) + } + if (tokenModule === 'regulated' && poolType !== 'burn-mint') { + throw new CCIPPoolDeployParamsInvalidError( + 'poolType', + "regulated tokens only support 'burn-mint' pools (regulated_token_pool is inherently burn-mint)", + ) + } + + if (!params.tokenAddress || params.tokenAddress.trim().length === 0) { + throw new CCIPPoolDeployParamsInvalidError('tokenAddress', 'must be non-empty') + } + if (!params.routerAddress || params.routerAddress.trim().length === 0) { + throw new CCIPPoolDeployParamsInvalidError('routerAddress', 'must be non-empty') + } + if (!params.mcmsAddress || params.mcmsAddress.trim().length === 0) { + throw new CCIPPoolDeployParamsInvalidError('mcmsAddress', 'must be non-empty') + } + + // regulated requires adminAddress + if ( + tokenModule === 'regulated' && + (!params.adminAddress || params.adminAddress.trim().length === 0) + ) { + throw new CCIPPoolDeployParamsInvalidError( + 'adminAddress', + "must be non-empty when tokenModule is 'regulated'", + ) + } + + return tokenModule +} + +/** + * Writes the ChainlinkCCIP dependency sources to the given directory. + * All pool types transitively depend on this package. + */ +export async function writeCcipDep(tmpDir: string): Promise { + const ccip = await import('./bytecodes/ccip.ts') + + const ccipDir = path.join(tmpDir, 'ccip') + const ccipSrc = path.join(ccipDir, 'sources') + const ccipUtilSrc = path.join(ccipSrc, 'util') + fs.mkdirSync(ccipUtilSrc, { recursive: true }) + + fs.writeFileSync(path.join(ccipDir, 'Move.toml'), ccip.CCIP_MOVE_TOML) + fs.writeFileSync(path.join(ccipSrc, 'allowlist.move'), ccip.CCIP_ALLOWLIST_MOVE) + fs.writeFileSync(path.join(ccipSrc, 'auth.move'), ccip.CCIP_AUTH_MOVE) + fs.writeFileSync(path.join(ccipSrc, 'client.move'), ccip.CCIP_CLIENT_MOVE) + fs.writeFileSync(path.join(ccipSrc, 'eth_abi.move'), ccip.CCIP_ETH_ABI_MOVE) + fs.writeFileSync(path.join(ccipSrc, 'fee_quoter.move'), ccip.CCIP_FEE_QUOTER_MOVE) + fs.writeFileSync(path.join(ccipSrc, 'merkle_proof.move'), ccip.CCIP_MERKLE_PROOF_MOVE) + fs.writeFileSync(path.join(ccipSrc, 'nonce_manager.move'), ccip.CCIP_NONCE_MANAGER_MOVE) + fs.writeFileSync(path.join(ccipSrc, 'ownable.move'), ccip.CCIP_OWNABLE_MOVE) + fs.writeFileSync( + path.join(ccipSrc, 'receiver_dispatcher.move'), + ccip.CCIP_RECEIVER_DISPATCHER_MOVE, + ) + fs.writeFileSync(path.join(ccipSrc, 'receiver_registry.move'), ccip.CCIP_RECEIVER_REGISTRY_MOVE) + fs.writeFileSync(path.join(ccipSrc, 'rmn_remote.move'), ccip.CCIP_RMN_REMOTE_MOVE) + fs.writeFileSync(path.join(ccipSrc, 'state_object.move'), ccip.CCIP_STATE_OBJECT_MOVE) + fs.writeFileSync( + path.join(ccipSrc, 'token_admin_dispatcher.move'), + ccip.CCIP_TOKEN_ADMIN_DISPATCHER_MOVE, + ) + fs.writeFileSync( + path.join(ccipSrc, 'token_admin_registry.move'), + ccip.CCIP_TOKEN_ADMIN_REGISTRY_MOVE, + ) + fs.writeFileSync(path.join(ccipUtilSrc, 'address.move'), ccip.CCIP_UTIL_ADDRESS_MOVE) +} + +/** + * Writes the ChainlinkManyChainMultisig (MCMS) dependency sources to the given directory. + * CCIP depends on this package, and all pool types transitively depend on CCIP. + */ +export async function writeMcmsDep(tmpDir: string): Promise { + const mcms = await import('./bytecodes/mcms.ts') + + const mcmsDir = path.join(tmpDir, 'mcms') + const mcmsSrc = path.join(mcmsDir, 'sources') + const mcmsUtilsSrc = path.join(mcmsSrc, 'utils') + fs.mkdirSync(mcmsUtilsSrc, { recursive: true }) + + fs.writeFileSync(path.join(mcmsDir, 'Move.toml'), mcms.MCMS_MOVE_TOML) + fs.writeFileSync(path.join(mcmsSrc, 'mcms.move'), mcms.MCMS_MCMS_MOVE) + fs.writeFileSync(path.join(mcmsSrc, 'mcms_registry.move'), mcms.MCMS_MCMS_REGISTRY_MOVE) + fs.writeFileSync(path.join(mcmsSrc, 'mcms_executor.move'), mcms.MCMS_MCMS_EXECUTOR_MOVE) + fs.writeFileSync(path.join(mcmsSrc, 'mcms_deployer.move'), mcms.MCMS_MCMS_DEPLOYER_MOVE) + fs.writeFileSync(path.join(mcmsSrc, 'mcms_account.move'), mcms.MCMS_MCMS_ACCOUNT_MOVE) + fs.writeFileSync(path.join(mcmsUtilsSrc, 'bcs_stream.move'), mcms.MCMS_UTILS_BCS_STREAM_MOVE) + fs.writeFileSync(path.join(mcmsUtilsSrc, 'params.move'), mcms.MCMS_UTILS_PARAMS_MOVE) +} + +/** + * Writes the token_pool shared dependency to the given directory. + * All pool types depend on this package. + */ +export async function writeTokenPoolDep(tmpDir: string): Promise { + const { + TOKEN_POOL_MOVE_TOML, + TOKEN_POOL_MOVE, + TOKEN_POOL_OWNABLE_MOVE, + RATE_LIMITER_MOVE, + TOKEN_POOL_RATE_LIMITER_MOVE, + } = await import('./bytecodes/managed_token_pool.ts') + + const tokenPoolDir = path.join(tmpDir, 'token_pool') + const tokenPoolSourcesDir = path.join(tokenPoolDir, 'sources') + fs.mkdirSync(tokenPoolSourcesDir, { recursive: true }) + + fs.writeFileSync(path.join(tokenPoolDir, 'Move.toml'), TOKEN_POOL_MOVE_TOML) + fs.writeFileSync(path.join(tokenPoolSourcesDir, 'token_pool.move'), TOKEN_POOL_MOVE) + fs.writeFileSync(path.join(tokenPoolSourcesDir, 'ownable.move'), TOKEN_POOL_OWNABLE_MOVE) + fs.writeFileSync(path.join(tokenPoolSourcesDir, 'rate_limiter.move'), RATE_LIMITER_MOVE) + fs.writeFileSync( + path.join(tokenPoolSourcesDir, 'token_pool_rate_limiter.move'), + TOKEN_POOL_RATE_LIMITER_MOVE, + ) +} + +/** + * Writes Move source files for the specified pool type to the temp directory. + * + * @param tmpDir - Temp directory to write sources into + * @param tokenModule - Token module variant ('managed' | 'generic' | 'regulated') + * @param poolType - Pool type ('burn-mint' | 'lock-release') + * @returns The path to the pool package directory (to pass to `aptos move build-publish-payload`) + */ +export async function writePoolSources( + tmpDir: string, + tokenModule: AptosTokenModule, + poolType: string, +): Promise { + if (tokenModule === 'managed') { + const { POOL_MOVE_TOML, MANAGED_TOKEN_POOL_MOVE } = + await import('./bytecodes/managed_token_pool.ts') + const { MOVE_TOML, ALLOWLIST_MOVE, OWNABLE_MOVE, MANAGED_TOKEN_MOVE } = + await import('./bytecodes/managed_token.ts') + + // managed_token_pool package + const poolDir = path.join(tmpDir, 'managed_token_pool') + const poolSrc = path.join(poolDir, 'sources') + fs.mkdirSync(poolSrc, { recursive: true }) + fs.writeFileSync(path.join(poolDir, 'Move.toml'), POOL_MOVE_TOML) + fs.writeFileSync(path.join(poolSrc, 'managed_token_pool.move'), MANAGED_TOKEN_POOL_MOVE) + + // managed_token dependency + const mtDir = path.join(tmpDir, 'managed_token') + const mtSrc = path.join(mtDir, 'sources') + fs.mkdirSync(mtSrc, { recursive: true }) + fs.writeFileSync(path.join(mtDir, 'Move.toml'), MOVE_TOML) + fs.writeFileSync(path.join(mtSrc, 'allowlist.move'), ALLOWLIST_MOVE) + fs.writeFileSync(path.join(mtSrc, 'ownable.move'), OWNABLE_MOVE) + fs.writeFileSync(path.join(mtSrc, 'managed_token.move'), MANAGED_TOKEN_MOVE) + + return poolDir + } + + if (tokenModule === 'generic') { + if (poolType === 'burn-mint') { + const { BURN_MINT_POOL_MOVE_TOML, BURN_MINT_TOKEN_POOL_MOVE } = + await import('./bytecodes/burn_mint_token_pool.ts') + + const poolDir = path.join(tmpDir, 'burn_mint_token_pool') + const poolSrc = path.join(poolDir, 'sources') + fs.mkdirSync(poolSrc, { recursive: true }) + fs.writeFileSync(path.join(poolDir, 'Move.toml'), BURN_MINT_POOL_MOVE_TOML) + fs.writeFileSync(path.join(poolSrc, 'burn_mint_token_pool.move'), BURN_MINT_TOKEN_POOL_MOVE) + + return poolDir + } + + // lock-release + const { LOCK_RELEASE_POOL_MOVE_TOML, LOCK_RELEASE_TOKEN_POOL_MOVE } = + await import('./bytecodes/lock_release_token_pool.ts') + + const poolDir = path.join(tmpDir, 'lock_release_token_pool') + const poolSrc = path.join(poolDir, 'sources') + fs.mkdirSync(poolSrc, { recursive: true }) + fs.writeFileSync(path.join(poolDir, 'Move.toml'), LOCK_RELEASE_POOL_MOVE_TOML) + fs.writeFileSync( + path.join(poolSrc, 'lock_release_token_pool.move'), + LOCK_RELEASE_TOKEN_POOL_MOVE, + ) + + return poolDir + } + + // regulated + const { REGULATED_POOL_MOVE_TOML, REGULATED_TOKEN_POOL_MOVE } = + await import('./bytecodes/regulated_token_pool.ts') + const { + REGULATED_TOKEN_MOVE_TOML, + REGULATED_TOKEN_MOVE, + REGULATED_ACCESS_CONTROL_MOVE, + REGULATED_OWNABLE_MOVE, + } = await import('./bytecodes/regulated_token_pool.ts') + + // regulated_token_pool package + const poolDir = path.join(tmpDir, 'regulated_token_pool') + const poolSrc = path.join(poolDir, 'sources') + fs.mkdirSync(poolSrc, { recursive: true }) + fs.writeFileSync(path.join(poolDir, 'Move.toml'), REGULATED_POOL_MOVE_TOML) + fs.writeFileSync(path.join(poolSrc, 'regulated_token_pool.move'), REGULATED_TOKEN_POOL_MOVE) + + // regulated_token dependency + const rtDir = path.join(tmpDir, 'regulated_token') + const rtSrc = path.join(rtDir, 'sources') + fs.mkdirSync(rtSrc, { recursive: true }) + fs.writeFileSync(path.join(rtDir, 'Move.toml'), REGULATED_TOKEN_MOVE_TOML) + fs.writeFileSync(path.join(rtSrc, 'regulated_token.move'), REGULATED_TOKEN_MOVE) + fs.writeFileSync(path.join(rtSrc, 'access_control.move'), REGULATED_ACCESS_CONTROL_MOVE) + fs.writeFileSync(path.join(rtSrc, 'ownable.move'), REGULATED_OWNABLE_MOVE) + + return poolDir +} + +/** + * Resolves the named addresses for Move compilation. + * + * CCIPTokenPool is published to `tokenPoolObjectAddress` (separate object). + * The pool itself is published to `poolObjectAddress`. + * + * For managed/regulated pools, `tokenCodeObjectAddress` is the code object resolved + * from the FA metadata via on-chain ownership traversal. For generic pools it is unused + * — `params.tokenAddress` (the FA metadata) is passed directly as the local token address. + * + * @param tokenPoolObjectAddress - Object address for the shared CCIPTokenPool package + * @param poolObjectAddress - Object address for the pool package itself + * @param tokenModule - Token module variant ('managed' | 'generic' | 'regulated') + * @param poolType - Pool type ('burn-mint' | 'lock-release') + * @param params - Pool deploy parameters + * @param tokenCodeObjectAddress - Code object address for managed/regulated tokens + * @returns Named-address map for `--named-addresses` + */ +export function resolveNamedAddresses( + tokenPoolObjectAddress: string, + poolObjectAddress: string, + tokenModule: AptosTokenModule, + poolType: string, + params: AptosDeployPoolParams, + tokenCodeObjectAddress?: string, +): Record { + const base: Record = { + ccip: params.routerAddress, + ccip_token_pool: tokenPoolObjectAddress, + mcms: params.mcmsAddress, + // mcms_owner is the account that created the MCMS resource account. + // Set to 0x0 — only needed at MCMS package init time, not for pool deploys. + mcms_owner: '0x0', + // mcms_register_entrypoints is a compile-time feature flag (0x0 = disabled, 0x1 = enabled). + // When enabled, init_module registers MCMS entrypoints for multisig control. + // MCMS is internal Chainlink infrastructure — external users always disable it. + mcms_register_entrypoints: '0x0', + } + + if (tokenModule === 'managed') { + return { + ...base, + managed_token_pool: poolObjectAddress, + managed_token: tokenCodeObjectAddress!, + } + } + + if (tokenModule === 'generic') { + if (poolType === 'burn-mint') { + return { + ...base, + burn_mint_token_pool: poolObjectAddress, + burn_mint_local_token: params.tokenAddress, + } + } + // lock-release + return { + ...base, + lock_release_token_pool: poolObjectAddress, + lock_release_local_token: params.tokenAddress, + } + } + + // regulated + return { + ...base, + regulated_token_pool: poolObjectAddress, + regulated_token: tokenCodeObjectAddress!, + admin: params.adminAddress!, + } +} + +/** Human-readable pool type label for log messages. */ +export function poolLabel(tokenModule: AptosTokenModule, poolType: string): string { + if (tokenModule === 'managed') return 'ManagedTokenPool' + if (tokenModule === 'regulated') return 'RegulatedTokenPool' + return poolType === 'burn-mint' ? 'BurnMintTokenPool' : 'LockReleaseTokenPool' +} + +/** + * Compiles a Move package and returns the metadata + bytecode from the publish payload. + * + * Uses `aptos move build-publish-payload` with `--skip-fetch-latest-git-deps` + * to ensure compiled bytecode matches what's deployed on-chain. + * + * @param packageDir - Path to the package to compile + * @param namedAddresses - All named addresses for compilation + * @param label - Human-readable label for log messages + * @param logger - Logger instance + * @returns metadataBytes and byteCode from the compiled payload + */ +export function compileMovePackage( + packageDir: string, + namedAddresses: Record, + label: string, + logger: Logger, +): { metadataBytes: string; byteCode: string[] } { + const outputFile = path.join(path.dirname(packageDir), `${label}-compiled.json`) + + const namedAddressesStr = Object.entries(namedAddresses) + .map(([k, v]) => `${k}=${v}`) + .join(',') + + const cmd = [ + 'aptos move build-publish-payload', + `--json-output-file ${outputFile}`, + `--package-dir ${packageDir}`, + `--named-addresses ${namedAddressesStr}`, + '--skip-fetch-latest-git-deps', + '--assume-yes', + ].join(' ') + + logger.debug(`compileMovePackage: compiling ${label}...`) + logger.debug(`compileMovePackage: cmd = ${cmd}`) + const result = execSync(cmd, { stdio: 'pipe' }) + const output = result.toString().trim() + // aptos CLI may exit 0 but return an error in JSON — check for it + if (output.includes('"Error"')) { + throw new CCIPPoolDeployFailedError(`Move compilation failed for ${label}:\n${output}`) + } + + const compiled = JSON.parse(fs.readFileSync(outputFile, 'utf8')) as { + args: [{ value: string }, { value: string[] }] + } + + logger.debug(`compileMovePackage: ${label} compiled`, compiled.args[1].value.length, 'modules') + return { metadataBytes: compiled.args[0].value, byteCode: compiled.args[1].value } +} + +/** + * Writes all shared dependencies and compiles both the CCIPTokenPool package and the + * pool-specific package. Returns publish payloads for both. + * + * Aptos Move `build-publish-payload` only includes modules from the TOP-LEVEL package + * in the output — local dependency modules are NOT included. Since CCIPTokenPool is a + * local dependency of every pool type, it must be compiled and published as a SEPARATE + * object before the pool itself. + * + * Deploy flow (2 publish transactions): + * 1. Publish CCIPTokenPool (4 modules: token_pool, ownable, rate_limiter, token_pool_rate_limiter) + * 2. Publish the pool (1 module), referencing the CCIPTokenPool object from step 1 + * + * @param tokenPoolObjectAddress - Object address for the shared CCIPTokenPool package + * @param poolObjectAddress - Object address for the pool package itself + * @param tokenModule - Token module variant ('managed' | 'generic' | 'regulated') + * @param poolType - Pool type ('burn-mint' | 'lock-release') + * @param namedAddresses - All named addresses for compilation + * @param logger - Logger instance + * @returns Two compiled payloads: tokenPool and pool + */ +export async function compilePoolPackages( + tokenPoolObjectAddress: string, + poolObjectAddress: string, + tokenModule: AptosTokenModule, + poolType: string, + namedAddresses: Record, + logger: Logger, +): Promise<{ + tokenPool: { metadataBytes: string; byteCode: string[] } + pool: { metadataBytes: string; byteCode: string[] } +}> { + const label = poolLabel(tokenModule, poolType) + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `aptos-pool-${tokenModule}-`)) + + try { + // Write all transitive dependencies as local packages. + // Order: mcms (leaf) → ccip (depends on mcms) → token_pool (depends on ccip) + await writeMcmsDep(tmpDir) + await writeCcipDep(tmpDir) + await writeTokenPoolDep(tmpDir) + + // Write pool-specific sources and get the pool package directory + const poolDir = await writePoolSources(tmpDir, tokenModule, poolType) + + // Step 1: Compile CCIPTokenPool (4 modules) + const tokenPoolDir = path.join(tmpDir, 'token_pool') + const tokenPool = compileMovePackage(tokenPoolDir, namedAddresses, 'CCIPTokenPool', logger) + + // Step 2: Compile pool package (1 module) — references the already-compiled token_pool + const pool = compileMovePackage(poolDir, namedAddresses, label, logger) + + return { tokenPool, pool } + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } +} + +/** + * Auto-discovers the pool module name from a pool address by querying + * account modules and filtering for `*token_pool`. + * + * @param chain - Aptos chain facade + * @param poolAddress - Pool object address (hex string) + * @returns The pool module name (e.g., 'managed_token_pool') + * @throws {@link CCIPTokenPoolInfoNotFoundError} if no pool module found + */ +export async function discoverPoolModule(chain: AptosChain, poolAddress: string): Promise { + const modulesNames = (await chain._getAccountModulesNames(poolAddress)) + .reverse() + .filter((name) => name.endsWith('token_pool')) + + if (modulesNames.length === 0) { + throw new CCIPTokenPoolInfoNotFoundError(poolAddress) + } + + // Try each module until one responds to get_token view + for (const name of modulesNames) { + try { + await chain.provider.view<[string]>({ + payload: { + function: `${poolAddress}::${name}::get_token`, + }, + }) + return name + } catch { + continue + } + } + + // If none respond, use the first one (best effort) + return modulesNames[0]! +} + +/** + * Checks whether an Aptos pool is initialized by attempting to call `get_token`. + * + * Generic pools (`burn_mint_token_pool`, `lock_release_token_pool`) have a + * two-phase lifecycle: `init_module()` creates a `*Deployment` struct, but the + * pool is not usable until `initialize()` creates the `*State` with ownership + * and pool functionality. The `get_token` view function only succeeds when + * the `*State` resource exists. + * + * Managed and regulated pools initialize fully in `init_module()`, so this + * always returns `true` for them. + * + * @param chain - Aptos chain facade + * @param poolAddress - Pool object address (hex string) + * @param poolModule - Pool module name (from `discoverPoolModule`) + * @returns `true` if pool state is initialized, `false` otherwise + */ +export async function isPoolInitialized( + chain: AptosChain, + poolAddress: string, + poolModule: string, +): Promise { + try { + await chain.provider.view<[string]>({ + payload: { + function: `${poolAddress}::${poolModule}::get_token`, + }, + }) + return true + } catch { + return false + } +} + +/** + * Guards pool operations by checking initialization status. + * + * Throws {@link CCIPPoolNotInitializedError} if the pool is not initialized, + * providing a clear error message instead of a cryptic `MutBorrowGlobal` failure. + * + * @param chain - Aptos chain facade + * @param poolAddress - Pool object address (hex string) + * @param poolModule - Pool module name + * @throws {@link CCIPPoolNotInitializedError} if pool is not initialized + */ +export async function ensurePoolInitialized( + chain: AptosChain, + poolAddress: string, + poolModule: string, +): Promise { + const initialized = await isPoolInitialized(chain, poolAddress, poolModule) + if (!initialized) { + throw new CCIPPoolNotInitializedError(poolAddress) + } +} + +/** + * Detects the pool type from a pool address, using the module name first and + * falling back to the `type_and_version` view function. + * + * @param chain - Aptos chain facade + * @param poolAddress - Pool object address (hex string) + * @returns The detected pool `type` and its `module` name + * @throws {@link CCIPGrantMintBurnAccessParamsInvalidError} if the pool type is unknown + */ +export async function detectPoolType( + chain: AptosChain, + poolAddress: string, +): Promise<{ + type: 'managed' | 'burn_mint' | 'regulated' | 'lock_release' + module: string +}> { + const poolModule = await discoverPoolModule(chain, poolAddress) + + if (poolModule === 'managed_token_pool') return { type: 'managed', module: poolModule } + if (poolModule === 'burn_mint_token_pool') return { type: 'burn_mint', module: poolModule } + if (poolModule === 'regulated_token_pool') return { type: 'regulated', module: poolModule } + if (poolModule === 'lock_release_token_pool') return { type: 'lock_release', module: poolModule } + + // Fallback: try type_and_version view function + try { + const [typeName] = await chain.provider.view<[string]>({ + payload: { + function: `${poolAddress}::${poolModule}::type_and_version`, + }, + }) + if (typeName.includes('ManagedTokenPool')) return { type: 'managed', module: poolModule } + if (typeName.includes('BurnMintTokenPool')) return { type: 'burn_mint', module: poolModule } + if (typeName.includes('RegulatedTokenPool')) return { type: 'regulated', module: poolModule } + if (typeName.includes('LockReleaseTokenPool')) + return { type: 'lock_release', module: poolModule } + } catch { + // type_and_version not available, fall through + } + + throw new CCIPGrantMintBurnAccessParamsInvalidError( + 'authority', + `unknown pool type at ${poolAddress}: module=${poolModule}`, + ) +} + +/** + * Resolves the token code object address from a Fungible Asset metadata address. + * + * Object hierarchy: code object → token state → FA metadata. + * The FA metadata object's owner is the token state object, whose owner is + * the code object. + * + * @param chain - Aptos chain facade + * @param tokenAddress - FA metadata address (hex string) + * @returns Code object address + */ +export async function resolveTokenCodeObject( + chain: AptosChain, + tokenAddress: string, +): Promise { + // FA metadata → owned by token state → owned by code object + // First get the owner of FA metadata (token state object) + const [tokenStateOwner] = await chain.provider.view<[string]>({ + payload: { + function: '0x1::object::owner', + typeArguments: ['0x1::fungible_asset::Metadata'], + functionArguments: [tokenAddress], + }, + }) + + // Then get the owner of the token state (code object) + const [codeObject] = await chain.provider.view<[string]>({ + payload: { + function: '0x1::object::owner', + typeArguments: ['0x1::object::ObjectCore'], + functionArguments: [tokenStateOwner], + }, + }) + + return codeObject +} diff --git a/ccip-sdk/src/cct/aptos/index.test.ts b/ccip-sdk/src/cct/aptos/index.test.ts new file mode 100644 index 00000000..a08fd037 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/index.test.ts @@ -0,0 +1,85 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { AptosTokenManager } from './index.ts' +import { AptosChain } from '../../aptos/index.ts' + +function stubChain(): AptosChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: {}, + } as unknown as AptosChain +} + +describe('AptosTokenManager (cct/aptos)', () => { + it('fromChain exposes flat Aptos CCT operations', () => { + const chain = stubChain() + const cct = AptosTokenManager.fromChain(chain) + assert.equal(cct.chain, chain) + assert.equal(cct.provider, chain.provider) + // token / token-pool + assert.equal(typeof cct.generateUnsignedDeployToken, 'function') + assert.equal(typeof cct.deployToken, 'function') + assert.equal(typeof cct.generateUnsignedDeployPool, 'function') + assert.equal(typeof cct.deployPool, 'function') + assert.equal(typeof cct.generateUnsignedGrantMintBurnAccess, 'function') + assert.equal(typeof cct.grantMintBurnAccess, 'function') + assert.equal(typeof cct.generateUnsignedRevokeMintBurnAccess, 'function') + assert.equal(typeof cct.revokeMintBurnAccess, 'function') + // token-admin-registry + assert.equal(typeof cct.generateUnsignedProposeAdminRole, 'function') + assert.equal(typeof cct.proposeAdminRole, 'function') + assert.equal(typeof cct.generateUnsignedAcceptAdminRole, 'function') + assert.equal(typeof cct.acceptAdminRole, 'function') + assert.equal(typeof cct.generateUnsignedTransferAdminRole, 'function') + assert.equal(typeof cct.transferAdminRole, 'function') + assert.equal(typeof cct.generateUnsignedSetPool, 'function') + assert.equal(typeof cct.setPool, 'function') + // pool + assert.equal(typeof cct.generateUnsignedApplyChainUpdates, 'function') + assert.equal(typeof cct.applyChainUpdates, 'function') + assert.equal(typeof cct.generateUnsignedAppendRemotePoolAddresses, 'function') + assert.equal(typeof cct.appendRemotePoolAddresses, 'function') + assert.equal(typeof cct.generateUnsignedRemoveRemotePoolAddresses, 'function') + assert.equal(typeof cct.removeRemotePoolAddresses, 'function') + assert.equal(typeof cct.generateUnsignedDeleteChainConfig, 'function') + assert.equal(typeof cct.deleteChainConfig, 'function') + assert.equal(typeof cct.generateUnsignedSetChainRateLimiterConfig, 'function') + assert.equal(typeof cct.setChainRateLimiterConfig, 'function') + assert.equal(typeof cct.generateUnsignedSetRateLimitAdmin, 'function') + assert.equal(typeof cct.setRateLimitAdmin, 'function') + assert.equal(typeof cct.generateUnsignedTransferOwnership, 'function') + assert.equal(typeof cct.transferOwnership, 'function') + assert.equal(typeof cct.generateUnsignedAcceptOwnership, 'function') + assert.equal(typeof cct.acceptOwnership, 'function') + assert.equal(typeof cct.generateUnsignedExecuteOwnershipTransfer, 'function') + assert.equal(typeof cct.executeOwnershipTransfer, 'function') + // read + assert.equal(typeof cct.getMintBurnRoles, 'function') + }) + + it('creates from an Aptos provider', async (t) => { + const chain = stubChain() + const provider = {} as unknown as Parameters[0] + t.mock.method(AptosChain, 'fromProvider', async (arg: unknown) => { + assert.equal(arg, provider) + return chain + }) + + const cct = await AptosTokenManager.fromProvider(provider) + + assert.equal(cct.chain, chain) + }) + + it('creates from an RPC URL', async (t) => { + const chain = stubChain() + t.mock.method(AptosChain, 'fromUrl', async (url: string) => { + assert.equal(url, 'http://localhost:8080') + return chain + }) + + const cct = await AptosTokenManager.fromUrl('http://localhost:8080') + + assert.equal(cct.chain, chain) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/index.ts b/ccip-sdk/src/cct/aptos/index.ts new file mode 100644 index 00000000..339f3d88 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/index.ts @@ -0,0 +1,410 @@ +/** + * Aptos Cross-Chain Token (CCT) admin operations. + * + * @packageDocumentation + */ + +import type { Aptos } from '@aptos-labs/ts-sdk' + +import { AptosChain } from '../../aptos/index.ts' +import type { ChainContext } from '../../chain.ts' +import type { ChainFamily } from '../../networks.ts' +import { TokenManager } from '../token-manager.ts' +import { + type ExecuteAcceptOwnershipParams, + type ExecuteAcceptOwnershipResult, + type ExecuteAppendRemotePoolAddressesParams, + type ExecuteAppendRemotePoolAddressesResult, + type ExecuteApplyChainUpdatesParams, + type ExecuteApplyChainUpdatesResult, + type ExecuteDeleteChainConfigParams, + type ExecuteDeleteChainConfigResult, + type ExecuteExecuteOwnershipTransferParams, + type ExecuteExecuteOwnershipTransferResult, + type ExecuteRemoveRemotePoolAddressesParams, + type ExecuteRemoveRemotePoolAddressesResult, + type ExecuteSetChainRateLimiterConfigParams, + type ExecuteSetChainRateLimiterConfigResult, + type ExecuteSetRateLimitAdminParams, + type ExecuteSetRateLimitAdminResult, + type ExecuteTransferOwnershipParams, + type ExecuteTransferOwnershipResult, + type GenerateAcceptOwnershipParams, + type GenerateAcceptOwnershipResult, + type GenerateAppendRemotePoolAddressesParams, + type GenerateAppendRemotePoolAddressesResult, + type GenerateApplyChainUpdatesParams, + type GenerateApplyChainUpdatesResult, + type GenerateDeleteChainConfigParams, + type GenerateDeleteChainConfigResult, + type GenerateExecuteOwnershipTransferParams, + type GenerateExecuteOwnershipTransferResult, + type GenerateRemoveRemotePoolAddressesParams, + type GenerateRemoveRemotePoolAddressesResult, + type GenerateSetChainRateLimiterConfigParams, + type GenerateSetChainRateLimiterConfigResult, + type GenerateSetRateLimitAdminParams, + type GenerateSetRateLimitAdminResult, + type GenerateTransferOwnershipParams, + type GenerateTransferOwnershipResult, + AcceptOwnership, + AppendRemotePoolAddresses, + ApplyChainUpdates, + DeleteChainConfig, + ExecuteOwnershipTransfer, + RemoveRemotePoolAddresses, + SetChainRateLimiterConfig, + SetRateLimitAdmin, + TransferOwnership, +} from './pool/operations/index.ts' +import { type MintBurnRolesResult, getMintBurnRoles } from './token/get-mint-burn-roles.ts' +import { + type ExecuteDeployTokenParams, + type ExecuteDeployTokenResult, + type ExecuteGrantMintBurnAccessParams, + type ExecuteGrantMintBurnAccessResult, + type ExecuteRevokeMintBurnAccessParams, + type ExecuteRevokeMintBurnAccessResult, + type GenerateDeployTokenParams, + type GenerateDeployTokenResult, + type GenerateGrantMintBurnAccessParams, + type GenerateGrantMintBurnAccessResult, + type GenerateRevokeMintBurnAccessParams, + type GenerateRevokeMintBurnAccessResult, + DeployToken, + GrantMintBurnAccess, + RevokeMintBurnAccess, +} from './token/operations/index.ts' +import { + type ExecuteAcceptAdminRoleParams, + type ExecuteAcceptAdminRoleResult, + type ExecuteProposeAdminRoleParams, + type ExecuteProposeAdminRoleResult, + type ExecuteSetPoolParams, + type ExecuteSetPoolResult, + type ExecuteTransferAdminRoleParams, + type ExecuteTransferAdminRoleResult, + type GenerateAcceptAdminRoleParams, + type GenerateAcceptAdminRoleResult, + type GenerateProposeAdminRoleParams, + type GenerateProposeAdminRoleResult, + type GenerateSetPoolParams, + type GenerateSetPoolResult, + type GenerateTransferAdminRoleParams, + type GenerateTransferAdminRoleResult, + AcceptAdminRole, + ProposeAdminRole, + SetPool, + TransferAdminRole, +} from './token-admin-registry/operations/index.ts' +import { + type ExecuteDeployPoolParams, + type ExecuteDeployPoolResult, + type GenerateDeployPoolParams, + type GenerateDeployPoolResult, + DeployPool, +} from './token-pool/operations/index.ts' + +/** CCT admin facade for Aptos. */ +export class AptosTokenManager extends TokenManager { + readonly chain: AptosChain + readonly #proposeAdminRole = new ProposeAdminRole() + readonly #acceptAdminRole = new AcceptAdminRole() + readonly #transferAdminRole = new TransferAdminRole() + readonly #setPool = new SetPool() + readonly #applyChainUpdates = new ApplyChainUpdates() + readonly #appendRemotePoolAddresses = new AppendRemotePoolAddresses() + readonly #removeRemotePoolAddresses = new RemoveRemotePoolAddresses() + readonly #deleteChainConfig = new DeleteChainConfig() + readonly #setChainRateLimiterConfig = new SetChainRateLimiterConfig() + readonly #setRateLimitAdmin = new SetRateLimitAdmin() + readonly #transferOwnership = new TransferOwnership() + readonly #acceptOwnership = new AcceptOwnership() + readonly #executeOwnershipTransfer = new ExecuteOwnershipTransfer() + readonly #deployPool = new DeployPool() + readonly #deployToken = new DeployToken() + readonly #grantMintBurnAccess = new GrantMintBurnAccess() + readonly #revokeMintBurnAccess = new RevokeMintBurnAccess() + + /** Creates an Aptos CCT manager for an existing chain. */ + constructor(chain: AptosChain) { + super() + this.chain = chain + } + + /** Wraps an existing {@link AptosChain}. */ + static fromChain(chain: AptosChain): AptosTokenManager { + return new AptosTokenManager(chain) + } + + /** Creates from an Aptos SDK provider. */ + static async fromProvider(provider: Aptos, ctx?: ChainContext): Promise { + return new AptosTokenManager(await AptosChain.fromProvider(provider, ctx)) + } + + /** Creates from an RPC URL. */ + static async fromUrl(url: string, ctx?: ChainContext): Promise { + return new AptosTokenManager(await AptosChain.fromUrl(url, ctx)) + } + + /** Provider of the underlying chain. */ + get provider(): Aptos { + return this.chain.provider + } + + /** + * Builds an unsigned Aptos `deployToken` transaction (managed fungible asset). + * + * @example + * ```ts + * const cct = AptosTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedDeployToken({ + * sender, + * name: 'My Token', + * symbol: 'MTK', + * decimals: 8, + * }) + * ``` + */ + generateUnsignedDeployToken(opts: GenerateDeployTokenParams): Promise { + return this.#deployToken.generate(this.chain, opts) + } + + /** + * Deploys an Aptos managed token; returns the transaction hash and token address. + * + * @example + * ```ts + * const cct = AptosTokenManager.fromChain(chain) + * const { hash, tokenAddress } = await cct.deployToken({ + * wallet, + * name: 'My Token', + * symbol: 'MTK', + * decimals: 8, + * }) + * ``` + */ + deployToken(opts: ExecuteDeployTokenParams): Promise { + return this.#deployToken.execute(this.chain, opts) + } + + /** + * Builds an unsigned Aptos `deployPool` transaction (token pool object). + * + * @example + * ```ts + * const cct = AptosTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedDeployPool({ + * sender, + * tokenAddress, + * }) + * ``` + */ + generateUnsignedDeployPool(opts: GenerateDeployPoolParams): Promise { + return this.#deployPool.generate(this.chain, opts) + } + + /** + * Deploys an Aptos token pool; returns the transaction hash and pool address. + * + * @example + * ```ts + * const cct = AptosTokenManager.fromChain(chain) + * const { hash, poolAddress } = await cct.deployPool({ + * wallet, + * tokenAddress, + * }) + * ``` + */ + deployPool(opts: ExecuteDeployPoolParams): Promise { + return this.#deployPool.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos `grantMintBurnAccess` transaction. */ + generateUnsignedGrantMintBurnAccess( + opts: GenerateGrantMintBurnAccessParams, + ): Promise { + return this.#grantMintBurnAccess.generate(this.chain, opts) + } + /** Grants mint/burn access on a token to an authority (Aptos). */ + grantMintBurnAccess( + opts: ExecuteGrantMintBurnAccessParams, + ): Promise { + return this.#grantMintBurnAccess.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos `revokeMintBurnAccess` transaction. */ + generateUnsignedRevokeMintBurnAccess( + opts: GenerateRevokeMintBurnAccessParams, + ): Promise { + return this.#revokeMintBurnAccess.generate(this.chain, opts) + } + /** Revokes mint/burn access on a token from an authority (Aptos). */ + revokeMintBurnAccess( + opts: ExecuteRevokeMintBurnAccessParams, + ): Promise { + return this.#revokeMintBurnAccess.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos `proposeAdminRole` transaction (owner proposes administrator). */ + generateUnsignedProposeAdminRole( + opts: GenerateProposeAdminRoleParams, + ): Promise { + return this.#proposeAdminRole.generate(this.chain, opts) + } + /** Proposes a token administrator in the TokenAdminRegistry. */ + proposeAdminRole(opts: ExecuteProposeAdminRoleParams): Promise { + return this.#proposeAdminRole.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos `acceptAdminRole` transaction (wallet must be the pending admin). */ + generateUnsignedAcceptAdminRole( + opts: GenerateAcceptAdminRoleParams, + ): Promise { + return this.#acceptAdminRole.generate(this.chain, opts) + } + /** Accepts a pending token administrator role. */ + acceptAdminRole(opts: ExecuteAcceptAdminRoleParams): Promise { + return this.#acceptAdminRole.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos `transferAdminRole` transaction (wallet must be the current admin). */ + generateUnsignedTransferAdminRole( + opts: GenerateTransferAdminRoleParams, + ): Promise { + return this.#transferAdminRole.generate(this.chain, opts) + } + /** Transfers the token administrator role to a new admin. */ + transferAdminRole(opts: ExecuteTransferAdminRoleParams): Promise { + return this.#transferAdminRole.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos `setPool` transaction (registers a token pool). */ + generateUnsignedSetPool(opts: GenerateSetPoolParams): Promise { + return this.#setPool.generate(this.chain, opts) + } + /** Registers a token pool. The wallet must be the token admin. */ + setPool(opts: ExecuteSetPoolParams): Promise { + return this.#setPool.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos pool `applyChainUpdates` transaction (add/remove remote chains). */ + generateUnsignedApplyChainUpdates( + opts: GenerateApplyChainUpdatesParams, + ): Promise { + return this.#applyChainUpdates.generate(this.chain, opts) + } + /** Applies remote-chain config updates to an Aptos token pool. */ + applyChainUpdates(opts: ExecuteApplyChainUpdatesParams): Promise { + return this.#applyChainUpdates.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos pool `appendRemotePoolAddresses` transaction. */ + generateUnsignedAppendRemotePoolAddresses( + opts: GenerateAppendRemotePoolAddressesParams, + ): Promise { + return this.#appendRemotePoolAddresses.generate(this.chain, opts) + } + /** Appends remote pool addresses to a remote-chain config on an Aptos token pool. */ + appendRemotePoolAddresses( + opts: ExecuteAppendRemotePoolAddressesParams, + ): Promise { + return this.#appendRemotePoolAddresses.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos pool `removeRemotePoolAddresses` transaction. */ + generateUnsignedRemoveRemotePoolAddresses( + opts: GenerateRemoveRemotePoolAddressesParams, + ): Promise { + return this.#removeRemotePoolAddresses.generate(this.chain, opts) + } + /** Removes remote pool addresses from a remote-chain config on an Aptos token pool. */ + removeRemotePoolAddresses( + opts: ExecuteRemoveRemotePoolAddressesParams, + ): Promise { + return this.#removeRemotePoolAddresses.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos pool `deleteChainConfig` transaction. */ + generateUnsignedDeleteChainConfig( + opts: GenerateDeleteChainConfigParams, + ): Promise { + return this.#deleteChainConfig.generate(this.chain, opts) + } + /** Removes a remote-chain config from an Aptos token pool. */ + deleteChainConfig(opts: ExecuteDeleteChainConfigParams): Promise { + return this.#deleteChainConfig.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos pool `setChainRateLimiterConfig` transaction (per-chain rate limits). */ + generateUnsignedSetChainRateLimiterConfig( + opts: GenerateSetChainRateLimiterConfigParams, + ): Promise { + return this.#setChainRateLimiterConfig.generate(this.chain, opts) + } + /** Sets per-chain rate limiter config on an Aptos token pool. */ + setChainRateLimiterConfig( + opts: ExecuteSetChainRateLimiterConfigParams, + ): Promise { + return this.#setChainRateLimiterConfig.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos pool `setRateLimitAdmin` transaction (unsupported on Aptos — rejects). */ + generateUnsignedSetRateLimitAdmin( + opts: GenerateSetRateLimitAdminParams, + ): Promise { + return this.#setRateLimitAdmin.generate(this.chain, opts) + } + /** Sets the rate-limit admin — unsupported on Aptos (owner-managed); always rejects. */ + setRateLimitAdmin(opts: ExecuteSetRateLimitAdminParams): Promise { + return this.#setRateLimitAdmin.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos pool `transferOwnership` transaction (propose new owner). */ + generateUnsignedTransferOwnership( + opts: GenerateTransferOwnershipParams, + ): Promise { + return this.#transferOwnership.generate(this.chain, opts) + } + /** Proposes a new owner for an Aptos token pool. */ + transferOwnership(opts: ExecuteTransferOwnershipParams): Promise { + return this.#transferOwnership.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos pool `acceptOwnership` transaction. */ + generateUnsignedAcceptOwnership( + opts: GenerateAcceptOwnershipParams, + ): Promise { + return this.#acceptOwnership.generate(this.chain, opts) + } + /** Accepts proposed ownership of an Aptos token pool. */ + acceptOwnership(opts: ExecuteAcceptOwnershipParams): Promise { + return this.#acceptOwnership.execute(this.chain, opts) + } + + /** Builds an unsigned Aptos pool `executeOwnershipTransfer` transaction (finalize transfer). */ + generateUnsignedExecuteOwnershipTransfer( + opts: GenerateExecuteOwnershipTransferParams, + ): Promise { + return this.#executeOwnershipTransfer.generate(this.chain, opts) + } + /** Finalizes an ownership transfer on an Aptos token pool. */ + executeOwnershipTransfer( + opts: ExecuteExecuteOwnershipTransferParams, + ): Promise { + return this.#executeOwnershipTransfer.execute(this.chain, opts) + } + + /** Reads a token's current mint/burn roles, read-only. */ + getMintBurnRoles(tokenAddress: string): Promise { + return getMintBurnRoles(this.chain, tokenAddress) + } +} + +export * from '../errors.ts' +export type { TransactionHash } from '../operation.ts' +export type { MintBurnRolesResult } from './token/get-mint-burn-roles.ts' +export type * from './pool/operations/index.ts' +export type * from './token/operations/index.ts' +export type * from './token-pool/operations/index.ts' +export type * from './token-admin-registry/operations/index.ts' diff --git a/ccip-sdk/src/cct/aptos/operation.ts b/ccip-sdk/src/cct/aptos/operation.ts new file mode 100644 index 00000000..ac434727 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/operation.ts @@ -0,0 +1,58 @@ +/** + * Aptos {@link Operation} lifecycle: validate → build unsigned tx → submit. + * Generation needs an explicit `sender` address; execution derives it from + * `wallet.accountAddress`. Mirrors the Solana operation base. + * + * @packageDocumentation + */ + +import type { AptosChain } from '../../aptos/index.ts' +import { type UnsignedAptosTx, isAptosAccount } from '../../aptos/types.ts' +import { CCIPWalletInvalidError } from '../../errors/index.ts' +import { type TransactionHash, Operation } from '../operation.ts' +import { submit } from './submit.ts' + +/** Unsigned Aptos operation params carry the sender address explicitly. */ +export type AptosGenerateParams

= P & { sender: string } + +/** Signed Aptos operation params derive the sender from `wallet.accountAddress`. */ +export type AptosExecuteParams

= P & { wallet: unknown } + +function withSender

( + params: AptosExecuteParams

, + sender: string, +): AptosGenerateParams

{ + const { wallet: _wallet, ...rest } = params + return { ...rest, sender } as AptosGenerateParams

+} + +/** Aptos CCT write base. Subclasses supply {@link validate} and {@link buildUnsigned}. */ +export abstract class AptosOperation< + P extends object, + Tx extends UnsignedAptosTx = UnsignedAptosTx, + Result = TransactionHash, +> extends Operation, Tx, Result> { + /** Build the unsigned transaction(s) after params have been validated. */ + protected abstract buildUnsigned(chain: AptosChain, params: AptosGenerateParams

): Promise + + /** Map the confirmed hash to the op's result; deploy ops override to add addresses. */ + protected resultFromGenerated(hash: TransactionHash, _tx: Tx): Result { + return hash as Result + } + + /** Run {@link validate} and {@link buildUnsigned}; no signing. */ + async generate(chain: AptosChain, params: AptosGenerateParams

): Promise { + this.validate(params) + return this.buildUnsigned(chain, params) + } + + /** Generate with the wallet's address as sender, then sign, submit, and confirm. */ + async execute(chain: AptosChain, params: AptosExecuteParams

): Promise { + const { wallet } = params + if (!isAptosAccount(wallet)) throw new CCIPWalletInvalidError(wallet) + + const tx = await this.generate(chain, withSender(params, wallet.accountAddress.toString())) + const hash = await submit(chain, wallet, tx.transactions, this.name) + return this.resultFromGenerated(hash, tx) + } +} diff --git a/ccip-sdk/src/cct/aptos/pool/operations/accept-ownership.test.ts b/ccip-sdk/src/cct/aptos/pool/operations/accept-ownership.test.ts new file mode 100644 index 00000000..0cabcafd --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/accept-ownership.test.ts @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { AcceptOwnership } from './accept-ownership.ts' +import { POOL, SENDER, stubChain } from './test-helpers.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos TokenPool acceptOwnership', () => { + it('builds a single Aptos transaction', async () => { + const unsigned = await new AcceptOwnership().generate(stubChain(), { + poolAddress: POOL, + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + assert.equal(unsigned.transactions.length, 1) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects an empty poolAddress before building', async () => { + await assert.rejects( + () => + new AcceptOwnership().generate(stubChain(), { + poolAddress: '', + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/pool/operations/accept-ownership.ts b/ccip-sdk/src/cct/aptos/pool/operations/accept-ownership.ts new file mode 100644 index 00000000..c53a0517 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/accept-ownership.ts @@ -0,0 +1,73 @@ +/** + * Aptos TokenPool `acceptOwnership` operation. + * + * Signals acceptance of a proposed pool ownership transfer (step 2 of the Aptos + * 3-step ownership transfer) via `poolAddress::moduleName::accept_ownership()`. + * Auto-discovers the pool module from the pool address. + * + * @packageDocumentation + */ + +import { AccountAddress } from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import type { UnsignedAptosTx } from '../../../../aptos/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { discoverPoolModule, ensurePoolInitialized } from '../../common.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' + +/** Parameters shared by Aptos TokenPool `acceptOwnership` generation and execution. */ +type AcceptOwnershipParams = { + /** Local pool object address (Aptos hex). */ + poolAddress: string +} + +/** Parameters for unsigned Aptos TokenPool `acceptOwnership` generation. */ +export type GenerateAcceptOwnershipParams = AptosGenerateParams + +/** Unsigned Aptos TokenPool `acceptOwnership` result. */ +export type GenerateAcceptOwnershipResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenPool `acceptOwnership`. */ +export type ExecuteAcceptOwnershipParams = AptosExecuteParams + +/** Result of executing Aptos TokenPool `acceptOwnership`. */ +export type ExecuteAcceptOwnershipResult = TransactionHash + +/** Aptos TokenPool `acceptOwnership` operation. */ +export class AcceptOwnership extends AptosOperation { + readonly name = 'acceptOwnership' + + /** Validates the pool address before any RPC. */ + protected validate(params: GenerateAcceptOwnershipParams): void { + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'poolAddress', 'must be non-empty') + } + } + + /** Discovers the pool module and builds an `accept_ownership` transaction. */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateAcceptOwnershipParams, + ): Promise { + const moduleName = await discoverPoolModule(chain, params.poolAddress) + await ensurePoolInitialized(chain, params.poolAddress, moduleName) + + const tx = await chain.provider.transaction.build.simple({ + sender: AccountAddress.from(params.sender), + data: { + function: `${params.poolAddress}::${moduleName}::accept_ownership`, + functionArguments: [], + }, + }) + + chain.logger.debug(`${this.name}: pool = ${params.poolAddress}, module = ${moduleName}`) + return { family: ChainFamily.Aptos, transactions: [tx.bcsToBytes()] } + } +} diff --git a/ccip-sdk/src/cct/aptos/pool/operations/append-remote-pool-addresses.test.ts b/ccip-sdk/src/cct/aptos/pool/operations/append-remote-pool-addresses.test.ts new file mode 100644 index 00000000..060e4994 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/append-remote-pool-addresses.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { AppendRemotePoolAddresses } from './append-remote-pool-addresses.ts' +import { POOL, REMOTE_POOL, SELECTOR, SENDER, stubChain } from './test-helpers.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos TokenPool appendRemotePoolAddresses', () => { + it('builds one transaction per remote pool address', async () => { + const unsigned = await new AppendRemotePoolAddresses().generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_POOL, REMOTE_POOL], + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + assert.equal(unsigned.transactions.length, 2) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects an empty remotePoolAddresses before building', async () => { + await assert.rejects( + () => + new AppendRemotePoolAddresses().generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [], + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/pool/operations/append-remote-pool-addresses.ts b/ccip-sdk/src/cct/aptos/pool/operations/append-remote-pool-addresses.ts new file mode 100644 index 00000000..8fb771e2 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/append-remote-pool-addresses.ts @@ -0,0 +1,135 @@ +/** + * Aptos TokenPool `appendRemotePoolAddresses` operation. + * + * Appends remote pool addresses to an existing chain config. Auto-discovers the + * pool module and builds **one `add_remote_pool` transaction per address**, each + * with a consecutive account sequence number. Overrides {@link execute} to submit + * every transaction in order and return the last hash. + * + * @packageDocumentation + */ + +import { AccountAddress } from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import { type UnsignedAptosTx, isAptosAccount } from '../../../../aptos/types.ts' +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { getAddressBytes } from '../../../../utils.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { discoverPoolModule, ensurePoolInitialized } from '../../common.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' +import { submit } from '../../submit.ts' + +/** Parameters shared by Aptos TokenPool `appendRemotePoolAddresses` generation and execution. */ +type AppendRemotePoolAddressesParams = { + /** Local pool object address (Aptos hex). */ + poolAddress: string + /** Remote chain selector (must already be configured via applyChainUpdates). */ + remoteChainSelector: bigint + /** Remote pool addresses to append, in native format. At least one required. */ + remotePoolAddresses: string[] +} + +/** Parameters for unsigned Aptos TokenPool `appendRemotePoolAddresses` generation. */ +export type GenerateAppendRemotePoolAddressesParams = + AptosGenerateParams + +/** Unsigned Aptos TokenPool `appendRemotePoolAddresses` result (one tx per address). */ +export type GenerateAppendRemotePoolAddressesResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenPool `appendRemotePoolAddresses`. */ +export type ExecuteAppendRemotePoolAddressesParams = + AptosExecuteParams + +/** Result of executing Aptos TokenPool `appendRemotePoolAddresses`. */ +export type ExecuteAppendRemotePoolAddressesResult = TransactionHash + +/** Aptos TokenPool `appendRemotePoolAddresses` operation (one tx per address). */ +export class AppendRemotePoolAddresses extends AptosOperation { + readonly name = 'appendRemotePoolAddresses' + + /** Validates the pool address, selector, and each remote pool address before any RPC. */ + protected validate(params: GenerateAppendRemotePoolAddressesParams): void { + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'poolAddress', 'must be non-empty') + } + if (params.remoteChainSelector === 0n) { + throw new CCTParamsInvalidError(this.name, 'remoteChainSelector', 'must be non-zero') + } + if (params.remotePoolAddresses.length === 0) { + throw new CCTParamsInvalidError( + this.name, + 'remotePoolAddresses', + 'must have at least one address', + ) + } + for (const [i, addr] of params.remotePoolAddresses.entries()) { + if (!addr || addr.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, `remotePoolAddresses[${i}]`, 'must be non-empty') + } + } + } + + /** Discovers the pool module and builds one `add_remote_pool` tx per address. */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateAppendRemotePoolAddressesParams, + ): Promise { + const poolModule = await discoverPoolModule(chain, params.poolAddress) + await ensurePoolInitialized(chain, params.poolAddress, poolModule) + + const senderAddr = AccountAddress.from(params.sender) + + // Fetch current sequence number so multi-tx batches get consecutive nonces. + const { sequence_number } = await chain.provider.getAccountInfo({ accountAddress: senderAddr }) + let nextSeq = BigInt(sequence_number) + + const transactions: Uint8Array[] = [] + for (const remotePoolAddress of params.remotePoolAddresses) { + const encodedAddress = Array.from(getAddressBytes(remotePoolAddress)) + const tx = await chain.provider.transaction.build.simple({ + sender: senderAddr, + data: { + function: `${params.poolAddress}::${poolModule}::add_remote_pool`, + functionArguments: [params.remoteChainSelector, encodedAddress], + }, + options: { accountSequenceNumber: nextSeq++ }, + }) + transactions.push(tx.bcsToBytes()) + } + + chain.logger.debug( + `${this.name}: pool = ${params.poolAddress}, module = ${poolModule}, addresses = ${params.remotePoolAddresses.length}`, + ) + return { + family: ChainFamily.Aptos, + transactions: transactions as [Uint8Array, ...Uint8Array[]], + } + } + + /** Signs and submits every generated transaction sequentially, returning the last hash. */ + override async execute( + chain: AptosChain, + params: ExecuteAppendRemotePoolAddressesParams, + ): Promise { + const { wallet } = params + if (!isAptosAccount(wallet)) throw new CCIPWalletInvalidError(wallet) + + const { wallet: _wallet, ...rest } = params + const sender = wallet.accountAddress.toString() + const { transactions } = await this.generate(chain, { ...rest, sender }) + + let last: TransactionHash | undefined + for (const txn of transactions) { + last = await submit(chain, wallet, [txn], this.name) + } + if (!last) throw new CCTTxFailedError(this.name, 'no transactions to submit') + return last + } +} diff --git a/ccip-sdk/src/cct/aptos/pool/operations/apply-chain-updates.test.ts b/ccip-sdk/src/cct/aptos/pool/operations/apply-chain-updates.test.ts new file mode 100644 index 00000000..a53e107f --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/apply-chain-updates.test.ts @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { ApplyChainUpdates } from './apply-chain-updates.ts' +import { + DISABLED_RATE_LIMITER, + POOL, + REMOTE_POOL, + REMOTE_TOKEN, + SELECTOR, + SENDER, + stubChain, +} from './test-helpers.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos TokenPool applyChainUpdates', () => { + it('builds apply + rate-limiter transactions when adding a chain', async () => { + const unsigned = await new ApplyChainUpdates().generate(stubChain(), { + poolAddress: POOL, + remoteChainSelectorsToRemove: [], + chainsToAdd: [ + { + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_POOL], + remoteTokenAddress: REMOTE_TOKEN, + outboundRateLimiterConfig: DISABLED_RATE_LIMITER, + inboundRateLimiterConfig: DISABLED_RATE_LIMITER, + }, + ], + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + assert.equal(unsigned.transactions.length, 2) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects an empty poolAddress before building', async () => { + await assert.rejects( + () => + new ApplyChainUpdates().generate(stubChain(), { + poolAddress: '', + remoteChainSelectorsToRemove: [], + chainsToAdd: [], + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/pool/operations/apply-chain-updates.ts b/ccip-sdk/src/cct/aptos/pool/operations/apply-chain-updates.ts new file mode 100644 index 00000000..e1bcfbd8 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/apply-chain-updates.ts @@ -0,0 +1,214 @@ +/** + * Aptos TokenPool `applyChainUpdates` operation. + * + * (Re)configures remote chains on a token pool. Auto-discovers the pool module + * from the pool address, then builds up to **two** transactions: + * 1. `apply_chain_updates` — adds/removes remote chain configs. + * 2. `set_chain_rate_limiter_configs` — configures rate limiters for the added + * chains (Aptos `apply_chain_updates` does not carry rate-limiter args). + * + * Because it may emit multiple transactions that must be submitted with + * consecutive account sequence numbers, this op overrides {@link execute} to + * submit each transaction in order and return the last hash. + * + * @packageDocumentation + */ + +import { AccountAddress } from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import { type UnsignedAptosTx, isAptosAccount } from '../../../../aptos/types.ts' +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { + encodeRemoteAddressBytes, + encodeRemotePoolAddressBytes, +} from '../../../../token-admin/apply-chain-updates-utils.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { discoverPoolModule, ensurePoolInitialized } from '../../common.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' +import { submit } from '../../submit.ts' + +/** Rate limiter bucket configuration (bigints encoded as strings to avoid precision loss). */ +export type RateLimiterConfig = { + /** Whether the rate limiter is enabled. */ + isEnabled: boolean + /** Maximum token capacity (bigint as string). */ + capacity: string + /** Token refill rate per second (bigint as string). */ + rate: string +} + +/** Configuration for a single remote chain to add. Addresses are in native format. */ +export type RemoteChainConfig = { + /** Remote chain selector. */ + remoteChainSelector: bigint + /** Remote pool address(es) in native format. At least one required. */ + remotePoolAddresses: string[] + /** Remote token address in native format. */ + remoteTokenAddress: string + /** Outbound rate limiter (local → remote). */ + outboundRateLimiterConfig: RateLimiterConfig + /** Inbound rate limiter (remote → local). */ + inboundRateLimiterConfig: RateLimiterConfig +} + +/** Parameters shared by Aptos TokenPool `applyChainUpdates` generation and execution. */ +type ApplyChainUpdatesParams = { + /** Local pool object address (Aptos hex). */ + poolAddress: string + /** Remote chain selectors to remove (can be empty). */ + remoteChainSelectorsToRemove: bigint[] + /** Remote chain configurations to add (can be empty). */ + chainsToAdd: RemoteChainConfig[] +} + +/** Parameters for unsigned Aptos TokenPool `applyChainUpdates` generation. */ +export type GenerateApplyChainUpdatesParams = AptosGenerateParams + +/** Unsigned Aptos TokenPool `applyChainUpdates` result (one or two transactions). */ +export type GenerateApplyChainUpdatesResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenPool `applyChainUpdates`. */ +export type ExecuteApplyChainUpdatesParams = AptosExecuteParams + +/** Result of executing Aptos TokenPool `applyChainUpdates`. */ +export type ExecuteApplyChainUpdatesResult = TransactionHash + +/** Aptos TokenPool `applyChainUpdates` operation (may emit multiple transactions). */ +export class ApplyChainUpdates extends AptosOperation { + readonly name = 'applyChainUpdates' + + /** Validates the pool address and each remote-chain config before any RPC. */ + protected validate(params: GenerateApplyChainUpdatesParams): void { + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'poolAddress', 'must be non-empty') + } + for (const [i, chain] of params.chainsToAdd.entries()) { + if (chain.remoteChainSelector === 0n) { + throw new CCTParamsInvalidError( + this.name, + `chainsToAdd[${i}].remoteChainSelector`, + 'must be non-zero', + ) + } + if (chain.remotePoolAddresses.length === 0) { + throw new CCTParamsInvalidError( + this.name, + `chainsToAdd[${i}].remotePoolAddresses`, + 'must have at least one address', + ) + } + if (!chain.remoteTokenAddress || chain.remoteTokenAddress.trim().length === 0) { + throw new CCTParamsInvalidError( + this.name, + `chainsToAdd[${i}].remoteTokenAddress`, + 'must be non-empty', + ) + } + } + } + + /** + * Discovers the pool module, then builds `apply_chain_updates` and, when there + * are chains to add, a follow-up `set_chain_rate_limiter_configs` transaction + * with a consecutive account sequence number. + */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateApplyChainUpdatesParams, + ): Promise { + const poolModule = await discoverPoolModule(chain, params.poolAddress) + await ensurePoolInitialized(chain, params.poolAddress, poolModule) + + const remoteChainSelectorsToRemove = params.remoteChainSelectorsToRemove + const remoteChainSelectorsToAdd = params.chainsToAdd.map((c) => c.remoteChainSelector) + + // Pool addresses: raw bytes (not padded) — matches chainlink-deployments. + const remotePoolAddressesToAdd = params.chainsToAdd.map((c) => + c.remotePoolAddresses.map((addr) => Array.from(encodeRemotePoolAddressBytes(addr))), + ) + + // Token addresses: 32-byte left-padded — matches chainlink-deployments. + const remoteTokenAddressesToAdd = params.chainsToAdd.map((c) => + Array.from(encodeRemoteAddressBytes(c.remoteTokenAddress)), + ) + + const senderAddr = AccountAddress.from(params.sender) + + // Fetch current sequence number so multi-tx batches get consecutive nonces. + const { sequence_number } = await chain.provider.getAccountInfo({ accountAddress: senderAddr }) + let nextSeq = BigInt(sequence_number) + + // Transaction 1: apply_chain_updates — adds/removes remote chains. + const applyTx = await chain.provider.transaction.build.simple({ + sender: senderAddr, + data: { + function: `${params.poolAddress}::${poolModule}::apply_chain_updates`, + functionArguments: [ + remoteChainSelectorsToRemove, + remoteChainSelectorsToAdd, + remotePoolAddressesToAdd, + remoteTokenAddressesToAdd, + ], + }, + options: { accountSequenceNumber: nextSeq++ }, + }) + + const transactions: [Uint8Array, ...Uint8Array[]] = [applyTx.bcsToBytes()] + + // Transaction 2: set_chain_rate_limiter_configs — only when chains are added. + if (params.chainsToAdd.length > 0) { + const rateLimiterTx = await chain.provider.transaction.build.simple({ + sender: senderAddr, + data: { + function: `${params.poolAddress}::${poolModule}::set_chain_rate_limiter_configs`, + functionArguments: [ + remoteChainSelectorsToAdd, + params.chainsToAdd.map((c) => c.outboundRateLimiterConfig.isEnabled), + params.chainsToAdd.map((c) => BigInt(c.outboundRateLimiterConfig.capacity)), + params.chainsToAdd.map((c) => BigInt(c.outboundRateLimiterConfig.rate)), + params.chainsToAdd.map((c) => c.inboundRateLimiterConfig.isEnabled), + params.chainsToAdd.map((c) => BigInt(c.inboundRateLimiterConfig.capacity)), + params.chainsToAdd.map((c) => BigInt(c.inboundRateLimiterConfig.rate)), + ], + }, + options: { accountSequenceNumber: nextSeq }, + }) + transactions.push(rateLimiterTx.bcsToBytes()) + } + + chain.logger.debug( + `${this.name}: pool = ${params.poolAddress}, module = ${poolModule}, adds = ${params.chainsToAdd.length}, removes = ${params.remoteChainSelectorsToRemove.length}, txs = ${transactions.length}`, + ) + return { family: ChainFamily.Aptos, transactions } + } + + /** + * Signs and submits every generated transaction sequentially (tx2 depends on + * tx1), returning the last confirmed hash. + */ + override async execute( + chain: AptosChain, + params: ExecuteApplyChainUpdatesParams, + ): Promise { + const { wallet } = params + if (!isAptosAccount(wallet)) throw new CCIPWalletInvalidError(wallet) + + const { wallet: _wallet, ...rest } = params + const sender = wallet.accountAddress.toString() + const { transactions } = await this.generate(chain, { ...rest, sender }) + + let last: TransactionHash | undefined + for (const txn of transactions) { + last = await submit(chain, wallet, [txn], this.name) + } + if (!last) throw new CCTTxFailedError(this.name, 'no transactions to submit') + return last + } +} diff --git a/ccip-sdk/src/cct/aptos/pool/operations/delete-chain-config.test.ts b/ccip-sdk/src/cct/aptos/pool/operations/delete-chain-config.test.ts new file mode 100644 index 00000000..325272f9 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/delete-chain-config.test.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { DeleteChainConfig } from './delete-chain-config.ts' +import { POOL, SELECTOR, SENDER, stubChain } from './test-helpers.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos TokenPool deleteChainConfig', () => { + it('builds a single Aptos transaction', async () => { + const unsigned = await new DeleteChainConfig().generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + assert.equal(unsigned.transactions.length, 1) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects a zero remoteChainSelector before building', async () => { + await assert.rejects( + () => + new DeleteChainConfig().generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: 0n, + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/pool/operations/delete-chain-config.ts b/ccip-sdk/src/cct/aptos/pool/operations/delete-chain-config.ts new file mode 100644 index 00000000..db2dfd16 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/delete-chain-config.ts @@ -0,0 +1,85 @@ +/** + * Aptos TokenPool `deleteChainConfig` operation. + * + * Removes an entire remote chain configuration from a token pool by calling + * `apply_chain_updates` with only the removal selector and empty add arrays. + * Auto-discovers the pool module from the pool address. + * + * @packageDocumentation + */ + +import { AccountAddress } from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import type { UnsignedAptosTx } from '../../../../aptos/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { discoverPoolModule, ensurePoolInitialized } from '../../common.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' + +/** Parameters shared by Aptos TokenPool `deleteChainConfig` generation and execution. */ +type DeleteChainConfigParams = { + /** Local pool object address (Aptos hex). */ + poolAddress: string + /** Remote chain selector to remove (must be currently configured). */ + remoteChainSelector: bigint +} + +/** Parameters for unsigned Aptos TokenPool `deleteChainConfig` generation. */ +export type GenerateDeleteChainConfigParams = AptosGenerateParams + +/** Unsigned Aptos TokenPool `deleteChainConfig` result. */ +export type GenerateDeleteChainConfigResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenPool `deleteChainConfig`. */ +export type ExecuteDeleteChainConfigParams = AptosExecuteParams + +/** Result of executing Aptos TokenPool `deleteChainConfig`. */ +export type ExecuteDeleteChainConfigResult = TransactionHash + +/** Aptos TokenPool `deleteChainConfig` operation. */ +export class DeleteChainConfig extends AptosOperation { + readonly name = 'deleteChainConfig' + + /** Validates the pool address and selector before any RPC. */ + protected validate(params: GenerateDeleteChainConfigParams): void { + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'poolAddress', 'must be non-empty') + } + if (params.remoteChainSelector === 0n) { + throw new CCTParamsInvalidError(this.name, 'remoteChainSelector', 'must be non-zero') + } + } + + /** Discovers the pool module and builds an `apply_chain_updates` removal-only transaction. */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateDeleteChainConfigParams, + ): Promise { + const poolModule = await discoverPoolModule(chain, params.poolAddress) + await ensurePoolInitialized(chain, params.poolAddress, poolModule) + + const applyTx = await chain.provider.transaction.build.simple({ + sender: AccountAddress.from(params.sender), + data: { + function: `${params.poolAddress}::${poolModule}::apply_chain_updates`, + functionArguments: [ + [params.remoteChainSelector], // remoteChainSelectorsToRemove + [], // remoteChainSelectorsToAdd + [], // remotePoolAddressesToAdd + [], // remoteTokenAddressesToAdd + ], + }, + }) + + chain.logger.debug( + `${this.name}: pool = ${params.poolAddress}, module = ${poolModule}, remoteChainSelector = ${params.remoteChainSelector}`, + ) + return { family: ChainFamily.Aptos, transactions: [applyTx.bcsToBytes()] } + } +} diff --git a/ccip-sdk/src/cct/aptos/pool/operations/execute-ownership-transfer.test.ts b/ccip-sdk/src/cct/aptos/pool/operations/execute-ownership-transfer.test.ts new file mode 100644 index 00000000..9226c5ab --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/execute-ownership-transfer.test.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { ExecuteOwnershipTransfer } from './execute-ownership-transfer.ts' +import { NEW_OWNER, POOL, SENDER, stubChain } from './test-helpers.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos TokenPool executeOwnershipTransfer', () => { + it('builds a single Aptos transaction', async () => { + const unsigned = await new ExecuteOwnershipTransfer().generate(stubChain(), { + poolAddress: POOL, + newOwner: NEW_OWNER, + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + assert.equal(unsigned.transactions.length, 1) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects an empty poolAddress before building', async () => { + await assert.rejects( + () => + new ExecuteOwnershipTransfer().generate(stubChain(), { + poolAddress: '', + newOwner: NEW_OWNER, + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/pool/operations/execute-ownership-transfer.ts b/ccip-sdk/src/cct/aptos/pool/operations/execute-ownership-transfer.ts new file mode 100644 index 00000000..65812dd5 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/execute-ownership-transfer.ts @@ -0,0 +1,87 @@ +/** + * Aptos TokenPool `executeOwnershipTransfer` operation. + * + * The Aptos-only 3rd step of ownership transfer: the **current owner** finalizes + * the AptosFramework object transfer after the proposed owner has accepted, via + * `poolAddress::moduleName::execute_ownership_transfer(newOwner)`. + * + * Aptos 3-step ownership transfer: + * 1. `transfer_ownership(newOwner)` — current owner proposes. + * 2. `accept_ownership()` — proposed owner signals acceptance. + * 3. `execute_ownership_transfer(newOwner)` — current owner finalizes. + * + * @packageDocumentation + */ + +import { AccountAddress } from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import type { UnsignedAptosTx } from '../../../../aptos/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { discoverPoolModule, ensurePoolInitialized } from '../../common.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' + +/** Parameters shared by Aptos TokenPool `executeOwnershipTransfer` generation and execution. */ +type ExecuteOwnershipTransferParams = { + /** Local pool object address (Aptos hex). */ + poolAddress: string + /** New owner address — must match the address that called acceptOwnership. */ + newOwner: string +} + +/** Parameters for unsigned Aptos TokenPool `executeOwnershipTransfer` generation. */ +export type GenerateExecuteOwnershipTransferParams = + AptosGenerateParams + +/** Unsigned Aptos TokenPool `executeOwnershipTransfer` result. */ +export type GenerateExecuteOwnershipTransferResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenPool `executeOwnershipTransfer`. */ +export type ExecuteExecuteOwnershipTransferParams = + AptosExecuteParams + +/** Result of executing Aptos TokenPool `executeOwnershipTransfer`. */ +export type ExecuteExecuteOwnershipTransferResult = TransactionHash + +/** Aptos TokenPool `executeOwnershipTransfer` operation (Aptos-only 3rd step). */ +export class ExecuteOwnershipTransfer extends AptosOperation { + readonly name = 'executeOwnershipTransfer' + + /** Validates the pool address and new owner before any RPC. */ + protected validate(params: GenerateExecuteOwnershipTransferParams): void { + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'poolAddress', 'must be non-empty') + } + if (!params.newOwner || params.newOwner.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'newOwner', 'must be non-empty') + } + } + + /** Discovers the pool module and builds an `execute_ownership_transfer` transaction. */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateExecuteOwnershipTransferParams, + ): Promise { + const moduleName = await discoverPoolModule(chain, params.poolAddress) + await ensurePoolInitialized(chain, params.poolAddress, moduleName) + + const tx = await chain.provider.transaction.build.simple({ + sender: AccountAddress.from(params.sender), + data: { + function: `${params.poolAddress}::${moduleName}::execute_ownership_transfer`, + functionArguments: [params.newOwner], + }, + }) + + chain.logger.debug( + `${this.name}: pool = ${params.poolAddress}, module = ${moduleName}, newOwner = ${params.newOwner}`, + ) + return { family: ChainFamily.Aptos, transactions: [tx.bcsToBytes()] } + } +} diff --git a/ccip-sdk/src/cct/aptos/pool/operations/index.ts b/ccip-sdk/src/cct/aptos/pool/operations/index.ts new file mode 100644 index 00000000..4c34e795 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/index.ts @@ -0,0 +1,9 @@ +export * from './accept-ownership.ts' +export * from './append-remote-pool-addresses.ts' +export * from './apply-chain-updates.ts' +export * from './delete-chain-config.ts' +export * from './execute-ownership-transfer.ts' +export * from './remove-remote-pool-addresses.ts' +export * from './set-chain-rate-limiter-config.ts' +export * from './set-rate-limit-admin.ts' +export * from './transfer-ownership.ts' diff --git a/ccip-sdk/src/cct/aptos/pool/operations/remove-remote-pool-addresses.test.ts b/ccip-sdk/src/cct/aptos/pool/operations/remove-remote-pool-addresses.test.ts new file mode 100644 index 00000000..b2187d78 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/remove-remote-pool-addresses.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { RemoveRemotePoolAddresses } from './remove-remote-pool-addresses.ts' +import { POOL, REMOTE_POOL, SELECTOR, SENDER, stubChain } from './test-helpers.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos TokenPool removeRemotePoolAddresses', () => { + it('builds one transaction per remote pool address', async () => { + const unsigned = await new RemoveRemotePoolAddresses().generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_POOL], + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + assert.equal(unsigned.transactions.length, 1) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects a zero remoteChainSelector before building', async () => { + await assert.rejects( + () => + new RemoveRemotePoolAddresses().generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: 0n, + remotePoolAddresses: [REMOTE_POOL], + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/pool/operations/remove-remote-pool-addresses.ts b/ccip-sdk/src/cct/aptos/pool/operations/remove-remote-pool-addresses.ts new file mode 100644 index 00000000..6753d357 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/remove-remote-pool-addresses.ts @@ -0,0 +1,136 @@ +/** + * Aptos TokenPool `removeRemotePoolAddresses` operation. + * + * Removes specific remote pool addresses from an existing chain config while + * preserving the chain config itself. Auto-discovers the pool module and builds + * **one `remove_remote_pool` transaction per address**, each with a consecutive + * account sequence number. Overrides {@link execute} to submit every transaction + * in order and return the last hash. + * + * @packageDocumentation + */ + +import { AccountAddress } from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import { type UnsignedAptosTx, isAptosAccount } from '../../../../aptos/types.ts' +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { getAddressBytes } from '../../../../utils.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { discoverPoolModule, ensurePoolInitialized } from '../../common.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' +import { submit } from '../../submit.ts' + +/** Parameters shared by Aptos TokenPool `removeRemotePoolAddresses` generation and execution. */ +type RemoveRemotePoolAddressesParams = { + /** Local pool object address (Aptos hex). */ + poolAddress: string + /** Remote chain selector (must already be configured via applyChainUpdates). */ + remoteChainSelector: bigint + /** Remote pool addresses to remove, in native format. At least one required. */ + remotePoolAddresses: string[] +} + +/** Parameters for unsigned Aptos TokenPool `removeRemotePoolAddresses` generation. */ +export type GenerateRemoveRemotePoolAddressesParams = + AptosGenerateParams + +/** Unsigned Aptos TokenPool `removeRemotePoolAddresses` result (one tx per address). */ +export type GenerateRemoveRemotePoolAddressesResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenPool `removeRemotePoolAddresses`. */ +export type ExecuteRemoveRemotePoolAddressesParams = + AptosExecuteParams + +/** Result of executing Aptos TokenPool `removeRemotePoolAddresses`. */ +export type ExecuteRemoveRemotePoolAddressesResult = TransactionHash + +/** Aptos TokenPool `removeRemotePoolAddresses` operation (one tx per address). */ +export class RemoveRemotePoolAddresses extends AptosOperation { + readonly name = 'removeRemotePoolAddresses' + + /** Validates the pool address, selector, and each remote pool address before any RPC. */ + protected validate(params: GenerateRemoveRemotePoolAddressesParams): void { + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'poolAddress', 'must be non-empty') + } + if (params.remoteChainSelector === 0n) { + throw new CCTParamsInvalidError(this.name, 'remoteChainSelector', 'must be non-zero') + } + if (params.remotePoolAddresses.length === 0) { + throw new CCTParamsInvalidError( + this.name, + 'remotePoolAddresses', + 'must have at least one address', + ) + } + for (const [i, addr] of params.remotePoolAddresses.entries()) { + if (!addr || addr.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, `remotePoolAddresses[${i}]`, 'must be non-empty') + } + } + } + + /** Discovers the pool module and builds one `remove_remote_pool` tx per address. */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateRemoveRemotePoolAddressesParams, + ): Promise { + const poolModule = await discoverPoolModule(chain, params.poolAddress) + await ensurePoolInitialized(chain, params.poolAddress, poolModule) + + const senderAddr = AccountAddress.from(params.sender) + + // Fetch current sequence number so multi-tx batches get consecutive nonces. + const { sequence_number } = await chain.provider.getAccountInfo({ accountAddress: senderAddr }) + let nextSeq = BigInt(sequence_number) + + const transactions: Uint8Array[] = [] + for (const remotePoolAddress of params.remotePoolAddresses) { + const encodedAddress = Array.from(getAddressBytes(remotePoolAddress)) + const tx = await chain.provider.transaction.build.simple({ + sender: senderAddr, + data: { + function: `${params.poolAddress}::${poolModule}::remove_remote_pool`, + functionArguments: [params.remoteChainSelector, encodedAddress], + }, + options: { accountSequenceNumber: nextSeq++ }, + }) + transactions.push(tx.bcsToBytes()) + } + + chain.logger.debug( + `${this.name}: pool = ${params.poolAddress}, module = ${poolModule}, addresses = ${params.remotePoolAddresses.length}`, + ) + return { + family: ChainFamily.Aptos, + transactions: transactions as [Uint8Array, ...Uint8Array[]], + } + } + + /** Signs and submits every generated transaction sequentially, returning the last hash. */ + override async execute( + chain: AptosChain, + params: ExecuteRemoveRemotePoolAddressesParams, + ): Promise { + const { wallet } = params + if (!isAptosAccount(wallet)) throw new CCIPWalletInvalidError(wallet) + + const { wallet: _wallet, ...rest } = params + const sender = wallet.accountAddress.toString() + const { transactions } = await this.generate(chain, { ...rest, sender }) + + let last: TransactionHash | undefined + for (const txn of transactions) { + last = await submit(chain, wallet, [txn], this.name) + } + if (!last) throw new CCTTxFailedError(this.name, 'no transactions to submit') + return last + } +} diff --git a/ccip-sdk/src/cct/aptos/pool/operations/set-chain-rate-limiter-config.test.ts b/ccip-sdk/src/cct/aptos/pool/operations/set-chain-rate-limiter-config.test.ts new file mode 100644 index 00000000..f816a5dc --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/set-chain-rate-limiter-config.test.ts @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { SetChainRateLimiterConfig } from './set-chain-rate-limiter-config.ts' +import { DISABLED_RATE_LIMITER, POOL, SELECTOR, SENDER, stubChain } from './test-helpers.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos TokenPool setChainRateLimiterConfig', () => { + it('builds a single Aptos transaction', async () => { + const unsigned = await new SetChainRateLimiterConfig().generate(stubChain(), { + poolAddress: POOL, + chainConfigs: [ + { + remoteChainSelector: SELECTOR, + outboundRateLimiterConfig: DISABLED_RATE_LIMITER, + inboundRateLimiterConfig: DISABLED_RATE_LIMITER, + }, + ], + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + assert.equal(unsigned.transactions.length, 1) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects an empty chainConfigs before building', async () => { + await assert.rejects( + () => + new SetChainRateLimiterConfig().generate(stubChain(), { + poolAddress: POOL, + chainConfigs: [], + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/pool/operations/set-chain-rate-limiter-config.ts b/ccip-sdk/src/cct/aptos/pool/operations/set-chain-rate-limiter-config.ts new file mode 100644 index 00000000..9da8b267 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/set-chain-rate-limiter-config.ts @@ -0,0 +1,105 @@ +/** + * Aptos TokenPool `setChainRateLimiterConfig` operation. + * + * Updates per-remote-chain rate limiter buckets on a token pool via a single + * `set_chain_rate_limiter_configs` call. Auto-discovers the pool module from the + * pool address. Deep rate-config validation is delegated to the shared + * {@link validateSetChainRateLimiterConfigParams} utility. + * + * @packageDocumentation + */ + +import { AccountAddress } from '@aptos-labs/ts-sdk' + +import type { RateLimiterConfig } from './apply-chain-updates.ts' +import type { AptosChain } from '../../../../aptos/index.ts' +import type { UnsignedAptosTx } from '../../../../aptos/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { validateSetChainRateLimiterConfigParams } from '../../../../token-admin/set-rate-limiter-config-utils.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { discoverPoolModule, ensurePoolInitialized } from '../../common.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' + +/** Rate limiter configuration for a single already-configured remote chain. */ +export type ChainRateLimiterConfig = { + /** Remote chain selector (must already be configured via applyChainUpdates). */ + remoteChainSelector: bigint + /** Outbound rate limiter (local → remote). */ + outboundRateLimiterConfig: RateLimiterConfig + /** Inbound rate limiter (remote → local). */ + inboundRateLimiterConfig: RateLimiterConfig +} + +/** Parameters shared by Aptos TokenPool `setChainRateLimiterConfig` generation and execution. */ +type SetChainRateLimiterConfigParams = { + /** Local pool object address (Aptos hex) whose rate limits are being updated. */ + poolAddress: string + /** Rate limiter configurations, one per already-configured remote chain. */ + chainConfigs: ChainRateLimiterConfig[] +} + +/** Parameters for unsigned Aptos TokenPool `setChainRateLimiterConfig` generation. */ +export type GenerateSetChainRateLimiterConfigParams = + AptosGenerateParams + +/** Unsigned Aptos TokenPool `setChainRateLimiterConfig` result. */ +export type GenerateSetChainRateLimiterConfigResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenPool `setChainRateLimiterConfig`. */ +export type ExecuteSetChainRateLimiterConfigParams = + AptosExecuteParams + +/** Result of executing Aptos TokenPool `setChainRateLimiterConfig`. */ +export type ExecuteSetChainRateLimiterConfigResult = TransactionHash + +/** Aptos TokenPool `setChainRateLimiterConfig` operation. */ +export class SetChainRateLimiterConfig extends AptosOperation { + readonly name = 'setChainRateLimiterConfig' + + /** Validates the pool address and rate limiter configs before any RPC. */ + protected validate(params: GenerateSetChainRateLimiterConfigParams): void { + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'poolAddress', 'must be non-empty') + } + if (params.chainConfigs.length === 0) { + throw new CCTParamsInvalidError(this.name, 'chainConfigs', 'must have at least one entry') + } + // Shared deep validation of selectors and rate limiter amounts. + validateSetChainRateLimiterConfigParams(params) + } + + /** Discovers the pool module and builds a single `set_chain_rate_limiter_configs` transaction. */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateSetChainRateLimiterConfigParams, + ): Promise { + const poolModule = await discoverPoolModule(chain, params.poolAddress) + await ensurePoolInitialized(chain, params.poolAddress, poolModule) + + const rateLimiterTx = await chain.provider.transaction.build.simple({ + sender: AccountAddress.from(params.sender), + data: { + function: `${params.poolAddress}::${poolModule}::set_chain_rate_limiter_configs`, + functionArguments: [ + params.chainConfigs.map((c) => c.remoteChainSelector), + params.chainConfigs.map((c) => c.outboundRateLimiterConfig.isEnabled), + params.chainConfigs.map((c) => BigInt(c.outboundRateLimiterConfig.capacity)), + params.chainConfigs.map((c) => BigInt(c.outboundRateLimiterConfig.rate)), + params.chainConfigs.map((c) => c.inboundRateLimiterConfig.isEnabled), + params.chainConfigs.map((c) => BigInt(c.inboundRateLimiterConfig.capacity)), + params.chainConfigs.map((c) => BigInt(c.inboundRateLimiterConfig.rate)), + ], + }, + }) + + chain.logger.debug( + `${this.name}: pool = ${params.poolAddress}, module = ${poolModule}, configs = ${params.chainConfigs.length}`, + ) + return { family: ChainFamily.Aptos, transactions: [rateLimiterTx.bcsToBytes()] } + } +} diff --git a/ccip-sdk/src/cct/aptos/pool/operations/set-rate-limit-admin.test.ts b/ccip-sdk/src/cct/aptos/pool/operations/set-rate-limit-admin.test.ts new file mode 100644 index 00000000..c05ba1a2 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/set-rate-limit-admin.test.ts @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { SetRateLimitAdmin } from './set-rate-limit-admin.ts' +import { NEW_OWNER, POOL, SENDER, stubChain } from './test-helpers.ts' +import { CCIPMethodUnsupportedError } from '../../../../errors/index.ts' + +describe('Aptos TokenPool setRateLimitAdmin', () => { + it('rejects generate as unsupported on Aptos', async () => { + await assert.rejects( + () => + new SetRateLimitAdmin().generate(stubChain(), { + poolAddress: POOL, + rateLimitAdmin: NEW_OWNER, + sender: SENDER, + }), + CCIPMethodUnsupportedError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/pool/operations/set-rate-limit-admin.ts b/ccip-sdk/src/cct/aptos/pool/operations/set-rate-limit-admin.ts new file mode 100644 index 00000000..cbc76157 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/set-rate-limit-admin.ts @@ -0,0 +1,57 @@ +/** + * Aptos TokenPool `setRateLimitAdmin` operation. + * + * **Not supported on Aptos** — rate limiting is managed directly by the pool + * owner, so there is no separate rate-limit-admin role. Both {@link generate} and + * {@link execute} reject with {@link CCIPMethodUnsupportedError}. + * + * @packageDocumentation + */ + +import type { AptosChain } from '../../../../aptos/index.ts' +import type { UnsignedAptosTx } from '../../../../aptos/types.ts' +import { CCIPMethodUnsupportedError } from '../../../../errors/index.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' + +/** Parameters shared by Aptos TokenPool `setRateLimitAdmin` generation and execution. */ +type SetRateLimitAdminParams = { + /** Local pool object address (Aptos hex). */ + poolAddress: string + /** New rate-limit admin address. */ + rateLimitAdmin: string +} + +/** Parameters for unsigned Aptos TokenPool `setRateLimitAdmin` generation. */ +export type GenerateSetRateLimitAdminParams = AptosGenerateParams + +/** Unsigned Aptos TokenPool `setRateLimitAdmin` result. */ +export type GenerateSetRateLimitAdminResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenPool `setRateLimitAdmin`. */ +export type ExecuteSetRateLimitAdminParams = AptosExecuteParams + +/** Result of executing Aptos TokenPool `setRateLimitAdmin`. */ +export type ExecuteSetRateLimitAdminResult = TransactionHash + +/** Aptos TokenPool `setRateLimitAdmin` operation — unsupported on Aptos. */ +export class SetRateLimitAdmin extends AptosOperation { + readonly name = 'setRateLimitAdmin' + + /** Always throws — rate limiting is managed directly by the pool owner on Aptos. */ + protected validate(_params: GenerateSetRateLimitAdminParams): void { + throw new CCIPMethodUnsupportedError('AptosTokenManager', this.name) + } + + /** Unreachable — {@link validate} always throws first. */ + protected buildUnsigned( + _chain: AptosChain, + _params: GenerateSetRateLimitAdminParams, + ): Promise { + throw new CCIPMethodUnsupportedError('AptosTokenManager', this.name) + } +} diff --git a/ccip-sdk/src/cct/aptos/pool/operations/test-helpers.ts b/ccip-sdk/src/cct/aptos/pool/operations/test-helpers.ts new file mode 100644 index 00000000..206429fd --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/test-helpers.ts @@ -0,0 +1,59 @@ +import type { AptosChain } from '../../../../aptos/index.ts' + +/** A test sender address (32-byte hex). */ +export const SENDER = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + +/** A test pool object address. */ +export const POOL = '0x00000000000000000000000000000000000000000000000000000000deadbeef' + +/** A test remote pool address (EVM-style 20-byte hex). */ +export const REMOTE_POOL = '0xd7BF0d8E6C242b6Dde4490Ab3aFc8C1e811ec9aD' + +/** A test remote token address (EVM-style 20-byte hex). */ +export const REMOTE_TOKEN = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' + +/** A test new-owner address. */ +export const NEW_OWNER = '0x0000000000000000000000000000000000000000000000000000abcdef123456' + +/** A test remote chain selector. */ +export const SELECTOR = 16015286601757825753n + +/** A disabled rate limiter config. */ +export const DISABLED_RATE_LIMITER = { + isEnabled: false, + capacity: '0', + rate: '0', +} as const + +/** + * Builds a minimal AptosChain stub sufficient for pool `buildUnsigned`. + * + * Stubs pool-module discovery (`_getAccountModulesNames` + `provider.view`), + * account sequence lookup (`getAccountInfo`), and transaction building + * (`transaction.build.simple`) so operations run fully offline. + */ +export function stubChain(): AptosChain { + const fakeTx = { bcsToBytes: () => new Uint8Array([1, 2, 3]) } + const provider = { + async view() { + return ['0xmanaged_token'] as [string] + }, + async getAccountInfo() { + return { sequence_number: '0' } + }, + transaction: { + build: { + async simple() { + return fakeTx + }, + }, + }, + } + return { + provider, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + async _getAccountModulesNames() { + return ['managed_token_pool'] + }, + } as unknown as AptosChain +} diff --git a/ccip-sdk/src/cct/aptos/pool/operations/transfer-ownership.test.ts b/ccip-sdk/src/cct/aptos/pool/operations/transfer-ownership.test.ts new file mode 100644 index 00000000..b9a54762 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/transfer-ownership.test.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { NEW_OWNER, POOL, SENDER, stubChain } from './test-helpers.ts' +import { TransferOwnership } from './transfer-ownership.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos TokenPool transferOwnership', () => { + it('builds a single Aptos transaction', async () => { + const unsigned = await new TransferOwnership().generate(stubChain(), { + poolAddress: POOL, + newOwner: NEW_OWNER, + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + assert.equal(unsigned.transactions.length, 1) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects an empty newOwner before building', async () => { + await assert.rejects( + () => + new TransferOwnership().generate(stubChain(), { + poolAddress: POOL, + newOwner: '', + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/pool/operations/transfer-ownership.ts b/ccip-sdk/src/cct/aptos/pool/operations/transfer-ownership.ts new file mode 100644 index 00000000..ca88a46c --- /dev/null +++ b/ccip-sdk/src/cct/aptos/pool/operations/transfer-ownership.ts @@ -0,0 +1,80 @@ +/** + * Aptos TokenPool `transferOwnership` operation. + * + * Proposes a new pool owner (step 1 of the Aptos 3-step ownership transfer) via + * `poolAddress::moduleName::transfer_ownership(newOwner)`. Auto-discovers the + * pool module from the pool address. + * + * @packageDocumentation + */ + +import { AccountAddress } from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import type { UnsignedAptosTx } from '../../../../aptos/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { discoverPoolModule, ensurePoolInitialized } from '../../common.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' + +/** Parameters shared by Aptos TokenPool `transferOwnership` generation and execution. */ +type TransferOwnershipParams = { + /** Local pool object address (Aptos hex). */ + poolAddress: string + /** New owner address to propose. */ + newOwner: string +} + +/** Parameters for unsigned Aptos TokenPool `transferOwnership` generation. */ +export type GenerateTransferOwnershipParams = AptosGenerateParams + +/** Unsigned Aptos TokenPool `transferOwnership` result. */ +export type GenerateTransferOwnershipResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenPool `transferOwnership`. */ +export type ExecuteTransferOwnershipParams = AptosExecuteParams + +/** Result of executing Aptos TokenPool `transferOwnership`. */ +export type ExecuteTransferOwnershipResult = TransactionHash + +/** Aptos TokenPool `transferOwnership` operation. */ +export class TransferOwnership extends AptosOperation { + readonly name = 'transferOwnership' + + /** Validates the pool address and proposed owner before any RPC. */ + protected validate(params: GenerateTransferOwnershipParams): void { + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'poolAddress', 'must be non-empty') + } + if (!params.newOwner || params.newOwner.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'newOwner', 'must be non-empty') + } + } + + /** Discovers the pool module and builds a `transfer_ownership` transaction. */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateTransferOwnershipParams, + ): Promise { + const moduleName = await discoverPoolModule(chain, params.poolAddress) + await ensurePoolInitialized(chain, params.poolAddress, moduleName) + + const tx = await chain.provider.transaction.build.simple({ + sender: AccountAddress.from(params.sender), + data: { + function: `${params.poolAddress}::${moduleName}::transfer_ownership`, + functionArguments: [params.newOwner], + }, + }) + + chain.logger.debug( + `${this.name}: pool = ${params.poolAddress}, module = ${moduleName}, newOwner = ${params.newOwner}`, + ) + return { family: ChainFamily.Aptos, transactions: [tx.bcsToBytes()] } + } +} diff --git a/ccip-sdk/src/cct/aptos/submit.ts b/ccip-sdk/src/cct/aptos/submit.ts new file mode 100644 index 00000000..42a9525b --- /dev/null +++ b/ccip-sdk/src/cct/aptos/submit.ts @@ -0,0 +1,52 @@ +/** + * Signs and submits an unsigned Aptos transaction, returning once confirmed. + * Shared by every {@link AptosOperation}; mirrors the Solana `submit` helper. + * + * @packageDocumentation + */ + +import { Deserializer, SimpleTransaction } from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../aptos/index.ts' +import { isAptosAccount } from '../../aptos/types.ts' +import { CCIPError, CCIPWalletInvalidError } from '../../errors/index.ts' +import { CCTTxFailedError } from '../errors.ts' +import type { TransactionHash } from '../operation.ts' + +/** + * Signs the first transaction of an unsigned Aptos tx with the given account, + * submits it, and waits for confirmation. + * + * @param chain - Aptos chain to submit through + * @param wallet - Aptos account with signing capability + * @param transactions - BCS-serialized `SimpleTransaction` bytes (first is submitted) + * @param operation - Operation name for error/log context + * @returns The confirmed transaction hash + * @throws {@link CCIPWalletInvalidError} if `wallet` is not an Aptos account + * @throws {@link CCTTxFailedError} if signing, submission, or confirmation fails + */ +export async function submit( + chain: AptosChain, + wallet: unknown, + transactions: [Uint8Array, ...Uint8Array[]], + operation: string, +): Promise { + if (!isAptosAccount(wallet)) throw new CCIPWalletInvalidError(wallet) + + try { + const unsigned = SimpleTransaction.deserialize(new Deserializer(transactions[0])) + const senderAuthenticator = await wallet.signTransactionWithAuthenticator(unsigned) + const pending = await chain.provider.transaction.submit.simple({ + transaction: unsigned, + senderAuthenticator, + }) + const { hash } = await chain.provider.waitForTransaction({ transactionHash: pending.hash }) + chain.logger.debug(`${operation}: tx = ${hash}`) + return { hash } + } catch (error) { + if (error instanceof CCIPError) throw error + throw new CCTTxFailedError(operation, error instanceof Error ? error.message : String(error), { + cause: error instanceof Error ? error : undefined, + }) + } +} diff --git a/ccip-sdk/src/cct/aptos/token-admin-registry/operations/accept-admin-role.test.ts b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/accept-admin-role.test.ts new file mode 100644 index 00000000..167a8abb --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/accept-admin-role.test.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { AcceptAdminRole } from './accept-admin-role.ts' +import { ROUTER, SENDER, TOKEN, stubChain } from './test-helpers.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos TokenAdminRegistry acceptAdminRole', () => { + it('builds a single Aptos transaction', async () => { + const unsigned = await new AcceptAdminRole().generate(stubChain(), { + tokenAddress: TOKEN, + routerAddress: ROUTER, + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + assert.equal(unsigned.transactions.length, 1) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects an empty routerAddress before building', async () => { + await assert.rejects( + () => + new AcceptAdminRole().generate(stubChain(), { + tokenAddress: TOKEN, + routerAddress: '', + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/token-admin-registry/operations/accept-admin-role.ts b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/accept-admin-role.ts new file mode 100644 index 00000000..15edaa13 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/accept-admin-role.ts @@ -0,0 +1,85 @@ +/** + * Aptos TokenAdminRegistry `acceptAdminRole` operation. + * + * Called by the pending administrator to complete a two-step admin handoff. + * Invokes `routerAddress::token_admin_registry::accept_admin_role`. + * + * @packageDocumentation + */ + +import { + buildTransaction, + generateTransactionPayloadWithABI, + parseTypeTag, +} from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import type { UnsignedAptosTx } from '../../../../aptos/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' + +/** Parameters shared by Aptos TokenAdminRegistry `acceptAdminRole` generation and execution. */ +type AcceptAdminRoleParams = { + /** Token address to accept the admin role for. */ + tokenAddress: string + /** CCIP router module address (bundles the TokenAdminRegistry on Aptos). */ + routerAddress: string +} + +/** Parameters for unsigned Aptos TokenAdminRegistry `acceptAdminRole` generation. */ +export type GenerateAcceptAdminRoleParams = AptosGenerateParams + +/** Unsigned Aptos TokenAdminRegistry `acceptAdminRole` result. */ +export type GenerateAcceptAdminRoleResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenAdminRegistry `acceptAdminRole`. */ +export type ExecuteAcceptAdminRoleParams = AptosExecuteParams + +/** Result of executing Aptos TokenAdminRegistry `acceptAdminRole`. */ +export type ExecuteAcceptAdminRoleResult = TransactionHash + +/** Aptos TokenAdminRegistry `acceptAdminRole` operation. */ +export class AcceptAdminRole extends AptosOperation { + readonly name = 'acceptAdminRole' + + /** Validates all params before building the transaction. */ + protected validate(params: GenerateAcceptAdminRoleParams): void { + if (!params.tokenAddress || params.tokenAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'tokenAddress', 'must be non-empty') + } + if (!params.routerAddress || params.routerAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'routerAddress', 'must be non-empty') + } + } + + /** Builds the unsigned Aptos `accept_admin_role` transaction. */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateAcceptAdminRoleParams, + ): Promise { + const payload = generateTransactionPayloadWithABI({ + function: `${params.routerAddress}::token_admin_registry::accept_admin_role`, + functionArguments: [params.tokenAddress], + abi: { + typeParameters: [], + parameters: [parseTypeTag('address')], + }, + }) + const tx = await buildTransaction({ + aptosConfig: chain.provider.config, + sender: params.sender, + payload, + }) + + chain.logger.debug( + `${this.name}: router = ${params.routerAddress}, token = ${params.tokenAddress}`, + ) + return { family: ChainFamily.Aptos, transactions: [tx.bcsToBytes()] } + } +} diff --git a/ccip-sdk/src/cct/aptos/token-admin-registry/operations/index.ts b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/index.ts new file mode 100644 index 00000000..c7c770ed --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/index.ts @@ -0,0 +1,4 @@ +export * from './accept-admin-role.ts' +export * from './propose-admin-role.ts' +export * from './set-pool.ts' +export * from './transfer-admin-role.ts' diff --git a/ccip-sdk/src/cct/aptos/token-admin-registry/operations/propose-admin-role.test.ts b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/propose-admin-role.test.ts new file mode 100644 index 00000000..707a456b --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/propose-admin-role.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { ProposeAdminRole } from './propose-admin-role.ts' +import { ADMIN, ROUTER, SENDER, TOKEN, stubChain } from './test-helpers.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos TokenAdminRegistry proposeAdminRole', () => { + it('builds a single Aptos transaction', async () => { + const unsigned = await new ProposeAdminRole().generate(stubChain(), { + tokenAddress: TOKEN, + administrator: ADMIN, + routerAddress: ROUTER, + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + assert.equal(unsigned.transactions.length, 1) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects an empty tokenAddress before building', async () => { + await assert.rejects( + () => + new ProposeAdminRole().generate(stubChain(), { + tokenAddress: '', + administrator: ADMIN, + routerAddress: ROUTER, + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/token-admin-registry/operations/propose-admin-role.ts b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/propose-admin-role.ts new file mode 100644 index 00000000..cb73d75b --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/propose-admin-role.ts @@ -0,0 +1,90 @@ +/** + * Aptos TokenAdminRegistry `proposeAdminRole` operation. + * + * On Aptos, the TokenAdminRegistry is a module within the CCIP router package + * (`routerAddress::token_admin_registry::propose_administrator`). + * + * @packageDocumentation + */ + +import { + buildTransaction, + generateTransactionPayloadWithABI, + parseTypeTag, +} from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import type { UnsignedAptosTx } from '../../../../aptos/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' + +/** Parameters shared by Aptos TokenAdminRegistry `proposeAdminRole` generation and execution. */ +type ProposeAdminRoleParams = { + /** Token address to propose an administrator for. */ + tokenAddress: string + /** Address of the proposed administrator. */ + administrator: string + /** CCIP router module address (bundles the TokenAdminRegistry on Aptos). */ + routerAddress: string +} + +/** Parameters for unsigned Aptos TokenAdminRegistry `proposeAdminRole` generation. */ +export type GenerateProposeAdminRoleParams = AptosGenerateParams + +/** Unsigned Aptos TokenAdminRegistry `proposeAdminRole` result. */ +export type GenerateProposeAdminRoleResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenAdminRegistry `proposeAdminRole`. */ +export type ExecuteProposeAdminRoleParams = AptosExecuteParams + +/** Result of executing Aptos TokenAdminRegistry `proposeAdminRole`. */ +export type ExecuteProposeAdminRoleResult = TransactionHash + +/** Aptos TokenAdminRegistry `proposeAdminRole` operation. */ +export class ProposeAdminRole extends AptosOperation { + readonly name = 'proposeAdminRole' + + /** Validates all params before building the transaction. */ + protected validate(params: GenerateProposeAdminRoleParams): void { + if (!params.tokenAddress || params.tokenAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'tokenAddress', 'must be non-empty') + } + if (!params.administrator || params.administrator.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'administrator', 'must be non-empty') + } + if (!params.routerAddress || params.routerAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'routerAddress', 'must be non-empty') + } + } + + /** Builds the unsigned Aptos `propose_administrator` transaction. */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateProposeAdminRoleParams, + ): Promise { + const payload = generateTransactionPayloadWithABI({ + function: `${params.routerAddress}::token_admin_registry::propose_administrator`, + functionArguments: [params.tokenAddress, params.administrator], + abi: { + typeParameters: [], + parameters: [parseTypeTag('address'), parseTypeTag('address')], + }, + }) + const tx = await buildTransaction({ + aptosConfig: chain.provider.config, + sender: params.sender, + payload, + }) + + chain.logger.debug( + `${this.name}: router = ${params.routerAddress}, token = ${params.tokenAddress}`, + ) + return { family: ChainFamily.Aptos, transactions: [tx.bcsToBytes()] } + } +} diff --git a/ccip-sdk/src/cct/aptos/token-admin-registry/operations/set-pool.test.ts b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/set-pool.test.ts new file mode 100644 index 00000000..d494bcca --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/set-pool.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { SetPool } from './set-pool.ts' +import { POOL, ROUTER, SENDER, TOKEN, stubChain } from './test-helpers.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos TokenAdminRegistry setPool', () => { + it('builds a single Aptos transaction', async () => { + const unsigned = await new SetPool().generate(stubChain(), { + tokenAddress: TOKEN, + poolAddress: POOL, + routerAddress: ROUTER, + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + assert.equal(unsigned.transactions.length, 1) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects an empty poolAddress before building', async () => { + await assert.rejects( + () => + new SetPool().generate(stubChain(), { + tokenAddress: TOKEN, + poolAddress: '', + routerAddress: ROUTER, + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/token-admin-registry/operations/set-pool.ts b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/set-pool.ts new file mode 100644 index 00000000..ef70034a --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/set-pool.ts @@ -0,0 +1,90 @@ +/** + * Aptos TokenAdminRegistry `setPool` operation. + * + * Registers a token pool in the TokenAdminRegistry. Invokes + * `routerAddress::token_admin_registry::set_pool`. + * + * @packageDocumentation + */ + +import { + buildTransaction, + generateTransactionPayloadWithABI, + parseTypeTag, +} from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import type { UnsignedAptosTx } from '../../../../aptos/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' + +/** Parameters shared by Aptos TokenAdminRegistry `setPool` generation and execution. */ +type SetPoolParams = { + /** Token address (Aptos hex) to register a pool for. */ + tokenAddress: string + /** Pool resource address (Aptos hex) to link to the token. */ + poolAddress: string + /** CCIP router module address (bundles the TokenAdminRegistry on Aptos). */ + routerAddress: string +} + +/** Parameters for unsigned Aptos TokenAdminRegistry `setPool` generation. */ +export type GenerateSetPoolParams = AptosGenerateParams + +/** Unsigned Aptos TokenAdminRegistry `setPool` result. */ +export type GenerateSetPoolResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenAdminRegistry `setPool`. */ +export type ExecuteSetPoolParams = AptosExecuteParams + +/** Result of executing Aptos TokenAdminRegistry `setPool`. */ +export type ExecuteSetPoolResult = TransactionHash + +/** Aptos TokenAdminRegistry `setPool` operation. */ +export class SetPool extends AptosOperation { + readonly name = 'setPool' + + /** Validates all params before building the transaction. */ + protected validate(params: GenerateSetPoolParams): void { + if (!params.tokenAddress || params.tokenAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'tokenAddress', 'must be non-empty') + } + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'poolAddress', 'must be non-empty') + } + if (!params.routerAddress || params.routerAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'routerAddress', 'must be non-empty') + } + } + + /** Builds the unsigned Aptos `set_pool` transaction. */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateSetPoolParams, + ): Promise { + const payload = generateTransactionPayloadWithABI({ + function: `${params.routerAddress}::token_admin_registry::set_pool`, + functionArguments: [params.tokenAddress, params.poolAddress], + abi: { + typeParameters: [], + parameters: [parseTypeTag('address'), parseTypeTag('address')], + }, + }) + const tx = await buildTransaction({ + aptosConfig: chain.provider.config, + sender: params.sender, + payload, + }) + + chain.logger.debug( + `${this.name}: router = ${params.routerAddress}, token = ${params.tokenAddress}, pool = ${params.poolAddress}`, + ) + return { family: ChainFamily.Aptos, transactions: [tx.bcsToBytes()] } + } +} diff --git a/ccip-sdk/src/cct/aptos/token-admin-registry/operations/test-helpers.ts b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/test-helpers.ts new file mode 100644 index 00000000..2b19f8a5 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/test-helpers.ts @@ -0,0 +1,86 @@ +import { + type Client, + type ClientRequest, + type ClientResponse, + Aptos, + AptosConfig, + Network, +} from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' + +/** A test sender address (32-byte hex). */ +export const SENDER = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + +/** A test router module address. */ +export const ROUTER = '0x00000000000000000000000000000000000000000000000000000000000000ab' + +/** A test token object address. */ +export const TOKEN = '0x0000000000000000000000000000000000000000000000000000000089fd6b14' + +/** A test pool resource address. */ +export const POOL = '0x00000000000000000000000000000000000000000000000000000000deadbeef' + +/** A test administrator address. */ +export const ADMIN = '0x0000000000000000000000000000000000000000000000000000abcdef123456' + +/** + * Fake Aptos client returning canned responses for the fullnode reads that + * `buildTransaction` performs (ledger info, account sequence number, gas price). + * Keeps `buildUnsigned` fully offline. + */ +function fakeClient(): Client { + return { + async provider(req: ClientRequest): Promise> { + const path = new URL(req.url).pathname + const ok = (data: unknown): ClientResponse => ({ + status: 200, + statusText: 'OK', + data: data as Res, + headers: {}, + config: req, + request: null, + response: null, + }) + + if (path.includes('estimate_gas_price')) { + return ok({ + deprioritized_gas_estimate: 100, + gas_estimate: 100, + prioritized_gas_estimate: 100, + }) + } + if (path.includes('/accounts/')) { + return ok({ + sequence_number: '0', + authentication_key: SENDER, + }) + } + // Ledger info (used for chain id). + return ok({ + chain_id: 1, + epoch: '1', + ledger_version: '1', + oldest_ledger_version: '0', + ledger_timestamp: '0', + node_role: 'full_node', + oldest_block_height: '0', + block_height: '1', + git_hash: '', + }) + }, + } +} + +/** Builds a minimal AptosChain stub sufficient for `buildUnsigned`. */ +export function stubChain(): AptosChain { + const config = new AptosConfig({ + network: Network.CUSTOM, + fullnode: 'http://localhost/v1', + client: fakeClient(), + }) + return { + provider: new Aptos(config), + logger: { debug() {}, info() {}, warn() {}, error() {} }, + } as unknown as AptosChain +} diff --git a/ccip-sdk/src/cct/aptos/token-admin-registry/operations/transfer-admin-role.test.ts b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/transfer-admin-role.test.ts new file mode 100644 index 00000000..f696ce48 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/transfer-admin-role.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { ADMIN, ROUTER, SENDER, TOKEN, stubChain } from './test-helpers.ts' +import { TransferAdminRole } from './transfer-admin-role.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos TokenAdminRegistry transferAdminRole', () => { + it('builds a single Aptos transaction', async () => { + const unsigned = await new TransferAdminRole().generate(stubChain(), { + tokenAddress: TOKEN, + newAdmin: ADMIN, + routerAddress: ROUTER, + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + assert.equal(unsigned.transactions.length, 1) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects an empty newAdmin before building', async () => { + await assert.rejects( + () => + new TransferAdminRole().generate(stubChain(), { + tokenAddress: TOKEN, + newAdmin: '', + routerAddress: ROUTER, + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/token-admin-registry/operations/transfer-admin-role.ts b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/transfer-admin-role.ts new file mode 100644 index 00000000..2e311d07 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token-admin-registry/operations/transfer-admin-role.ts @@ -0,0 +1,92 @@ +/** + * Aptos TokenAdminRegistry `transferAdminRole` operation. + * + * Called by the current administrator to hand off the admin role to a new + * address; the new admin must accept it to complete the transfer. Pass `@0x0` + * as `newAdmin` to cancel a pending transfer. Invokes + * `routerAddress::token_admin_registry::transfer_admin_role`. + * + * @packageDocumentation + */ + +import { + buildTransaction, + generateTransactionPayloadWithABI, + parseTypeTag, +} from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import type { UnsignedAptosTx } from '../../../../aptos/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' + +/** Parameters shared by Aptos TokenAdminRegistry `transferAdminRole` generation and execution. */ +type TransferAdminRoleParams = { + /** Token address to transfer the admin role for. */ + tokenAddress: string + /** Address of the new administrator (`@0x0` cancels a pending transfer). */ + newAdmin: string + /** CCIP router module address (bundles the TokenAdminRegistry on Aptos). */ + routerAddress: string +} + +/** Parameters for unsigned Aptos TokenAdminRegistry `transferAdminRole` generation. */ +export type GenerateTransferAdminRoleParams = AptosGenerateParams + +/** Unsigned Aptos TokenAdminRegistry `transferAdminRole` result. */ +export type GenerateTransferAdminRoleResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenAdminRegistry `transferAdminRole`. */ +export type ExecuteTransferAdminRoleParams = AptosExecuteParams + +/** Result of executing Aptos TokenAdminRegistry `transferAdminRole`. */ +export type ExecuteTransferAdminRoleResult = TransactionHash + +/** Aptos TokenAdminRegistry `transferAdminRole` operation. */ +export class TransferAdminRole extends AptosOperation { + readonly name = 'transferAdminRole' + + /** Validates all params before building the transaction. */ + protected validate(params: GenerateTransferAdminRoleParams): void { + if (!params.tokenAddress || params.tokenAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'tokenAddress', 'must be non-empty') + } + if (!params.newAdmin || params.newAdmin.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'newAdmin', 'must be non-empty') + } + if (!params.routerAddress || params.routerAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'routerAddress', 'must be non-empty') + } + } + + /** Builds the unsigned Aptos `transfer_admin_role` transaction. */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateTransferAdminRoleParams, + ): Promise { + const payload = generateTransactionPayloadWithABI({ + function: `${params.routerAddress}::token_admin_registry::transfer_admin_role`, + functionArguments: [params.tokenAddress, params.newAdmin], + abi: { + typeParameters: [], + parameters: [parseTypeTag('address'), parseTypeTag('address')], + }, + }) + const tx = await buildTransaction({ + aptosConfig: chain.provider.config, + sender: params.sender, + payload, + }) + + chain.logger.debug( + `${this.name}: router = ${params.routerAddress}, token = ${params.tokenAddress}, newAdmin = ${params.newAdmin}`, + ) + return { family: ChainFamily.Aptos, transactions: [tx.bcsToBytes()] } + } +} diff --git a/ccip-sdk/src/cct/aptos/token-pool/operations/deploy-pool.test.ts b/ccip-sdk/src/cct/aptos/token-pool/operations/deploy-pool.test.ts new file mode 100644 index 00000000..38b58425 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token-pool/operations/deploy-pool.test.ts @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { DeployPool } from './deploy-pool.ts' +import type { AptosChain } from '../../../../aptos/index.ts' +import { + CCIPPoolDeployParamsInvalidError, + CCIPWalletInvalidError, +} from '../../../../errors/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +/** A test sender address (32-byte hex). */ +const SENDER = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + +/** A test fungible-asset metadata address. */ +const TOKEN = '0x00000000000000000000000000000000000000000000000000000000deadbeef' + +/** A minimal chain stub — validation rejects before any RPC/CLI is reached. */ +const chain = {} as unknown as AptosChain + +describe('Aptos TokenPool deployPool', () => { + it('rejects an empty tokenAddress with CCTParamsInvalidError before any RPC', async () => { + await assert.rejects( + () => + new DeployPool().generate(chain, { + poolType: 'burn-mint', + tokenAddress: '', + localTokenDecimals: 8, + routerAddress: '0xabc', + mcmsAddress: '0x123', + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) + + it('rejects an invalid poolType via validatePoolParams', async () => { + await assert.rejects( + () => + new DeployPool().generate(chain, { + // @ts-expect-error deliberately invalid poolType for the rejection path + poolType: 'nope', + tokenAddress: TOKEN, + localTokenDecimals: 8, + routerAddress: '0xabc', + mcmsAddress: '0x123', + sender: SENDER, + }), + CCIPPoolDeployParamsInvalidError, + ) + }) + + it('rejects a non-Aptos wallet on execute with CCIPWalletInvalidError', async () => { + await assert.rejects( + () => + new DeployPool().execute(chain, { + poolType: 'burn-mint', + tokenAddress: TOKEN, + localTokenDecimals: 8, + routerAddress: '0xabc', + mcmsAddress: '0x123', + wallet: {}, + }), + CCIPWalletInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/token-pool/operations/deploy-pool.ts b/ccip-sdk/src/cct/aptos/token-pool/operations/deploy-pool.ts new file mode 100644 index 00000000..051b4c78 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token-pool/operations/deploy-pool.ts @@ -0,0 +1,301 @@ +/** + * Aptos TokenPool `deployPool` operation. + * + * Deploys a Move token-pool package (compile → publish CCIPTokenPool → publish + * pool). This is a **multi-step imperative deploy**: it produces **two sequential + * publish transactions** where the second depends on the object created by the + * first, so the single-tx base {@link AptosOperation.execute} is insufficient and + * {@link DeployPool.execute} is overridden. + * + * **Requires the `aptos` CLI** — the Move source is compiled at deploy time. + * + * @packageDocumentation + */ + +import { Buffer } from 'buffer' + +import { + AccountAddress, + buildTransaction, + createObjectAddress, + generateTransactionPayloadWithABI, + parseTypeTag, +} from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import { type UnsignedAptosTx, isAptosAccount } from '../../../../aptos/types.ts' +import { CCIPPoolDeployFailedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { AptosDeployPoolParams } from '../../../../token-admin/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + compilePoolPackages, + ensureAptosCli, + poolLabel, + resolveCodeObjectAddress, + resolveNamedAddresses, + validatePoolParams, +} from '../../common.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' +import { submit } from '../../submit.ts' + +/** Domain separator used by `object_code_deployment::publish` to derive object addresses. */ +const OBJECT_CODE_DEPLOYMENT_DOMAIN = 'aptos_framework::object_code_deployment' + +/** + * Parameters for deploying an Aptos CCIP token pool. + * + * Alias of the legacy {@link AptosDeployPoolParams}: `poolType`, `tokenAddress`, + * `localTokenDecimals`, plus Aptos-specific `tokenModule`, `routerAddress`, + * `mcmsAddress`, and optional `adminAddress`. + */ +export type DeployPoolParams = AptosDeployPoolParams + +/** Parameters for unsigned Aptos TokenPool `deployPool` generation (carries `sender`). */ +export type GenerateDeployPoolParams = AptosGenerateParams + +/** Unsigned Aptos TokenPool `deployPool` result — two sequential publish transactions. */ +export type GenerateDeployPoolResult = UnsignedAptosTx + +/** Parameters for executing Aptos TokenPool `deployPool` (signs with `wallet`). */ +export type ExecuteDeployPoolParams = AptosExecuteParams + +/** Result of executing Aptos TokenPool `deployPool`: the last tx hash and the pool object address. */ +export type ExecuteDeployPoolResult = TransactionHash & { + /** Deployed pool object address (Aptos hex). */ + poolAddress: string + /** + * Whether the pool is ready for use. `false` for generic pools + * (`burn_mint`/`lock_release`), which still require the token creator module to + * call `initialize()` with the stored capability refs before CCIP operations. + */ + initialized: boolean +} + +/** + * Aptos TokenPool `deployPool` operation. + * + * Compiles and publishes a Move token-pool package in two sequential transactions + * (CCIPTokenPool object first, then the pool object referencing it). Overrides + * {@link execute} to submit both transactions in order and return the deployed + * `poolAddress` alongside the last confirmed hash. + */ +export class DeployPool extends AptosOperation< + DeployPoolParams, + UnsignedAptosTx, + ExecuteDeployPoolResult +> { + readonly name = 'deployPool' + + /** Front-checks `tokenAddress`, then delegates to {@link validatePoolParams}. */ + protected validate(params: GenerateDeployPoolParams): void { + if (!params.tokenAddress || params.tokenAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'tokenAddress', 'must be non-empty') + } + validatePoolParams(params) + } + + /** Compiles the pool packages and builds the two publish transactions. */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateDeployPoolParams, + ): Promise { + const { tx } = await this.prepare(chain, params) + return tx + } + + /** + * Compiles both pool packages and builds the two sequential publish transactions. + * + * Derives the two deterministic object addresses (`seq+1` → CCIPTokenPool, + * `seq+2` → pool), resolves the token code object for managed/regulated pools, + * compiles via the `aptos` CLI, and builds the publish payloads. + */ + private async prepare( + chain: AptosChain, + params: GenerateDeployPoolParams, + ): Promise<{ tx: UnsignedAptosTx; poolAddress: string }> { + const tokenModule = validatePoolParams(params) + ensureAptosCli() + + const { sender } = params + + // We need 2 sequential object addresses: + // seq+1 → CCIPTokenPool object + // seq+2 → pool object + const { sequence_number } = await chain.provider.getAccountInfo({ accountAddress: sender }) + const sequenceNumber = BigInt(sequence_number) + + const domainBytes = Buffer.from(OBJECT_CODE_DEPLOYMENT_DOMAIN, 'utf8') + const uleb = Buffer.from([domainBytes.length]) + + // Object address for CCIPTokenPool (seq + 1) + const seqBuf1 = Buffer.alloc(8) + seqBuf1.writeBigUInt64LE(sequenceNumber + 1n) + const tokenPoolObjectAddress = createObjectAddress( + AccountAddress.from(sender), + new Uint8Array(Buffer.concat([uleb, domainBytes, seqBuf1])), + ).toString() + + // Object address for the pool (seq + 2) + const seqBuf2 = Buffer.alloc(8) + seqBuf2.writeBigUInt64LE(sequenceNumber + 2n) + const poolObjectAddress = createObjectAddress( + AccountAddress.from(sender), + new Uint8Array(Buffer.concat([uleb, domainBytes, seqBuf2])), + ).toString() + + const label = poolLabel(tokenModule, params.poolType) + + chain.logger.debug( + `${this.name}: ${label} tokenPool =`, + tokenPoolObjectAddress, + 'pool =', + poolObjectAddress, + ) + + // For managed/regulated pools, resolve the code object address from the FA metadata + // by walking the Aptos object ownership chain on-chain. + // Generic pools use tokenAddress (FA metadata) directly as a named address. + let tokenCodeObjectAddress: string | undefined + if (tokenModule === 'managed' || tokenModule === 'regulated') { + tokenCodeObjectAddress = await resolveCodeObjectAddress(chain.provider, params.tokenAddress) + chain.logger.debug( + `${this.name}: resolved code object =`, + tokenCodeObjectAddress, + 'from FA metadata =', + params.tokenAddress, + ) + } + + const namedAddresses = resolveNamedAddresses( + tokenPoolObjectAddress, + poolObjectAddress, + tokenModule, + params.poolType, + params, + tokenCodeObjectAddress, + ) + + const { tokenPool, pool } = await compilePoolPackages( + tokenPoolObjectAddress, + poolObjectAddress, + tokenModule, + params.poolType, + namedAddresses, + chain.logger, + ) + + // Build 2 publish transactions (sequential: token_pool first, then pool) + const buildPublishTx = ( + compiled: { metadataBytes: string; byteCode: string[] }, + seq: bigint, + ) => { + const payload = generateTransactionPayloadWithABI({ + function: '0x1::object_code_deployment::publish', + functionArguments: [ + Buffer.from(compiled.metadataBytes.replace(/^0x/, ''), 'hex'), + compiled.byteCode.map((b) => Buffer.from(b.replace(/^0x/, ''), 'hex')), + ], + abi: { + typeParameters: [], + parameters: [parseTypeTag('vector'), parseTypeTag('vector>')], + }, + }) + return buildTransaction({ + aptosConfig: chain.provider.config, + sender, + payload, + options: { accountSequenceNumber: seq }, + }) + } + + const tokenPoolTx = await buildPublishTx(tokenPool, sequenceNumber) + const poolTx = await buildPublishTx(pool, sequenceNumber + 1n) + + chain.logger.debug( + `${this.name}: ${label} sender =`, + sender, + 'tokenPool =', + tokenPoolObjectAddress, + 'pool =', + poolObjectAddress, + 'transactions = 2', + ) + + return { + tx: { + family: ChainFamily.Aptos, + transactions: [tokenPoolTx.bcsToBytes(), poolTx.bcsToBytes()], + }, + poolAddress: poolObjectAddress, + } + } + + /** + * Compiles, publishes, and initializes the pool by submitting the two publish + * transactions sequentially, returning the deployed `poolAddress` and last hash. + * + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid Aptos account + * @throws {@link CCIPPoolDeployFailedError} if compilation or any publish tx fails + */ + override async execute( + chain: AptosChain, + params: ExecuteDeployPoolParams, + ): Promise { + const { wallet } = params + if (!isAptosAccount(wallet)) throw new CCIPWalletInvalidError(wallet) + + const { wallet: _wallet, ...rest } = params + const sender = wallet.accountAddress.toString() + const genParams: GenerateDeployPoolParams = { ...rest, sender } + + this.validate(genParams) + + const { tx, poolAddress } = await this.prepare(chain, genParams) + + const tokenModule = params.tokenModule ?? 'managed' + const label = poolLabel(tokenModule, params.poolType) + chain.logger.debug( + `${this.name}: deploying ${label}...`, + tx.transactions.length, + 'transactions', + ) + + try { + let lastHash = '' + for (const [i, txn] of tx.transactions.entries()) { + const { hash } = await submit(chain, wallet, [txn], this.name) + lastHash = hash + chain.logger.debug(`${this.name}: tx ${i + 1}/${tx.transactions.length} confirmed:`, hash) + } + + // Generic pools (burn_mint / lock_release) are NOT usable until the token + // creator module calls initialize() with the stored capability refs. + if (tokenModule === 'generic') { + const poolModule = + params.poolType === 'burn-mint' ? 'burn_mint_token_pool' : 'lock_release_token_pool' + chain.logger.warn( + `${this.name}: Generic pool deployed but NOT initialized. ` + + `The token creator module must call ${poolModule}::initialize() ` + + `with the stored capability refs (BurnRef/MintRef/TransferRef) ` + + `before the pool can be used for CCIP operations.`, + ) + } + + chain.logger.info(`${this.name}: pool at`, poolAddress, 'tx =', lastHash) + + return { hash: lastHash, poolAddress, initialized: tokenModule !== 'generic' } + } catch (error) { + if (error instanceof CCIPPoolDeployFailedError) throw error + throw new CCIPPoolDeployFailedError(error instanceof Error ? error.message : String(error), { + cause: error instanceof Error ? error : undefined, + }) + } + } +} diff --git a/ccip-sdk/src/cct/aptos/token-pool/operations/index.ts b/ccip-sdk/src/cct/aptos/token-pool/operations/index.ts new file mode 100644 index 00000000..2b950e45 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token-pool/operations/index.ts @@ -0,0 +1 @@ +export * from './deploy-pool.ts' diff --git a/ccip-sdk/src/cct/aptos/token/get-mint-burn-roles.test.ts b/ccip-sdk/src/cct/aptos/token/get-mint-burn-roles.test.ts new file mode 100644 index 00000000..4b1c4281 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token/get-mint-burn-roles.test.ts @@ -0,0 +1,45 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { getMintBurnRoles } from './get-mint-burn-roles.ts' +import type { AptosChain } from '../../../aptos/index.ts' + +const TOKEN = '0x0000000000000000000000000000000000000000000000000000000089fd6b14' + +/** + * Stub whose `provider.view` returns a single address for `0x1::object::owner` + * lookups and a member list for the managed-token role view functions. + */ +function stubChain(): AptosChain { + const provider = { + async view({ payload }: { payload: { function: string } }) { + if (payload.function.endsWith('::managed_token::get_allowed_minters')) { + return [['0x000000000000000000000000000000000000000000000000000000000000aaaa']] + } + if (payload.function.endsWith('::managed_token::get_allowed_burners')) { + return [['0x000000000000000000000000000000000000000000000000000000000000bbbb']] + } + // 0x1::object::owner lookups (code-object resolution + owner). + return ['0x000000000000000000000000000000000000000000000000000000000000code'] + }, + } + return { + provider, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + } as unknown as AptosChain +} + +describe('Aptos token getMintBurnRoles', () => { + it('reads managed-token minter/burner allowlists', async () => { + const roles = await getMintBurnRoles(stubChain(), TOKEN) + + assert.equal(roles.tokenModule, 'managed') + assert.deepEqual(roles.allowedMinters, [ + '0x000000000000000000000000000000000000000000000000000000000000aaaa', + ]) + assert.deepEqual(roles.allowedBurners, [ + '0x000000000000000000000000000000000000000000000000000000000000bbbb', + ]) + assert.equal(roles.owner, '0x000000000000000000000000000000000000000000000000000000000000code') + }) +}) diff --git a/ccip-sdk/src/cct/aptos/token/get-mint-burn-roles.ts b/ccip-sdk/src/cct/aptos/token/get-mint-burn-roles.ts new file mode 100644 index 00000000..6dae756a --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token/get-mint-burn-roles.ts @@ -0,0 +1,108 @@ +/** + * getMintBurnRoles (Aptos) — reads the mint/burn role members on a managed or + * regulated token. + * + * Resolves the token's code object from its Fungible Asset metadata address, then + * probes `managed_token` view functions first and falls back to `regulated_token`. + * Read-only — a free `async function`, not a write {@link Operation}. + * + * - **managed**: `get_allowed_minters()` / `get_allowed_burners()` + * - **regulated**: `get_minters()` / `get_burners()` / `get_bridge_minters_or_burners()` + * + * @packageDocumentation + */ + +import type { AptosChain } from '../../../aptos/index.ts' +import { resolveTokenCodeObject } from '../common.ts' + +/** A token's mint/burn role members, with the detected token module type. */ +export type MintBurnRolesResult = { + /** Detected token module type. */ + tokenModule: 'managed' | 'regulated' | 'unknown' + /** Owner of the code object — can always mint/burn, independent of the allowed lists. */ + owner?: string + /** Addresses allowed to mint. */ + allowedMinters?: string[] + /** Addresses allowed to burn. */ + allowedBurners?: string[] + /** Addresses with the BRIDGE_MINTER_OR_BURNER role (regulated only). */ + bridgeMintersOrBurners?: string[] +} + +/** + * Reads the mint/burn role members for an Aptos managed or regulated token. + * + * @param chain - Aptos chain facade + * @param tokenAddress - Fungible asset metadata address (hex string) + * @returns Role info including the detected token module type + */ +export async function getMintBurnRoles( + chain: AptosChain, + tokenAddress: string, +): Promise { + const codeObject = await resolveTokenCodeObject(chain, tokenAddress) + + // Resolve the owner of the code object (wallet that deployed the token). + let owner: string | undefined + try { + const [codeObjectOwner] = await chain.provider.view<[string]>({ + payload: { + function: '0x1::object::owner', + typeArguments: ['0x1::object::ObjectCore'], + functionArguments: [codeObject], + }, + }) + owner = codeObjectOwner + } catch { + chain.logger.debug('getMintBurnRoles: failed to resolve code object owner') + } + + // Try managed_token first. + try { + const [minters] = await chain.provider.view<[string[]]>({ + payload: { function: `${codeObject}::managed_token::get_allowed_minters` }, + }) + const [burners] = await chain.provider.view<[string[]]>({ + payload: { function: `${codeObject}::managed_token::get_allowed_burners` }, + }) + + chain.logger.debug( + `getMintBurnRoles: managed token, minters=${minters.length}, burners=${burners.length}`, + ) + + return { tokenModule: 'managed', owner, allowedMinters: minters, allowedBurners: burners } + } catch { + // Not a managed token, try regulated. + } + + // Try regulated_token. + try { + const [minters] = await chain.provider.view<[string[]]>({ + payload: { function: `${codeObject}::regulated_token::get_minters` }, + }) + const [burners] = await chain.provider.view<[string[]]>({ + payload: { function: `${codeObject}::regulated_token::get_burners` }, + }) + const [bridgeMintersOrBurners] = await chain.provider.view<[string[]]>({ + payload: { function: `${codeObject}::regulated_token::get_bridge_minters_or_burners` }, + }) + + chain.logger.debug( + `getMintBurnRoles: regulated token, minters=${minters.length}, burners=${burners.length}, bridge=${bridgeMintersOrBurners.length}`, + ) + + return { + tokenModule: 'regulated', + owner, + allowedMinters: minters, + allowedBurners: burners, + bridgeMintersOrBurners, + } + } catch { + // Not a regulated token either. + } + + chain.logger.debug('getMintBurnRoles: unknown token module type') + + return { tokenModule: 'unknown', owner } +} diff --git a/ccip-sdk/src/cct/aptos/token/operations/deploy-token.test.ts b/ccip-sdk/src/cct/aptos/token/operations/deploy-token.test.ts new file mode 100644 index 00000000..4dde22ce --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token/operations/deploy-token.test.ts @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { DeployToken } from './deploy-token.ts' +import { SENDER, stubChain } from './test-helpers.ts' +import { CCIPTokenDeployParamsInvalidError } from '../../../../errors/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos ManagedToken deployToken', () => { + it('rejects an empty name via validateParams (before any CLI compile)', async () => { + await assert.rejects( + () => + new DeployToken().generate(stubChain(), { + name: '', + symbol: 'MTK', + decimals: 8, + sender: SENDER, + }), + CCIPTokenDeployParamsInvalidError, + ) + }) + + it('rejects a negative initialSupply via validateParams', async () => { + await assert.rejects( + () => + new DeployToken().generate(stubChain(), { + name: 'My Token', + symbol: 'MTK', + decimals: 8, + initialSupply: -1n, + sender: SENDER, + }), + CCIPTokenDeployParamsInvalidError, + ) + }) + + it('rejects non-integer decimals via the CCT front-check', async () => { + await assert.rejects( + () => + new DeployToken().generate(stubChain(), { + name: 'My Token', + symbol: 'MTK', + decimals: -1, + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/token/operations/deploy-token.ts b/ccip-sdk/src/cct/aptos/token/operations/deploy-token.ts new file mode 100644 index 00000000..5da14945 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token/operations/deploy-token.ts @@ -0,0 +1,255 @@ +/** + * Aptos ManagedToken `deployToken` operation. + * + * Deploys a CCIP-compatible `managed_token` Fungible Asset by compiling the Move + * source with the deployer's deterministic object address, publishing it, calling + * `initialize`, and — when `initialSupply > 0` — pre-minting the initial supply to + * the recipient (or the deployer). This is an imperative **multi-step** deploy: each + * transaction depends on the previous one, so {@link execute} is overridden to sign + * and submit them sequentially rather than using the single-tx base flow. + * + * **Node.js/CLI only** — Move compilation requires the `aptos` CLI plus filesystem + * and child-process access, so it cannot run in a browser. See the module docs on the + * legacy `AptosTokenAdmin` for the backend-relay pattern for frontend integration. + * + * @packageDocumentation + */ + +import { Buffer } from 'buffer' + +import { + buildTransaction, + generateTransactionPayloadWithABI, + parseTypeTag, +} from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import { type UnsignedAptosTx, isAptosAccount } from '../../../../aptos/types.ts' +import { CCIPTokenDeployFailedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + compilePackage, + computeObjectAddress, + deriveFungibleAssetAddress, + ensureAptosCli, + validateParams, +} from '../../common.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' +import { submit } from '../../submit.ts' + +/** Parameters shared by Aptos ManagedToken `deployToken` generation and execution. */ +type DeployTokenParams = { + /** Token display name. Must be non-empty. */ + name: string + /** Token symbol. Must be non-empty; also seeds the FA metadata address. */ + symbol: string + /** Token decimals. */ + decimals: number + /** Maximum supply cap. `undefined` or `0n` means unlimited. */ + maxSupply?: bigint + /** Amount to pre-mint to `recipient`/deployer. `undefined` or `0n` means none. */ + initialSupply?: bigint + /** Token icon URI. Passed to `initialize()` as empty string if omitted. */ + icon?: string + /** Project URL. Passed to `initialize()` as empty string if omitted. */ + project?: string + /** Recipient for `initialSupply`. Default: sender/deployer address. */ + recipient?: string +} + +/** Parameters for unsigned Aptos ManagedToken `deployToken` generation. */ +export type GenerateDeployTokenParams = AptosGenerateParams + +/** Unsigned Aptos ManagedToken `deployToken` result (publish → initialize → optional mint). */ +export type GenerateDeployTokenResult = UnsignedAptosTx + +/** Parameters for executing Aptos ManagedToken `deployToken`. */ +export type ExecuteDeployTokenParams = AptosExecuteParams + +/** Result of executing Aptos ManagedToken `deployToken`: primary tx hash + deployed FA address. */ +export type ExecuteDeployTokenResult = TransactionHash & { + /** Deployed fungible-asset (managed_token) metadata address. */ + tokenAddress: string + /** Deployed code object address (the published `managed_token` package object). */ + codeObjectAddress: string +} + +/** Aptos ManagedToken `deployToken` operation — multi-step, preserves initial-supply minting. */ +export class DeployToken extends AptosOperation< + DeployTokenParams, + GenerateDeployTokenResult, + ExecuteDeployTokenResult +> { + readonly name = 'deployToken' + + /** Validates deploy params before any RPC or Move compilation. */ + protected validate(params: GenerateDeployTokenParams): void { + if (!Number.isInteger(params.decimals) || params.decimals < 0) { + throw new CCTParamsInvalidError(this.name, 'decimals', 'must be a non-negative integer') + } + // Deep name/symbol/supply validation (throws CCIPTokenDeployParamsInvalidError). + validateParams(params) + } + + /** + * Compiles the ManagedToken Move package and builds the sequential deploy + * transactions: publish → initialize → (optional) mint of `initialSupply`. + * + * Each transaction carries an explicit, consecutive account sequence number so + * they can be signed up-front and submitted in order. + */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateDeployTokenParams, + ): Promise { + ensureAptosCli() + + // Step 1: Compute the deterministic object address from sender + sequence number. + const { objectAddress, sequenceNumber } = await computeObjectAddress( + chain.provider, + params.sender, + ) + let nextSeq = sequenceNumber + + chain.logger.debug(`${this.name}: object address = ${objectAddress}`) + + // Step 2: Compile Move package with the object address as named address. + const { metadataBytes, byteCode } = await compilePackage(objectAddress, chain.logger) + + // Step 3: Build publish transaction via object_code_deployment::publish. + const publishPayload = generateTransactionPayloadWithABI({ + function: '0x1::object_code_deployment::publish', + functionArguments: [ + Buffer.from(metadataBytes.replace(/^0x/, ''), 'hex'), + byteCode.map((b) => Buffer.from(b.replace(/^0x/, ''), 'hex')), + ], + abi: { + typeParameters: [], + parameters: [parseTypeTag('vector'), parseTypeTag('vector>')], + }, + }) + const publishTx = await buildTransaction({ + aptosConfig: chain.provider.config, + sender: params.sender, + payload: publishPayload, + options: { accountSequenceNumber: nextSeq++ }, + }) + + const transactions: [Uint8Array, ...Uint8Array[]] = [publishTx.bcsToBytes()] + + // Step 4: Build initialize transaction using local ABI (module not yet on-chain). + // initialize(max_supply: Option, name, symbol, decimals, icon, project) + const maxSupply = + params.maxSupply !== undefined && params.maxSupply > 0n ? params.maxSupply : null + + const initPayload = generateTransactionPayloadWithABI({ + function: `${objectAddress}::managed_token::initialize`, + functionArguments: [ + maxSupply, + params.name, + params.symbol, + params.decimals, + params.icon ?? '', + params.project ?? '', + ], + abi: { + typeParameters: [], + parameters: [ + parseTypeTag('0x1::option::Option'), + parseTypeTag('0x1::string::String'), + parseTypeTag('0x1::string::String'), + parseTypeTag('u8'), + parseTypeTag('0x1::string::String'), + parseTypeTag('0x1::string::String'), + ], + }, + }) + const initTx = await buildTransaction({ + aptosConfig: chain.provider.config, + sender: params.sender, + payload: initPayload, + options: { accountSequenceNumber: nextSeq++ }, + }) + transactions.push(initTx.bcsToBytes()) + + // Step 5: Build mint transaction (only if initialSupply > 0) — PRESERVED for parity. + const initialSupply = params.initialSupply ?? 0n + if (initialSupply > 0n) { + const recipient = params.recipient ?? params.sender + const mintPayload = generateTransactionPayloadWithABI({ + function: `${objectAddress}::managed_token::mint`, + functionArguments: [recipient, initialSupply.toString()], + abi: { + typeParameters: [], + parameters: [parseTypeTag('address'), parseTypeTag('u64')], + }, + }) + const mintTx = await buildTransaction({ + aptosConfig: chain.provider.config, + sender: params.sender, + payload: mintPayload, + options: { accountSequenceNumber: nextSeq }, + }) + transactions.push(mintTx.bcsToBytes()) + } + + chain.logger.debug( + `${this.name}: sender = ${params.sender}, object = ${objectAddress}, transactions = ${transactions.length}`, + ) + return { family: ChainFamily.Aptos, transactions } + } + + /** + * Signs and submits the deploy transactions in order (publish → initialize → + * optional mint), then returns the primary (publish) hash and the deployed FA + * metadata address. Overrides the single-tx base flow because deploy is a + * dependent, multi-step sequence. + */ + override async execute( + chain: AptosChain, + params: ExecuteDeployTokenParams, + ): Promise { + const { wallet } = params + if (!isAptosAccount(wallet)) throw new CCIPWalletInvalidError(wallet) + + const sender = wallet.accountAddress.toString() + const { wallet: _wallet, ...rest } = params + const genParams: GenerateDeployTokenParams = { ...rest, sender } + + // Derive the deployed FA metadata address (deterministic from sender + symbol). + const { objectAddress } = await computeObjectAddress(chain.provider, sender) + const tokenAddress = deriveFungibleAssetAddress(objectAddress, params.symbol) + + // Validate + compile + build the sequential transactions. + const unsigned = await this.generate(chain, genParams) + + chain.logger.debug( + `${this.name}: deploying ManagedToken, ${unsigned.transactions.length} transactions`, + ) + + try { + let firstHash = '' + for (let i = 0; i < unsigned.transactions.length; i++) { + const bytes = unsigned.transactions[i] + if (bytes === undefined) continue + const { hash } = await submit(chain, wallet, [bytes], this.name) + if (i === 0) firstHash = hash + chain.logger.debug(`${this.name}: tx ${i} confirmed: ${hash}`) + } + + chain.logger.info(`${this.name}: FA at ${tokenAddress}, tx = ${firstHash}`) + return { hash: firstHash, tokenAddress, codeObjectAddress: objectAddress } + } catch (error) { + if (error instanceof CCIPTokenDeployFailedError) throw error + throw new CCIPTokenDeployFailedError(error instanceof Error ? error.message : String(error), { + cause: error instanceof Error ? error : undefined, + }) + } + } +} diff --git a/ccip-sdk/src/cct/aptos/token/operations/grant-mint-burn-access.test.ts b/ccip-sdk/src/cct/aptos/token/operations/grant-mint-burn-access.test.ts new file mode 100644 index 00000000..720d0b26 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token/operations/grant-mint-burn-access.test.ts @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { GrantMintBurnAccess } from './grant-mint-burn-access.ts' +import { AUTHORITY, SENDER, TOKEN, stubChain } from './test-helpers.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos token grantMintBurnAccess', () => { + it('builds Aptos transaction(s) for a managed pool', async () => { + const unsigned = await new GrantMintBurnAccess().generate(stubChain(), { + tokenAddress: TOKEN, + authority: AUTHORITY, + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + // Default role is mintAndBurn → managed token produces two transactions. + assert.equal(unsigned.transactions.length, 2) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects an empty tokenAddress before any RPC', async () => { + await assert.rejects( + () => + new GrantMintBurnAccess().generate(stubChain(), { + tokenAddress: '', + authority: AUTHORITY, + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/token/operations/grant-mint-burn-access.ts b/ccip-sdk/src/cct/aptos/token/operations/grant-mint-burn-access.ts new file mode 100644 index 00000000..20cd56a8 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token/operations/grant-mint-burn-access.ts @@ -0,0 +1,193 @@ +/** + * Aptos token `grantMintBurnAccess` operation. + * + * Grants mint and/or burn access on a managed or regulated token to a pool's + * resource signer. Auto-detects the pool type from the `authority` (pool object) + * address. Managed tokens use minter/burner allowlists; regulated tokens use + * numeric role grants. `mintAndBurn` on a managed token produces **two** + * transactions, so {@link execute} submits them sequentially. + * + * Lock-release pools (which neither mint nor burn) and generic `burn_mint` + * pools (which require creator-only initialization) are rejected. + * + * @packageDocumentation + */ + +import { AccountAddress } from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import { type UnsignedAptosTx, isAptosAccount } from '../../../../aptos/types.ts' +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { detectPoolType, ensurePoolInitialized, resolveTokenCodeObject } from '../../common.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' +import { submit } from '../../submit.ts' + +/** Which role(s) to grant. */ +type MintBurnRole = 'mint' | 'burn' | 'mintAndBurn' + +/** Parameters shared by Aptos token `grantMintBurnAccess` generation and execution. */ +type GrantMintBurnAccessParams = { + /** Fungible asset metadata address (the token to grant access on). */ + tokenAddress: string + /** Pool object address whose resource signer receives mint/burn access. */ + authority: string + /** Which role(s) to grant. Defaults to `'mintAndBurn'`. */ + role?: MintBurnRole +} + +/** Parameters for unsigned Aptos token `grantMintBurnAccess` generation. */ +export type GenerateGrantMintBurnAccessParams = AptosGenerateParams + +/** Unsigned Aptos token `grantMintBurnAccess` result (one or two transactions). */ +export type GenerateGrantMintBurnAccessResult = UnsignedAptosTx + +/** Parameters for executing Aptos token `grantMintBurnAccess`. */ +export type ExecuteGrantMintBurnAccessParams = AptosExecuteParams + +/** Result of executing Aptos token `grantMintBurnAccess`. */ +export type ExecuteGrantMintBurnAccessResult = TransactionHash + +/** Aptos token `grantMintBurnAccess` operation. */ +export class GrantMintBurnAccess extends AptosOperation { + readonly name = 'grantMintBurnAccess' + + /** Validates the token and authority addresses before any RPC. */ + protected validate(params: GenerateGrantMintBurnAccessParams): void { + if (!params.tokenAddress || params.tokenAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'tokenAddress', 'must be non-empty') + } + if (!params.authority || params.authority.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'authority', 'must be non-empty') + } + } + + /** + * Detects the pool type, resolves the pool resource signer and token code + * object, and builds the minter/burner (managed) or role-grant (regulated) + * transactions. + */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateGrantMintBurnAccessParams, + ): Promise { + const poolInfo = await detectPoolType(chain, params.authority) + + if (poolInfo.type === 'lock_release') { + throw new CCTParamsInvalidError( + this.name, + 'authority', + 'lock-release pools do not mint or burn tokens — no access to grant', + ) + } + + if (poolInfo.type === 'burn_mint') { + throw new CCTParamsInvalidError( + this.name, + 'authority', + 'burn_mint_token_pool requires initialization by the token creator module. ' + + 'The token creator must call burn_mint_token_pool::initialize() with stored BurnRef/MintRef. ' + + 'This cannot be done via SDK because the capability refs are only available to the token creator.', + ) + } + + await ensurePoolInitialized(chain, params.authority, poolInfo.module) + + // Pool resource signer address (the address that calls mint/burn). + const [poolResourceSigner] = await chain.provider.view<[string]>({ + payload: { + function: `${params.authority}::${poolInfo.module}::get_store_address`, + }, + }) + + const tokenCodeObject = await resolveTokenCodeObject(chain, params.tokenAddress) + + const role = params.role ?? 'mintAndBurn' + const parts: Uint8Array[] = [] + + if (poolInfo.type === 'managed') { + // managed_token: add pool resource signer to allowed minters and/or burners. + // Consecutive sequence numbers so a two-tx batch signs and submits in order. + const { sequence_number } = await chain.provider.getAccountInfo({ + accountAddress: AccountAddress.from(params.sender), + }) + let nextSeq = BigInt(sequence_number) + + if (role === 'mint' || role === 'mintAndBurn') { + const tx = await chain.provider.transaction.build.simple({ + sender: AccountAddress.from(params.sender), + data: { + function: `${tokenCodeObject}::managed_token::apply_allowed_minter_updates`, + functionArguments: [[], [poolResourceSigner]], + }, + options: { accountSequenceNumber: nextSeq++ }, + }) + parts.push(tx.bcsToBytes()) + } + if (role === 'burn' || role === 'mintAndBurn') { + const tx = await chain.provider.transaction.build.simple({ + sender: AccountAddress.from(params.sender), + data: { + function: `${tokenCodeObject}::managed_token::apply_allowed_burner_updates`, + functionArguments: [[], [poolResourceSigner]], + }, + options: { accountSequenceNumber: nextSeq }, + }) + parts.push(tx.bcsToBytes()) + } + } else { + // regulated_token: MINTER_ROLE=4, BURNER_ROLE=5, BRIDGE_MINTER_OR_BURNER_ROLE=6. + const roleNumber = role === 'mint' ? 4 : role === 'burn' ? 5 : 6 + const tx = await chain.provider.transaction.build.simple({ + sender: AccountAddress.from(params.sender), + data: { + function: `${tokenCodeObject}::regulated_token::grant_role`, + functionArguments: [roleNumber, poolResourceSigner], + }, + }) + parts.push(tx.bcsToBytes()) + } + + const [first, ...others] = parts + if (first === undefined) { + throw new CCTParamsInvalidError(this.name, 'role', "must be 'mint', 'burn', or 'mintAndBurn'") + } + + chain.logger.debug( + `${this.name}: pool type = ${poolInfo.type}, poolResourceSigner = ${poolResourceSigner}, txs = ${parts.length}`, + ) + return { family: ChainFamily.Aptos, transactions: [first, ...others] } + } + + /** + * Signs and submits each grant transaction in order, returning the final hash. + * Overrides the single-tx base flow because a managed `mintAndBurn` grant + * produces two dependent transactions. + */ + override async execute( + chain: AptosChain, + params: ExecuteGrantMintBurnAccessParams, + ): Promise { + const { wallet } = params + if (!isAptosAccount(wallet)) throw new CCIPWalletInvalidError(wallet) + + const sender = wallet.accountAddress.toString() + const { wallet: _wallet, ...rest } = params + const unsigned = await this.generate(chain, { ...rest, sender }) + + let last: TransactionHash | undefined + for (const bytes of unsigned.transactions) { + last = await submit(chain, wallet, [bytes], this.name) + } + if (last === undefined) { + throw new CCTParamsInvalidError(this.name, 'role', 'produced no transactions to submit') + } + return last + } +} diff --git a/ccip-sdk/src/cct/aptos/token/operations/index.ts b/ccip-sdk/src/cct/aptos/token/operations/index.ts new file mode 100644 index 00000000..69f8f711 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token/operations/index.ts @@ -0,0 +1,3 @@ +export * from './deploy-token.ts' +export * from './grant-mint-burn-access.ts' +export * from './revoke-mint-burn-access.ts' diff --git a/ccip-sdk/src/cct/aptos/token/operations/revoke-mint-burn-access.test.ts b/ccip-sdk/src/cct/aptos/token/operations/revoke-mint-burn-access.test.ts new file mode 100644 index 00000000..595bf24f --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token/operations/revoke-mint-burn-access.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { RevokeMintBurnAccess } from './revoke-mint-burn-access.ts' +import { AUTHORITY, SENDER, TOKEN, stubChain } from './test-helpers.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +describe('Aptos token revokeMintBurnAccess', () => { + it('builds a single Aptos transaction for a managed pool', async () => { + const unsigned = await new RevokeMintBurnAccess().generate(stubChain(), { + tokenAddress: TOKEN, + authority: AUTHORITY, + role: 'mint', + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.Aptos) + assert.equal(unsigned.transactions.length, 1) + assert.ok(unsigned.transactions[0] instanceof Uint8Array) + }) + + it('rejects an empty authority before any RPC', async () => { + await assert.rejects( + () => + new RevokeMintBurnAccess().generate(stubChain(), { + tokenAddress: TOKEN, + authority: '', + role: 'mint', + sender: SENDER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/aptos/token/operations/revoke-mint-burn-access.ts b/ccip-sdk/src/cct/aptos/token/operations/revoke-mint-burn-access.ts new file mode 100644 index 00000000..6ae83367 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token/operations/revoke-mint-burn-access.ts @@ -0,0 +1,135 @@ +/** + * Aptos token `revokeMintBurnAccess` operation. + * + * Revokes mint or burn access on a managed or regulated token from a pool's + * resource signer. Auto-detects the pool type from the `authority` (pool object) + * address. Managed tokens remove the signer from the minter/burner allowlist; + * regulated tokens revoke the numeric role. Always a single transaction, so it + * uses the single-tx base flow. + * + * Lock-release pools (which neither mint nor burn) and generic `burn_mint` + * pools (which require creator-only initialization) are rejected. + * + * @packageDocumentation + */ + +import { AccountAddress } from '@aptos-labs/ts-sdk' + +import type { AptosChain } from '../../../../aptos/index.ts' +import type { UnsignedAptosTx } from '../../../../aptos/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { detectPoolType, ensurePoolInitialized, resolveTokenCodeObject } from '../../common.ts' +import { + type AptosExecuteParams, + type AptosGenerateParams, + AptosOperation, +} from '../../operation.ts' + +/** Parameters shared by Aptos token `revokeMintBurnAccess` generation and execution. */ +type RevokeMintBurnAccessParams = { + /** Fungible asset metadata address (the token to revoke access on). */ + tokenAddress: string + /** Pool object address whose resource signer loses mint/burn access. */ + authority: string + /** Which role to revoke — must be specified explicitly. */ + role: 'mint' | 'burn' +} + +/** Parameters for unsigned Aptos token `revokeMintBurnAccess` generation. */ +export type GenerateRevokeMintBurnAccessParams = AptosGenerateParams + +/** Unsigned Aptos token `revokeMintBurnAccess` result. */ +export type GenerateRevokeMintBurnAccessResult = UnsignedAptosTx + +/** Parameters for executing Aptos token `revokeMintBurnAccess`. */ +export type ExecuteRevokeMintBurnAccessParams = AptosExecuteParams + +/** Result of executing Aptos token `revokeMintBurnAccess`. */ +export type ExecuteRevokeMintBurnAccessResult = TransactionHash + +/** Aptos token `revokeMintBurnAccess` operation. */ +export class RevokeMintBurnAccess extends AptosOperation { + readonly name = 'revokeMintBurnAccess' + + /** Validates the token, authority, and role before any RPC. */ + protected validate(params: GenerateRevokeMintBurnAccessParams): void { + if (!params.tokenAddress || params.tokenAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'tokenAddress', 'must be non-empty') + } + if (!params.authority || params.authority.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'authority', 'must be non-empty') + } + const role: string = params.role + if (role !== 'mint' && role !== 'burn') { + throw new CCTParamsInvalidError(this.name, 'role', "must be 'mint' or 'burn'") + } + } + + /** + * Detects the pool type, resolves the pool resource signer and token code + * object, and builds the single revoke transaction. + */ + protected async buildUnsigned( + chain: AptosChain, + params: GenerateRevokeMintBurnAccessParams, + ): Promise { + const poolInfo = await detectPoolType(chain, params.authority) + + if (poolInfo.type === 'lock_release') { + throw new CCTParamsInvalidError( + this.name, + 'authority', + 'lock-release pools do not mint or burn tokens — no access to revoke', + ) + } + + if (poolInfo.type === 'burn_mint') { + throw new CCTParamsInvalidError( + this.name, + 'authority', + 'burn_mint_token_pool requires initialization by the token creator module. Revoke is not supported via SDK.', + ) + } + + await ensurePoolInitialized(chain, params.authority, poolInfo.module) + + const [poolResourceSigner] = await chain.provider.view<[string]>({ + payload: { + function: `${params.authority}::${poolInfo.module}::get_store_address`, + }, + }) + + const tokenCodeObject = await resolveTokenCodeObject(chain, params.tokenAddress) + + let tx + if (poolInfo.type === 'managed') { + // managed_token: remove pool resource signer from minters or burners. + const fnName = + params.role === 'mint' ? 'apply_allowed_minter_updates' : 'apply_allowed_burner_updates' + tx = await chain.provider.transaction.build.simple({ + sender: AccountAddress.from(params.sender), + data: { + function: `${tokenCodeObject}::managed_token::${fnName}`, + functionArguments: [[poolResourceSigner], []], // remove=[signer], add=[] + }, + }) + } else { + // regulated_token: MINTER_ROLE=4, BURNER_ROLE=5. + const roleNumber = params.role === 'mint' ? 4 : 5 + tx = await chain.provider.transaction.build.simple({ + sender: AccountAddress.from(params.sender), + data: { + function: `${tokenCodeObject}::regulated_token::revoke_role`, + functionArguments: [roleNumber, poolResourceSigner], + }, + }) + } + + chain.logger.debug( + `${this.name}: pool type = ${poolInfo.type}, role = ${params.role}, poolResourceSigner = ${poolResourceSigner}`, + ) + return { family: ChainFamily.Aptos, transactions: [tx.bcsToBytes()] } + } +} diff --git a/ccip-sdk/src/cct/aptos/token/operations/test-helpers.ts b/ccip-sdk/src/cct/aptos/token/operations/test-helpers.ts new file mode 100644 index 00000000..764c48b3 --- /dev/null +++ b/ccip-sdk/src/cct/aptos/token/operations/test-helpers.ts @@ -0,0 +1,45 @@ +import type { AptosChain } from '../../../../aptos/index.ts' + +/** A test sender address (32-byte hex). */ +export const SENDER = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + +/** A test fungible-asset metadata (token) address. */ +export const TOKEN = '0x0000000000000000000000000000000000000000000000000000000089fd6b14' + +/** A test pool object address (used as `authority`). */ +export const AUTHORITY = '0x00000000000000000000000000000000000000000000000000000000deadbeef' + +/** + * Builds a minimal AptosChain stub sufficient for grant/revoke `buildUnsigned`. + * + * Stubs pool-module discovery (`_getAccountModulesNames` + `provider.view`), + * account sequence lookup (`getAccountInfo`), and transaction building + * (`transaction.build.simple`) so operations run fully offline. `provider.view` + * returns a single address so `get_store_address` / `resolveTokenCodeObject` + * resolve to a canned value. + */ +export function stubChain(): AptosChain { + const fakeTx = { bcsToBytes: () => new Uint8Array([1, 2, 3]) } + const provider = { + async view() { + return ['0xmanaged_token'] as [string] + }, + async getAccountInfo() { + return { sequence_number: '0' } + }, + transaction: { + build: { + async simple() { + return fakeTx + }, + }, + }, + } + return { + provider, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + async _getAccountModulesNames() { + return ['managed_token_pool'] + }, + } as unknown as AptosChain +} diff --git a/ccip-sdk/src/cct/errors.ts b/ccip-sdk/src/cct/errors.ts new file mode 100644 index 00000000..f36c79a7 --- /dev/null +++ b/ccip-sdk/src/cct/errors.ts @@ -0,0 +1,102 @@ +/** + * CCT-specific error classes for write operations (validate → encode → submit). + * Shared CCIP errors (`CCIPWalletInvalidError`, etc.) live in `../errors/`. + * + * @packageDocumentation + */ + +import { type CCIPErrorOptions, CCIPError, CCIPErrorCode } from '../errors/index.ts' + +// Parameter validation + +/** + * Thrown before any RPC when operation params fail validation. Permanent. + * + * @example + * ```typescript + * try { + * await cct.setPool({ tokenAddress: 'not-an-address', poolAddress, address, wallet }) + * } catch (error) { + * if (error instanceof CCTParamsInvalidError) { + * console.log(`Invalid ${error.context.operation} param "${error.context.param}"`) + * } + * } + * ``` + */ +export class CCTParamsInvalidError extends CCIPError { + override readonly name = 'CCTParamsInvalidError' + /** Creates a params-invalid error. */ + constructor(operation: string, param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.CCT_PARAMS_INVALID, + `Invalid ${operation} parameter "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, operation, param, reason }, + }, + ) + } +} + +// Transaction submission + +/** + * Thrown when a CCT write fails before broadcast or the transaction reverts after mining. + * Pre-broadcast failures (signing/RPC) may set `isTransient: true` for network errors; + * on-chain reverts are permanent. Reverts include `context.txHash`. + * + * @example + * ```typescript + * try { + * await cct.setPool({ ...opts, wallet }) + * } catch (error) { + * if (error instanceof CCTTxFailedError) { + * console.log(`${error.context.operation} failed: ${error.context.reason}`) + * } + * } + * ``` + */ +export class CCTTxFailedError extends CCIPError { + override readonly name = 'CCTTxFailedError' + /** Creates a tx-failed error. */ + constructor(operation: string, reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.CCT_TX_FAILED, `${operation} failed: ${reason}`, { + ...options, + isTransient: options?.isTransient ?? false, + context: { ...options?.context, operation, reason }, + }) + } +} + +/** + * Thrown when a transaction was broadcast but not confirmed within the timeout. + * Transient — it may still mine; check `context.txHash` before resubmitting. + * + * @example + * ```typescript + * try { + * await cct.setPool({ ...opts, wallet }) + * } catch (error) { + * if (error instanceof CCTTxNotConfirmedError) { + * console.log(`Not confirmed (tx ${error.context.txHash}); retry in ${error.retryAfterMs}ms`) + * } + * } + * ``` + */ +export class CCTTxNotConfirmedError extends CCIPError { + override readonly name = 'CCTTxNotConfirmedError' + /** Creates a tx-not-confirmed error. */ + constructor(operation: string, txHash: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.CCT_TX_NOT_CONFIRMED, + `${operation} transaction not confirmed within timeout: ${txHash}`, + { + ...options, + isTransient: true, + retryAfterMs: 5000, + context: { ...options?.context, operation, txHash }, + }, + ) + } +} diff --git a/ccip-sdk/src/cct/evm/deploy-verification.ts b/ccip-sdk/src/cct/evm/deploy-verification.ts new file mode 100644 index 00000000..8aa8469e --- /dev/null +++ b/ccip-sdk/src/cct/evm/deploy-verification.ts @@ -0,0 +1,126 @@ +/** + * Block-explorer (Etherscan) verification handles for freshly-deployed EVM CCT contracts. + * + * A {@link DeployVerification} carries the two things a source-explorer verification call + * needs that aren't already known from the deploy result: which bundled contract was + * deployed and its ABI-encoded constructor args (the exact bytes appended to the init code). + * These are metadata derived from the deploy calldata — computing them never changes the + * on-chain transaction. + * + * @packageDocumentation + */ + +import { AbiCoder, ZeroAddress } from 'ethers' + +/** + * Source-explorer verification handle for a freshly-deployed EVM contract. + * + * Present on EVM deploy results only; feed it to a contract-verification call as the + * encoded-constructor-args input. + */ +export interface DeployVerification { + /** Bundled verification-registry key, e.g. `'CrossChainToken'`. */ + contract: string + /** ABI-encoded constructor arguments, `0x`-prefixed (the bytes appended to the init code). */ + encodedConstructorArgs: string +} + +/** + * A {@link DeployVerification} handle together with the deployed address it refers to. + * + * Used for factory deploys, where several contracts are created in internal CREATE2 calls + * and each needs its address carried alongside its constructor args. + */ +export interface DeployVerificationTarget extends DeployVerification { + /** The deployed contract address this verification handle is for. */ + address: string +} + +/** + * Builds a verification handle for a just-deployed contract. The init code is + * `bytecode || abiEncodedConstructorArgs`, so the encoded args are exactly the bytes after + * the (known) creation bytecode — recovered here as the single source of truth. + * + * @param contract - Bundled verification-registry key for the deployed contract. + * @param deployData - The full contract-creation calldata (`bytecode || encodedArgs`). + * @param bytecode - The known creation bytecode prefix of `deployData`. + * @returns The {@link DeployVerification} handle carrying the recovered constructor args. + */ +export function buildDeployVerification( + contract: string, + deployData: string, + bytecode: string, +): DeployVerification { + return { contract, encodedConstructorArgs: `0x${deployData.slice(bytecode.length)}` } +} + +/** Discriminated input for {@link buildFactoryPoolVerification}; lock-release carries its lockbox. */ +export type FactoryPoolVerificationInput = + | { + poolType: 'burn-mint' + token: string + decimals: number + rmnProxy: string + router: string + poolAddress: string + } + | { + poolType: 'lock-release' + token: string + decimals: number + rmnProxy: string + router: string + poolAddress: string + /** The resolved `ERC20LockBox` the factory bound to the pool. */ + lockBoxAddress: string + } + +/** + * Reconstructs the pool's (and lock-release lockbox's) verification handles for a factory + * deploy. The factory appends the pool constructor args itself (token, decimals, zero-hooks, + * rmnProxy, router, and lockBox for lock-release) using its own immutables, so they are rebuilt + * here from the factory's static config rather than sliced from a top-level creation tx. + * + * @param input - The resolved pool addresses + factory static config (rmnProxy, router). + * @returns The pool verification target, plus the lockbox target for lock-release pools. + */ +export function buildFactoryPoolVerification(input: FactoryPoolVerificationInput): { + poolVerification: DeployVerificationTarget + lockBoxVerification?: DeployVerificationTarget +} { + const coder = AbiCoder.defaultAbiCoder() + if (input.poolType === 'burn-mint') { + return { + poolVerification: { + contract: 'BurnMintTokenPool', + address: input.poolAddress, + encodedConstructorArgs: coder.encode( + ['address', 'uint8', 'address', 'address', 'address'], + [input.token, input.decimals, ZeroAddress, input.rmnProxy, input.router], + ), + }, + } + } + return { + poolVerification: { + contract: 'LockReleaseTokenPool', + address: input.poolAddress, + encodedConstructorArgs: coder.encode( + ['address', 'uint8', 'address', 'address', 'address', 'address'], + [ + input.token, + input.decimals, + ZeroAddress, + input.rmnProxy, + input.router, + input.lockBoxAddress, + ], + ), + }, + lockBoxVerification: { + contract: 'ERC20LockBox', + address: input.lockBoxAddress, + encodedConstructorArgs: coder.encode(['address'], [input.token]), + }, + } +} diff --git a/ccip-sdk/src/cct/evm/index.test.ts b/ccip-sdk/src/cct/evm/index.test.ts new file mode 100644 index 00000000..cefda225 --- /dev/null +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -0,0 +1,132 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, id } from 'ethers' + +import { EVMTokenManager } from './index.ts' +import { CCIPWalletInvalidError } from '../../errors/index.ts' +import type { EVMChain } from '../../evm/index.ts' +import { ChainFamily } from '../../networks.ts' +import { CCTParamsInvalidError } from '../errors.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const POOL = '0x' + '22'.repeat(20) +const ROUTER = '0x' + '33'.repeat(20) +const TAR = '0x' + '44'.repeat(20) + +/** Minimal EVMChain stub — only the members EVMTokenManager touches. */ +function stubChain(overrides: Partial = {}): EVMChain { + return { + provider: {} as never, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + getTokenAdminRegistryFor: (_address: string) => Promise.resolve(TAR), + ...overrides, + } as unknown as EVMChain +} + +const SET_POOL_SELECTOR = id('setPool(address,address)').slice(0, 10) +const EXPECTED_DATA = new Interface([ + 'function setPool(address localToken, address pool)', +]).encodeFunctionData('setPool', [TOKEN, POOL]) + +describe('EVMTokenManager (cct/evm)', () => { + describe('construction', () => { + it('fromChain wraps an existing chain and exposes its provider', () => { + const chain = stubChain() + const cct = EVMTokenManager.fromChain(chain) + assert.ok(cct instanceof EVMTokenManager) + assert.equal(cct.chain, chain) + assert.equal(cct.provider, chain.provider) + }) + }) + + describe('generateUnsignedSetPool', () => { + it('encodes setPool(token, pool) to the discovered TAR', async () => { + const cct = EVMTokenManager.fromChain(stubChain()) + const unsigned = await cct.generateUnsignedSetPool({ + tokenAddress: TOKEN, + poolAddress: POOL, + address: ROUTER, + sender: TOKEN, + }) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + + const tx = unsigned.transactions[0]! + assert.equal(tx.to, TAR) + assert.equal(tx.from, TOKEN) + assert.ok(tx.data!.startsWith(SET_POOL_SELECTOR), 'data starts with setPool selector') + assert.equal(tx.data, EXPECTED_DATA) + }) + + it('discovers the TAR from the router address', async () => { + let seen: string | undefined + const cct = EVMTokenManager.fromChain( + stubChain({ + getTokenAdminRegistryFor: (address: string) => { + seen = address + return Promise.resolve(TAR) + }, + }), + ) + await cct.generateUnsignedSetPool({ + tokenAddress: TOKEN, + poolAddress: POOL, + address: ROUTER, + }) + assert.equal(seen, ROUTER) + }) + + it('omits `from` when no sender is given', async () => { + const cct = EVMTokenManager.fromChain(stubChain()) + const unsigned = await cct.generateUnsignedSetPool({ + tokenAddress: TOKEN, + poolAddress: POOL, + address: ROUTER, + }) + assert.equal(unsigned.transactions[0]!.from, undefined) + }) + + it('rejects an invalid address before any RPC, tagged with the operation', async () => { + let called = false + const cct = EVMTokenManager.fromChain( + stubChain({ + getTokenAdminRegistryFor: () => { + called = true + return Promise.resolve(TAR) + }, + }), + ) + await assert.rejects( + () => + cct.generateUnsignedSetPool({ + tokenAddress: 'not-an-address', + poolAddress: POOL, + address: ROUTER, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setPool' && + err.context.param === 'tokenAddress', + ) + assert.equal(called, false, 'validation fails before TAR discovery') + }) + }) + + describe('setPool', () => { + it('rejects a non-signer wallet', async () => { + const cct = EVMTokenManager.fromChain(stubChain()) + await assert.rejects( + () => + cct.setPool({ + tokenAddress: TOKEN, + poolAddress: POOL, + address: ROUTER, + wallet: {}, + }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts new file mode 100644 index 00000000..0851f0ce --- /dev/null +++ b/ccip-sdk/src/cct/evm/index.ts @@ -0,0 +1,536 @@ +/** + * EVM Cross-Chain Token (CCT) admin operations. + * {@link EVMTokenManager} wraps an {@link EVMChain}: build with + * `generateUnsigned` (sender in opts), then `` with `wallet` in opts. + * + * @packageDocumentation + */ + +import type { JsonRpcApiProvider } from 'ethers' + +import type { ChainContext } from '../../chain.ts' +import { EVMChain } from '../../evm/index.ts' +import type { UnsignedEVMTx } from '../../evm/types.ts' +import type { ChainFamily } from '../../networks.ts' +import type { TransactionHash } from '../operation.ts' +import { TokenManager } from '../token-manager.ts' +import { type AcceptOwnershipParams, AcceptOwnership } from './pool/operations/accept-ownership.ts' +import { + type AppendRemotePoolAddressesParams, + AppendRemotePoolAddresses, +} from './pool/operations/append-remote-pool-addresses.ts' +import { + type ApplyChainUpdatesParams, + ApplyChainUpdates, +} from './pool/operations/apply-chain-updates.ts' +import { + type DeleteChainConfigParams, + DeleteChainConfig, +} from './pool/operations/delete-chain-config.ts' +import { + type RemoveRemotePoolAddressesParams, + RemoveRemotePoolAddresses, +} from './pool/operations/remove-remote-pool-addresses.ts' +import { + type SetAllowedFinalityConfigParams, + SetAllowedFinalityConfig, +} from './pool/operations/set-allowed-finality-config.ts' +import { + type SetChainRateLimiterConfigParams, + SetChainRateLimiterConfig, +} from './pool/operations/set-chain-rate-limiter-config.ts' +import { type SetFeeAdminParams, SetFeeAdmin } from './pool/operations/set-fee-admin.ts' +import { + type SetRateLimitAdminParams, + SetRateLimitAdmin, +} from './pool/operations/set-rate-limit-admin.ts' +import { + type SetTokenTransferFeeConfigParams, + SetTokenTransferFeeConfig, +} from './pool/operations/set-token-transfer-fee-config.ts' +import { + type TransferOwnershipParams, + TransferOwnership, +} from './pool/operations/transfer-ownership.ts' +import { type MintBurnRolesResult, getMintBurnRoles } from './token/get-mint-burn-roles.ts' +import { + type DeployCrossChainPoolTokenParams, + type DeployCrossChainPoolTokenResult, + DeployCrossChainPoolToken, +} from './token/operations/deploy-cross-chain-pool-token.ts' +import { + type DeployTokenParams, + type DeployTokenResult, + DeployToken, +} from './token/operations/deploy-token.ts' +import { + type GrantMintBurnAccessParams, + GrantMintBurnAccess, +} from './token/operations/grant-mint-burn-access.ts' +import { + type RevokeMintBurnAccessParams, + RevokeMintBurnAccess, +} from './token/operations/revoke-mint-burn-access.ts' +import { + type AcceptAdminRoleParams, + AcceptAdminRole, +} from './token-admin-registry/operations/accept-admin-role.ts' +import { + type ProposeAdminRoleParams, + ProposeAdminRole, +} from './token-admin-registry/operations/propose-admin-role.ts' +import { type SetPoolParams, SetPool } from './token-admin-registry/operations/set-pool.ts' +import { + type TransferAdminRoleParams, + TransferAdminRole, +} from './token-admin-registry/operations/transfer-admin-role.ts' +import { + type DeployPoolViaFactoryParams, + type DeployPoolViaFactoryResult, + DeployPoolViaFactory, +} from './token-pool/operations/deploy-pool-via-factory.ts' +import { + type DeployPoolParams, + type DeployPoolResult, + DeployPool, +} from './token-pool/operations/deploy-pool.ts' +import { + type DeployTokenAndPoolViaFactoryParams, + type DeployTokenAndPoolViaFactoryResult, + DeployTokenAndPoolViaFactory, +} from './token-pool/operations/deploy-token-and-pool-via-factory.ts' +import { + type ProvideLiquidityParams, + ProvideLiquidity, +} from './token-pool/operations/provide-liquidity.ts' + +/** CCT admin operations for EVM chains, delegating each op to an operation class. */ +export class EVMTokenManager extends TokenManager { + readonly chain: EVMChain + readonly #setPool = new SetPool() + readonly #proposeAdminRole = new ProposeAdminRole() + readonly #acceptAdminRole = new AcceptAdminRole() + readonly #transferAdminRole = new TransferAdminRole() + readonly #applyChainUpdates = new ApplyChainUpdates() + readonly #deleteChainConfig = new DeleteChainConfig() + readonly #appendRemotePoolAddresses = new AppendRemotePoolAddresses() + readonly #removeRemotePoolAddresses = new RemoveRemotePoolAddresses() + readonly #setRateLimitAdmin = new SetRateLimitAdmin() + readonly #setFeeAdmin = new SetFeeAdmin() + readonly #setAllowedFinalityConfig = new SetAllowedFinalityConfig() + readonly #setChainRateLimiterConfig = new SetChainRateLimiterConfig() + readonly #setTokenTransferFeeConfig = new SetTokenTransferFeeConfig() + readonly #transferOwnership = new TransferOwnership() + readonly #acceptOwnership = new AcceptOwnership() + readonly #grantMintBurnAccess = new GrantMintBurnAccess() + readonly #revokeMintBurnAccess = new RevokeMintBurnAccess() + readonly #deployToken = new DeployToken() + readonly #deployCrossChainPoolToken = new DeployCrossChainPoolToken() + readonly #deployPool = new DeployPool() + readonly #deployPoolViaFactory = new DeployPoolViaFactory() + readonly #deployTokenAndPoolViaFactory = new DeployTokenAndPoolViaFactory() + readonly #provideLiquidity = new ProvideLiquidity() + + /** Wraps the chain this manager builds and submits through. */ + constructor(chain: EVMChain) { + super() + this.chain = chain + } + + /** Wraps an existing {@link EVMChain}. */ + static fromChain(chain: EVMChain): EVMTokenManager { + return new EVMTokenManager(chain) + } + + /** Creates from an ethers provider. */ + static async fromProvider( + provider: JsonRpcApiProvider, + ctx?: ChainContext, + ): Promise { + return new EVMTokenManager(await EVMChain.fromProvider(provider, ctx)) + } + + /** Creates from an RPC URL. */ + static async fromUrl(url: string, ctx?: ChainContext): Promise { + return new EVMTokenManager(await EVMChain.fromUrl(url, ctx)) + } + + /** Provider of the underlying chain. */ + get provider(): JsonRpcApiProvider { + return this.chain.provider + } + + /** + * Builds an unsigned `setPool` tx (for multisig / offline signing). + * A zero/empty `poolAddress` delists the token from the registry. + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @example + * ```typescript + * // build only — sign later (multisig / offline). `sender` must be the token's current admin. + * const unsigned = await cct.generateUnsignedSetPool({ + * tokenAddress: '0xToken...', + * poolAddress: '0xPool...', // pass the zero address to delist the token + * address: '0xTokenAdminRegistry...', // the TAR, or a Router/pool to resolve it from + * sender: '0xTokenAdmin...', + * }) + * ``` + */ + generateUnsignedSetPool(opts: SetPoolParams): Promise { + return this.#setPool.generate(this.chain, opts) + } + + /** + * Registers a pool, signing + submitting with `opts.wallet` (the token admin). + * A zero/empty `poolAddress` delists the token from the registry. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @throws {@link CCTTxFailedError} if the tx reverts or fails + * @example + * ```typescript + * // `wallet` must sign as the token's current administrator + * const { hash } = await cct.setPool({ + * tokenAddress: '0xToken...', + * poolAddress: '0xPool...', // pass the zero address to delist the token + * address: '0xTokenAdminRegistry...', + * wallet, + * }) + * ``` + */ + setPool(opts: SetPoolParams & { wallet: unknown }): Promise { + return this.#setPool.execute(this.chain, opts) + } + + /** + * Builds an unsigned `proposeAdminRole` tx (for multisig / offline signing). + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @example + * ```typescript + * const unsigned = await cct.generateUnsignedProposeAdminRole({ + * tokenAddress: '0xToken...', + * registryModuleAddress: '0xRegistryModuleOwnerCustom...', + * registrationMethod: 'owner', // default; how the module verifies your authority + * sender: '0xTokenOwner...', + * }) + * ``` + */ + generateUnsignedProposeAdminRole(opts: ProposeAdminRoleParams): Promise { + return this.#proposeAdminRole.generate(this.chain, opts) + } + + /** + * Proposes the caller as token administrator, signing + submitting with `opts.wallet`. + * The wallet must hold the authority the `registrationMethod` checks (token owner by default). + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @throws {@link CCTTxFailedError} if the tx reverts or fails + * @example + * ```typescript + * const { hash } = await cct.proposeAdminRole({ + * tokenAddress: '0xToken...', + * registryModuleAddress: '0xRegistryModuleOwnerCustom...', + * wallet, + * }) + * ``` + */ + proposeAdminRole(opts: ProposeAdminRoleParams & { wallet: unknown }): Promise { + return this.#proposeAdminRole.execute(this.chain, opts) + } + + /** Builds an unsigned `acceptAdminRole` tx. @throws {@link CCTParamsInvalidError} */ + generateUnsignedAcceptAdminRole(opts: AcceptAdminRoleParams): Promise { + return this.#acceptAdminRole.generate(this.chain, opts) + } + /** Accepts the pending admin role, signing with `opts.wallet`. */ + acceptAdminRole(opts: AcceptAdminRoleParams & { wallet: unknown }): Promise { + return this.#acceptAdminRole.execute(this.chain, opts) + } + + /** Builds an unsigned `transferAdminRole` tx (proposes a new pending admin). */ + generateUnsignedTransferAdminRole(opts: TransferAdminRoleParams): Promise { + return this.#transferAdminRole.generate(this.chain, opts) + } + /** Transfers (proposes) the admin role to `newAdmin`, signing with `opts.wallet`. */ + transferAdminRole(opts: TransferAdminRoleParams & { wallet: unknown }): Promise { + return this.#transferAdminRole.execute(this.chain, opts) + } + + /** Builds unsigned `applyChainUpdates` tx (add/remove remote chain configs on a pool). */ + generateUnsignedApplyChainUpdates(opts: ApplyChainUpdatesParams): Promise { + return this.#applyChainUpdates.generate(this.chain, opts) + } + /** Applies remote-chain config updates to a pool, signing with `opts.wallet`. */ + applyChainUpdates(opts: ApplyChainUpdatesParams & { wallet: unknown }): Promise { + return this.#applyChainUpdates.execute(this.chain, opts) + } + + /** Builds unsigned `deleteChainConfig` tx (removes a remote chain from a pool). */ + generateUnsignedDeleteChainConfig(opts: DeleteChainConfigParams): Promise { + return this.#deleteChainConfig.generate(this.chain, opts) + } + /** Deletes a remote-chain config from a pool, signing with `opts.wallet`. */ + deleteChainConfig(opts: DeleteChainConfigParams & { wallet: unknown }): Promise { + return this.#deleteChainConfig.execute(this.chain, opts) + } + + /** Builds unsigned `appendRemotePoolAddresses` txs (one per address). */ + generateUnsignedAppendRemotePoolAddresses( + opts: AppendRemotePoolAddressesParams, + ): Promise { + return this.#appendRemotePoolAddresses.generate(this.chain, opts) + } + /** Appends remote pool addresses to a pool (multi-tx), signing with `opts.wallet`. */ + appendRemotePoolAddresses( + opts: AppendRemotePoolAddressesParams & { wallet: unknown }, + ): Promise { + return this.#appendRemotePoolAddresses.execute(this.chain, opts) + } + + /** Builds unsigned `removeRemotePoolAddresses` txs (one per address). */ + generateUnsignedRemoveRemotePoolAddresses( + opts: RemoveRemotePoolAddressesParams, + ): Promise { + return this.#removeRemotePoolAddresses.generate(this.chain, opts) + } + /** Removes remote pool addresses from a pool (multi-tx), signing with `opts.wallet`. */ + removeRemotePoolAddresses( + opts: RemoveRemotePoolAddressesParams & { wallet: unknown }, + ): Promise { + return this.#removeRemotePoolAddresses.execute(this.chain, opts) + } + + /** Builds unsigned `setRateLimitAdmin` tx. */ + generateUnsignedSetRateLimitAdmin(opts: SetRateLimitAdminParams): Promise { + return this.#setRateLimitAdmin.generate(this.chain, opts) + } + /** Sets the pool's rate-limit admin, signing with `opts.wallet`. */ + setRateLimitAdmin(opts: SetRateLimitAdminParams & { wallet: unknown }): Promise { + return this.#setRateLimitAdmin.execute(this.chain, opts) + } + + /** Builds unsigned `setFeeAdmin` tx (v2.0+ pools). */ + generateUnsignedSetFeeAdmin(opts: SetFeeAdminParams): Promise { + return this.#setFeeAdmin.generate(this.chain, opts) + } + /** Sets the pool's fee admin (v2.0+), signing with `opts.wallet`. */ + setFeeAdmin(opts: SetFeeAdminParams & { wallet: unknown }): Promise { + return this.#setFeeAdmin.execute(this.chain, opts) + } + + /** Builds unsigned `setAllowedFinalityConfig` tx (v2.0+ pools). */ + generateUnsignedSetAllowedFinalityConfig( + opts: SetAllowedFinalityConfigParams, + ): Promise { + return this.#setAllowedFinalityConfig.generate(this.chain, opts) + } + /** Sets the pool's allowed finality config (v2.0+), signing with `opts.wallet`. */ + setAllowedFinalityConfig( + opts: SetAllowedFinalityConfigParams & { wallet: unknown }, + ): Promise { + return this.#setAllowedFinalityConfig.execute(this.chain, opts) + } + + /** Builds unsigned `setChainRateLimiterConfig` tx(s) (v1.6 = one per chain, v2.0 = batched). */ + generateUnsignedSetChainRateLimiterConfig( + opts: SetChainRateLimiterConfigParams, + ): Promise { + return this.#setChainRateLimiterConfig.generate(this.chain, opts) + } + /** Sets per-chain rate limiter config on a pool, signing with `opts.wallet`. */ + setChainRateLimiterConfig( + opts: SetChainRateLimiterConfigParams & { wallet: unknown }, + ): Promise { + return this.#setChainRateLimiterConfig.execute(this.chain, opts) + } + + /** Builds unsigned `setTokenTransferFeeConfig` tx (v2.0+ pools). */ + generateUnsignedSetTokenTransferFeeConfig( + opts: SetTokenTransferFeeConfigParams, + ): Promise { + return this.#setTokenTransferFeeConfig.generate(this.chain, opts) + } + /** Sets token-transfer fee config on a pool (v2.0+), signing with `opts.wallet`. */ + setTokenTransferFeeConfig( + opts: SetTokenTransferFeeConfigParams & { wallet: unknown }, + ): Promise { + return this.#setTokenTransferFeeConfig.execute(this.chain, opts) + } + + /** Builds unsigned `transferOwnership` tx (Ownable2Step, pool). */ + generateUnsignedTransferOwnership(opts: TransferOwnershipParams): Promise { + return this.#transferOwnership.generate(this.chain, opts) + } + /** Transfers pool ownership (proposes new owner), signing with `opts.wallet`. */ + transferOwnership(opts: TransferOwnershipParams & { wallet: unknown }): Promise { + return this.#transferOwnership.execute(this.chain, opts) + } + + /** Builds unsigned `acceptOwnership` tx (Ownable2Step, pool). */ + generateUnsignedAcceptOwnership(opts: AcceptOwnershipParams): Promise { + return this.#acceptOwnership.generate(this.chain, opts) + } + /** Accepts pending pool ownership, signing with `opts.wallet`. */ + acceptOwnership(opts: AcceptOwnershipParams & { wallet: unknown }): Promise { + return this.#acceptOwnership.execute(this.chain, opts) + } + + /** Builds unsigned `grantMintBurnAccess` tx (grant mint/burn role on a token). */ + generateUnsignedGrantMintBurnAccess(opts: GrantMintBurnAccessParams): Promise { + return this.#grantMintBurnAccess.generate(this.chain, opts) + } + /** Grants mint/burn role(s) on a token, signing with `opts.wallet`. */ + grantMintBurnAccess( + opts: GrantMintBurnAccessParams & { wallet: unknown }, + ): Promise { + return this.#grantMintBurnAccess.execute(this.chain, opts) + } + + /** Builds unsigned `revokeMintBurnAccess` tx (revoke mint/burn role on a token). */ + generateUnsignedRevokeMintBurnAccess(opts: RevokeMintBurnAccessParams): Promise { + return this.#revokeMintBurnAccess.generate(this.chain, opts) + } + /** Revokes a mint/burn role on a token, signing with `opts.wallet`. */ + revokeMintBurnAccess( + opts: RevokeMintBurnAccessParams & { wallet: unknown }, + ): Promise { + return this.#revokeMintBurnAccess.execute(this.chain, opts) + } + + /** + * Builds an unsigned CrossChainToken deploy tx (contract creation). + * `ownerAddress` is required here (no signer to derive it from). + */ + generateUnsignedDeployToken(opts: DeployTokenParams): Promise { + return this.#deployToken.generate(this.chain, opts) + } + /** + * Deploys a CrossChainToken, signing with `opts.wallet`; returns the deployed + * `tokenAddress`. `ownerAddress` defaults to the signer's address. + */ + deployToken(opts: DeployTokenParams & { wallet: unknown }): Promise { + return this.#deployToken.execute(this.chain, opts) + } + + /** Builds an unsigned CrossChainPoolToken deploy tx (combined token+pool, creation). */ + generateUnsignedDeployCrossChainPoolToken( + opts: DeployCrossChainPoolTokenParams, + ): Promise { + return this.#deployCrossChainPoolToken.generate(this.chain, opts) + } + /** Deploys a CrossChainPoolToken (token == pool), signing with `opts.wallet`; returns its address. */ + deployCrossChainPoolToken( + opts: DeployCrossChainPoolTokenParams & { wallet: unknown }, + ): Promise { + return this.#deployCrossChainPoolToken.execute(this.chain, opts) + } + + /** + * Builds an unsigned pool-creation tx. `lock-release` requires `lockBoxAddress` + * here (the signed `deployPool` auto-deploys one). + */ + generateUnsignedDeployPool(opts: DeployPoolParams): Promise { + return this.#deployPool.generate(this.chain, opts) + } + /** + * Deploys a CCIP token pool, signing with `opts.wallet`; returns the `poolAddress`. + * For `lock-release`, auto-deploys an `ERC20LockBox` and authorizes the pool on it. + */ + deployPool(opts: DeployPoolParams & { wallet: unknown }): Promise { + return this.#deployPool.execute(this.chain, opts) + } + + /** Builds an unsigned `TokenPoolFactory` pool-deploy tx for an existing token. Requires `futureOwner`. */ + generateUnsignedDeployPoolViaFactory(opts: DeployPoolViaFactoryParams): Promise { + return this.#deployPoolViaFactory.generate(this.chain, opts) + } + /** Deploys a pool for an existing token via TokenPoolFactory 2.0.0 (CREATE2); returns the `poolAddress`. */ + deployPoolViaFactory( + opts: DeployPoolViaFactoryParams & { wallet: unknown }, + ): Promise { + return this.#deployPoolViaFactory.execute(this.chain, opts) + } + + /** Builds an unsigned `TokenPoolFactory` token+pool-deploy tx. Requires `futureOwner`. */ + generateUnsignedDeployTokenAndPoolViaFactory( + opts: DeployTokenAndPoolViaFactoryParams, + ): Promise { + return this.#deployTokenAndPoolViaFactory.generate(this.chain, opts) + } + /** Deploys a new token + its pool in one tx via TokenPoolFactory 2.0.0 (CREATE2); returns both addresses. */ + deployTokenAndPoolViaFactory( + opts: DeployTokenAndPoolViaFactoryParams & { wallet: unknown }, + ): Promise { + return this.#deployTokenAndPoolViaFactory.execute(this.chain, opts) + } + + /** Builds unsigned `provideLiquidity` txs (`[approve, provide/deposit]`) for a lock-release pool. */ + generateUnsignedProvideLiquidity(opts: ProvideLiquidityParams): Promise { + return this.#provideLiquidity.generate(this.chain, opts) + } + /** Provides liquidity to a lock-release pool, signing with `opts.wallet`. */ + provideLiquidity(opts: ProvideLiquidityParams & { wallet: unknown }): Promise { + return this.#provideLiquidity.execute(this.chain, opts) + } + + /** Reads the current MINTER/BURNER role holders on a CrossChainToken (read-only). */ + getMintBurnRoles(tokenAddress: string): Promise { + return getMintBurnRoles(this.chain, tokenAddress) + } +} + +export * from '../errors.ts' +export type { DeployVerification, DeployVerificationTarget } from './deploy-verification.ts' +export type { SetPoolParams } from './token-admin-registry/operations/set-pool.ts' +export type { + EVMRegistrationMethod, + ProposeAdminRoleParams, +} from './token-admin-registry/operations/propose-admin-role.ts' +export type { AcceptAdminRoleParams } from './token-admin-registry/operations/accept-admin-role.ts' +export type { TransferAdminRoleParams } from './token-admin-registry/operations/transfer-admin-role.ts' +export type { + ApplyChainUpdatesParams, + RateLimiterConfig, + RemoteChainConfig, +} from './pool/operations/apply-chain-updates.ts' +export type { DeleteChainConfigParams } from './pool/operations/delete-chain-config.ts' +export type { AppendRemotePoolAddressesParams } from './pool/operations/append-remote-pool-addresses.ts' +export type { RemoveRemotePoolAddressesParams } from './pool/operations/remove-remote-pool-addresses.ts' +export type { SetRateLimitAdminParams } from './pool/operations/set-rate-limit-admin.ts' +export type { SetFeeAdminParams } from './pool/operations/set-fee-admin.ts' +export type { SetAllowedFinalityConfigParams } from './pool/operations/set-allowed-finality-config.ts' +export type { + ChainRateLimiterConfig, + SetChainRateLimiterConfigParams, +} from './pool/operations/set-chain-rate-limiter-config.ts' +export type { SetTokenTransferFeeConfigParams } from './pool/operations/set-token-transfer-fee-config.ts' +export type { TransferOwnershipParams } from './pool/operations/transfer-ownership.ts' +export type { AcceptOwnershipParams } from './pool/operations/accept-ownership.ts' +export type { + GrantMintBurnAccessParams, + GrantMintBurnRole, +} from './token/operations/grant-mint-burn-access.ts' +export type { + RevokeMintBurnAccessParams, + RevokeMintBurnRole, +} from './token/operations/revoke-mint-burn-access.ts' +export type { DeployTokenParams, DeployTokenResult } from './token/operations/deploy-token.ts' +export type { + DeployCrossChainPoolTokenParams, + DeployCrossChainPoolTokenResult, +} from './token/operations/deploy-cross-chain-pool-token.ts' +export type { ProvideLiquidityParams } from './token-pool/operations/provide-liquidity.ts' +export type { + DeployPoolParams, + DeployPoolResult, + EVMPoolType, +} from './token-pool/operations/deploy-pool.ts' +export type { + DeployPoolViaFactoryParams, + DeployPoolViaFactoryResult, + FactoryPoolType, +} from './token-pool/operations/deploy-pool-via-factory.ts' +export type { + DeployTokenAndPoolViaFactoryParams, + DeployTokenAndPoolViaFactoryResult, +} from './token-pool/operations/deploy-token-and-pool-via-factory.ts' +export type { MintBurnRolesResult } from './token/get-mint-burn-roles.ts' +export type { TransactionHash } from '../operation.ts' diff --git a/ccip-sdk/src/cct/evm/operation.ts b/ccip-sdk/src/cct/evm/operation.ts new file mode 100644 index 00000000..33dc6b75 --- /dev/null +++ b/ccip-sdk/src/cct/evm/operation.ts @@ -0,0 +1,59 @@ +/** + * EVM {@link Operation} lifecycle: validate → encode → submit. + * Concrete ops implement {@link EVMOperation.encode}; this base wires + * {@link generate} and {@link execute}. + * + * @packageDocumentation + */ + +import type { TransactionReceipt } from 'ethers' + +import type { EVMChain } from '../../evm/index.ts' +import type { UnsignedEVMTx } from '../../evm/types.ts' +import { type TransactionHash, Operation } from '../operation.ts' +import { submitForReceipt } from './submit.ts' + +/** + * EVM CCT write base. Subclasses supply {@link validate} and {@link buildUnsigned}. + * `Result` defaults to {@link TransactionHash}; deploy ops widen it (e.g. to add + * `tokenAddress`) and override {@link resultFromReceipt} to read the mined receipt. + */ +export abstract class EVMOperation< + P extends { sender?: string }, + Result = TransactionHash, +> extends Operation { + /** Build calldata into an unsigned tx; versioned ops resolve their encoder here. */ + protected abstract buildUnsigned( + chain: EVMChain, + params: P, + ): Promise | UnsignedEVMTx + + /** + * Map the confirmed hash + mined receipt of the last submitted tx to the op's + * result. Default returns just the hash; deploy ops override to add the deployed + * address (`receipt.contractAddress`) and, from the submitted `unsigned` calldata, + * a block-explorer `verification` handle. + */ + protected resultFromReceipt( + hash: TransactionHash, + _receipt: TransactionReceipt, + _unsigned: UnsignedEVMTx, + ): Result { + return hash as Result + } + + /** Run {@link validate} and {@link buildUnsigned}, applying optional `sender`; no signing. */ + async generate(chain: EVMChain, params: P): Promise { + this.validate(params) + const unsigned = await this.buildUnsigned(chain, params) + if (params.sender && unsigned.transactions[0]) unsigned.transactions[0].from = params.sender + return unsigned + } + + /** {@link generate}, then sign and submit; returns once confirmed. */ + async execute(chain: EVMChain, params: P & { wallet: unknown }): Promise { + const unsigned = await this.generate(chain, params) + const { hash, receipt } = await submitForReceipt(chain, params.wallet, unsigned, this.name) + return this.resultFromReceipt({ hash }, receipt, unsigned) + } +} diff --git a/ccip-sdk/src/cct/evm/pool/apply-chain-updates-utils.ts b/ccip-sdk/src/cct/evm/pool/apply-chain-updates-utils.ts new file mode 100644 index 00000000..835437cf --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/apply-chain-updates-utils.ts @@ -0,0 +1,62 @@ +/** + * Shared helpers for the EVM TokenPool `applyChainUpdates` family of ops + * (`applyChainUpdates` and its removal-only wrapper `deleteChainConfig`). + * + * Holds the version → ABI/interface resolution and the remote-address encoding + * (32-byte left-padded, matching the on-chain `bytes` layout) shared by both ops. + * + * @packageDocumentation + */ + +import { type Interface, hexlify, zeroPadValue } from 'ethers' + +import { interfaces } from '../../../evm/const.ts' +import { CCIPVersion } from '../../../types.ts' +import { getAddressBytes } from '../../../utils.ts' + +/** Rate limiter config; `capacity`/`rate` are decimal strings to avoid JS precision loss. */ +export type RateLimiterConfig = { + /** Whether the rate limiter is enabled. */ + isEnabled: boolean + /** Maximum token capacity (bigint as string). */ + capacity: string + /** Token refill rate per second (bigint as string). */ + rate: string +} + +/** + * Resolves the cached TokenPool {@link Interface} for a pool version, mirroring the + * monolith's `getPoolVersionAndABI`: + * - `<= v1.5` → v1.5 (legacy `applyChainUpdates(ChainUpdate[])`) + * - `< v2.0` → v1.6 (`applyChainUpdates(uint64[], ChainUpdate[])`) + * - else → v2.0 + * @param version - Pool version string from `typeAndVersion` (e.g. `'1.6.0'`). + * @returns The matching cached ethers {@link Interface}. + */ +export function resolvePoolInterface(version: string): Interface { + if (version <= CCIPVersion.V1_5) return interfaces.TokenPool_v1_5 + if (version < CCIPVersion.V2_0) return interfaces.TokenPool_v1_6 + return interfaces.TokenPool_v2_0 +} + +/** + * Whether a pool version uses the legacy single-address `applyChainUpdates(ChainUpdate[])` + * encoding (with the `allowed` flag) rather than the `(uint64[] removes, ChainUpdate[] adds)` + * form. Only pre-v1.5 pools use the legacy encoding. + * @param version - Pool version string from `typeAndVersion`. + * @returns `true` for the legacy encoding. + */ +export function isLegacyPoolVersion(version: string): boolean { + return version < CCIPVersion.V1_5 +} + +/** + * Encodes a remote address to a 32-byte left-padded hex string. + * Accepts any chain family's native format via {@link getAddressBytes} and matches + * `common.LeftPadBytes(addr.Bytes(), 32)`. + * @param address - Remote address in native format (hex, base58, base64). + * @returns 32-byte left-padded, `0x`-prefixed hex string. + */ +export function encodeRemoteAddress(address: string): string { + return zeroPadValue(hexlify(getAddressBytes(address)), 32) +} diff --git a/ccip-sdk/src/cct/evm/pool/operations/accept-ownership.test.ts b/ccip-sdk/src/cct/evm/pool/operations/accept-ownership.test.ts new file mode 100644 index 00000000..3439185e --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/accept-ownership.test.ts @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface } from 'ethers' + +import { AcceptOwnership } from './accept-ownership.ts' +import TokenPool_1_6_ABI from '../../../../evm/abi/LockReleaseTokenPool_1_6_1.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const POOL = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const stubChain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, +} as unknown as EVMChain + +describe('EVM cct acceptOwnership', () => { + const op = new AcceptOwnership() + + it('encodes acceptOwnership() byte-identical to a direct ethers encode', async () => { + const unsigned = await op.generate(stubChain, { poolAddress: POOL }) + const expected = new Interface(TokenPool_1_6_ABI).encodeFunctionData('acceptOwnership', []) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('applies sender to from', async () => { + const unsigned = await op.generate(stubChain, { poolAddress: POOL, sender: POOL }) + assert.equal(unsigned.transactions[0]!.from, POOL) + }) + + it('rejects an invalid poolAddress before RPC', async () => { + await assert.rejects( + () => op.generate(stubChain, { poolAddress: 'nope' }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/accept-ownership.ts b/ccip-sdk/src/cct/evm/pool/operations/accept-ownership.ts new file mode 100644 index 00000000..03a35b2a --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/accept-ownership.ts @@ -0,0 +1,42 @@ +/** + * acceptOwnership — accepts a pending ownership transfer on a token pool (step 2 of + * the OpenZeppelin `Ownable2Step` handshake; called by the address a prior + * `transferOwnership` proposed). + * + * Version-independent: `acceptOwnership()` is inherited unchanged across + * v1.5 / v1.6 / v2.0, so the calldata (just the selector) is identical for every + * pool version — no `typeAndVersion` lookup is needed. Encoded via the cached + * `TokenPool_v1_6` interface. + * + * @packageDocumentation + */ + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Parameters for `acceptOwnership`. */ +export type AcceptOwnershipParams = { + /** Pool address whose pending ownership is being accepted. */ + poolAddress: string + sender?: string +} + +/** Accepts a pending pool ownership transfer via `Ownable2Step.acceptOwnership`. */ +export class AcceptOwnership extends EVMOperation { + readonly name = 'acceptOwnership' + + /** Validates the pool address before any RPC. */ + protected validate(p: AcceptOwnershipParams): void { + validateAddress(this.name, 'poolAddress', p.poolAddress) + } + + /** Builds `acceptOwnership()` calldata (version-stable across v1.5–v2.0). */ + protected buildUnsigned(_chain: EVMChain, p: AcceptOwnershipParams): UnsignedEVMTx { + const data = interfaces.TokenPool_v1_6.encodeFunctionData('acceptOwnership', []) + return { family: ChainFamily.EVM, transactions: [{ to: p.poolAddress, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/pool/operations/append-remote-pool-addresses.test.ts b/ccip-sdk/src/cct/evm/pool/operations/append-remote-pool-addresses.test.ts new file mode 100644 index 00000000..7170fa6c --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/append-remote-pool-addresses.test.ts @@ -0,0 +1,153 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, hexlify, zeroPadValue } from 'ethers' + +import { AppendRemotePoolAddresses } from './append-remote-pool-addresses.ts' +import TokenPool_1_6_ABI from '../../../../evm/abi/LockReleaseTokenPool_1_6_1.ts' +import TokenPool_2_0_ABI from '../../../../evm/abi/TokenPool_2_0.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { getAddressBytes } from '../../../../utils.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const POOL = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const SELECTOR = 16015286601757825753n +const REMOTE_EVM = '0xd7BF0e3D34B4C4F7d5F3C4C6B2A1e0f9c8b7a6D5' +const REMOTE_EVM_2 = '0xaabbccddeeff00112233445566778899aabbccdd' +// Solana pool address in base58 (non-EVM native format). +const REMOTE_SOL = 'GaM2p1FfyE8YkfB1D3aXHjTr5Z8mQ4wYbNkPcVjRs2A' + +const encodeRemote = (a: string) => zeroPadValue(hexlify(getAddressBytes(a)), 32) + +function stubChain(version: CCIPVersion = CCIPVersion.V1_6): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + typeAndVersion: () => Promise.resolve(['LockReleaseTokenPool', version, '', undefined]), + } as unknown as EVMChain +} + +describe('EVM cct appendRemotePoolAddresses', () => { + const op = new AppendRemotePoolAddresses() + + it('encodes addRemotePool — byte-identical to a direct ethers encode (v1.6)', async () => { + const unsigned = await op.generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_EVM], + }) + const expected = new Interface(TokenPool_1_6_ABI).encodeFunctionData('addRemotePool', [ + SELECTOR, + encodeRemote(REMOTE_EVM), + ]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('emits one byte-identical tx per address (incl. array + non-EVM native format)', async () => { + const addrs = [REMOTE_EVM, REMOTE_EVM_2, REMOTE_SOL] + const unsigned = await op.generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: addrs, + }) + assert.equal(unsigned.transactions.length, addrs.length) + for (let i = 0; i < addrs.length; i++) { + const expected = new Interface(TokenPool_1_6_ABI).encodeFunctionData('addRemotePool', [ + SELECTOR, + encodeRemote(addrs[i]!), + ]) + assert.equal(unsigned.transactions[i]!.to, POOL) + assert.equal(unsigned.transactions[i]!.data, expected) + } + }) + + it('uses the v2.0 interface for v2.0 pools (identical selector, byte-parity)', async () => { + const unsigned = await op.generate(stubChain(CCIPVersion.V2_0), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_EVM], + }) + const expected = new Interface(TokenPool_2_0_ABI).encodeFunctionData('addRemotePool', [ + SELECTOR, + encodeRemote(REMOTE_EVM), + ]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('applies sender to from on the first tx', async () => { + const unsigned = await op.generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_EVM], + sender: POOL, + }) + assert.equal(unsigned.transactions[0]!.from, POOL) + }) + + it('rejects an invalid pool address before RPC', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + poolAddress: 'nope', + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_EVM], + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) + + it('rejects a zero remoteChainSelector', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: 0n, + remotePoolAddresses: [REMOTE_EVM], + }), + (e: unknown) => + e instanceof CCTParamsInvalidError && e.context.param === 'remoteChainSelector', + ) + }) + + it('rejects an empty address list', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [], + }), + (e: unknown) => + e instanceof CCTParamsInvalidError && e.context.param === 'remotePoolAddresses', + ) + }) + + it('rejects a blank entry within the address list', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_EVM, ' '], + }), + (e: unknown) => + e instanceof CCTParamsInvalidError && e.context.param === 'remotePoolAddresses[1]', + ) + }) + + it('rejects v1.5 pools (no addRemotePool)', async () => { + await assert.rejects( + () => + op.generate(stubChain(CCIPVersion.V1_5), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_EVM], + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/append-remote-pool-addresses.ts b/ccip-sdk/src/cct/evm/pool/operations/append-remote-pool-addresses.ts new file mode 100644 index 00000000..2c497492 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/append-remote-pool-addresses.ts @@ -0,0 +1,92 @@ +/** + * appendRemotePoolAddresses — registers additional remote pool addresses for an + * already-configured remote chain on a local TokenPool, by encoding + * `addRemotePool(uint64 remoteChainSelector, bytes remotePoolAddress)` — one + * transaction per address. Requires a v1.5.1+ pool (v1.5 has no `addRemotePool`; + * use `applyChainUpdates` to re-initialize the chain config instead). + * + * @packageDocumentation + */ + +import { hexlify, zeroPadValue } from 'ethers' + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { getAddressBytes } from '../../../../utils.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Parameters for `appendRemotePoolAddresses`. */ +export type AppendRemotePoolAddressesParams = { + /** Local TokenPool address (EVM). */ + poolAddress: string + /** Remote chain selector; must already be configured via `applyChainUpdates`. */ + remoteChainSelector: bigint + /** Remote pool addresses in native format (hex/base58/base64). At least one required. */ + remotePoolAddresses: string[] + sender?: string +} + +/** + * Encodes a remote pool address to raw left-padded 32-byte hex, matching the + * on-chain `bytes` representation. Handles all chain families via + * {@link getAddressBytes} (hex/base58/base64). + */ +function encodeRemoteAddress(address: string): string { + return zeroPadValue(hexlify(getAddressBytes(address)), 32) +} + +/** Appends remote pool addresses to an existing chain config on a local TokenPool. */ +export class AppendRemotePoolAddresses extends EVMOperation { + readonly name = 'appendRemotePoolAddresses' + + /** Validates the pool address, selector, and address list before any RPC. */ + protected validate(p: AppendRemotePoolAddressesParams): void { + validateAddress(this.name, 'poolAddress', p.poolAddress) + if (p.remoteChainSelector == null || p.remoteChainSelector === 0n) { + throw new CCTParamsInvalidError(this.name, 'remoteChainSelector', 'must be non-zero') + } + if (p.remotePoolAddresses.length === 0) { + throw new CCTParamsInvalidError( + this.name, + 'remotePoolAddresses', + 'must have at least one address', + ) + } + for (let i = 0; i < p.remotePoolAddresses.length; i++) { + const addr = p.remotePoolAddresses[i] + if (!addr || addr.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, `remotePoolAddresses[${i}]`, 'must be non-empty') + } + } + } + + /** Builds one `addRemotePool` tx per address; rejects v1.5 pools. */ + protected async buildUnsigned( + chain: EVMChain, + p: AppendRemotePoolAddressesParams, + ): Promise { + const [, version] = await chain.typeAndVersion(p.poolAddress) + if (version <= CCIPVersion.V1_5) { + throw new CCTParamsInvalidError( + this.name, + 'poolAddress', + 'addRemotePool is not available on v1.5 pools. Use applyChainUpdates to re-initialize the chain config instead.', + ) + } + // addRemotePool(uint64,bytes) encoding is version-stable across v1.5.1–v2.0. + const iface = version < CCIPVersion.V2_0 ? interfaces.TokenPool_v1_6 : interfaces.TokenPool_v2_0 + const transactions = p.remotePoolAddresses.map((remotePoolAddress) => ({ + to: p.poolAddress, + data: iface.encodeFunctionData('addRemotePool', [ + p.remoteChainSelector, + encodeRemoteAddress(remotePoolAddress), + ]), + })) + return { family: ChainFamily.EVM, transactions } + } +} diff --git a/ccip-sdk/src/cct/evm/pool/operations/apply-chain-updates.fork.test.ts b/ccip-sdk/src/cct/evm/pool/operations/apply-chain-updates.fork.test.ts new file mode 100644 index 00000000..43ce6169 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/apply-chain-updates.fork.test.ts @@ -0,0 +1,265 @@ +import assert from 'node:assert/strict' +import { execSync } from 'node:child_process' +import { after, before, describe, it } from 'node:test' + +import { Contract, Interface, JsonRpcProvider, Wallet } from 'ethers' +import { Instance } from 'prool' + +import TokenPool_2_0_ABI from '../../../../evm/abi/TokenPool_2_0.ts' +import { EVMTokenManager } from '../../index.ts' + +// ── Constants ── + +const SEPOLIA_RPC = process.env['RPC_SEPOLIA'] || 'https://ethereum-sepolia-rpc.publicnode.com' +const SEPOLIA_ROUTER = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' +const SEPOLIA_REGISTRY_MODULE = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const ANVIL_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + +// A valid chain selector for testing (Solana devnet) +const REMOTE_CHAIN_SELECTOR = 16423721717087811551n + +// ── Helpers ── + +function isAnvilAvailable(): boolean { + try { + execSync('anvil --version', { stdio: 'ignore' }) + return true + } catch { + return false + } +} + +// ── Tests ── + +const skip = !!process.env.SKIP_INTEGRATION_TESTS || !isAnvilAvailable() + +const testLogger = process.env.VERBOSE + ? console + : { debug() {}, info() {}, warn: console.warn, error: console.error } + +describe('EVMTokenManager applyChainUpdates Fork Tests', { skip, timeout: 120_000 }, () => { + let provider: JsonRpcProvider + let wallet: Wallet + let mgr: EVMTokenManager + let anvilInstance: ReturnType | undefined + let tokenAddress: string + let poolAddress: string + + before(async () => { + // Fork Sepolia so we have a real Router + anvilInstance = Instance.anvil({ + port: 8751, + forkUrl: SEPOLIA_RPC, + forkBlockNumber: undefined, + }) + await anvilInstance.start() + + const anvilUrl = `http://${anvilInstance.host}:${anvilInstance.port}` + provider = new JsonRpcProvider(anvilUrl, undefined, { cacheTimeout: -1 }) + wallet = new Wallet(ANVIL_PRIVATE_KEY, provider) + + mgr = await EVMTokenManager.fromProvider(provider, { logger: testLogger, apiClient: null }) + + // 1. Deploy token + const tokenResult = await mgr.deployToken({ + name: 'Apply Chain Updates Test Token', + symbol: 'ACUT', + decimals: 18, + initialSupply: 1_000_000n * 10n ** 18n, + wallet, + }) + tokenAddress = tokenResult.tokenAddress + + // 2. Deploy pool + const poolResult = await mgr.deployPool({ + poolType: 'burn-mint', + tokenAddress, + localTokenDecimals: 18, + routerAddress: SEPOLIA_ROUTER, + wallet, + }) + poolAddress = poolResult.poolAddress + + // 3. Propose + accept admin (for setting pool later) + await mgr.proposeAdminRole({ + tokenAddress, + registryModuleAddress: SEPOLIA_REGISTRY_MODULE, + registrationMethod: 'getCCIPAdmin', + wallet, + }) + + await mgr.acceptAdminRole({ + tokenAddress, + address: SEPOLIA_ROUTER, + wallet, + }) + + // 4. Set pool in TAR + await mgr.setPool({ + tokenAddress, + poolAddress, + address: SEPOLIA_ROUTER, + wallet, + }) + }) + + after(async () => { + provider.destroy() + await anvilInstance?.stop() + }) + + // =========================================================================== + // applyChainUpdates — Happy Path + // =========================================================================== + + it('should apply chain updates and verify on-chain', async () => { + const remotePoolAddress = '0xd7BF0d8E6C242b6Dde4490Ab3aFc8C1e811ec9aD' + const remoteTokenAddress = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' + + const result = await mgr.applyChainUpdates({ + poolAddress, + remoteChainSelectorsToRemove: [], + chainsToAdd: [ + { + remoteChainSelector: REMOTE_CHAIN_SELECTOR, + remotePoolAddresses: [remotePoolAddress], + remoteTokenAddress: remoteTokenAddress, + outboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + inboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + }, + ], + wallet, + }) + + assert.ok(result.hash, 'should return tx hash') + assert.match(result.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify on-chain: isSupportedChain should return true + const pool = new Contract(poolAddress, TokenPool_2_0_ABI, provider) + const isSupported = await pool.getFunction('isSupportedChain')(REMOTE_CHAIN_SELECTOR) + assert.equal(isSupported, true, 'chain should be supported after applyChainUpdates') + }) + + // =========================================================================== + // generateUnsignedApplyChainUpdates — shape verification + // =========================================================================== + + it('should produce unsigned tx with correct shape', async () => { + const unsigned = await mgr.generateUnsignedApplyChainUpdates({ + poolAddress, + remoteChainSelectorsToRemove: [], + chainsToAdd: [ + { + remoteChainSelector: 999n, + remotePoolAddresses: ['0x1111111111111111111111111111111111111111'], + remoteTokenAddress: '0x2222222222222222222222222222222222222222', + outboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + inboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + }, + ], + }) + + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal( + (tx.to as string).toLowerCase(), + poolAddress.toLowerCase(), + 'to should be pool address', + ) + assert.ok(tx.data, 'should have calldata') + + // Verify function selector + const iface = new Interface(TokenPool_2_0_ABI) + const selector = iface.getFunction('applyChainUpdates')!.selector + assert.ok(tx.data.startsWith(selector), 'should use applyChainUpdates selector') + }) + + // =========================================================================== + // appendRemotePoolAddresses — Happy Path + // =========================================================================== + + it('should append a remote pool address to an existing chain config', async () => { + // The chain config was already created by the applyChainUpdates test above + const newRemotePool = '0x3333333333333333333333333333333333333333' + + const result = await mgr.appendRemotePoolAddresses({ + poolAddress, + remoteChainSelector: REMOTE_CHAIN_SELECTOR, + remotePoolAddresses: [newRemotePool], + wallet, + }) + + assert.ok(result.hash, 'should return tx hash') + assert.match(result.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify on-chain: getRemotePools should include the new pool address + const pool = new Contract(poolAddress, TokenPool_2_0_ABI, provider) + const remotePools = (await pool.getFunction('getRemotePools')( + REMOTE_CHAIN_SELECTOR, + )) as string[] + // The new pool address should be in the list (encoded as 32-byte left-padded bytes) + assert.ok(remotePools.length >= 2, 'should have at least 2 remote pools after append') + }) + + // =========================================================================== + // removeRemotePoolAddresses — Happy Path + // =========================================================================== + + it('should remove a remote pool address and verify on-chain', async () => { + // The chain config was already created by applyChainUpdates + appendRemotePoolAddresses + // At this point there should be at least 2 remote pools + const pool = new Contract(poolAddress, TokenPool_2_0_ABI, provider) + const remotePoolsBefore = (await pool.getFunction('getRemotePools')( + REMOTE_CHAIN_SELECTOR, + )) as string[] + assert.ok(remotePoolsBefore.length >= 2, 'should have at least 2 remote pools before remove') + + // Remove the pool that was added by appendRemotePoolAddresses + const poolToRemove = '0x3333333333333333333333333333333333333333' + const result = await mgr.removeRemotePoolAddresses({ + poolAddress, + remoteChainSelector: REMOTE_CHAIN_SELECTOR, + remotePoolAddresses: [poolToRemove], + wallet, + }) + + assert.ok(result.hash, 'should return tx hash') + assert.match(result.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify on-chain: getRemotePools should have one fewer pool + const remotePoolsAfter = (await pool.getFunction('getRemotePools')( + REMOTE_CHAIN_SELECTOR, + )) as string[] + assert.equal( + remotePoolsAfter.length, + remotePoolsBefore.length - 1, + 'should have one fewer remote pool after remove', + ) + }) + + // =========================================================================== + // deleteChainConfig — Happy Path + // =========================================================================== + + it('should delete a chain config and verify on-chain', async () => { + // The chain config was already created by applyChainUpdates test above + const pool = new Contract(poolAddress, TokenPool_2_0_ABI, provider) + + // Verify chain is currently supported + const isSupportedBefore = await pool.getFunction('isSupportedChain')(REMOTE_CHAIN_SELECTOR) + assert.equal(isSupportedBefore, true, 'chain should be supported before delete') + + const result = await mgr.deleteChainConfig({ + poolAddress, + remoteChainSelector: REMOTE_CHAIN_SELECTOR, + wallet, + }) + + assert.ok(result.hash, 'should return tx hash') + assert.match(result.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify on-chain: isSupportedChain should return false + const isSupportedAfter = await pool.getFunction('isSupportedChain')(REMOTE_CHAIN_SELECTOR) + assert.equal(isSupportedAfter, false, 'chain should not be supported after deleteChainConfig') + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/apply-chain-updates.test.ts b/ccip-sdk/src/cct/evm/pool/operations/apply-chain-updates.test.ts new file mode 100644 index 00000000..761be448 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/apply-chain-updates.test.ts @@ -0,0 +1,201 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface } from 'ethers' + +import { + type RateLimiterConfig, + type RemoteChainConfig, + ApplyChainUpdates, +} from './apply-chain-updates.ts' +import TokenPool_1_6_ABI from '../../../../evm/abi/LockReleaseTokenPool_1_6_1.ts' +import TokenPool_2_0_ABI from '../../../../evm/abi/TokenPool_2_0.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { encodeRemoteAddress } from '../apply-chain-updates-utils.ts' + +const POOL = '0x1234567890AbcdEF1234567890aBcdef12345678' +const REMOTE_POOL = '0xd7BF0d8E6C242b6Dde4490Ab3aFc8C1e811ec9aD' +const REMOTE_POOL_2 = '0xAaBbCcDdEeFf00112233445566778899aAbBcCdD' +const REMOTE_TOKEN = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' + +/** Stub EVMChain whose `typeAndVersion` reports a fixed pool version. */ +function chainFor(version: string): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + typeAndVersion: async (_addr: string) => ['LockReleaseTokenPool', version, `pool ${version}`], + } as unknown as EVMChain +} + +const disabled = { isEnabled: false, capacity: '0', rate: '0' } as const +const enabled = { + isEnabled: true, + capacity: '100000000000000000000000', + rate: '167000000000000000000', +} as const + +function configFor( + selector: bigint, + pools: string[], + out: RateLimiterConfig = disabled, + inb: RateLimiterConfig = disabled, +) { + return { + remoteChainSelector: selector, + remotePoolAddresses: pools, + remoteTokenAddress: REMOTE_TOKEN, + outboundRateLimiterConfig: out, + inboundRateLimiterConfig: inb, + } satisfies RemoteChainConfig +} + +/** Mirrors the op's new-style (uint64[] removes, ChainUpdate[] adds) arg assembly. */ +function encodeAdds(configs: RemoteChainConfig[]) { + return configs.map((c) => ({ + remoteChainSelector: c.remoteChainSelector, + remotePoolAddresses: c.remotePoolAddresses.map((a) => encodeRemoteAddress(a)), + remoteTokenAddress: encodeRemoteAddress(c.remoteTokenAddress), + outboundRateLimiterConfig: { + isEnabled: c.outboundRateLimiterConfig.isEnabled, + capacity: BigInt(c.outboundRateLimiterConfig.capacity), + rate: BigInt(c.outboundRateLimiterConfig.rate), + }, + inboundRateLimiterConfig: { + isEnabled: c.inboundRateLimiterConfig.isEnabled, + capacity: BigInt(c.inboundRateLimiterConfig.capacity), + rate: BigInt(c.inboundRateLimiterConfig.rate), + }, + })) +} + +describe('EVM cct applyChainUpdates', () => { + const op = new ApplyChainUpdates() + + it('encodes a single v1.6 add — byte-identical to a direct ethers encode', async () => { + const configs = [configFor(16015286601757825753n, [REMOTE_POOL], enabled, disabled)] + const unsigned = await op.generate(chainFor('1.6.0'), { + poolAddress: POOL, + remoteChainSelectorsToRemove: [], + chainsToAdd: configs, + }) + const expected = new Interface(TokenPool_1_6_ABI).encodeFunctionData('applyChainUpdates', [ + [], + encodeAdds(configs), + ]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('encodes a multi-update array (multiple chains, multiple pools, plus removes)', async () => { + const configs = [ + configFor(16015286601757825753n, [REMOTE_POOL, REMOTE_POOL_2], enabled, enabled), + configFor(3734403246176062136n, [REMOTE_POOL_2], disabled, disabled), + ] + const removes = [1234567890n, 9876543210n] + const unsigned = await op.generate(chainFor('1.6.0'), { + poolAddress: POOL, + remoteChainSelectorsToRemove: removes, + chainsToAdd: configs, + }) + const expected = new Interface(TokenPool_1_6_ABI).encodeFunctionData('applyChainUpdates', [ + removes, + encodeAdds(configs), + ]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('selects the v2.0 ABI for a v2.0 pool', async () => { + const configs = [configFor(16015286601757825753n, [REMOTE_POOL], disabled, disabled)] + const unsigned = await op.generate(chainFor('2.0.0'), { + poolAddress: POOL, + remoteChainSelectorsToRemove: [], + chainsToAdd: configs, + }) + const expected = new Interface(TokenPool_2_0_ABI).encodeFunctionData('applyChainUpdates', [ + [], + encodeAdds(configs), + ]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('encodes a removes-only update', async () => { + const removes = [16015286601757825753n] + const unsigned = await op.generate(chainFor('1.6.0'), { + poolAddress: POOL, + remoteChainSelectorsToRemove: removes, + chainsToAdd: [], + }) + const expected = new Interface(TokenPool_1_6_ABI).encodeFunctionData('applyChainUpdates', [ + removes, + [], + ]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('applies sender to from', async () => { + const unsigned = await op.generate(chainFor('1.6.0'), { + poolAddress: POOL, + remoteChainSelectorsToRemove: [], + chainsToAdd: [configFor(16015286601757825753n, [REMOTE_POOL])], + sender: REMOTE_TOKEN, + }) + assert.equal(unsigned.transactions[0]!.from, REMOTE_TOKEN) + }) + + it('rejects an empty pool address before RPC', async () => { + await assert.rejects( + () => + op.generate(chainFor('1.6.0'), { + poolAddress: '', + remoteChainSelectorsToRemove: [], + chainsToAdd: [], + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) + + it('rejects a zero remote chain selector', async () => { + await assert.rejects( + () => + op.generate(chainFor('1.6.0'), { + poolAddress: POOL, + remoteChainSelectorsToRemove: [], + chainsToAdd: [configFor(0n, [REMOTE_POOL])], + }), + (e: unknown) => + e instanceof CCTParamsInvalidError && + e.context.param === 'chainsToAdd[0].remoteChainSelector', + ) + }) + + it('rejects an empty remotePoolAddresses list', async () => { + await assert.rejects( + () => + op.generate(chainFor('1.6.0'), { + poolAddress: POOL, + remoteChainSelectorsToRemove: [], + chainsToAdd: [configFor(16015286601757825753n, [])], + }), + (e: unknown) => + e instanceof CCTParamsInvalidError && + e.context.param === 'chainsToAdd[0].remotePoolAddresses', + ) + }) + + it('rejects an empty remote token address', async () => { + const bad = { ...configFor(16015286601757825753n, [REMOTE_POOL]), remoteTokenAddress: '' } + await assert.rejects( + () => + op.generate(chainFor('1.6.0'), { + poolAddress: POOL, + remoteChainSelectorsToRemove: [], + chainsToAdd: [bad], + }), + (e: unknown) => + e instanceof CCTParamsInvalidError && + e.context.param === 'chainsToAdd[0].remoteTokenAddress', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/apply-chain-updates.ts b/ccip-sdk/src/cct/evm/pool/operations/apply-chain-updates.ts new file mode 100644 index 00000000..ba1c5694 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/apply-chain-updates.ts @@ -0,0 +1,162 @@ +/** + * applyChainUpdates — (re)configures remote chains on a TokenPool: adds remote + * chain configs (remote pool addresses, remote token, rate limits) and/or removes + * remote chain selectors. Encoding is version-dispatched off the pool's + * `typeAndVersion`: + * - pre-v1.5: `applyChainUpdates(ChainUpdate[])` (single pool address, `allowed` flag) + * - v1.5.1+/v2.0: `applyChainUpdates(uint64[] removes, ChainUpdate[] adds)` + * + * @packageDocumentation + */ + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMOperation } from '../../operation.ts' +import { + type RateLimiterConfig, + encodeRemoteAddress, + isLegacyPoolVersion, + resolvePoolInterface, +} from '../apply-chain-updates-utils.ts' + +export type { RateLimiterConfig } from '../apply-chain-updates-utils.ts' + +/** Configuration for a single remote chain to add to a pool. Addresses are in native format. */ +export type RemoteChainConfig = { + /** Remote chain selector. */ + remoteChainSelector: bigint + /** Remote pool address(es) in native format. At least one required. */ + remotePoolAddresses: string[] + /** Remote token address in native format. */ + remoteTokenAddress: string + /** Outbound rate limiter (local → remote). */ + outboundRateLimiterConfig: RateLimiterConfig + /** Inbound rate limiter (remote → local). */ + inboundRateLimiterConfig: RateLimiterConfig +} + +/** Parameters for `applyChainUpdates`. */ +export type ApplyChainUpdatesParams = { + /** Local pool address. */ + poolAddress: string + /** Remote chain selectors to remove (can be empty). */ + remoteChainSelectorsToRemove: bigint[] + /** Remote chain configurations to add (can be empty). */ + chainsToAdd: RemoteChainConfig[] + sender?: string +} + +/** Adds and/or removes remote chain configs on a TokenPool, version-dispatched. */ +export class ApplyChainUpdates extends EVMOperation { + readonly name = 'applyChainUpdates' + + /** Validates the pool address and each remote-chain config before any RPC. */ + protected validate(p: ApplyChainUpdatesParams): void { + if (!p.poolAddress || p.poolAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'poolAddress', 'must be non-empty') + } + for (const [i, chain] of p.chainsToAdd.entries()) { + if (chain.remoteChainSelector == null || chain.remoteChainSelector === 0n) { + throw new CCTParamsInvalidError( + this.name, + `chainsToAdd[${i}].remoteChainSelector`, + 'must be non-zero', + ) + } + if (chain.remotePoolAddresses.length === 0) { + throw new CCTParamsInvalidError( + this.name, + `chainsToAdd[${i}].remotePoolAddresses`, + 'must have at least one address', + ) + } + if (!chain.remoteTokenAddress || chain.remoteTokenAddress.trim().length === 0) { + throw new CCTParamsInvalidError( + this.name, + `chainsToAdd[${i}].remoteTokenAddress`, + 'must be non-empty', + ) + } + } + } + + /** Detects the pool version and builds the matching `applyChainUpdates` calldata. */ + protected async buildUnsigned( + chain: EVMChain, + p: ApplyChainUpdatesParams, + ): Promise { + const [, version] = await chain.typeAndVersion(p.poolAddress) + const iface = resolvePoolInterface(version) + + let data: string + if (isLegacyPoolVersion(version)) { + // pre-v1.5: applyChainUpdates(ChainUpdate[]) — single pool address, `allowed` field + const chains = [ + ...p.chainsToAdd.map((c) => ({ + remoteChainSelector: c.remoteChainSelector, + allowed: true, + remotePoolAddress: encodeRemoteAddress(this.#firstPoolAddress(c.remotePoolAddresses)), + remoteTokenAddress: encodeRemoteAddress(c.remoteTokenAddress), + outboundRateLimiterConfig: { + isEnabled: c.outboundRateLimiterConfig.isEnabled, + capacity: BigInt(c.outboundRateLimiterConfig.capacity), + rate: BigInt(c.outboundRateLimiterConfig.rate), + }, + inboundRateLimiterConfig: { + isEnabled: c.inboundRateLimiterConfig.isEnabled, + capacity: BigInt(c.inboundRateLimiterConfig.capacity), + rate: BigInt(c.inboundRateLimiterConfig.rate), + }, + })), + ...p.remoteChainSelectorsToRemove.map((s) => ({ + remoteChainSelector: BigInt(s), + allowed: false, + remotePoolAddress: '0x', + remoteTokenAddress: '0x', + outboundRateLimiterConfig: { isEnabled: false, capacity: 0n, rate: 0n }, + inboundRateLimiterConfig: { isEnabled: false, capacity: 0n, rate: 0n }, + })), + ] + data = iface.encodeFunctionData('applyChainUpdates', [chains]) + } else { + // v1.5.1+ and v2.0: applyChainUpdates(uint64[] removes, ChainUpdate[] adds) + const chainsToAdd = p.chainsToAdd.map((c) => ({ + remoteChainSelector: c.remoteChainSelector, + remotePoolAddresses: c.remotePoolAddresses.map((addr) => encodeRemoteAddress(addr)), + remoteTokenAddress: encodeRemoteAddress(c.remoteTokenAddress), + outboundRateLimiterConfig: { + isEnabled: c.outboundRateLimiterConfig.isEnabled, + capacity: BigInt(c.outboundRateLimiterConfig.capacity), + rate: BigInt(c.outboundRateLimiterConfig.rate), + }, + inboundRateLimiterConfig: { + isEnabled: c.inboundRateLimiterConfig.isEnabled, + capacity: BigInt(c.inboundRateLimiterConfig.capacity), + rate: BigInt(c.inboundRateLimiterConfig.rate), + }, + })) + const remoteChainSelectorsToRemove = p.remoteChainSelectorsToRemove.map((s) => BigInt(s)) + data = iface.encodeFunctionData('applyChainUpdates', [ + remoteChainSelectorsToRemove, + chainsToAdd, + ]) + } + + return { family: ChainFamily.EVM, transactions: [{ to: p.poolAddress, data }] } + } + + /** First remote pool address; validate() guarantees the array is non-empty. */ + #firstPoolAddress(addresses: string[]): string { + const first = addresses[0] + if (first == null) { + throw new CCTParamsInvalidError( + this.name, + 'chainsToAdd[].remotePoolAddresses', + 'must have at least one address', + ) + } + return first + } +} diff --git a/ccip-sdk/src/cct/evm/pool/operations/delete-chain-config.test.ts b/ccip-sdk/src/cct/evm/pool/operations/delete-chain-config.test.ts new file mode 100644 index 00000000..183e61ae --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/delete-chain-config.test.ts @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface } from 'ethers' + +import { DeleteChainConfig } from './delete-chain-config.ts' +import TokenPool_1_6_ABI from '../../../../evm/abi/LockReleaseTokenPool_1_6_1.ts' +import TokenPool_2_0_ABI from '../../../../evm/abi/TokenPool_2_0.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const POOL = '0x1234567890AbcdEF1234567890aBcdef12345678' +const SELECTOR = 16015286601757825753n + +/** Stub EVMChain whose `typeAndVersion` reports a fixed pool version. */ +function chainFor(version: string): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + typeAndVersion: async (_addr: string) => ['LockReleaseTokenPool', version, `pool ${version}`], + } as unknown as EVMChain +} + +describe('EVM cct deleteChainConfig', () => { + const op = new DeleteChainConfig() + + it('encodes a v1.6 removal — byte-identical to a direct ethers encode', async () => { + const unsigned = await op.generate(chainFor('1.6.0'), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + }) + const expected = new Interface(TokenPool_1_6_ABI).encodeFunctionData('applyChainUpdates', [ + [SELECTOR], + [], + ]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('selects the v2.0 ABI for a v2.0 pool', async () => { + const unsigned = await op.generate(chainFor('2.0.0'), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + }) + const expected = new Interface(TokenPool_2_0_ABI).encodeFunctionData('applyChainUpdates', [ + [SELECTOR], + [], + ]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('applies sender to from', async () => { + const unsigned = await op.generate(chainFor('1.6.0'), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + sender: POOL, + }) + assert.equal(unsigned.transactions[0]!.from, POOL) + }) + + it('rejects an empty pool address before RPC', async () => { + await assert.rejects( + () => op.generate(chainFor('1.6.0'), { poolAddress: '', remoteChainSelector: SELECTOR }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) + + it('rejects a zero remote chain selector', async () => { + await assert.rejects( + () => op.generate(chainFor('1.6.0'), { poolAddress: POOL, remoteChainSelector: 0n }), + (e: unknown) => + e instanceof CCTParamsInvalidError && e.context.param === 'remoteChainSelector', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/delete-chain-config.ts b/ccip-sdk/src/cct/evm/pool/operations/delete-chain-config.ts new file mode 100644 index 00000000..4d2421a5 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/delete-chain-config.ts @@ -0,0 +1,69 @@ +/** + * deleteChainConfig — removes an entire remote chain configuration from a + * TokenPool. A removal-only wrapper over `applyChainUpdates`, version-dispatched: + * - pre-v1.5: `applyChainUpdates([{ remoteChainSelector, allowed: false, ... }])` + * - v1.5.1+/v2.0: `applyChainUpdates([remoteChainSelector], [])` + * + * @packageDocumentation + */ + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMOperation } from '../../operation.ts' +import { isLegacyPoolVersion, resolvePoolInterface } from '../apply-chain-updates-utils.ts' + +/** Parameters for `deleteChainConfig`. */ +export type DeleteChainConfigParams = { + /** Local pool address. */ + poolAddress: string + /** Remote chain selector to remove. Must be currently configured. */ + remoteChainSelector: bigint + sender?: string +} + +/** Removes a remote chain config from a TokenPool, version-dispatched. */ +export class DeleteChainConfig extends EVMOperation { + readonly name = 'deleteChainConfig' + + /** Validates the pool address and remote chain selector before any RPC. */ + protected validate(p: DeleteChainConfigParams): void { + if (!p.poolAddress || p.poolAddress.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, 'poolAddress', 'must be non-empty') + } + if (p.remoteChainSelector == null || p.remoteChainSelector === 0n) { + throw new CCTParamsInvalidError(this.name, 'remoteChainSelector', 'must be non-zero') + } + } + + /** Detects the pool version and builds the removal-only `applyChainUpdates` calldata. */ + protected async buildUnsigned( + chain: EVMChain, + p: DeleteChainConfigParams, + ): Promise { + const [, version] = await chain.typeAndVersion(p.poolAddress) + const iface = resolvePoolInterface(version) + + let data: string + if (isLegacyPoolVersion(version)) { + // pre-v1.5: applyChainUpdates(ChainUpdate[]) — mark chain as not allowed + const chains = [ + { + remoteChainSelector: p.remoteChainSelector, + allowed: false, + remotePoolAddress: '0x', + remoteTokenAddress: '0x', + outboundRateLimiterConfig: { isEnabled: false, capacity: 0n, rate: 0n }, + inboundRateLimiterConfig: { isEnabled: false, capacity: 0n, rate: 0n }, + }, + ] + data = iface.encodeFunctionData('applyChainUpdates', [chains]) + } else { + // v1.5.1+ and v2.0: applyChainUpdates(uint64[] removes, ChainUpdate[] adds) + data = iface.encodeFunctionData('applyChainUpdates', [[p.remoteChainSelector], []]) + } + + return { family: ChainFamily.EVM, transactions: [{ to: p.poolAddress, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/pool/operations/remove-remote-pool-addresses.test.ts b/ccip-sdk/src/cct/evm/pool/operations/remove-remote-pool-addresses.test.ts new file mode 100644 index 00000000..1cc29113 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/remove-remote-pool-addresses.test.ts @@ -0,0 +1,153 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, hexlify, zeroPadValue } from 'ethers' + +import { RemoveRemotePoolAddresses } from './remove-remote-pool-addresses.ts' +import TokenPool_1_6_ABI from '../../../../evm/abi/LockReleaseTokenPool_1_6_1.ts' +import TokenPool_2_0_ABI from '../../../../evm/abi/TokenPool_2_0.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { getAddressBytes } from '../../../../utils.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const POOL = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const SELECTOR = 16015286601757825753n +const REMOTE_EVM = '0xd7BF0e3D34B4C4F7d5F3C4C6B2A1e0f9c8b7a6D5' +const REMOTE_EVM_2 = '0xaabbccddeeff00112233445566778899aabbccdd' +// Solana pool address in base58 (non-EVM native format). +const REMOTE_SOL = 'GaM2p1FfyE8YkfB1D3aXHjTr5Z8mQ4wYbNkPcVjRs2A' + +const encodeRemote = (a: string) => zeroPadValue(hexlify(getAddressBytes(a)), 32) + +function stubChain(version: CCIPVersion = CCIPVersion.V1_6): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + typeAndVersion: () => Promise.resolve(['LockReleaseTokenPool', version, '', undefined]), + } as unknown as EVMChain +} + +describe('EVM cct removeRemotePoolAddresses', () => { + const op = new RemoveRemotePoolAddresses() + + it('encodes removeRemotePool — byte-identical to a direct ethers encode (v1.6)', async () => { + const unsigned = await op.generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_EVM], + }) + const expected = new Interface(TokenPool_1_6_ABI).encodeFunctionData('removeRemotePool', [ + SELECTOR, + encodeRemote(REMOTE_EVM), + ]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('emits one byte-identical tx per address (incl. array + non-EVM native format)', async () => { + const addrs = [REMOTE_EVM, REMOTE_EVM_2, REMOTE_SOL] + const unsigned = await op.generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: addrs, + }) + assert.equal(unsigned.transactions.length, addrs.length) + for (let i = 0; i < addrs.length; i++) { + const expected = new Interface(TokenPool_1_6_ABI).encodeFunctionData('removeRemotePool', [ + SELECTOR, + encodeRemote(addrs[i]!), + ]) + assert.equal(unsigned.transactions[i]!.to, POOL) + assert.equal(unsigned.transactions[i]!.data, expected) + } + }) + + it('uses the v2.0 interface for v2.0 pools (identical selector, byte-parity)', async () => { + const unsigned = await op.generate(stubChain(CCIPVersion.V2_0), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_EVM], + }) + const expected = new Interface(TokenPool_2_0_ABI).encodeFunctionData('removeRemotePool', [ + SELECTOR, + encodeRemote(REMOTE_EVM), + ]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('applies sender to from on the first tx', async () => { + const unsigned = await op.generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_EVM], + sender: POOL, + }) + assert.equal(unsigned.transactions[0]!.from, POOL) + }) + + it('rejects an invalid pool address before RPC', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + poolAddress: 'nope', + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_EVM], + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) + + it('rejects a zero remoteChainSelector', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: 0n, + remotePoolAddresses: [REMOTE_EVM], + }), + (e: unknown) => + e instanceof CCTParamsInvalidError && e.context.param === 'remoteChainSelector', + ) + }) + + it('rejects an empty address list', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [], + }), + (e: unknown) => + e instanceof CCTParamsInvalidError && e.context.param === 'remotePoolAddresses', + ) + }) + + it('rejects a blank entry within the address list', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_EVM, ' '], + }), + (e: unknown) => + e instanceof CCTParamsInvalidError && e.context.param === 'remotePoolAddresses[1]', + ) + }) + + it('rejects v1.5 pools (no removeRemotePool)', async () => { + await assert.rejects( + () => + op.generate(stubChain(CCIPVersion.V1_5), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddresses: [REMOTE_EVM], + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/remove-remote-pool-addresses.ts b/ccip-sdk/src/cct/evm/pool/operations/remove-remote-pool-addresses.ts new file mode 100644 index 00000000..444eeb89 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/remove-remote-pool-addresses.ts @@ -0,0 +1,93 @@ +/** + * removeRemotePoolAddresses — removes specific remote pool addresses from an + * already-configured remote chain on a local TokenPool, by encoding + * `removeRemotePool(uint64 remoteChainSelector, bytes remotePoolAddress)` — one + * transaction per address. Unlike deleting the chain config, this preserves the + * config and only removes the listed addresses. Requires a v1.5.1+ pool (v1.5 has + * no `removeRemotePool`; use `applyChainUpdates` to re-initialize instead). + * + * @packageDocumentation + */ + +import { hexlify, zeroPadValue } from 'ethers' + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { getAddressBytes } from '../../../../utils.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Parameters for `removeRemotePoolAddresses`. */ +export type RemoveRemotePoolAddressesParams = { + /** Local TokenPool address (EVM). */ + poolAddress: string + /** Remote chain selector; must already be configured via `applyChainUpdates`. */ + remoteChainSelector: bigint + /** Remote pool addresses to remove, in native format (hex/base58/base64). At least one required. */ + remotePoolAddresses: string[] + sender?: string +} + +/** + * Encodes a remote pool address to raw left-padded 32-byte hex, matching the + * on-chain `bytes` representation. Handles all chain families via + * {@link getAddressBytes} (hex/base58/base64). + */ +function encodeRemoteAddress(address: string): string { + return zeroPadValue(hexlify(getAddressBytes(address)), 32) +} + +/** Removes specific remote pool addresses from an existing chain config on a local TokenPool. */ +export class RemoveRemotePoolAddresses extends EVMOperation { + readonly name = 'removeRemotePoolAddresses' + + /** Validates the pool address, selector, and address list before any RPC. */ + protected validate(p: RemoveRemotePoolAddressesParams): void { + validateAddress(this.name, 'poolAddress', p.poolAddress) + if (p.remoteChainSelector == null || p.remoteChainSelector === 0n) { + throw new CCTParamsInvalidError(this.name, 'remoteChainSelector', 'must be non-zero') + } + if (p.remotePoolAddresses.length === 0) { + throw new CCTParamsInvalidError( + this.name, + 'remotePoolAddresses', + 'must have at least one address', + ) + } + for (let i = 0; i < p.remotePoolAddresses.length; i++) { + const addr = p.remotePoolAddresses[i] + if (!addr || addr.trim().length === 0) { + throw new CCTParamsInvalidError(this.name, `remotePoolAddresses[${i}]`, 'must be non-empty') + } + } + } + + /** Builds one `removeRemotePool` tx per address; rejects v1.5 pools. */ + protected async buildUnsigned( + chain: EVMChain, + p: RemoveRemotePoolAddressesParams, + ): Promise { + const [, version] = await chain.typeAndVersion(p.poolAddress) + if (version <= CCIPVersion.V1_5) { + throw new CCTParamsInvalidError( + this.name, + 'poolAddress', + 'removeRemotePool is not available on v1.5 pools. Use applyChainUpdates to re-initialize the chain config instead.', + ) + } + // removeRemotePool(uint64,bytes) encoding is version-stable across v1.5.1–v2.0. + const iface = version < CCIPVersion.V2_0 ? interfaces.TokenPool_v1_6 : interfaces.TokenPool_v2_0 + const transactions = p.remotePoolAddresses.map((remotePoolAddress) => ({ + to: p.poolAddress, + data: iface.encodeFunctionData('removeRemotePool', [ + p.remoteChainSelector, + encodeRemoteAddress(remotePoolAddress), + ]), + })) + return { family: ChainFamily.EVM, transactions } + } +} diff --git a/ccip-sdk/src/cct/evm/pool/operations/set-allowed-finality-config.test.ts b/ccip-sdk/src/cct/evm/pool/operations/set-allowed-finality-config.test.ts new file mode 100644 index 00000000..26a36dc4 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/set-allowed-finality-config.test.ts @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, toBeHex } from 'ethers' + +import { SetAllowedFinalityConfig } from './set-allowed-finality-config.ts' +import TokenPool_2_0_ABI from '../../../../evm/abi/TokenPool_2_0.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { encodeFinality } from '../../../../extra-args.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const POOL = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' + +const logger = { debug() {}, info() {}, warn() {}, error() {} } + +/** Builds a stub EVMChain that reports `version`. */ +function makeChain(version: CCIPVersion): EVMChain { + return { + logger, + typeAndVersion: () => Promise.resolve(['TokenPool', version, 'x'] as const), + } as unknown as EVMChain +} + +describe('EVM cct setAllowedFinalityConfig', () => { + const op = new SetAllowedFinalityConfig() + + for (const finality of ['finalized', 'safe', 5] as const) { + it(`v2.0 encodes bytes4 finality (${finality}) — byte-identical to direct encode`, async () => { + const unsigned = await op.generate(makeChain(CCIPVersion.V2_0), { + poolAddress: POOL, + finality, + }) + const allowedFinality = toBeHex(encodeFinality(finality), 4) + const expected = new Interface(TokenPool_2_0_ABI).encodeFunctionData( + 'setAllowedFinalityConfig', + [allowedFinality], + ) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + } + + it('applies sender to from', async () => { + const unsigned = await op.generate(makeChain(CCIPVersion.V2_0), { + poolAddress: POOL, + finality: 'finalized', + sender: POOL, + }) + assert.equal(unsigned.transactions[0]!.from, POOL) + }) + + it('rejects pools below v2.0', async () => { + await assert.rejects( + () => op.generate(makeChain(CCIPVersion.V1_6), { poolAddress: POOL, finality: 'finalized' }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) + + it('rejects an out-of-range block-depth finality', async () => { + await assert.rejects( + () => op.generate(makeChain(CCIPVersion.V2_0), { poolAddress: POOL, finality: 70_000 }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'finality', + ) + }) + + it('rejects invalid pool address before RPC', async () => { + await assert.rejects( + () => + op.generate(makeChain(CCIPVersion.V2_0), { poolAddress: 'nope', finality: 'finalized' }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/set-allowed-finality-config.ts b/ccip-sdk/src/cct/evm/pool/operations/set-allowed-finality-config.ts new file mode 100644 index 00000000..b9ba9d24 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/set-allowed-finality-config.ts @@ -0,0 +1,74 @@ +/** + * setAllowedFinalityConfig — sets the bytes4 allowed-finality config on a CCIP + * TokenPool. **EVM v2.0+ pools only.** + * + * The `finality` value is run through the SDK finality codec + * ({@link encodeFinality}) and serialized as a bytes4 for + * `setAllowedFinalityConfig(bytes4)`. Access: pool owner. + * + * @packageDocumentation + */ + +import { toBeHex } from 'ethers' + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { type FinalityAllowed, encodeFinality } from '../../../../extra-args.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Parameters for `setAllowedFinalityConfig`. */ +export type SetAllowedFinalityConfigParams = { + /** Local TokenPool address (must be a v2.0+ pool). */ + poolAddress: string + /** Allowed finality: `'finalized'`, `'safe'`, a block depth, or a {@link FinalityAllowed}. */ + finality: FinalityAllowed | 'finalized' | 'safe' | number + sender?: string +} + +/** Sets the allowed-finality bytes4 config on a v2.0+ TokenPool. */ +export class SetAllowedFinalityConfig extends EVMOperation { + readonly name = 'setAllowedFinalityConfig' + + /** Validates the pool address before any RPC (finality is checked at encode time). */ + protected validate(p: SetAllowedFinalityConfigParams): void { + validateAddress(this.name, 'poolAddress', p.poolAddress) + } + + /** Gates on v2.0+, then encodes the bytes4 allowed-finality value. */ + protected async buildUnsigned( + chain: EVMChain, + p: SetAllowedFinalityConfigParams, + ): Promise { + const [, version] = await chain.typeAndVersion(p.poolAddress) + if (version < CCIPVersion.V2_0) { + throw new CCTParamsInvalidError( + this.name, + 'poolAddress', + `setAllowedFinalityConfig is only available on EVM v2.0+ pools (pool version: ${version})`, + ) + } + + // encodeFinality throws on out-of-range block depth; surface as a params error. + let allowedFinality: string + try { + allowedFinality = toBeHex(encodeFinality(p.finality), 4) + } catch (error) { + throw new CCTParamsInvalidError( + this.name, + 'finality', + error instanceof Error ? error.message : String(error), + ) + } + + const data = interfaces.TokenPool_v2_0.encodeFunctionData('setAllowedFinalityConfig', [ + allowedFinality, + ]) + + return { family: ChainFamily.EVM, transactions: [{ to: p.poolAddress, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/pool/operations/set-chain-rate-limiter-config.fork.test.ts b/ccip-sdk/src/cct/evm/pool/operations/set-chain-rate-limiter-config.fork.test.ts new file mode 100644 index 00000000..0d892ef2 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/set-chain-rate-limiter-config.fork.test.ts @@ -0,0 +1,205 @@ +import assert from 'node:assert/strict' +import { execSync } from 'node:child_process' +import { after, before, describe, it } from 'node:test' + +import { Contract, Interface, JsonRpcProvider, Wallet } from 'ethers' +import { Instance } from 'prool' + +import TokenPool_2_0_ABI from '../../../../evm/abi/TokenPool_2_0.ts' +import { EVMTokenManager } from '../../index.ts' + +// ── Constants ── + +const SEPOLIA_RPC = process.env['RPC_SEPOLIA'] || 'https://ethereum-sepolia-rpc.publicnode.com' +const SEPOLIA_ROUTER = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' +const SEPOLIA_REGISTRY_MODULE = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const ANVIL_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + +// A valid chain selector for testing (Solana devnet) +const REMOTE_CHAIN_SELECTOR = 16423721717087811551n + +// ── Helpers ── + +function isAnvilAvailable(): boolean { + try { + execSync('anvil --version', { stdio: 'ignore' }) + return true + } catch { + return false + } +} + +// ── Tests ── + +const skip = !!process.env.SKIP_INTEGRATION_TESTS || !isAnvilAvailable() + +const testLogger = process.env.VERBOSE + ? console + : { debug() {}, info() {}, warn: console.warn, error: console.error } + +describe('EVMTokenManager setChainRateLimiterConfig Fork Tests', { skip, timeout: 120_000 }, () => { + let provider: JsonRpcProvider + let wallet: Wallet + let mgr: EVMTokenManager + let anvilInstance: ReturnType | undefined + let poolAddress: string + + before(async () => { + // Fork Sepolia so we have a real Router + anvilInstance = Instance.anvil({ + port: 8752, + forkUrl: SEPOLIA_RPC, + forkBlockNumber: undefined, + }) + await anvilInstance.start() + + const anvilUrl = `http://${anvilInstance.host}:${anvilInstance.port}` + provider = new JsonRpcProvider(anvilUrl, undefined, { cacheTimeout: -1 }) + wallet = new Wallet(ANVIL_PRIVATE_KEY, provider) + + mgr = await EVMTokenManager.fromProvider(provider, { logger: testLogger, apiClient: null }) + + // 1. Deploy token + const tokenResult = await mgr.deployToken({ + name: 'Rate Limiter Config Test Token', + symbol: 'RLCT', + decimals: 18, + initialSupply: 1_000_000n * 10n ** 18n, + wallet, + }) + const tokenAddress = tokenResult.tokenAddress + + // 2. Deploy pool (deploys a v2.0 BurnMintTokenPool) + const poolResult = await mgr.deployPool({ + poolType: 'burn-mint', + tokenAddress, + localTokenDecimals: 18, + routerAddress: SEPOLIA_ROUTER, + wallet, + }) + poolAddress = poolResult.poolAddress + + // 3. Propose + accept admin + await mgr.proposeAdminRole({ + tokenAddress, + registryModuleAddress: SEPOLIA_REGISTRY_MODULE, + registrationMethod: 'getCCIPAdmin', + wallet, + }) + + await mgr.acceptAdminRole({ + tokenAddress, + address: SEPOLIA_ROUTER, + wallet, + }) + + // 4. Set pool in TAR + await mgr.setPool({ + tokenAddress, + poolAddress, + address: SEPOLIA_ROUTER, + wallet, + }) + + // 5. Apply chain updates (add a remote chain so we can set rate limits) + await mgr.applyChainUpdates({ + poolAddress, + remoteChainSelectorsToRemove: [], + chainsToAdd: [ + { + remoteChainSelector: REMOTE_CHAIN_SELECTOR, + remotePoolAddresses: ['0xd7BF0d8E6C242b6Dde4490Ab3aFc8C1e811ec9aD'], + remoteTokenAddress: '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888', + outboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + inboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + }, + ], + wallet, + }) + }) + + after(async () => { + provider.destroy() + await anvilInstance?.stop() + }) + + // =========================================================================== + // setChainRateLimiterConfig — Happy Path (v2.0 pool) + // =========================================================================== + + it('should set rate limiter config and verify on-chain', async () => { + const result = await mgr.setChainRateLimiterConfig({ + poolAddress, + chainConfigs: [ + { + remoteChainSelector: REMOTE_CHAIN_SELECTOR, + outboundRateLimiterConfig: { + isEnabled: true, + capacity: '100000000000000000000', + rate: '167000000000000000', + }, + inboundRateLimiterConfig: { + isEnabled: true, + capacity: '200000000000000000000', + rate: '334000000000000000', + }, + }, + ], + wallet, + }) + + assert.ok(result.hash, 'should return tx hash') + assert.match(result.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify on-chain: v2.0 returns both directions from getCurrentRateLimiterState + // (the second arg selects the standard, non-fast-finality bucket). + const pool = new Contract(poolAddress, TokenPool_2_0_ABI, provider) + + const [outbound, inbound] = (await pool.getFunction('getCurrentRateLimiterState')( + REMOTE_CHAIN_SELECTOR, + false, + )) as [ + { isEnabled: boolean; capacity: bigint; rate: bigint }, + { isEnabled: boolean; capacity: bigint; rate: bigint }, + ] + assert.equal(outbound.isEnabled, true, 'outbound should be enabled') + assert.equal(outbound.capacity, 100000000000000000000n, 'outbound capacity should match') + assert.equal(outbound.rate, 167000000000000000n, 'outbound rate should match') + + assert.equal(inbound.isEnabled, true, 'inbound should be enabled') + assert.equal(inbound.capacity, 200000000000000000000n, 'inbound capacity should match') + assert.equal(inbound.rate, 334000000000000000n, 'inbound rate should match') + }) + + // =========================================================================== + // generateUnsignedSetChainRateLimiterConfig — shape verification + // =========================================================================== + + it('should produce unsigned tx with correct shape for v2.0', async () => { + const unsigned = await mgr.generateUnsignedSetChainRateLimiterConfig({ + poolAddress, + chainConfigs: [ + { + remoteChainSelector: REMOTE_CHAIN_SELECTOR, + outboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + inboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + }, + ], + }) + + // v2.0 batches all chain configs into a single setRateLimitConfig tx + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal( + (tx.to as string).toLowerCase(), + poolAddress.toLowerCase(), + 'to should be pool address', + ) + assert.ok(tx.data, 'should have calldata') + + // Verify function selector matches setRateLimitConfig (v2.0) + const iface = new Interface(TokenPool_2_0_ABI) + const selector = iface.getFunction('setRateLimitConfig')!.selector + assert.ok(tx.data.startsWith(selector), 'should use setRateLimitConfig selector for v2.0') + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/set-chain-rate-limiter-config.test.ts b/ccip-sdk/src/cct/evm/pool/operations/set-chain-rate-limiter-config.test.ts new file mode 100644 index 00000000..30d4e77a --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/set-chain-rate-limiter-config.test.ts @@ -0,0 +1,160 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface } from 'ethers' + +import { + type ChainRateLimiterConfig, + SetChainRateLimiterConfig, +} from './set-chain-rate-limiter-config.ts' +import TokenPool_1_6_ABI from '../../../../evm/abi/LockReleaseTokenPool_1_6_1.ts' +import TokenPool_2_0_ABI from '../../../../evm/abi/TokenPool_2_0.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const POOL = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const SELECTOR = 16015286601757825753n + +const CONFIG: ChainRateLimiterConfig = { + remoteChainSelector: SELECTOR, + outboundRateLimiterConfig: { + isEnabled: true, + capacity: '100000000000000000000000', + rate: '167000000000000000000', + }, + inboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, +} + +/** Chain stub whose `typeAndVersion` reports a chosen pool version. */ +function stubChain(version: string): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + typeAndVersion: () => Promise.resolve(['LockReleaseTokenPool', version, `pool ${version}`]), + } as unknown as EVMChain +} + +const bucket = (c: ChainRateLimiterConfig['outboundRateLimiterConfig']) => ({ + isEnabled: c.isEnabled, + capacity: BigInt(c.capacity), + rate: BigInt(c.rate), +}) + +describe('EVM cct setChainRateLimiterConfig', () => { + const op = new SetChainRateLimiterConfig() + + it('v1.6 branch — one setChainRateLimiterConfig tx per chain, byte-identical', async () => { + const cfgB: ChainRateLimiterConfig = { ...CONFIG, remoteChainSelector: 12345n } + const unsigned = await op.generate(stubChain('1.6.1'), { + poolAddress: POOL, + chainConfigs: [CONFIG, cfgB], + }) + const iface = new Interface(TokenPool_1_6_ABI) + const expectedA = iface.encodeFunctionData('setChainRateLimiterConfig', [ + CONFIG.remoteChainSelector, + bucket(CONFIG.outboundRateLimiterConfig), + bucket(CONFIG.inboundRateLimiterConfig), + ]) + const expectedB = iface.encodeFunctionData('setChainRateLimiterConfig', [ + cfgB.remoteChainSelector, + bucket(cfgB.outboundRateLimiterConfig), + bucket(cfgB.inboundRateLimiterConfig), + ]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 2) + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, expectedA) + assert.equal(unsigned.transactions[1]!.data, expectedB) + }) + + it('v2.0 branch — single batched setRateLimitConfig tx, byte-identical', async () => { + const unsigned = await op.generate(stubChain('2.0.0'), { + poolAddress: POOL, + chainConfigs: [{ ...CONFIG, customBlockConfirmations: true }], + }) + const iface = new Interface(TokenPool_2_0_ABI) + const expected = iface.encodeFunctionData('setRateLimitConfig', [ + [ + { + remoteChainSelector: SELECTOR, + customBlockConfirmations: true, + outboundRateLimiterConfig: bucket(CONFIG.outboundRateLimiterConfig), + inboundRateLimiterConfig: bucket(CONFIG.inboundRateLimiterConfig), + }, + ], + ]) + assert.equal(unsigned.transactions.length, 1) + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('v2.0 branch defaults customBlockConfirmations to false', async () => { + const unsigned = await op.generate(stubChain('2.0.0'), { + poolAddress: POOL, + chainConfigs: [CONFIG], + }) + const iface = new Interface(TokenPool_2_0_ABI) + const expected = iface.encodeFunctionData('setRateLimitConfig', [ + [ + { + remoteChainSelector: SELECTOR, + customBlockConfirmations: false, + outboundRateLimiterConfig: bucket(CONFIG.outboundRateLimiterConfig), + inboundRateLimiterConfig: bucket(CONFIG.inboundRateLimiterConfig), + }, + ], + ]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('applies sender to from', async () => { + const unsigned = await op.generate(stubChain('2.0.0'), { + poolAddress: POOL, + chainConfigs: [CONFIG], + sender: POOL, + }) + assert.equal(unsigned.transactions[0]!.from, POOL) + }) + + it('rejects an invalid pool address before RPC', async () => { + await assert.rejects( + () => op.generate(stubChain('2.0.0'), { poolAddress: 'nope', chainConfigs: [CONFIG] }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) + + it('rejects an empty chainConfigs list', async () => { + await assert.rejects( + () => op.generate(stubChain('2.0.0'), { poolAddress: POOL, chainConfigs: [] }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'chainConfigs', + ) + }) + + it('rejects a zero remoteChainSelector', async () => { + await assert.rejects( + () => + op.generate(stubChain('2.0.0'), { + poolAddress: POOL, + chainConfigs: [{ ...CONFIG, remoteChainSelector: 0n }], + }), + (e: unknown) => + e instanceof CCTParamsInvalidError && + e.context.param === 'chainConfigs[0].remoteChainSelector', + ) + }) + + it('rejects a non-integer capacity string', async () => { + await assert.rejects( + () => + op.generate(stubChain('2.0.0'), { + poolAddress: POOL, + chainConfigs: [ + { ...CONFIG, outboundRateLimiterConfig: { isEnabled: true, capacity: 'x', rate: '0' } }, + ], + }), + (e: unknown) => + e instanceof CCTParamsInvalidError && + e.context.param === 'chainConfigs[0].outboundRateLimiterConfig.capacity', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/set-chain-rate-limiter-config.ts b/ccip-sdk/src/cct/evm/pool/operations/set-chain-rate-limiter-config.ts new file mode 100644 index 00000000..10d64757 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/set-chain-rate-limiter-config.ts @@ -0,0 +1,92 @@ +/** + * setChainRateLimiterConfig — updates per-remote-chain rate limiter buckets on a + * token pool. + * + * Encoding is **version-dispatched** (the pool's `typeAndVersion` decides): + * - v1.5 / v1.6: `setChainRateLimiterConfig(selector, outbound, inbound)` — one tx + * per remote chain (encoded via the cached `TokenPool_v1_6` interface; the tuple + * layout is identical to v1.5, so the bytes match either ABI). + * - v2.0+: `setRateLimitConfig(RateLimitConfigArgs[])` — a single batched tx that + * also carries the FTF (`customBlockConfirmations`) flag per entry. + * + * @packageDocumentation + */ + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { EVMOperation } from '../../operation.ts' +import { + type ChainRateLimiterConfig, + type RateLimiterConfig, + validateSetChainRateLimiterConfigParams, +} from '../set-rate-limiter-config-utils.ts' + +export type { ChainRateLimiterConfig, RateLimiterConfig } + +/** Parameters for `setChainRateLimiterConfig`. */ +export type SetChainRateLimiterConfigParams = { + /** Local pool address whose rate limits are being updated. */ + poolAddress: string + /** Rate limiter configurations, one per already-configured remote chain. */ + chainConfigs: ChainRateLimiterConfig[] + sender?: string +} + +/** Encodes a rate limiter bucket into the tuple `TokenPool` expects. */ +function toBucket(config: RateLimiterConfig): { + isEnabled: boolean + capacity: bigint + rate: bigint +} { + return { + isEnabled: config.isEnabled, + capacity: BigInt(config.capacity), + rate: BigInt(config.rate), + } +} + +/** Updates rate limiter buckets on a token pool, dispatching on the pool version. */ +export class SetChainRateLimiterConfig extends EVMOperation { + readonly name = 'setChainRateLimiterConfig' + + /** Validates the pool address, selectors, and rate limiter amounts before any RPC. */ + protected validate(p: SetChainRateLimiterConfigParams): void { + validateSetChainRateLimiterConfigParams(this.name, p.poolAddress, p.chainConfigs) + } + + /** Detects the pool version and builds the matching rate-limiter calldata. */ + protected async buildUnsigned( + chain: EVMChain, + p: SetChainRateLimiterConfigParams, + ): Promise { + const [, version] = await chain.typeAndVersion(p.poolAddress) + + if (version >= CCIPVersion.V2_0) { + // v2.0: single batched setRateLimitConfig(RateLimitConfigArgs[]). + const rateLimitConfigArgs = p.chainConfigs.map((config) => ({ + remoteChainSelector: config.remoteChainSelector, + customBlockConfirmations: config.customBlockConfirmations ?? false, + outboundRateLimiterConfig: toBucket(config.outboundRateLimiterConfig), + inboundRateLimiterConfig: toBucket(config.inboundRateLimiterConfig), + })) + const data = interfaces.TokenPool_v2_0.encodeFunctionData('setRateLimitConfig', [ + rateLimitConfigArgs, + ]) + return { family: ChainFamily.EVM, transactions: [{ to: p.poolAddress, data }] } + } + + // v1.5 / v1.6: one setChainRateLimiterConfig(selector, outbound, inbound) per chain. + const transactions = p.chainConfigs.map((config) => { + const data = interfaces.TokenPool_v1_6.encodeFunctionData('setChainRateLimiterConfig', [ + config.remoteChainSelector, + toBucket(config.outboundRateLimiterConfig), + toBucket(config.inboundRateLimiterConfig), + ]) + return { to: p.poolAddress, data } + }) + return { family: ChainFamily.EVM, transactions } + } +} diff --git a/ccip-sdk/src/cct/evm/pool/operations/set-fee-admin.test.ts b/ccip-sdk/src/cct/evm/pool/operations/set-fee-admin.test.ts new file mode 100644 index 00000000..eb0cc1c2 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/set-fee-admin.test.ts @@ -0,0 +1,75 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { AbiCoder, Interface } from 'ethers' + +import { SetFeeAdmin } from './set-fee-admin.ts' +import TokenPool_2_0_ABI from '../../../../evm/abi/TokenPool_2_0.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const POOL = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const NEW_FEE_ADMIN = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const ROUTER = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' +const RL_ADMIN = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' +const OLD_FEE_ADMIN = '0x1F98431c8aD98523631AE4a59f267346ea31F984' + +const logger = { debug() {}, info() {}, warn() {}, error() {} } + +/** Builds a stub EVMChain that reports `version` and serves getDynamicConfig. */ +function makeChain(version: CCIPVersion): EVMChain { + const dynamicConfig = AbiCoder.defaultAbiCoder().encode( + ['address', 'address', 'address'], + [ROUTER, RL_ADMIN, OLD_FEE_ADMIN], + ) + return { + logger, + typeAndVersion: () => Promise.resolve(['TokenPool', version, 'x'] as const), + provider: { call: () => Promise.resolve(dynamicConfig) }, + } as unknown as EVMChain +} + +describe('EVM cct setFeeAdmin', () => { + const op = new SetFeeAdmin() + + it('v2.0 reads dynamic config and rewrites only feeAdmin — byte-identical to direct encode', async () => { + const unsigned = await op.generate(makeChain(CCIPVersion.V2_0), { + poolAddress: POOL, + feeAdmin: NEW_FEE_ADMIN, + }) + const expected = new Interface(TokenPool_2_0_ABI).encodeFunctionData('setDynamicConfig', [ + ROUTER, + RL_ADMIN, + NEW_FEE_ADMIN, + ]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('applies sender to from', async () => { + const unsigned = await op.generate(makeChain(CCIPVersion.V2_0), { + poolAddress: POOL, + feeAdmin: NEW_FEE_ADMIN, + sender: NEW_FEE_ADMIN, + }) + assert.equal(unsigned.transactions[0]!.from, NEW_FEE_ADMIN) + }) + + it('rejects pools below v2.0', async () => { + await assert.rejects( + () => + op.generate(makeChain(CCIPVersion.V1_6), { poolAddress: POOL, feeAdmin: NEW_FEE_ADMIN }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) + + it('rejects invalid addresses before RPC', async () => { + await assert.rejects( + () => op.generate(makeChain(CCIPVersion.V2_0), { poolAddress: POOL, feeAdmin: 'nope' }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'feeAdmin', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/set-fee-admin.ts b/ccip-sdk/src/cct/evm/pool/operations/set-fee-admin.ts new file mode 100644 index 00000000..b9b6b096 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/set-fee-admin.ts @@ -0,0 +1,68 @@ +/** + * setFeeAdmin — delegates token-transfer fee management to a separate admin on a + * CCIP TokenPool. **EVM v2.0+ pools only.** + * + * Reads the current `getDynamicConfig()` and rewrites only `feeAdmin` via + * `setDynamicConfig(router, rateLimitAdmin, feeAdmin)`, preserving `router` + * and `rateLimitAdmin`. + * + * @packageDocumentation + */ + +import { Contract } from 'ethers' + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Parameters for `setFeeAdmin`. */ +export type SetFeeAdminParams = { + /** Local TokenPool address (must be a v2.0+ pool). */ + poolAddress: string + /** New fee admin address. */ + feeAdmin: string + sender?: string +} + +/** Sets the fee admin on a v2.0+ TokenPool, preserving other dynamic-config fields. */ +export class SetFeeAdmin extends EVMOperation { + readonly name = 'setFeeAdmin' + + /** Validates addresses before any RPC. */ + protected validate(p: SetFeeAdminParams): void { + validateAddress(this.name, 'poolAddress', p.poolAddress) + validateAddress(this.name, 'feeAdmin', p.feeAdmin) + } + + /** Gates on v2.0+, reads dynamic config, and rewrites only `feeAdmin`. */ + protected async buildUnsigned(chain: EVMChain, p: SetFeeAdminParams): Promise { + const [, version] = await chain.typeAndVersion(p.poolAddress) + if (version < CCIPVersion.V2_0) { + throw new CCTParamsInvalidError( + this.name, + 'poolAddress', + `setFeeAdmin is only available on EVM v2.0+ pools (pool version: ${version})`, + ) + } + + // Read current dynamic config, update only feeAdmin. + const contract = new Contract(p.poolAddress, interfaces.TokenPool_v2_0, chain.provider) + const [router, rateLimitAdmin] = (await contract.getFunction('getDynamicConfig')()) as [ + string, + string, + unknown, + ] + const data = interfaces.TokenPool_v2_0.encodeFunctionData('setDynamicConfig', [ + router, + rateLimitAdmin, + p.feeAdmin, + ]) + + return { family: ChainFamily.EVM, transactions: [{ to: p.poolAddress, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/pool/operations/set-rate-limit-admin.fork.test.ts b/ccip-sdk/src/cct/evm/pool/operations/set-rate-limit-admin.fork.test.ts new file mode 100644 index 00000000..4b9ea6c7 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/set-rate-limit-admin.fork.test.ts @@ -0,0 +1,155 @@ +import assert from 'node:assert/strict' +import { execSync } from 'node:child_process' +import { after, before, describe, it } from 'node:test' + +import { Contract, JsonRpcProvider, Wallet } from 'ethers' +import { Instance } from 'prool' + +import TokenPool_2_0_ABI from '../../../../evm/abi/TokenPool_2_0.ts' +import { EVMTokenManager } from '../../index.ts' + +// ── Constants ── + +const SEPOLIA_RPC = process.env['RPC_SEPOLIA'] || 'https://ethereum-sepolia-rpc.publicnode.com' +const SEPOLIA_ROUTER = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' +const ANVIL_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + +// Second anvil account for the new rate limit admin +const NEW_RATE_LIMIT_ADMIN = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' + +// ── Helpers ── + +function isAnvilAvailable(): boolean { + try { + execSync('anvil --version', { stdio: 'ignore' }) + return true + } catch { + return false + } +} + +// ── Tests ── + +const skip = !!process.env.SKIP_INTEGRATION_TESTS || !isAnvilAvailable() + +const testLogger = process.env.VERBOSE + ? console + : { debug() {}, info() {}, warn: console.warn, error: console.error } + +describe('EVMTokenManager setRateLimitAdmin Fork Tests', { skip, timeout: 120_000 }, () => { + let provider: JsonRpcProvider + let wallet: Wallet + let mgr: EVMTokenManager + let anvilInstance: ReturnType | undefined + let poolAddress: string + + before(async () => { + // Fork Sepolia so we have a real Router + anvilInstance = Instance.anvil({ + port: 8753, + forkUrl: SEPOLIA_RPC, + forkBlockNumber: undefined, + }) + await anvilInstance.start() + + const anvilUrl = `http://${anvilInstance.host}:${anvilInstance.port}` + provider = new JsonRpcProvider(anvilUrl, undefined, { cacheTimeout: -1 }) + wallet = new Wallet(ANVIL_PRIVATE_KEY, provider) + + mgr = await EVMTokenManager.fromProvider(provider, { logger: testLogger, apiClient: null }) + + // 1. Deploy token + const tokenResult = await mgr.deployToken({ + name: 'Rate Limit Admin Test Token', + symbol: 'RLAT', + decimals: 18, + initialSupply: 1_000_000n * 10n ** 18n, + wallet, + }) + + // 2. Deploy pool (deploys a v2.0 BurnMintTokenPool) + const poolResult = await mgr.deployPool({ + poolType: 'burn-mint', + tokenAddress: tokenResult.tokenAddress, + localTokenDecimals: 18, + routerAddress: SEPOLIA_ROUTER, + wallet, + }) + poolAddress = poolResult.poolAddress + }) + + after(async () => { + provider.destroy() + await anvilInstance?.stop() + }) + + // =========================================================================== + // setRateLimitAdmin — Happy Path (v2.0 pool) + // =========================================================================== + + it('should set rate limit admin and verify on-chain via getDynamicConfig', async () => { + const result = await mgr.setRateLimitAdmin({ + poolAddress, + rateLimitAdmin: NEW_RATE_LIMIT_ADMIN, + wallet, + }) + + assert.ok(result.hash, 'should return tx hash') + assert.match(result.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify on-chain: v2.0 exposes rateLimitAdmin via getDynamicConfig() + const pool = new Contract(poolAddress, TokenPool_2_0_ABI, provider) + const [, rateLimitAdmin] = (await pool.getFunction('getDynamicConfig')()) as [ + string, + string, + string, + ] + assert.equal( + rateLimitAdmin.toLowerCase(), + NEW_RATE_LIMIT_ADMIN.toLowerCase(), + 'rate limit admin should match', + ) + }) + + it('should update rate limit admin to a different address', async () => { + const anotherAdmin = '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC' // anvil account #2 + await mgr.setRateLimitAdmin({ + poolAddress, + rateLimitAdmin: anotherAdmin, + wallet, + }) + + // Verify on-chain + const pool = new Contract(poolAddress, TokenPool_2_0_ABI, provider) + const [, rateLimitAdmin] = (await pool.getFunction('getDynamicConfig')()) as [ + string, + string, + string, + ] + assert.equal( + rateLimitAdmin.toLowerCase(), + anotherAdmin.toLowerCase(), + 'rate limit admin should be updated', + ) + }) + + // =========================================================================== + // generateUnsignedSetRateLimitAdmin — shape verification + // =========================================================================== + + it('should produce unsigned tx with correct shape', async () => { + const unsigned = await mgr.generateUnsignedSetRateLimitAdmin({ + poolAddress, + rateLimitAdmin: NEW_RATE_LIMIT_ADMIN, + }) + + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal( + (tx.to as string).toLowerCase(), + poolAddress.toLowerCase(), + 'to should be pool address', + ) + assert.ok(tx.data, 'should have calldata') + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/set-rate-limit-admin.test.ts b/ccip-sdk/src/cct/evm/pool/operations/set-rate-limit-admin.test.ts new file mode 100644 index 00000000..af1fa33d --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/set-rate-limit-admin.test.ts @@ -0,0 +1,99 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { AbiCoder, Interface } from 'ethers' + +import { SetRateLimitAdmin } from './set-rate-limit-admin.ts' +import TokenPool_1_5_ABI from '../../../../evm/abi/LockReleaseTokenPool_1_5.ts' +import TokenPool_1_6_ABI from '../../../../evm/abi/LockReleaseTokenPool_1_6_1.ts' +import TokenPool_2_0_ABI from '../../../../evm/abi/TokenPool_2_0.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const POOL = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const NEW_ADMIN = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const ROUTER = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' +const FEE_ADMIN = '0x1F98431c8aD98523631AE4a59f267346ea31F984' +const OLD_RL_ADMIN = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' + +const logger = { debug() {}, info() {}, warn() {}, error() {} } + +/** Builds a stub EVMChain that reports `version` and (v2.0) serves getDynamicConfig. */ +function makeChain(version: CCIPVersion): EVMChain { + const dynamicConfig = AbiCoder.defaultAbiCoder().encode( + ['address', 'address', 'address'], + [ROUTER, OLD_RL_ADMIN, FEE_ADMIN], + ) + return { + logger, + typeAndVersion: () => Promise.resolve(['LockReleaseTokenPool', version, 'x'] as const), + provider: { call: () => Promise.resolve(dynamicConfig) }, + } as unknown as EVMChain +} + +describe('EVM cct setRateLimitAdmin', () => { + const op = new SetRateLimitAdmin() + + it('v1.5 encodes standalone setRateLimitAdmin — byte-identical to a direct ethers encode', async () => { + const unsigned = await op.generate(makeChain(CCIPVersion.V1_5), { + poolAddress: POOL, + rateLimitAdmin: NEW_ADMIN, + }) + const expected = new Interface(TokenPool_1_5_ABI).encodeFunctionData('setRateLimitAdmin', [ + NEW_ADMIN, + ]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('v1.6 encodes standalone setRateLimitAdmin', async () => { + const unsigned = await op.generate(makeChain(CCIPVersion.V1_6), { + poolAddress: POOL, + rateLimitAdmin: NEW_ADMIN, + }) + const expected = new Interface(TokenPool_1_6_ABI).encodeFunctionData('setRateLimitAdmin', [ + NEW_ADMIN, + ]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('v2.0 reads dynamic config and rewrites only rateLimitAdmin', async () => { + const unsigned = await op.generate(makeChain(CCIPVersion.V2_0), { + poolAddress: POOL, + rateLimitAdmin: NEW_ADMIN, + }) + const expected = new Interface(TokenPool_2_0_ABI).encodeFunctionData('setDynamicConfig', [ + ROUTER, + NEW_ADMIN, + FEE_ADMIN, + ]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('applies sender to from', async () => { + const unsigned = await op.generate(makeChain(CCIPVersion.V1_6), { + poolAddress: POOL, + rateLimitAdmin: NEW_ADMIN, + sender: NEW_ADMIN, + }) + assert.equal(unsigned.transactions[0]!.from, NEW_ADMIN) + }) + + it('rejects invalid addresses before RPC', async () => { + await assert.rejects( + () => + op.generate(makeChain(CCIPVersion.V1_6), { + poolAddress: 'nope', + rateLimitAdmin: NEW_ADMIN, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + await assert.rejects( + () => op.generate(makeChain(CCIPVersion.V1_6), { poolAddress: POOL, rateLimitAdmin: 'nope' }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'rateLimitAdmin', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/set-rate-limit-admin.ts b/ccip-sdk/src/cct/evm/pool/operations/set-rate-limit-admin.ts new file mode 100644 index 00000000..d1a763c1 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/set-rate-limit-admin.ts @@ -0,0 +1,72 @@ +/** + * setRateLimitAdmin — updates the rate-limit admin on a CCIP TokenPool. + * + * Version-dispatched: + * - **v1.5 / v1.6**: standalone `setRateLimitAdmin(address)`. + * - **v2.0+**: reads the current `getDynamicConfig()` and rewrites only + * `rateLimitAdmin` via `setDynamicConfig(router, rateLimitAdmin, feeAdmin)`, + * preserving `router` and `feeAdmin`. + * + * @packageDocumentation + */ + +import { Contract } from 'ethers' + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Parameters for `setRateLimitAdmin`. */ +export type SetRateLimitAdminParams = { + /** Local TokenPool address. */ + poolAddress: string + /** New rate-limit admin address. */ + rateLimitAdmin: string + sender?: string +} + +/** Sets the rate-limit admin on a TokenPool, dispatching on the pool version. */ +export class SetRateLimitAdmin extends EVMOperation { + readonly name = 'setRateLimitAdmin' + + /** Validates addresses before any RPC. */ + protected validate(p: SetRateLimitAdminParams): void { + validateAddress(this.name, 'poolAddress', p.poolAddress) + validateAddress(this.name, 'rateLimitAdmin', p.rateLimitAdmin) + } + + /** Detects pool version, then encodes the version-appropriate calldata. */ + protected async buildUnsigned( + chain: EVMChain, + p: SetRateLimitAdminParams, + ): Promise { + const [, version] = await chain.typeAndVersion(p.poolAddress) + + let data: string + if (version >= CCIPVersion.V2_0) { + // v2.0: read current dynamic config, update only rateLimitAdmin. + const contract = new Contract(p.poolAddress, interfaces.TokenPool_v2_0, chain.provider) + const [router, , feeAdmin] = (await contract.getFunction('getDynamicConfig')()) as [ + string, + unknown, + string, + ] + data = interfaces.TokenPool_v2_0.encodeFunctionData('setDynamicConfig', [ + router, + p.rateLimitAdmin, + feeAdmin, + ]) + } else { + // v1.5/v1.6: standalone setRateLimitAdmin(address). + const iface = + version <= CCIPVersion.V1_5 ? interfaces.TokenPool_v1_5 : interfaces.TokenPool_v1_6 + data = iface.encodeFunctionData('setRateLimitAdmin', [p.rateLimitAdmin]) + } + + return { family: ChainFamily.EVM, transactions: [{ to: p.poolAddress, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/pool/operations/set-token-transfer-fee-config.test.ts b/ccip-sdk/src/cct/evm/pool/operations/set-token-transfer-fee-config.test.ts new file mode 100644 index 00000000..0d862745 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/set-token-transfer-fee-config.test.ts @@ -0,0 +1,118 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface } from 'ethers' + +import { + type TokenTransferFeeConfigUpdate, + SetTokenTransferFeeConfig, +} from './set-token-transfer-fee-config.ts' +import TokenPool_2_0_ABI from '../../../../evm/abi/TokenPool_2_0.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const POOL = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const SELECTOR = 14767482510784806043n + +const UPDATE: TokenTransferFeeConfigUpdate = { + remoteChainSelector: SELECTOR, + config: { + destGasOverhead: 90000, + destBytesOverhead: 32, + finalityFeeUSDCents: 10, + fastFinalityFeeUSDCents: 50, + finalityTransferFeeBps: 5, + fastFinalityTransferFeeBps: 25, + isEnabled: true, + }, +} + +function stubChain(version: string): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + typeAndVersion: () => Promise.resolve(['BurnMintTokenPool', version, `pool ${version}`]), + } as unknown as EVMChain +} + +describe('EVM cct setTokenTransferFeeConfig', () => { + const op = new SetTokenTransferFeeConfig() + + it('encodes applyTokenTransferFeeConfigUpdates byte-identical on a v2.0 pool', async () => { + const unsigned = await op.generate(stubChain('2.0.0'), { + poolAddress: POOL, + updates: [UPDATE], + disable: [999n], + }) + const iface = new Interface(TokenPool_2_0_ABI) + const expected = iface.encodeFunctionData('applyTokenTransferFeeConfigUpdates', [ + [ + { + destChainSelector: SELECTOR, + tokenTransferFeeConfig: { + destGasOverhead: 90000, + destBytesOverhead: 32, + finalityFeeUSDCents: 10, + fastFinalityFeeUSDCents: 50, + finalityTransferFeeBps: 5, + fastFinalityTransferFeeBps: 25, + isEnabled: true, + }, + }, + ], + [999n], + ]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('supports disable-only (empty updates)', async () => { + const unsigned = await op.generate(stubChain('2.0.0'), { + poolAddress: POOL, + updates: [], + disable: [SELECTOR], + }) + const iface = new Interface(TokenPool_2_0_ABI) + const expected = iface.encodeFunctionData('applyTokenTransferFeeConfigUpdates', [ + [], + [SELECTOR], + ]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('rejects a pre-v2.0 pool', async () => { + await assert.rejects( + () => op.generate(stubChain('1.6.1'), { poolAddress: POOL, updates: [UPDATE] }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) + + it('rejects an invalid pool address before RPC', async () => { + await assert.rejects( + () => op.generate(stubChain('2.0.0'), { poolAddress: 'nope', updates: [UPDATE] }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) + + it('rejects when neither updates nor disable are provided', async () => { + await assert.rejects( + () => op.generate(stubChain('2.0.0'), { poolAddress: POOL, updates: [] }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'updates', + ) + }) + + it('rejects an out-of-range uint32 field', async () => { + await assert.rejects( + () => + op.generate(stubChain('2.0.0'), { + poolAddress: POOL, + updates: [{ ...UPDATE, config: { ...UPDATE.config, destGasOverhead: 4_294_967_296 } }], + }), + (e: unknown) => + e instanceof CCTParamsInvalidError && + e.context.param === 'updates[0].config.destGasOverhead', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/set-token-transfer-fee-config.ts b/ccip-sdk/src/cct/evm/pool/operations/set-token-transfer-fee-config.ts new file mode 100644 index 00000000..ac6f7419 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/set-token-transfer-fee-config.ts @@ -0,0 +1,132 @@ +/** + * setTokenTransferFeeConfig — sets per-destination token-transfer fee configs on a + * token pool. **EVM v2.0+ pools only.** + * + * Encodes `applyTokenTransferFeeConfigUpdates(args[], disableSelectors[])` in a single + * tx: `updates` become the fee-config args, `disable` becomes the list of destination + * selectors to clear. Version is checked via the pool's `typeAndVersion`; pre-v2.0 + * pools do not expose this function and are rejected before encoding. + * + * @packageDocumentation + */ + +import type { TokenTransferFeeConfig } from '../../../../chain.ts' +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +export type { TokenTransferFeeConfig } + +/** Pairs a destination chain selector with its full token-transfer fee config. */ +export type TokenTransferFeeConfigUpdate = { + /** Destination chain selector (uint64). */ + remoteChainSelector: bigint + /** Full token transfer fee config for this destination. */ + config: TokenTransferFeeConfig +} + +/** Parameters for `setTokenTransferFeeConfig`. EVM v2.0+ pools only. */ +export type SetTokenTransferFeeConfigParams = { + /** Local pool address. */ + poolAddress: string + /** Per-destination fee config updates (may be empty if only disabling). */ + updates: TokenTransferFeeConfigUpdate[] + /** Destination chain selectors whose fee config should be cleared. */ + disable?: bigint[] + sender?: string +} + +/** uint32 / uint16 upper bounds for fee-config field validation. */ +const UINT32_MAX = 4_294_967_295 +const UINT16_MAX = 65_535 + +/** Sets per-destination token-transfer fee configs on a v2.0+ token pool. */ +export class SetTokenTransferFeeConfig extends EVMOperation { + readonly name = 'setTokenTransferFeeConfig' + + /** Validates the pool address and every fee-config field before any RPC. */ + protected validate(p: SetTokenTransferFeeConfigParams): void { + validateAddress(this.name, 'poolAddress', p.poolAddress) + const disableCount = p.disable?.length ?? 0 + if (p.updates.length === 0 && disableCount === 0) { + throw new CCTParamsInvalidError( + this.name, + 'updates', + 'provide at least one update or one disable selector', + ) + } + const checkUint = (value: number, max: number, path: string) => { + if (!Number.isInteger(value) || value < 0 || value > max) { + throw new CCTParamsInvalidError(this.name, path, `must be an integer between 0 and ${max}`) + } + } + for (let i = 0; i < p.updates.length; i++) { + const { remoteChainSelector, config } = p.updates[i]! + if (remoteChainSelector === 0n) { + throw new CCTParamsInvalidError( + this.name, + `updates[${i}].remoteChainSelector`, + 'must be non-zero', + ) + } + checkUint(config.destGasOverhead, UINT32_MAX, `updates[${i}].config.destGasOverhead`) + checkUint(config.destBytesOverhead, UINT32_MAX, `updates[${i}].config.destBytesOverhead`) + checkUint(config.finalityFeeUSDCents, UINT32_MAX, `updates[${i}].config.finalityFeeUSDCents`) + checkUint( + config.fastFinalityFeeUSDCents, + UINT32_MAX, + `updates[${i}].config.fastFinalityFeeUSDCents`, + ) + checkUint( + config.finalityTransferFeeBps, + UINT16_MAX, + `updates[${i}].config.finalityTransferFeeBps`, + ) + checkUint( + config.fastFinalityTransferFeeBps, + UINT16_MAX, + `updates[${i}].config.fastFinalityTransferFeeBps`, + ) + } + } + + /** Asserts the pool is v2.0+ and encodes `applyTokenTransferFeeConfigUpdates`. */ + protected async buildUnsigned( + chain: EVMChain, + p: SetTokenTransferFeeConfigParams, + ): Promise { + const [, version] = await chain.typeAndVersion(p.poolAddress) + if (version < CCIPVersion.V2_0) { + throw new CCTParamsInvalidError( + this.name, + 'poolAddress', + `setTokenTransferFeeConfig is only available on EVM v2.0+ pools (pool version: ${version})`, + ) + } + + const feeConfigArgs = p.updates.map((u) => ({ + destChainSelector: u.remoteChainSelector, + tokenTransferFeeConfig: { + destGasOverhead: u.config.destGasOverhead, + destBytesOverhead: u.config.destBytesOverhead, + finalityFeeUSDCents: u.config.finalityFeeUSDCents, + fastFinalityFeeUSDCents: u.config.fastFinalityFeeUSDCents, + finalityTransferFeeBps: u.config.finalityTransferFeeBps, + fastFinalityTransferFeeBps: u.config.fastFinalityTransferFeeBps, + isEnabled: u.config.isEnabled, + }, + })) + const disableSelectors = (p.disable ?? []).map((s) => BigInt(s)) + + const data = interfaces.TokenPool_v2_0.encodeFunctionData( + 'applyTokenTransferFeeConfigUpdates', + [feeConfigArgs, disableSelectors], + ) + return { family: ChainFamily.EVM, transactions: [{ to: p.poolAddress, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/pool/operations/transfer-ownership.fork.test.ts b/ccip-sdk/src/cct/evm/pool/operations/transfer-ownership.fork.test.ts new file mode 100644 index 00000000..dcbafe59 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/transfer-ownership.fork.test.ts @@ -0,0 +1,174 @@ +import assert from 'node:assert/strict' +import { execSync } from 'node:child_process' +import { after, before, describe, it } from 'node:test' + +import { Contract, JsonRpcProvider, Wallet } from 'ethers' +import { Instance } from 'prool' + +import { EVMTokenManager } from '../../index.ts' + +// ── Constants ── + +const SEPOLIA_RPC = process.env['RPC_SEPOLIA'] || 'https://ethereum-sepolia-rpc.publicnode.com' +const SEPOLIA_ROUTER = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' +const SEPOLIA_REGISTRY_MODULE = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const ANVIL_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' +// Second Anvil default account +const BURNER_PRIVATE_KEY = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d' + +// ── Helpers ── + +function isAnvilAvailable(): boolean { + try { + execSync('anvil --version', { stdio: 'ignore' }) + return true + } catch { + return false + } +} + +// Minimal ABI for reading pool owner +const OWNABLE_ABI = [ + { + inputs: [], + name: 'owner', + outputs: [{ name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, +] as const + +// ── Tests ── + +const skip = !!process.env.SKIP_INTEGRATION_TESTS || !isAnvilAvailable() + +const testLogger = process.env.VERBOSE + ? console + : { debug() {}, info() {}, warn: console.warn, error: console.error } + +describe('EVMTokenManager transferOwnership Fork Tests', { skip, timeout: 120_000 }, () => { + let provider: JsonRpcProvider + let ownerWallet: Wallet + let burnerWallet: Wallet + let mgr: EVMTokenManager + let anvilInstance: ReturnType | undefined + let poolAddress: string + + before(async () => { + anvilInstance = Instance.anvil({ + port: 8754, + forkUrl: SEPOLIA_RPC, + forkBlockNumber: undefined, + }) + await anvilInstance.start() + + const anvilUrl = `http://${anvilInstance.host}:${anvilInstance.port}` + provider = new JsonRpcProvider(anvilUrl, undefined, { cacheTimeout: -1 }) + ownerWallet = new Wallet(ANVIL_PRIVATE_KEY, provider) + burnerWallet = new Wallet(BURNER_PRIVATE_KEY, provider) + + mgr = await EVMTokenManager.fromProvider(provider, { logger: testLogger, apiClient: null }) + + // 1. Deploy token + const tokenResult = await mgr.deployToken({ + name: 'Ownership Test Token', + symbol: 'OTT', + decimals: 18, + initialSupply: 1_000_000n * 10n ** 18n, + wallet: ownerWallet, + }) + + // 2. Deploy pool + const poolResult = await mgr.deployPool({ + poolType: 'burn-mint', + tokenAddress: tokenResult.tokenAddress, + localTokenDecimals: 18, + routerAddress: SEPOLIA_ROUTER, + wallet: ownerWallet, + }) + poolAddress = poolResult.poolAddress + + // 3. Propose + accept admin (needed to register pool) + await mgr.proposeAdminRole({ + tokenAddress: tokenResult.tokenAddress, + registryModuleAddress: SEPOLIA_REGISTRY_MODULE, + registrationMethod: 'getCCIPAdmin', + wallet: ownerWallet, + }) + + await mgr.acceptAdminRole({ + tokenAddress: tokenResult.tokenAddress, + address: SEPOLIA_ROUTER, + wallet: ownerWallet, + }) + + // 4. Set pool + await mgr.setPool({ + tokenAddress: tokenResult.tokenAddress, + poolAddress, + address: SEPOLIA_ROUTER, + wallet: ownerWallet, + }) + }) + + after(async () => { + provider.destroy() + await anvilInstance?.stop() + }) + + // =========================================================================== + // Verify initial owner + // =========================================================================== + + it('should have owner as initial pool owner', async () => { + const pool = new Contract(poolAddress, OWNABLE_ABI, provider) + const currentOwner = (await pool.getFunction('owner')()) as string + assert.equal( + currentOwner.toLowerCase(), + ownerWallet.address.toLowerCase(), + 'initial pool owner should be deployer', + ) + }) + + // =========================================================================== + // transferOwnership + acceptOwnership round-trip + // =========================================================================== + + it('should transfer ownership to burner wallet and accept', async () => { + // Transfer ownership (propose burner as new owner) + const transferResult = await mgr.transferOwnership({ + poolAddress, + newOwner: burnerWallet.address, + wallet: ownerWallet, + }) + + assert.ok(transferResult.hash, 'should return tx hash') + assert.match(transferResult.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Owner should still be the original owner (pending transfer) + const pool = new Contract(poolAddress, OWNABLE_ABI, provider) + const ownerAfterProposal = (await pool.getFunction('owner')()) as string + assert.equal( + ownerAfterProposal.toLowerCase(), + ownerWallet.address.toLowerCase(), + 'owner should not change until acceptance', + ) + + // Accept ownership from burner wallet + const acceptResult = await mgr.acceptOwnership({ + poolAddress, + wallet: burnerWallet, + }) + + assert.ok(acceptResult.hash, 'should return tx hash') + assert.match(acceptResult.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify new owner + const newOwner = (await pool.getFunction('owner')()) as string + assert.equal( + newOwner.toLowerCase(), + burnerWallet.address.toLowerCase(), + 'pool owner should be burner wallet after acceptance', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/transfer-ownership.test.ts b/ccip-sdk/src/cct/evm/pool/operations/transfer-ownership.test.ts new file mode 100644 index 00000000..5963b34e --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/transfer-ownership.test.ts @@ -0,0 +1,53 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface } from 'ethers' + +import { TransferOwnership } from './transfer-ownership.ts' +import TokenPool_1_6_ABI from '../../../../evm/abi/LockReleaseTokenPool_1_6_1.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const POOL = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const NEW_OWNER = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const stubChain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, +} as unknown as EVMChain + +describe('EVM cct transferOwnership', () => { + const op = new TransferOwnership() + + it('encodes transferOwnership(newOwner) byte-identical to a direct ethers encode', async () => { + const unsigned = await op.generate(stubChain, { poolAddress: POOL, newOwner: NEW_OWNER }) + const expected = new Interface(TokenPool_1_6_ABI).encodeFunctionData('transferOwnership', [ + NEW_OWNER, + ]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('applies sender to from', async () => { + const unsigned = await op.generate(stubChain, { + poolAddress: POOL, + newOwner: NEW_OWNER, + sender: NEW_OWNER, + }) + assert.equal(unsigned.transactions[0]!.from, NEW_OWNER) + }) + + it('rejects an invalid newOwner before RPC', async () => { + await assert.rejects( + () => op.generate(stubChain, { poolAddress: POOL, newOwner: 'nope' }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'newOwner', + ) + }) + + it('rejects an invalid poolAddress before RPC', async () => { + await assert.rejects( + () => op.generate(stubChain, { poolAddress: 'nope', newOwner: NEW_OWNER }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/pool/operations/transfer-ownership.ts b/ccip-sdk/src/cct/evm/pool/operations/transfer-ownership.ts new file mode 100644 index 00000000..c106b4ed --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/operations/transfer-ownership.ts @@ -0,0 +1,44 @@ +/** + * transferOwnership — proposes a new owner for a token pool (step 1 of the + * OpenZeppelin `Ownable2Step` handshake; the proposee later calls `acceptOwnership`). + * + * Version-independent: `transferOwnership(address)` is inherited unchanged across + * v1.5 / v1.6 / v2.0, so the calldata (and selector) is identical for every pool + * version — no `typeAndVersion` lookup is needed. Encoded via the cached + * `TokenPool_v1_6` interface. + * + * @packageDocumentation + */ + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Parameters for `transferOwnership`. */ +export type TransferOwnershipParams = { + /** Pool address whose ownership is being transferred. */ + poolAddress: string + /** New owner address to propose. */ + newOwner: string + sender?: string +} + +/** Proposes a new pool owner via `Ownable2Step.transferOwnership`. */ +export class TransferOwnership extends EVMOperation { + readonly name = 'transferOwnership' + + /** Validates the pool and new-owner addresses before any RPC. */ + protected validate(p: TransferOwnershipParams): void { + validateAddress(this.name, 'poolAddress', p.poolAddress) + validateAddress(this.name, 'newOwner', p.newOwner) + } + + /** Builds `transferOwnership(newOwner)` calldata (version-stable across v1.5–v2.0). */ + protected buildUnsigned(_chain: EVMChain, p: TransferOwnershipParams): UnsignedEVMTx { + const data = interfaces.TokenPool_v1_6.encodeFunctionData('transferOwnership', [p.newOwner]) + return { family: ChainFamily.EVM, transactions: [{ to: p.poolAddress, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/pool/set-rate-limiter-config-utils.ts b/ccip-sdk/src/cct/evm/pool/set-rate-limiter-config-utils.ts new file mode 100644 index 00000000..bdc049e0 --- /dev/null +++ b/ccip-sdk/src/cct/evm/pool/set-rate-limiter-config-utils.ts @@ -0,0 +1,80 @@ +/** + * Shared validation for the EVM `setChainRateLimiterConfig` pool op. + * Kept separate from the operation class so the (non-trivial) rate-limiter + * parsing rules live in one place and stay easy to test. + * + * @packageDocumentation + */ + +import type { RateLimiterConfig } from './apply-chain-updates-utils.ts' +import { CCTParamsInvalidError } from '../../errors.ts' +import { validateAddress } from '../validate.ts' + +export type { RateLimiterConfig } + +/** Rate limiter configuration for one already-configured remote chain. */ +export type ChainRateLimiterConfig = { + /** Remote chain selector (uint64). */ + remoteChainSelector: bigint + /** Outbound rate limiter (local → remote). */ + outboundRateLimiterConfig: RateLimiterConfig + /** Inbound rate limiter (remote → local). */ + inboundRateLimiterConfig: RateLimiterConfig + /** + * Sets the FTF (Faster-Than-Finality) bucket instead of the default one. + * Only applies to EVM v2.0+ pools; ignored on v1.5/v1.6. + */ + customBlockConfirmations?: boolean +} + +/** + * Parses a decimal bigint string, asserting it is present and non-negative. + * @throws {@link CCTParamsInvalidError} if the value is empty, unparseable, or negative + */ +function validateAmount(operation: string, param: string, value: string): void { + if (typeof value !== 'string' || value.trim().length === 0) { + throw new CCTParamsInvalidError(operation, param, 'must be a non-empty integer string') + } + let parsed: bigint + try { + parsed = BigInt(value) + } catch { + throw new CCTParamsInvalidError( + operation, + param, + `must be a valid integer string, got ${value}`, + ) + } + if (parsed < 0n) throw new CCTParamsInvalidError(operation, param, 'must be non-negative') +} + +/** + * Validates every field of `setChainRateLimiterConfig` params before any RPC. + * @throws {@link CCTParamsInvalidError} if pool address, selectors, or amounts are invalid + */ +export function validateSetChainRateLimiterConfigParams( + operation: string, + poolAddress: string, + chainConfigs: readonly ChainRateLimiterConfig[], +): void { + validateAddress(operation, 'poolAddress', poolAddress) + if (chainConfigs.length === 0) { + throw new CCTParamsInvalidError(operation, 'chainConfigs', 'must have at least one entry') + } + for (let i = 0; i < chainConfigs.length; i++) { + const config = chainConfigs[i]! + if (config.remoteChainSelector == null || config.remoteChainSelector === 0n) { + throw new CCTParamsInvalidError( + operation, + `chainConfigs[${i}].remoteChainSelector`, + 'must be non-zero', + ) + } + const out = config.outboundRateLimiterConfig + const inb = config.inboundRateLimiterConfig + validateAmount(operation, `chainConfigs[${i}].outboundRateLimiterConfig.capacity`, out.capacity) + validateAmount(operation, `chainConfigs[${i}].outboundRateLimiterConfig.rate`, out.rate) + validateAmount(operation, `chainConfigs[${i}].inboundRateLimiterConfig.capacity`, inb.capacity) + validateAmount(operation, `chainConfigs[${i}].inboundRateLimiterConfig.rate`, inb.rate) + } +} diff --git a/ccip-sdk/src/cct/evm/submit.test.ts b/ccip-sdk/src/cct/evm/submit.test.ts new file mode 100644 index 00000000..bc95d9d4 --- /dev/null +++ b/ccip-sdk/src/cct/evm/submit.test.ts @@ -0,0 +1,141 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { makeError } from 'ethers' + +import { submit } from './submit.ts' +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../errors/index.ts' +import type { EVMChain } from '../../evm/index.ts' +import type { UnsignedEVMTx } from '../../evm/types.ts' +import { ChainFamily } from '../../networks.ts' +import { CCTTxFailedError, CCTTxNotConfirmedError } from '../errors.ts' + +const TAR = '0x' + '44'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +const UNSIGNED: UnsignedEVMTx = { + family: ChainFamily.EVM, + transactions: [{ to: TAR, data: '0x1234' }], +} + +function stubChain(): EVMChain { + return { + provider: {} as never, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + nextNonce: async () => 0, + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +/** + * Fake ethers Signer. `wait` resolves to `receipt` (or rejects with `waitError`); + * `submitError` makes both send and sign paths reject (pre-broadcast failure). + */ +function fakeSigner(opts: { + receipt?: { status: number } | null + waitError?: Error + submitError?: Error +}) { + const fail = opts.submitError + return { + signTransaction: () => (fail ? Promise.reject(fail) : Promise.resolve('0x')), + getAddress: () => Promise.resolve('0x' + '55'.repeat(20)), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: (_tx: unknown) => + fail + ? Promise.reject(fail) + : Promise.resolve({ + hash: HASH, + wait: (_c?: number, _t?: number) => + opts.waitError + ? Promise.reject(opts.waitError) + : Promise.resolve(opts.receipt ?? null), + }), + } +} + +describe('submit (shared CCT submit pipeline)', () => { + it('returns the hash on a successful receipt', async () => { + const result = await submit( + stubChain(), + fakeSigner({ receipt: { status: 1 } }), + UNSIGNED, + 'setPool', + ) + assert.deepEqual(result, { hash: HASH }) + }) + + it('throws CCIPExecTxRevertedError (non-transient) when wait() throws CALL_EXCEPTION', async () => { + await assert.rejects( + () => + submit( + stubChain(), + fakeSigner({ waitError: makeError('execution reverted', 'CALL_EXCEPTION') }), + UNSIGNED, + 'setPool', + ), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && + err.context.operation === 'setPool' && + err.context.txHash === HASH && + !err.isTransient && + err.message.includes('reverted'), + ) + }) + + it('throws CCTTxNotConfirmedError (transient) when wait() throws TRANSACTION_REPLACED', async () => { + await assert.rejects( + () => + submit( + stubChain(), + fakeSigner({ waitError: makeError('transaction replaced', 'TRANSACTION_REPLACED') }), + UNSIGNED, + 'setPool', + ), + (err: unknown) => + err instanceof CCTTxNotConfirmedError && err.context.txHash === HASH && err.isTransient, + ) + }) + + it('throws CCTTxNotConfirmedError (transient, keeps hash) when no receipt arrives', async () => { + await assert.rejects( + () => submit(stubChain(), fakeSigner({ receipt: null }), UNSIGNED, 'setPool'), + (err: unknown) => + err instanceof CCTTxNotConfirmedError && err.context.txHash === HASH && err.isTransient, + ) + }) + + it('throws CCTTxNotConfirmedError (transient, keeps hash) on confirmation timeout', async () => { + await assert.rejects( + () => + submit( + stubChain(), + fakeSigner({ waitError: makeError('timed out', 'TIMEOUT') }), + UNSIGNED, + 'setPool', + ), + (err: unknown) => + err instanceof CCTTxNotConfirmedError && err.context.txHash === HASH && err.isTransient, + ) + }) + + it('throws a transient CCTTxFailedError when submission fails with a network error', async () => { + await assert.rejects( + () => + submit( + stubChain(), + fakeSigner({ submitError: makeError('network down', 'NETWORK_ERROR') }), + UNSIGNED, + 'setPool', + ), + (err: unknown) => err instanceof CCTTxFailedError && err.isTransient, + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => submit(stubChain(), {}, UNSIGNED, 'setPool'), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/submit.ts b/ccip-sdk/src/cct/evm/submit.ts new file mode 100644 index 00000000..e9aaabae --- /dev/null +++ b/ccip-sdk/src/cct/evm/submit.ts @@ -0,0 +1,129 @@ +/** + * Shared sign-and-submit pipeline for EVM CCT operations. Maps broadcast and + * confirmation failures to {@link CCTTxFailedError} / {@link CCTTxNotConfirmedError}, + * and on-chain reverts to {@link CCIPExecTxRevertedError}. + * + * @packageDocumentation + */ + +import { + type Signer, + type TransactionReceipt, + type TransactionRequest, + type TransactionResponse, + isError, +} from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../errors/index.ts' +import { type EVMChain, isSigner, submitTransaction } from '../../evm/index.ts' +import type { UnsignedEVMTx } from '../../evm/types.ts' +import { CCTTxFailedError, CCTTxNotConfirmedError } from '../errors.ts' +import type { TransactionHash } from '../operation.ts' + +/** Max ms to wait for one confirmation before throwing {@link CCTTxNotConfirmedError}. */ +const CONFIRM_TIMEOUT_MS = 60_000 + +/** True for ethers infra errors worth retrying (not an on-chain revert). */ +function isTransientError(error: unknown): boolean { + return ( + isError(error, 'TIMEOUT') || isError(error, 'NETWORK_ERROR') || isError(error, 'SERVER_ERROR') + ) +} + +/** + * Signs and submits every transaction in `unsigned` sequentially, waiting for one + * confirmation each, and returns the hash of the last confirmed transaction. + * Most ops are single-tx; some (e.g. append/removeRemotePoolAddresses, per-chain + * setChainRateLimiterConfig on v1.6) emit one tx per item. `operation` labels logs + * and error context. A failure mid-sequence throws, leaving already-mined txs applied. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCIPExecTxRevertedError} if a tx reverts on-chain + * @throws {@link CCTTxNotConfirmedError} if broadcast but not confirmed in time + */ +export async function submit( + chain: EVMChain, + wallet: unknown, + unsigned: UnsignedEVMTx, + operation: string, +): Promise { + const { hash } = await submitForReceipt(chain, wallet, unsigned, operation) + return { hash } +} + +/** + * Like {@link submit}, but also returns the last transaction's mined receipt — + * used by deploy ops that need `receipt.contractAddress`. Same sequential + * multi-tx semantics and error mapping. + */ +export async function submitForReceipt( + chain: EVMChain, + wallet: unknown, + unsigned: UnsignedEVMTx, + operation: string, +): Promise { + if (!isSigner(wallet)) throw new CCIPWalletInvalidError(wallet) + const sender = await wallet.getAddress() + + let last: (TransactionHash & { receipt: TransactionReceipt }) | undefined + const total = unsigned.transactions.length + for (let i = 0; i < total; i++) { + const label = total > 1 ? `${operation} [${i + 1}/${total}]` : operation + last = await submitOne(chain, wallet, sender, unsigned.transactions[i]!, label) + } + if (!last) throw new CCTTxFailedError(operation, 'no transactions to submit') + return last +} + +/** Signs, submits, and confirms one transaction; shared by every op. */ +async function submitOne( + chain: EVMChain, + wallet: Signer, + sender: string, + txRequest: TransactionRequest, + operation: string, +): Promise { + chain.logger.debug(`${operation}: submitting...`) + + let response: TransactionResponse + let nonceConsumed = false + try { + let tx: TransactionRequest = { ...txRequest } + tx.from = undefined // drop any builder-set sender before populate, else ethers throws on a from/signer mismatch + if (tx.nonce == null) { + tx.nonce = await chain.nextNonce(sender) + nonceConsumed = true + } + tx = await wallet.populateTransaction(tx) + tx.from = undefined // some signers reject a pre-populated `from` + response = await submitTransaction(wallet, tx, chain.provider) + } catch (error) { + if (nonceConsumed) chain.rollbackNonce(sender) + throw new CCTTxFailedError(operation, error instanceof Error ? error.message : String(error), { + cause: error instanceof Error ? error : undefined, + isTransient: isTransientError(error), + }) + } + + chain.logger.debug(`${operation}: waiting for confirmation, tx =`, response.hash) + + let receipt + try { + receipt = await response.wait(1, CONFIRM_TIMEOUT_MS) + } catch (error) { + if (isError(error, 'CALL_EXCEPTION')) { + // mined revert — permanent; reuse the core revert error so consumers catch + // one type across core `execute` and CCT ops. + throw new CCIPExecTxRevertedError(response.hash, { cause: error, context: { operation } }) + } + // broadcast already succeeded; any non-revert error leaves the tx in an unknown state + throw new CCTTxNotConfirmedError(operation, response.hash, { + cause: error instanceof Error ? error : undefined, + }) + } + + if (!receipt) throw new CCTTxNotConfirmedError(operation, response.hash) + + chain.logger.info(`${operation}: confirmed, tx =`, response.hash) + return { hash: response.hash, receipt } +} diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin-role.fork.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin-role.fork.test.ts new file mode 100644 index 00000000..673ae1ea --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin-role.fork.test.ts @@ -0,0 +1,283 @@ +import assert from 'node:assert/strict' +import { execSync } from 'node:child_process' +import { after, before, describe, it } from 'node:test' + +import { Contract, JsonRpcProvider, Wallet, ZeroAddress } from 'ethers' +import { Instance } from 'prool' + +import { EVMTokenManager } from '../../index.ts' + +// ── Constants ── + +const SEPOLIA_RPC = process.env['RPC_SEPOLIA'] || 'https://ethereum-sepolia-rpc.publicnode.com' +const SEPOLIA_ROUTER = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' +const SEPOLIA_REGISTRY_MODULE = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const ANVIL_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + +// ── Helpers ── + +function isAnvilAvailable(): boolean { + try { + execSync('anvil --version', { stdio: 'ignore' }) + return true + } catch { + return false + } +} + +// Minimal ABI +const TAR_ABI = [ + { + inputs: [{ name: 'token', type: 'address' }], + name: 'getTokenConfig', + outputs: [ + { + type: 'tuple', + components: [ + { name: 'administrator', type: 'address' }, + { name: 'pendingAdministrator', type: 'address' }, + { name: 'tokenPool', type: 'address' }, + ], + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const + +// ── Tests ── + +const skip = !!process.env.SKIP_INTEGRATION_TESTS || !isAnvilAvailable() + +const testLogger = process.env.VERBOSE + ? console + : { debug() {}, info() {}, warn: console.warn, error: console.error } + +describe('EVMTokenManager acceptAdminRole Fork Tests', { skip, timeout: 120_000 }, () => { + let provider: JsonRpcProvider + let wallet: Wallet + let mgr: EVMTokenManager + let anvilInstance: ReturnType | undefined + let tokenAddress: string + let walletAddress: string + let tarAddress: string + + before(async () => { + // Fork Sepolia so we have a real Router with offRamps + anvilInstance = Instance.anvil({ + port: 8750, + forkUrl: SEPOLIA_RPC, + forkBlockNumber: undefined, // latest + }) + await anvilInstance.start() + + const anvilUrl = `http://${anvilInstance.host}:${anvilInstance.port}` + provider = new JsonRpcProvider(anvilUrl, undefined, { cacheTimeout: -1 }) + wallet = new Wallet(ANVIL_PRIVATE_KEY, provider) + walletAddress = await wallet.getAddress() + + mgr = await EVMTokenManager.fromProvider(provider, { logger: testLogger, apiClient: null }) + + // Deploy a token + const tokenResult = await mgr.deployToken({ + name: 'Accept Admin Test Token', + symbol: 'AATT', + decimals: 18, + initialSupply: 1_000_000n * 10n ** 18n, + wallet, + }) + tokenAddress = tokenResult.tokenAddress + + // Discover TAR + tarAddress = await mgr.chain.getTokenAdminRegistryFor(SEPOLIA_ROUTER) + + // Propose admin first (required before accept) + await mgr.proposeAdminRole({ + tokenAddress, + registryModuleAddress: SEPOLIA_REGISTRY_MODULE, + registrationMethod: 'getCCIPAdmin', + wallet, + }) + }) + + after(async () => { + provider.destroy() + await anvilInstance?.stop() + }) + + // =========================================================================== + // Verify pending administrator is set after propose + // =========================================================================== + + it('should have pending administrator set after propose', async () => { + const tar = new Contract(tarAddress, TAR_ABI, provider) + const config = await tar.getFunction('getTokenConfig')(tokenAddress) + + assert.equal( + (config.pendingAdministrator as string).toLowerCase(), + walletAddress.toLowerCase(), + 'pendingAdministrator should match wallet address', + ) + }) + + // =========================================================================== + // acceptAdminRole — Happy Path + // =========================================================================== + + it('should accept admin role and verify on-chain', async () => { + const result = await mgr.acceptAdminRole({ + tokenAddress, + address: SEPOLIA_ROUTER, + wallet, + }) + + assert.ok(result.hash, 'should return tx hash') + assert.match(result.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify on-chain: administrator should be set, pendingAdministrator cleared + const tar = new Contract(tarAddress, TAR_ABI, provider) + const config = await tar.getFunction('getTokenConfig')(tokenAddress) + + assert.equal( + (config.administrator as string).toLowerCase(), + walletAddress.toLowerCase(), + 'administrator should match wallet address', + ) + assert.equal( + (config.pendingAdministrator as string).toLowerCase(), + ZeroAddress.toLowerCase(), + 'pendingAdministrator should be cleared', + ) + }) + + // =========================================================================== + // generateUnsignedAcceptAdminRole — structure verification + // =========================================================================== + + it('should produce unsigned tx with correct shape', async () => { + // Deploy + propose another token for this test + const tokenResult = await mgr.deployToken({ + name: 'Unsigned Accept Test', + symbol: 'UAT', + decimals: 18, + wallet, + }) + + await mgr.proposeAdminRole({ + tokenAddress: tokenResult.tokenAddress, + registryModuleAddress: SEPOLIA_REGISTRY_MODULE, + registrationMethod: 'getCCIPAdmin', + wallet, + }) + + const unsigned = await mgr.generateUnsignedAcceptAdminRole({ + tokenAddress: tokenResult.tokenAddress, + address: SEPOLIA_ROUTER, + }) + + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.ok(tx.to, 'should have a to address (TAR contract)') + assert.equal( + (tx.to as string).toLowerCase(), + tarAddress.toLowerCase(), + 'to should be TAR address', + ) + assert.ok(tx.data, 'should have calldata') + }) + + // =========================================================================== + // transferAdminRole — Round-trip + // =========================================================================== + + it('should transfer admin to second wallet and verify on-chain', async () => { + // Second anvil account + const wallet2 = new Wallet( + '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d', + provider, + ) + const wallet2Address = await wallet2.getAddress() + + // Transfer admin from wallet → wallet2 + const transferResult = await mgr.transferAdminRole({ + tokenAddress, + newAdmin: wallet2Address, + address: SEPOLIA_ROUTER, + wallet, + }) + + assert.ok(transferResult.hash, 'should return tx hash') + assert.match(transferResult.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify on-chain: pendingAdministrator should be wallet2 + const tar = new Contract(tarAddress, TAR_ABI, provider) + const config1 = await tar.getFunction('getTokenConfig')(tokenAddress) + + assert.equal( + (config1.administrator as string).toLowerCase(), + walletAddress.toLowerCase(), + 'administrator should still be wallet (not yet accepted)', + ) + assert.equal( + (config1.pendingAdministrator as string).toLowerCase(), + wallet2Address.toLowerCase(), + 'pendingAdministrator should be wallet2', + ) + + // Accept with wallet2 + const acceptResult = await mgr.acceptAdminRole({ + tokenAddress, + address: SEPOLIA_ROUTER, + wallet: wallet2, + }) + + assert.ok(acceptResult.hash, 'accept should return tx hash') + + // Verify on-chain: administrator is now wallet2 + const config2 = await tar.getFunction('getTokenConfig')(tokenAddress) + + assert.equal( + (config2.administrator as string).toLowerCase(), + wallet2Address.toLowerCase(), + 'administrator should now be wallet2', + ) + assert.equal( + (config2.pendingAdministrator as string).toLowerCase(), + ZeroAddress.toLowerCase(), + 'pendingAdministrator should be cleared', + ) + + // Transfer back: wallet2 → wallet + const transferBackResult = await mgr.transferAdminRole({ + tokenAddress, + newAdmin: walletAddress, + address: SEPOLIA_ROUTER, + wallet: wallet2, + }) + + assert.ok(transferBackResult.hash, 'transfer back should return tx hash') + + // Accept with wallet + const acceptBackResult = await mgr.acceptAdminRole({ + tokenAddress, + address: SEPOLIA_ROUTER, + wallet, + }) + + assert.ok(acceptBackResult.hash, 'accept back should return tx hash') + + // Verify on-chain: administrator is back to wallet + const config3 = await tar.getFunction('getTokenConfig')(tokenAddress) + + assert.equal( + (config3.administrator as string).toLowerCase(), + walletAddress.toLowerCase(), + 'administrator should be back to original wallet', + ) + assert.equal( + (config3.pendingAdministrator as string).toLowerCase(), + ZeroAddress.toLowerCase(), + 'pendingAdministrator should be cleared after round-trip', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin-role.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin-role.test.ts new file mode 100644 index 00000000..18dc9e2c --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin-role.test.ts @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface } from 'ethers' + +import { AcceptAdminRole } from './accept-admin-role.ts' +import TokenAdminRegistryABI from '../../../../evm/abi/TokenAdminRegistry_1_5.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const TOKEN = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const ROUTER = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' +const TAR = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const stubChain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + getTokenAdminRegistryFor: () => Promise.resolve(TAR), +} as unknown as EVMChain + +describe('EVM cct acceptAdminRole', () => { + const op = new AcceptAdminRole() + + it('encodes acceptAdminRole against the resolved TAR — byte-identical to a direct ethers encode', async () => { + const unsigned = await op.generate(stubChain, { + tokenAddress: TOKEN, + address: ROUTER, + }) + const expected = new Interface(TokenAdminRegistryABI).encodeFunctionData('acceptAdminRole', [ + TOKEN, + ]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, TAR) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('applies sender to from', async () => { + const unsigned = await op.generate(stubChain, { + tokenAddress: TOKEN, + address: ROUTER, + sender: TOKEN, + }) + assert.equal(unsigned.transactions[0]!.from, TOKEN) + }) + + it('rejects invalid addresses before RPC', async () => { + await assert.rejects( + () => op.generate(stubChain, { tokenAddress: 'nope', address: ROUTER }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'tokenAddress', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin-role.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin-role.ts new file mode 100644 index 00000000..464fddd5 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin-role.ts @@ -0,0 +1,43 @@ +/** + * acceptAdminRole — accepts a pending administrator role for a token in the + * TokenAdminRegistry. Calls `acceptAdminRole(localToken)` directly on the TAR, + * resolved from the provided router address. + * + * @packageDocumentation + */ + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Parameters for `acceptAdminRole`. */ +export type AcceptAdminRoleParams = { + /** Token to accept the administrator role for. */ + tokenAddress: string + /** Contract to resolve the TokenAdminRegistry from (the TAR, or a Router/pool). */ + address: string + sender?: string +} + +/** Accepts a pending administrator role for a token in the TokenAdminRegistry. */ +export class AcceptAdminRole extends EVMOperation { + readonly name = 'acceptAdminRole' + + /** Validates all addresses before any RPC. */ + protected validate(p: AcceptAdminRoleParams): void { + validateAddress(this.name, 'tokenAddress', p.tokenAddress) + validateAddress(this.name, 'address', p.address) + } + + /** Builds `acceptAdminRole` calldata against the TAR resolved from `address`. */ + protected async buildUnsigned(chain: EVMChain, p: AcceptAdminRoleParams): Promise { + const to = await chain.getTokenAdminRegistryFor(p.address) + const data = interfaces.TokenAdminRegistry.encodeFunctionData('acceptAdminRole', [ + p.tokenAddress, + ]) + return { family: ChainFamily.EVM, transactions: [{ to, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/propose-admin-role.fork.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/propose-admin-role.fork.test.ts new file mode 100644 index 00000000..231761e6 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/propose-admin-role.fork.test.ts @@ -0,0 +1,208 @@ +import assert from 'node:assert/strict' +import { execSync } from 'node:child_process' +import { after, before, describe, it } from 'node:test' + +import { Contract, JsonRpcProvider, Wallet } from 'ethers' +import { Instance } from 'prool' + +import { EVMTokenManager } from '../../index.ts' + +// ── Constants ── + +const SEPOLIA_RPC = process.env['RPC_SEPOLIA'] || 'https://ethereum-sepolia-rpc.publicnode.com' +const SEPOLIA_ROUTER = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' +const SEPOLIA_REGISTRY_MODULE = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const ANVIL_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + +// ── Helpers ── + +function isAnvilAvailable(): boolean { + try { + execSync('anvil --version', { stdio: 'ignore' }) + return true + } catch { + return false + } +} + +// Minimal ABIs +const TAR_ABI = [ + { + inputs: [{ name: 'token', type: 'address' }], + name: 'getTokenConfig', + outputs: [ + { + type: 'tuple', + components: [ + { name: 'administrator', type: 'address' }, + { name: 'pendingAdministrator', type: 'address' }, + { name: 'tokenPool', type: 'address' }, + ], + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [{ type: 'address' }], + stateMutability: 'view', + type: 'function', + }, +] as const + +// ── Tests ── + +const skip = !!process.env.SKIP_INTEGRATION_TESTS || !isAnvilAvailable() + +const testLogger = process.env.VERBOSE + ? console + : { debug() {}, info() {}, warn: console.warn, error: console.error } + +describe('EVMTokenManager proposeAdminRole Fork Tests', { skip, timeout: 120_000 }, () => { + let provider: JsonRpcProvider + let wallet: Wallet + let mgr: EVMTokenManager + let anvilInstance: ReturnType | undefined + let tokenAddress: string + let walletAddress: string + let tarAddress: string + + before(async () => { + // Fork Sepolia so we have a real Router with offRamps + anvilInstance = Instance.anvil({ + port: 8749, + forkUrl: SEPOLIA_RPC, + forkBlockNumber: undefined, // latest + }) + await anvilInstance.start() + + const anvilUrl = `http://${anvilInstance.host}:${anvilInstance.port}` + provider = new JsonRpcProvider(anvilUrl, undefined, { cacheTimeout: -1 }) + wallet = new Wallet(ANVIL_PRIVATE_KEY, provider) + walletAddress = await wallet.getAddress() + + mgr = await EVMTokenManager.fromProvider(provider, { logger: testLogger, apiClient: null }) + + // Deploy a token first (needed to propose admin for it) + const tokenResult = await mgr.deployToken({ + name: 'Admin Test Token', + symbol: 'ATT', + decimals: 18, + initialSupply: 1_000_000n * 10n ** 18n, + wallet, + }) + tokenAddress = tokenResult.tokenAddress + + // Discover TAR for verification + tarAddress = await mgr.chain.getTokenAdminRegistryFor(SEPOLIA_ROUTER) + }) + + after(async () => { + provider.destroy() + await anvilInstance?.stop() + }) + + // =========================================================================== + // getTokenAdminRegistryFor — inherited from EVMChain + // =========================================================================== + + it('should discover TokenAdminRegistry from router', async () => { + assert.ok(tarAddress, 'should return TAR address') + assert.match(tarAddress, /^0x[0-9a-fA-F]{40}$/, 'should be valid address') + assert.notEqual( + tarAddress.toLowerCase(), + '0x0000000000000000000000000000000000000000', + 'should not be zero address', + ) + }) + + // =========================================================================== + // proposeAdminRole — Happy Path (via registerAdminViaGetCCIPAdmin) + // =========================================================================== + + it('should propose admin role via registerAdminViaGetCCIPAdmin and verify on-chain', async () => { + // BurnMintERC20 implements getCCIPAdmin() (not owner()), so use 'getCCIPAdmin' method + const result = await mgr.proposeAdminRole({ + tokenAddress, + registryModuleAddress: SEPOLIA_REGISTRY_MODULE, + registrationMethod: 'getCCIPAdmin', + wallet, + }) + + assert.ok(result.hash, 'should return tx hash') + assert.match(result.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify on-chain: read getTokenConfig from the TAR + const tar = new Contract(tarAddress, TAR_ABI, provider) + const config = await tar.getFunction('getTokenConfig')(tokenAddress) + + assert.equal( + (config.pendingAdministrator as string).toLowerCase(), + walletAddress.toLowerCase(), + 'pendingAdministrator should match wallet address (token owner)', + ) + }) + + // =========================================================================== + // generateUnsignedProposeAdminRole — structure verification + // =========================================================================== + + it('should produce unsigned tx with correct shape', async () => { + const unsigned = await mgr.generateUnsignedProposeAdminRole({ + tokenAddress, + registryModuleAddress: SEPOLIA_REGISTRY_MODULE, + registrationMethod: 'getCCIPAdmin', + }) + + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.ok(tx.to, 'should have a to address (RegistryModule contract)') + assert.equal( + (tx.to as string).toLowerCase(), + SEPOLIA_REGISTRY_MODULE.toLowerCase(), + 'to should be RegistryModule address', + ) + assert.ok(tx.data, 'should have calldata') + }) + + // =========================================================================== + // generateUnsignedProposeAdminRole — manual sign (token owner) + // =========================================================================== + + it('should produce unsigned tx that succeeds when signed by token owner', async () => { + // Deploy a fresh token for this test + const tokenResult = await mgr.deployToken({ + name: 'Manual Sign Test Token', + symbol: 'MST', + decimals: 18, + wallet, + }) + + const unsigned = await mgr.generateUnsignedProposeAdminRole({ + tokenAddress: tokenResult.tokenAddress, + registryModuleAddress: SEPOLIA_REGISTRY_MODULE, + registrationMethod: 'getCCIPAdmin', + }) + + // Use the wallet (token owner) to submit + const tx = unsigned.transactions[0]! + const populated = await wallet.populateTransaction(tx) + const response = await wallet.sendTransaction(populated) + const receipt = await response.wait(1, 30_000) + + assert.ok(receipt, 'should get receipt') + assert.equal(receipt.status, 1, 'tx should succeed') + + // Verify on-chain + const tar = new Contract(tarAddress, TAR_ABI, provider) + const config = await tar.getFunction('getTokenConfig')(tokenResult.tokenAddress) + + assert.equal( + (config.pendingAdministrator as string).toLowerCase(), + walletAddress.toLowerCase(), + 'pendingAdministrator should match', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/propose-admin-role.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/propose-admin-role.test.ts new file mode 100644 index 00000000..75d2fbfb --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/propose-admin-role.test.ts @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface } from 'ethers' + +import { ProposeAdminRole } from './propose-admin-role.ts' +import RegistryModuleOwnerCustomABI from '../../../../evm/abi/RegistryModuleOwnerCustom_1_6.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const TOKEN = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const REGISTRY = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const stubChain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, +} as unknown as EVMChain + +describe('EVM cct proposeAdminRole', () => { + const op = new ProposeAdminRole() + + it('encodes registerAdminViaOwner by default — byte-identical to a direct ethers encode', async () => { + const unsigned = await op.generate(stubChain, { + tokenAddress: TOKEN, + registryModuleAddress: REGISTRY, + }) + const expected = new Interface(RegistryModuleOwnerCustomABI).encodeFunctionData( + 'registerAdminViaOwner', + [TOKEN], + ) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, REGISTRY) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('selects the function for each registration method', async () => { + for (const [method, fn] of [ + ['getCCIPAdmin', 'registerAdminViaGetCCIPAdmin'], + ['accessControlDefaultAdmin', 'registerAccessControlDefaultAdmin'], + ] as const) { + const unsigned = await op.generate(stubChain, { + tokenAddress: TOKEN, + registryModuleAddress: REGISTRY, + registrationMethod: method, + }) + const expected = new Interface(RegistryModuleOwnerCustomABI).encodeFunctionData(fn, [TOKEN]) + assert.equal(unsigned.transactions[0]!.data, expected) + } + }) + + it('applies sender to from', async () => { + const unsigned = await op.generate(stubChain, { + tokenAddress: TOKEN, + registryModuleAddress: REGISTRY, + sender: TOKEN, + }) + assert.equal(unsigned.transactions[0]!.from, TOKEN) + }) + + it('rejects invalid addresses before RPC', async () => { + await assert.rejects( + () => op.generate(stubChain, { tokenAddress: 'nope', registryModuleAddress: REGISTRY }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'tokenAddress', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/propose-admin-role.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/propose-admin-role.ts new file mode 100644 index 00000000..f271522d --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/propose-admin-role.ts @@ -0,0 +1,64 @@ +/** + * proposeAdminRole — proposes the caller as administrator for a token via the + * RegistryModuleOwnerCustom contract, which verifies the caller's authority + * (token owner, CCIP admin, or AccessControl default admin) and then calls + * `proposeAdministrator(token, caller)` on the TokenAdminRegistry. + * + * @packageDocumentation + */ + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** How RegistryModuleOwnerCustom verifies the caller's authority over the token. */ +export type EVMRegistrationMethod = 'owner' | 'getCCIPAdmin' | 'accessControlDefaultAdmin' + +/** RegistryModuleOwnerCustom function per registration method. */ +const REGISTRATION_FUNCTION_NAMES: Record = { + owner: 'registerAdminViaOwner', + getCCIPAdmin: 'registerAdminViaGetCCIPAdmin', + accessControlDefaultAdmin: 'registerAccessControlDefaultAdmin', +} + +/** Parameters for `proposeAdminRole`. */ +export type ProposeAdminRoleParams = { + /** Token to propose an administrator for. */ + tokenAddress: string + /** RegistryModuleOwnerCustom contract address (from the CCIP chains API `registryModule`). */ + registryModuleAddress: string + /** How the contract verifies caller authority. Defaults to `'owner'`. */ + registrationMethod?: EVMRegistrationMethod + sender?: string +} + +/** Proposes the caller as token administrator via RegistryModuleOwnerCustom. */ +export class ProposeAdminRole extends EVMOperation { + readonly name = 'proposeAdminRole' + + /** Validates addresses and the registration method before any RPC. */ + protected validate(p: ProposeAdminRoleParams): void { + validateAddress(this.name, 'tokenAddress', p.tokenAddress) + validateAddress(this.name, 'registryModuleAddress', p.registryModuleAddress) + if (p.registrationMethod && !(p.registrationMethod in REGISTRATION_FUNCTION_NAMES)) { + throw new CCTParamsInvalidError( + this.name, + 'registrationMethod', + `must be one of ${Object.keys(REGISTRATION_FUNCTION_NAMES).join(', ')}`, + ) + } + } + + /** Builds the RegistryModuleOwnerCustom registration calldata. */ + protected buildUnsigned(_chain: EVMChain, p: ProposeAdminRoleParams): UnsignedEVMTx { + const functionName = REGISTRATION_FUNCTION_NAMES[p.registrationMethod ?? 'owner'] + const data = interfaces.RegistryModuleOwnerCustom.encodeFunctionData(functionName, [ + p.tokenAddress, + ]) + return { family: ChainFamily.EVM, transactions: [{ to: p.registryModuleAddress, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.fork.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.fork.test.ts new file mode 100644 index 00000000..666ce01f --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.fork.test.ts @@ -0,0 +1,290 @@ +import assert from 'node:assert/strict' +import { execSync } from 'node:child_process' +import { after, before, describe, it } from 'node:test' + +import { Contract, JsonRpcProvider, Wallet, ZeroAddress } from 'ethers' +import { Instance } from 'prool' + +import { EVMTokenManager } from '../../index.ts' + +// ── Constants ── + +const SEPOLIA_RPC = process.env['RPC_SEPOLIA'] || 'https://ethereum-sepolia-rpc.publicnode.com' +const SEPOLIA_ROUTER = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' +const SEPOLIA_REGISTRY_MODULE = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const ANVIL_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + +// ── Helpers ── + +function isAnvilAvailable(): boolean { + try { + execSync('anvil --version', { stdio: 'ignore' }) + return true + } catch { + return false + } +} + +// Minimal ABI for reading TAR config +const TAR_ABI = [ + { + inputs: [{ name: 'token', type: 'address' }], + name: 'getTokenConfig', + outputs: [ + { + type: 'tuple', + components: [ + { name: 'administrator', type: 'address' }, + { name: 'pendingAdministrator', type: 'address' }, + { name: 'tokenPool', type: 'address' }, + ], + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const + +// ── Tests ── + +const skip = !!process.env.SKIP_INTEGRATION_TESTS || !isAnvilAvailable() + +const testLogger = process.env.VERBOSE + ? console + : { debug() {}, info() {}, warn: console.warn, error: console.error } + +describe('EVMTokenManager setPool Fork Tests', { skip, timeout: 120_000 }, () => { + let provider: JsonRpcProvider + let wallet: Wallet + let mgr: EVMTokenManager + let anvilInstance: ReturnType | undefined + let tokenAddress: string + let poolAddress: string + let tarAddress: string + + before(async () => { + anvilInstance = Instance.anvil({ + port: 8755, + forkUrl: SEPOLIA_RPC, + forkBlockNumber: undefined, + }) + await anvilInstance.start() + + const anvilUrl = `http://${anvilInstance.host}:${anvilInstance.port}` + provider = new JsonRpcProvider(anvilUrl, undefined, { cacheTimeout: -1 }) + wallet = new Wallet(ANVIL_PRIVATE_KEY, provider) + + mgr = await EVMTokenManager.fromProvider(provider, { logger: testLogger, apiClient: null }) + + // 1. Deploy token + const tokenResult = await mgr.deployToken({ + name: 'Set Pool Test Token', + symbol: 'SPTT', + decimals: 18, + initialSupply: 1_000_000n * 10n ** 18n, + wallet, + }) + tokenAddress = tokenResult.tokenAddress + + // 2. Deploy pool + const poolResult = await mgr.deployPool({ + poolType: 'burn-mint', + tokenAddress, + localTokenDecimals: 18, + routerAddress: SEPOLIA_ROUTER, + wallet, + }) + poolAddress = poolResult.poolAddress + + // 3. Propose + accept admin + await mgr.proposeAdminRole({ + tokenAddress, + registryModuleAddress: SEPOLIA_REGISTRY_MODULE, + registrationMethod: 'getCCIPAdmin', + wallet, + }) + + await mgr.acceptAdminRole({ + tokenAddress, + address: SEPOLIA_ROUTER, + wallet, + }) + + // Discover TAR for verification + tarAddress = await mgr.chain.getTokenAdminRegistryFor(SEPOLIA_ROUTER) + }) + + after(async () => { + provider.destroy() + await anvilInstance?.stop() + }) + + // =========================================================================== + // Verify pool is not set before setPool + // =========================================================================== + + it('should have no pool set before setPool', async () => { + const tar = new Contract(tarAddress, TAR_ABI, provider) + const config = await tar.getFunction('getTokenConfig')(tokenAddress) + + assert.equal( + (config.tokenPool as string).toLowerCase(), + ZeroAddress.toLowerCase(), + 'tokenPool should be zero address before setPool', + ) + }) + + // =========================================================================== + // setPool — Happy Path + // =========================================================================== + + it('should set pool and verify on-chain', async () => { + const result = await mgr.setPool({ + tokenAddress, + poolAddress, + address: SEPOLIA_ROUTER, + wallet, + }) + + assert.ok(result.hash, 'should return tx hash') + assert.match(result.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify on-chain: tokenPool should be set + const tar = new Contract(tarAddress, TAR_ABI, provider) + const config = await tar.getFunction('getTokenConfig')(tokenAddress) + + assert.equal( + (config.tokenPool as string).toLowerCase(), + poolAddress.toLowerCase(), + 'tokenPool should match pool address after setPool', + ) + }) + + // =========================================================================== + // generateUnsignedSetPool — structure verification + // =========================================================================== + + it('should produce unsigned tx with correct shape', async () => { + // Deploy another token + pool for this test + const tokenResult = await mgr.deployToken({ + name: 'Unsigned SetPool Test', + symbol: 'USPT', + decimals: 18, + wallet, + }) + + const poolResult = await mgr.deployPool({ + poolType: 'burn-mint', + tokenAddress: tokenResult.tokenAddress, + localTokenDecimals: 18, + routerAddress: SEPOLIA_ROUTER, + wallet, + }) + + await mgr.proposeAdminRole({ + tokenAddress: tokenResult.tokenAddress, + registryModuleAddress: SEPOLIA_REGISTRY_MODULE, + registrationMethod: 'getCCIPAdmin', + wallet, + }) + + await mgr.acceptAdminRole({ + tokenAddress: tokenResult.tokenAddress, + address: SEPOLIA_ROUTER, + wallet, + }) + + const unsigned = await mgr.generateUnsignedSetPool({ + tokenAddress: tokenResult.tokenAddress, + poolAddress: poolResult.poolAddress, + address: SEPOLIA_ROUTER, + }) + + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.ok(tx.to, 'should have a to address (TAR contract)') + assert.equal( + (tx.to as string).toLowerCase(), + tarAddress.toLowerCase(), + 'to should be TAR address', + ) + assert.ok(tx.data, 'should have calldata') + }) + + // =========================================================================== + // grantMintBurnAccess — Happy Path + // =========================================================================== + + it('should grant mint/burn access to pool and verify on-chain', async () => { + const result = await mgr.grantMintBurnAccess({ + tokenAddress, + authority: poolAddress, + wallet, + }) + + assert.ok(result.hash, 'should return tx hash') + assert.match(result.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify on-chain using hasRole directly (faster than getMintBurnRoles which scans events) + const ROLE_ABI = [ + { + inputs: [ + { name: 'role', type: 'bytes32' }, + { name: 'account', type: 'address' }, + ], + name: 'hasRole', + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'MINTER_ROLE', + outputs: [{ name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'BURNER_ROLE', + outputs: [{ name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + ] as const + + const token = new Contract(tokenAddress, ROLE_ABI, provider) + const [minterRole, burnerRole] = await Promise.all([ + token.getFunction('MINTER_ROLE')() as Promise, + token.getFunction('BURNER_ROLE')() as Promise, + ]) + + const [hasMinter, hasBurner] = await Promise.all([ + token.getFunction('hasRole')(minterRole, poolAddress) as Promise, + token.getFunction('hasRole')(burnerRole, poolAddress) as Promise, + ]) + + assert.ok(hasMinter, 'pool should have MINTER_ROLE') + assert.ok(hasBurner, 'pool should have BURNER_ROLE') + }) + + // =========================================================================== + // generateUnsignedGrantMintBurnAccess — structure verification + // =========================================================================== + + it('should produce unsigned grantMintBurnAccess tx with correct shape', async () => { + const unsigned = await mgr.generateUnsignedGrantMintBurnAccess({ + tokenAddress, + authority: poolAddress, + }) + + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal( + (tx.to as string).toLowerCase(), + tokenAddress.toLowerCase(), + 'to should be token address', + ) + assert.ok(tx.data, 'should have calldata') + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.ts new file mode 100644 index 00000000..4643d16c --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.ts @@ -0,0 +1,50 @@ +/** + * setPool — registers a pool for a token in the TokenAdminRegistry. + * Version-independent (v1.5–v2.0 share one encoding). + * + * @packageDocumentation + */ + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Parameters for `setPool`. Zero `poolAddress` delists the token. */ +export type SetPoolParams = { + tokenAddress: string + /** A zero/empty `poolAddress` delists the token from the registry. */ + poolAddress: string + /** + * Contract to resolve the TokenAdminRegistry from. Pass the registry itself for a + * direct lookup; a Router, OnRamp, OffRamp, or TokenPool also work but add hops and + * need a configured lane. + */ + address: string + sender?: string +} + +/** Registers a pool for a token in the TokenAdminRegistry resolved from `address`. */ +export class SetPool extends EVMOperation { + readonly name = 'setPool' + + /** Validates all addresses before any RPC. */ + protected validate(p: SetPoolParams): void { + validateAddress(this.name, 'tokenAddress', p.tokenAddress) + validateAddress(this.name, 'poolAddress', p.poolAddress) + validateAddress(this.name, 'address', p.address) + } + + /** Builds `setPool` calldata against the TokenAdminRegistry resolved from `address`. */ + protected async buildUnsigned(chain: EVMChain, p: SetPoolParams): Promise { + const to = await chain.getTokenAdminRegistryFor(p.address) + // TAR.setPool encoding is version-stable across v1.5–v2.0; no version dispatch needed. + const data = interfaces.TokenAdminRegistry.encodeFunctionData('setPool', [ + p.tokenAddress, + p.poolAddress, + ]) + return { family: ChainFamily.EVM, transactions: [{ to, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin-role.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin-role.test.ts new file mode 100644 index 00000000..26c9e960 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin-role.test.ts @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface } from 'ethers' + +import { TransferAdminRole } from './transfer-admin-role.ts' +import TokenAdminRegistryABI from '../../../../evm/abi/TokenAdminRegistry_1_5.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const TOKEN = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const NEW_ADMIN = '0x1234567890AbcdEF1234567890aBcdef12345678' +const ROUTER = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' +const TAR = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const stubChain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + getTokenAdminRegistryFor: () => Promise.resolve(TAR), +} as unknown as EVMChain + +describe('EVM cct transferAdminRole', () => { + const op = new TransferAdminRole() + + it('encodes transferAdminRole against the resolved TAR — byte-identical to a direct ethers encode', async () => { + const unsigned = await op.generate(stubChain, { + tokenAddress: TOKEN, + newAdmin: NEW_ADMIN, + address: ROUTER, + }) + const expected = new Interface(TokenAdminRegistryABI).encodeFunctionData('transferAdminRole', [ + TOKEN, + NEW_ADMIN, + ]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, TAR) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('applies sender to from', async () => { + const unsigned = await op.generate(stubChain, { + tokenAddress: TOKEN, + newAdmin: NEW_ADMIN, + address: ROUTER, + sender: TOKEN, + }) + assert.equal(unsigned.transactions[0]!.from, TOKEN) + }) + + it('rejects invalid newAdmin before RPC', async () => { + await assert.rejects( + () => + op.generate(stubChain, { + tokenAddress: TOKEN, + newAdmin: 'nope', + address: ROUTER, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'newAdmin', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin-role.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin-role.ts new file mode 100644 index 00000000..d82ad6d3 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin-role.ts @@ -0,0 +1,51 @@ +/** + * transferAdminRole — transfers the administrator role for a token in the + * TokenAdminRegistry to a new admin. Encodes + * `transferAdminRole(localToken, newAdmin)` on the TAR, resolved from the + * provided router address. + * + * @packageDocumentation + */ + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Parameters for `transferAdminRole`. */ +export type TransferAdminRoleParams = { + /** Token to transfer the administrator role for. */ + tokenAddress: string + /** Address of the new administrator. */ + newAdmin: string + /** Contract to resolve the TokenAdminRegistry from (the TAR, or a Router/pool). */ + address: string + sender?: string +} + +/** Transfers the administrator role for a token in the TokenAdminRegistry. */ +export class TransferAdminRole extends EVMOperation { + readonly name = 'transferAdminRole' + + /** Validates all addresses before any RPC. */ + protected validate(p: TransferAdminRoleParams): void { + validateAddress(this.name, 'tokenAddress', p.tokenAddress) + validateAddress(this.name, 'newAdmin', p.newAdmin) + validateAddress(this.name, 'address', p.address) + } + + /** Builds `transferAdminRole` calldata against the TAR resolved from `address`. */ + protected async buildUnsigned( + chain: EVMChain, + p: TransferAdminRoleParams, + ): Promise { + const to = await chain.getTokenAdminRegistryFor(p.address) + const data = interfaces.TokenAdminRegistry.encodeFunctionData('transferAdminRole', [ + p.tokenAddress, + p.newAdmin, + ]) + return { family: ChainFamily.EVM, transactions: [{ to, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/bytecodes/BurnMintTokenPool.ts b/ccip-sdk/src/cct/evm/token-pool/bytecodes/BurnMintTokenPool.ts new file mode 100644 index 00000000..5bae13f7 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/bytecodes/BurnMintTokenPool.ts @@ -0,0 +1,8 @@ +/** + * BurnMintTokenPool (v2.0.0) deployment bytecode. Lazy-loaded via dynamic import(). + * + * Source: chainlink-ccip (refs/heads/main) + * chains/evm/gobindings/generated/v2_0_0/burn_mint_token_pool/burn_mint_token_pool.go + */ +export const BURN_MINT_TOKEN_POOL_BYTECODE = + '0x60e080604052346101f65760a081615db2803803809161001f8285610247565b8339810103126101f65780516001600160a01b038116908190036101f65761004960208301610280565b6100556040840161028e565b9161006e60806100676060870161028e565b950161028e565b93331561023657600180546001600160a01b0319163317905581158015610225575b8015610214575b610203578160805260c052308103610170575b5060a052600380546001600160a01b039283166001600160a01b03199182161790915560028054939092169216919091179055604051615b0f90816102a3823960805181818161023e01528181610491015281816122660152818161243e01528181612aa101528181612c9c0152818161318e0152818161373b0152613795015260a051818181613601015281816149140152818161495e0152614ea8015260c0518181816102d9015281816113eb0152818161230001528181612b3c01526132290152f35b60206004916040519283809263313ce56760e01b82525afa600091816101c2575b50156100aa5760ff1660ff82168181036101ab57506100aa565b6332ad3e0760e11b60005260045260245260446000fd5b9091506020813d6020116101fb575b816101de60209383610247565b810103126101f6576101ef90610280565b9038610191565b600080fd5b3d91506101d1565b630a64406560e11b60005260046000fd5b506001600160a01b03811615610097565b506001600160a01b03851615610090565b639b15e16f60e01b60005260046000fd5b601f909101601f19168101906001600160401b0382119082101761026a57604052565b634e487b7160e01b600052604160045260246000fd5b519060ff821682036101f657565b51906001600160a01b03821682036101f65756fe608080604052600436101561001357600080fd5b600090813560e01c90816301ffc9a7146139b65750806306b859ef146138d1578063181f5a77146138705780631826b1e7146137b957806321df0da714613768578063240028e8146137045780632422ac451461362557806324f65ee7146135e75780632cab0fb6146130f357806337a3210d146130bf57806339077537146129f65780634c5ef0ed146129af57806362ddd3c4146129285780637437ff9f146128da57806379ba5097146128135780638926f54f146127cd5780638da5cb5b146127995780639a4575b9146121ed578063a42a7b8b14612086578063acfecf9114611f8e578063ae39a25714611e03578063b6cfa3b714611d48578063b794658014611d10578063bfeffd3f14611c64578063c4bffe2b14611b39578063c7230a6014611893578063dc04fa1f1461140f578063dc0bd971146113be578063dcbd41bc146111ba578063e8a1da1714610ade578063ea6396db146109a0578063ec6ae7a71461095d578063f2fde38b1461088e5763fbc801a71461019757600080fd5b346105d15760606003193601126105d1576004359067ffffffffffffffff82116105d1578160040160a060031984360301126105df576101d5613ae8565b9060443567ffffffffffffffff811161070557906101fa610217923690600401613c13565b92906102046145d0565b5061020f858461510c565b933691613d8d565b9260848601936102268561455d565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361084457602487019677ffffffffffffffff0000000000000000000000000000000061028c8961457e565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156107b7578891610815575b506107ed5767ffffffffffffffff6103208961457e565b16610338816000526007602052604060002054151590565b156107c257602073ffffffffffffffffffffffffffffffffffffffff60025416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa80156107b7578890610766575b73ffffffffffffffffffffffffffffffffffffffff915016330361073a576064810135936103c78686613f74565b7fffffffff00000000000000000000000000000000000000000000000000000000851694851561071857610423907fffffffff0000000000000000000000000000000000000000000000000000000060025460401b1690614a68565b61043f816104308a61455d565b6104398d61457e565b906153f4565b73ffffffffffffffffffffffffffffffffffffffff6003541693846105e3575b5050505050509061046f91613f74565b916104798461457e565b5073ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803b156105df578180916024604051809481937f42966c680000000000000000000000000000000000000000000000000000000083528960048401525af180156105d4576105bc575b6105b28461058161057c88877ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff61054261053c8561457e565b9361455d565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252336020830152810188905292169180606081015b0390a261457e565b614741565b9061058a614ea1565b6040519261059784613cf8565b83526020830152604051928392604084526040840190613e55565b9060208301520390f35b6105c7828092613d4c565b6105d157806104fa565b80fd5b6040513d84823e3d90fd5b5080fd5b843b15610714578994928b9694928692604051988997889687957fa8027c0f0000000000000000000000000000000000000000000000000000000087526004870160809052806106329161535e565b6084880160a0905261012488019061064992613fa2565b9261065390613bfe565b67ffffffffffffffff1660a487015260440161066e90613baf565b73ffffffffffffffffffffffffffffffffffffffff1660c48601528d8c60e487015261069990613baf565b73ffffffffffffffffffffffffffffffffffffffff1661010486015260248501528381036003190160448501526106cf91613c41565b90606483015203925af18015610709579085916106f0575b8080808061045f565b816106fa91613d4c565b6107055783386106e7565b8380fd5b6040513d87823e3d90fd5b8980fd5b50610735816107268a61455d565b61072f8d61457e565b906153ae565b61043f565b6024877f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b506020813d6020116107af575b8161078060209383613d4c565b810103126107ab576107a673ffffffffffffffffffffffffffffffffffffffff91613f81565b610399565b8780fd5b3d9150610773565b6040513d8a823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008852600452602487fd5b6004877f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b610837915060203d60201161083d575b61082f8183613d4c565b810190614bd4565b38610309565b503d610825565b60248673ffffffffffffffffffffffffffffffffffffffff6108658861455d565b7f961c9a4f00000000000000000000000000000000000000000000000000000000835216600452fd5b50346105d15760206003193601126105d15773ffffffffffffffffffffffffffffffffffffffff6108bd613b46565b6108c5614bec565b1633811461093557807fffffffffffffffffffffffff000000000000000000000000000000000000000083541617825573ffffffffffffffffffffffffffffffffffffffff600154167fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12788380a380f35b6004827fdad89dca000000000000000000000000000000000000000000000000000000008152fd5b50346105d157806003193601126105d15760207fffffffff0000000000000000000000000000000000000000000000000000000060025460401b16604051908152f35b50346105d15760806003193601126105d1576109ba613b46565b506109c3613bd0565b6109cb613b17565b5060643567ffffffffffffffff8111610ada579167ffffffffffffffff6040926109fb60e0953690600401613c13565b50508260c08551610a0b81613d30565b82815282602082015282878201528260608201528260808201528260a08201520152168152600b6020522060405190610a4382613d30565b5461ffff818163ffffffff82169485815263ffffffff60208201818560201c1681528160408401818760401c168152816060860193818960601c16855260ff60c060808901988a8c60801c168a528a60a082019c60901c168c52019b60a01c1615158b526040519b8c52511660208b0152511660408901525116606087015251166080850152511660a083015251151560c0820152f35b8280fd5b50346105d15760406003193601126105d15760043567ffffffffffffffff81116105df57610b10903690600401613e7f565b9060243567ffffffffffffffff81116107055790610b3384923690600401613e7f565b939091610b3e614bec565b83905b828210610ffb5750505081927ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee182360301935b81811015610ff7578060051b83013585811215610ff357830161012081360312610ff35760405194610ba586613d14565b610bae82613bfe565b8652602082013567ffffffffffffffff81116105df5782019436601f870112156105df57853595610bde87613ee1565b96610bec6040519889613d4c565b80885260208089019160051b83010190368211610ff35760208301905b828210610fc0575050505060208701958652604083013567ffffffffffffffff8111610ada57610c3c9036908501613df2565b9160408801928352610c66610c5436606087016147ed565b9460608a0195865260c03691016147ed565b956080890196875283515115610f9857610c8a67ffffffffffffffff8a5116615791565b15610f615767ffffffffffffffff8951168252600860205260408220610cb1865182614edc565b610cbf885160028301614edc565b6004855191019080519067ffffffffffffffff8211610f3457610ce2835461462c565b601f8111610ef9575b50602090601f8311600114610e5a57610d399291869183610e4f575b50507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b90555b815b88518051821015610d735790610d6d600192610d668367ffffffffffffffff8f5116926145e9565b5190614c37565b01610d3e565b5050977f8d340f17e19058004c20453540862a9c62778504476f6756755cb33bcd6c38c2939199975095610e4167ffffffffffffffff6001979694985116925193519151610e0d610dd860405196879687526101006020880152610100870190613c41565b9360408601906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b60a08401906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b0390a1019392909193610b74565b015190508e80610d07565b83865281862091907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08416875b818110610ee15750908460019594939210610eaa575b505050811b019055610d3c565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690558d8080610e9d565b92936020600181928786015181550195019301610e87565b610f249084875260208720601f850160051c81019160208610610f2a575b601f0160051c0190614889565b8d610ceb565b9091508190610f17565b6024857f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b60248267ffffffffffffffff8b51167f1d5ad3c5000000000000000000000000000000000000000000000000000000008252600452fd5b6004827f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b813567ffffffffffffffff8111610fef57602091610fe48392833691890101613df2565b815201910190610c09565b8680fd5b8480fd5b8380f35b9267ffffffffffffffff61101d6110188486889a9699979a6147c0565b61457e565b1691611028836154c7565b1561118e57828452600860205261104460056040862001615464565b94845b865181101561107d5760019085875260086020526110766005604089200161106f838b6145e9565b519061565d565b5001611047565b50939692909450949094808752600860205260056040882088815588600182015588600282015588600382015588600482016110b9815461462c565b8061114d575b505050018054908881558161112f575b5050907f5204aec90a3c794d8e90fded8b46ae9c7c552803e7e832e0c1d358396d8599166020836001948a52600482528985604082208281550155808a52600582528985604082208281550155604051908152a101909194939294610b41565b885260208820908101905b818110156110cf5788815560010161113a565b601f81116001146111635750555b888a806110bf565b8183526020832061117e91601f01861c810190600101614889565b808252816020812091555561115b565b602484847f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b50346105d15760206003193601126105d15760043567ffffffffffffffff81116105df576111ec903690600401613eb0565b73ffffffffffffffffffffffffffffffffffffffff600a54163314158061139c575b61137057825b81811061121f578380f35b61122a818385614763565b67ffffffffffffffff61123c8261457e565b1690611255826000526007602052604060002054151590565b1561134457907f41f7c8f7cfdad9350aa495e6c54cbbf750a07ab38a9098aed1256e30dd1682bb60e0836113046112de602060019897018b61129682614773565b1561130b5787905260046020526112bd60408d206112b736604088016147ed565b90614edc565b868c5260056020526112d960408d206112b73660a088016147ed565b614773565b9160405192151583526112f76020840160408301614845565b60a0608084019101614845565ba201611214565b60026040828a6112d99452600860205261132d8282206112b736858c016147ed565b8a8152600860205220016112b73660a088016147ed565b602486837f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b6024837f8e4a23d600000000000000000000000000000000000000000000000000000000815233600452fd5b5073ffffffffffffffffffffffffffffffffffffffff6001541633141561120e565b50346105d157806003193601126105d157602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346105d15760406003193601126105d15760043567ffffffffffffffff81116105df57611441903690600401613eb0565b60243567ffffffffffffffff811161070557611461903690600401613e7f565b91909261146c614bec565b845b8281106114d857505050825b818110611485578380f35b8067ffffffffffffffff61149f61101860019486886147c0565b16808652600b6020528560408120557f5479bbc0288b7eaeaf2ace0943b88016cc648964fcd42919a86fd93b15fdbee88680a20161147a565b67ffffffffffffffff6114ef611018838686614763565b16611507816000526007602052604060002054151590565b1561186857611517828585614763565b602081019060e081019061152a82614773565b1561183c5760a0810161271061ffff61154283614780565b16101561182d5760c082019161271061ffff61155d85614780565b1610156117f55763ffffffff6115728661478f565b16156117c957858c52600b60205260408c2061158d8661478f565b63ffffffff169080549060408401916115a58361478f565b60201b67ffffffff00000000169360608601946115c18661478f565b60401b6bffffffff00000000000000001696608001966115e08861478f565b60601b6fffffffff00000000000000000000000016916115ff8a614780565b60801b71ffff0000000000000000000000000000000016936116208c614780565b60901b73ffff00000000000000000000000000000000000016957fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016177fffffffffffffffffffffffffffffffff00000000ffffffffffffffffffffffff16177fffffffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffff16177fffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffffffff1617171781556116d387614773565b81547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff00000000000000000000000000000000000000001617905560405196611724906147a0565b63ffffffff168752611735906147a0565b63ffffffff166020870152611749906147a0565b63ffffffff16604086015261175d906147a0565b63ffffffff166060850152611771906147b1565b61ffff166080840152611783906147b1565b61ffff1660a083015261179590613ca0565b151560c082015260e07ffae1e296719dac5269c3886fb5002bb29bf17ae403060c6eb063a55abaaa104191a260010161146e565b60248c877f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b60248c61ffff61180486614780565b7f95f3517a00000000000000000000000000000000000000000000000000000000835216600452fd5b8a61ffff611804602493614780565b60248a857f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b7f1e670e4b000000000000000000000000000000000000000000000000000000008752600452602486fd5b50346105d15760406003193601126105d15760043567ffffffffffffffff81116105df576118c5903690600401613e7f565b906118ce613b8c565b9173ffffffffffffffffffffffffffffffffffffffff6001541633141580611b17575b611aeb5773ffffffffffffffffffffffffffffffffffffffff8316908115611ac357845b818110611920578580f35b73ffffffffffffffffffffffffffffffffffffffff6119486119438385886147c0565b61455d565b166040517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152602081602481855afa9081156107b7578891611a90575b508061199d575b5050600101611915565b6040517fa9059cbb00000000000000000000000000000000000000000000000000000000602080830191825273ffffffffffffffffffffffffffffffffffffffff8a16602484015260448084018590528352918a91906119fe606482613d4c565b519082865af115611a855787513d611a7c5750813b155b611a505790847f508d7d183612c18fc339b42618912b9fa3239f631dd7ec0671f950200a0fa66e602060019594604051908152a39038611993565b602488837f5274afe7000000000000000000000000000000000000000000000000000000008252600452fd5b60011415611a15565b6040513d89823e3d90fd5b905060203d8111611abc575b611aa68183613d4c565b602082600092810103126105d15750513861198c565b503d611a9c565b6004857f8579befe000000000000000000000000000000000000000000000000000000008152fd5b6024847fcb1afbd700000000000000000000000000000000000000000000000000000000815233600452fd5b5073ffffffffffffffffffffffffffffffffffffffff600c54163314156118f1565b50346105d157806003193601126105d157604051906006548083528260208101600684526020842092845b818110611c4b575050611b7992500383613d4c565b8151611b9d611b8782613ee1565b91611b956040519384613d4c565b808352613ee1565b917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0602083019301368437805b8451811015611bfc578067ffffffffffffffff611be9600193886145e9565b5116611bf582866145e9565b5201611bca565b50925090604051928392602084019060208552518091526040840192915b818110611c28575050500390f35b825167ffffffffffffffff16845285945060209384019390920191600101611c1a565b8454835260019485019487945060209093019201611b64565b50346105d15760206003193601126105d15760043573ffffffffffffffffffffffffffffffffffffffff81168091036105df57611c9f614bec565b7fffffffffffffffffffffffff00000000000000000000000000000000000000006003547fbaff46844acf36d6ee996f489a1a288709c4542bd33cd557770afd267d5812096040805173ffffffffffffffffffffffffffffffffffffffff84168152856020820152a1161760035580f35b50346105d15760206003193601126105d157611d44611d3061057c613be7565b604051918291602083526020830190613c41565b0390f35b50346105d15760206003193601126105d1577f307cf716eade81675bea3ccb6917b0f91baa2160056765d9a83d76f819caf06a6020611d85613ab4565b611d8d614bec565b6002547fffffffffffffffff00000000ffffffffffffffffffffffffffffffffffffffff77ffffffff0000000000000000000000000000000000000000808460401c16169116176002557fffffffff0000000000000000000000000000000000000000000000000000000060405191168152a180f35b50346105d15760606003193601126105d157611e1d613b46565b90611e26613b8c565b6044359273ffffffffffffffffffffffffffffffffffffffff841680850361070557611e50614bec565b73ffffffffffffffffffffffffffffffffffffffff82168015611f665794611f60917f3f1036e85d016a93254a0b1415844f79b85424959d90ae5ad51ce8f4533fe70195967fffffffffffffffffffffffff0000000000000000000000000000000000000000600254161760025573ffffffffffffffffffffffffffffffffffffffff85167fffffffffffffffffffffffff0000000000000000000000000000000000000000600a541617600a557fffffffffffffffffffffffff0000000000000000000000000000000000000000600c541617600c556040519384938491604091949373ffffffffffffffffffffffffffffffffffffffff809281606087019816865216602085015216910152565b0390a180f35b6004857f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b50346105d15767ffffffffffffffff611fa636613e10565b929091611fb1614bec565b1691611fca836000526007602052604060002054151590565b1561118e578284526008602052611ff960056040862001611fec368486613d8d565b602081519101209061565d565b1561203e57907f52d00ee4d9bd51b40168f2afc5848837288ce258784ad914278791464b3f4d7691612038604051928392602084526020840191613fa2565b0390a280f35b82612082836040519384937f74f23c7c0000000000000000000000000000000000000000000000000000000085526004850152604060248501526044840191613fa2565b0390fd5b50346105d15760206003193601126105d15767ffffffffffffffff6120a9613be7565b16815260086020526120c060056040832001615464565b80517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06121056120ef83613ee1565b926120fd6040519485613d4c565b808452613ee1565b01835b8181106121dc575050825b82518110156121595780612129600192856145e9565b518552600960205261213d6040862061467f565b61214782856145e9565b5261215281846145e9565b5001612113565b81846040519182916020830160208452825180915260408401602060408360051b870101940192905b82821061219157505050500390f35b919360206121cc827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc060019597998495030186528851613c41565b9601920192018594939192612182565b806060602080938601015201612108565b50346105d15760206003193601126105d15760043567ffffffffffffffff81116105df57806004019060a06003198236030112610ada5761222c6145d0565b5060405160209361223d8583613d4c565b808252608483019161224e8361455d565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361277857602484019477ffffffffffffffff000000000000000000000000000000006122b48761457e565b60801b16604051907f2cbc26bb0000000000000000000000000000000000000000000000000000000082526004820152878160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156126fd57849161275b575b506127335767ffffffffffffffff6123478761457e565b1661235f816000526007602052604060002054151590565b15612708578773ffffffffffffffffffffffffffffffffffffffff60025416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa80156126fd5784906126b5575b73ffffffffffffffffffffffffffffffffffffffff9150163303612689576064850135946123f9866123f08761455d565b61072f8a61457e565b73ffffffffffffffffffffffffffffffffffffffff60035416918261256c575b505050506124268461457e565b5073ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803b156105df578180916024604051809481937f42966c680000000000000000000000000000000000000000000000000000000083528960048401525af180156105d457612557575b8561252761057c87877ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff896105746124f06124ea8761457e565b9261455d565b6040805173ffffffffffffffffffffffffffffffffffffffff90921682523360208301528101959095529116929081906060820190565b90612530614ea1565b6040519261253d84613cf8565b835281830152611d44604051928284938452830190613e55565b612562828092613d4c565b6105d157806124a7565b823b15610ff357918791858094604051968795869485937fa8027c0f0000000000000000000000000000000000000000000000000000000085526004850160809052806125b89161535e565b6084860160a090526101248601906125cf92613fa2565b916125d990613bfe565b67ffffffffffffffff1660a48501526044016125f490613baf565b73ffffffffffffffffffffffffffffffffffffffff1660c48401528b60e484015261261e8b613baf565b73ffffffffffffffffffffffffffffffffffffffff1661010484015283602484015282810360031901604484015261265591613c41565b8a606483015203925af180156105d457908291612674575b8080612419565b8161267e91613d4c565b6105d157803861266d565b6024837f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b508781813d83116126f6575b6126cb8183613d4c565b81010312610705576126f173ffffffffffffffffffffffffffffffffffffffff91613f81565b6123bf565b503d6126c1565b6040513d86823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008452600452602483fd5b6004837f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b6127729150883d8a1161083d5761082f8183613d4c565b38612330565b5073ffffffffffffffffffffffffffffffffffffffff61086560249361455d565b50346105d157806003193601126105d157602073ffffffffffffffffffffffffffffffffffffffff60015416604051908152f35b50346105d15760206003193601126105d157602061280967ffffffffffffffff6127f5613be7565b166000526007602052604060002054151590565b6040519015158152f35b50346105d157806003193601126105d157805473ffffffffffffffffffffffffffffffffffffffff811633036128b2577fffffffffffffffffffffffff000000000000000000000000000000000000000060015491338284161760015516825573ffffffffffffffffffffffffffffffffffffffff3391167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08380a380f35b6004827f02b543c6000000000000000000000000000000000000000000000000000000008152fd5b50346105d157806003193601126105d157600254600a54600c546040805173ffffffffffffffffffffffffffffffffffffffff94851681529284166020840152921691810191909152606090f35b50346105d15761293736613e10565b61294393929193614bec565b67ffffffffffffffff8216612965816000526007602052604060002054151590565b156129845750612981929361297b913691613d8d565b90614c37565b80f35b7f1e670e4b000000000000000000000000000000000000000000000000000000008452600452602483fd5b50346105d15760406003193601126105d1576129c9613be7565b906024359067ffffffffffffffff82116105d1576020612809846129f03660048701613df2565b90614593565b50346105d15760206003193601126105d1576004359067ffffffffffffffff82116105d157816004019061010060031984360301126105d15780604051612a3c81613cad565b5280604051612a4a81613cad565b52606483013560c4840193612a7a612a74612a6f612a68888861450c565b3691613d8d565b6148a0565b8361495b565b936084820195612a898761455d565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361309e57602483019377ffffffffffffffff00000000000000000000000000000000612aef8661457e565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115611a8557879161307f575b506130575767ffffffffffffffff612b838661457e565b16612b9b816000526007602052604060002054151590565b1561302c57602073ffffffffffffffffffffffffffffffffffffffff60025416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa908115611a8557879161300d575b5015612fe157612c128561457e565b92612c2860a48601946129f0612a68878561450c565b15612f9a57612c4988612c3a8b61455d565b612c438961457e565b90615275565b73ffffffffffffffffffffffffffffffffffffffff600354169283612dcc575b505050505060440191612c7b8361455d565b612c848361457e565b5073ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001690813b15610ada576040517f40c10f1900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff919091166004820152602481018690529082908290604490829084905af180156105d457612db7575b5050608067ffffffffffffffff60209573ffffffffffffffffffffffffffffffffffffffff612d83612d7d61053c7ffc5e3a5bddc11d92c2dc20fae6f7d5eb989f056be35239f7de7e86150609abc09761457e565b9661455d565b816040519716875233898801521660408601528560608601521692a260405190612dac82613cad565b815260405190518152f35b612dc2828092613d4c565b6105d15780612d28565b833b156107ab57878795938195938c93604051988997889687957f6371157400000000000000000000000000000000000000000000000000000000875260048701606090528d612e1c878061535e565b60648a0161010090526101648a0190612e3492613fa2565b94612e3e90613bfe565b67ffffffffffffffff166084890152604401612e5990613baf565b73ffffffffffffffffffffffffffffffffffffffff1660a488015260c4870152612e8290613baf565b73ffffffffffffffffffffffffffffffffffffffff1660e4860152612ea7908461535e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c86840301610104870152612edc9291613fa2565b90612ee7908361535e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c85840301610124860152612f1c9291613fa2565b9060e48a01612f2a9161535e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c84840301610144850152612f5f9291613fa2565b8b602483015282604483015203925af180156126fd57908491612f85575b808080612c69565b81612f8f91613d4c565b610ada578238612f7d565b83612fa49161450c565b6120826040519283927f24eb47e5000000000000000000000000000000000000000000000000000000008452602060048501526024840191613fa2565b6024867f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b613026915060203d60201161083d5761082f8183613d4c565b38612c03565b7fa9902c7e000000000000000000000000000000000000000000000000000000008752600452602486fd5b6004867f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b613098915060203d60201161083d5761082f8183613d4c565b38612b6c565b60248573ffffffffffffffffffffffffffffffffffffffff6108658a61455d565b50346105d157806003193601126105d157602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b50346105d15760406003193601126105d1576004359067ffffffffffffffff82116105d157816004019061010060031984360301126105d157613134613ae8565b918160405161314281613cad565b5260648401359360c4810193613167613161612a6f612a68888761450c565b8761495b565b9460848301966131768861455d565b73ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116036135c657602484019477ffffffffffffffff000000000000000000000000000000006131dc8761457e565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156107b75788916135a7575b506107ed5767ffffffffffffffff6132708761457e565b16613288816000526007602052604060002054151590565b156107c257602073ffffffffffffffffffffffffffffffffffffffff60025416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa9081156107b7578891613588575b501561073a576132ff8661457e565b9361331560a48701956129f0612a68888661450c565b1561357e577fffffffff00000000000000000000000000000000000000000000000000000000169081156135635761335f896133508c61455d565b6133598a61457e565b906152ee565b73ffffffffffffffffffffffffffffffffffffffff600354169384613392575b50505050505060440191612c7b8361455d565b843b1561355f57868995938c959387938b6040519a8b998a9889977f6371157400000000000000000000000000000000000000000000000000000000895260048901606090526133e2878061535e565b60648b0161010090526101648b01906133fa92613fa2565b9461340490613bfe565b67ffffffffffffffff1660848a015260440161341f90613baf565b73ffffffffffffffffffffffffffffffffffffffff1660a489015260c488015261344890613baf565b73ffffffffffffffffffffffffffffffffffffffff1660e487015261346d908461535e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c878403016101048801526134a29291613fa2565b906134ad908361535e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c868403016101248701526134e29291613fa2565b9060e48b016134f09161535e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c858403016101448601526135259291613fa2565b908c6024840152604483015203925af180156126fd5761354a575b808080808061337f565b926135588160449395613d4c565b9290613540565b8880fd5b613579896135708c61455d565b612c438a61457e565b61335f565b612fa4858361450c565b6135a1915060203d60201161083d5761082f8183613d4c565b386132f0565b6135c0915060203d60201161083d5761082f8183613d4c565b38613259565b60248673ffffffffffffffffffffffffffffffffffffffff6108658b61455d565b50346105d157806003193601126105d157602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346105d15760406003193601126105d15761363f613be7565b6024359182151583036105d15761014061370261365c8585614489565b6136b260409392935180946fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565b60a08301906fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565bf35b50346105d15760206003193601126105d157602090613721613b46565b905073ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116146040519015158152f35b50346105d157806003193601126105d157602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346105d15760c06003193601126105d1576137d3613b46565b506137dc613bd0565b6137e4613b69565b50608435917fffffffff00000000000000000000000000000000000000000000000000000000831683036105d15760a4359067ffffffffffffffff82116105d15760a063ffffffff8061ffff61384988886138423660048b01613c13565b50506142d9565b95926040979194975197885216602087015216604085015216606083015215156080820152f35b50346105d157806003193601126105d15750611d44604051613893604082613d4c565b601781527f4275726e4d696e74546f6b656e506f6f6c20322e302e300000000000000000006020820152604051918291602083526020830190613c41565b50346105d15760c06003193601126105d1576138eb613b46565b6138f3613bd0565b906064357fffffffff00000000000000000000000000000000000000000000000000000000811681036107055760843567ffffffffffffffff8111610ff357613940903690600401613c13565b9160a435936002851015610fef5761395b9560443591613fe1565b90604051918291602083016020845282518091526020604085019301915b818110613987575050500390f35b825173ffffffffffffffffffffffffffffffffffffffff16845285945060209384019390920191600101613979565b9050346105df5760206003193601126105df576020907fffffffff000000000000000000000000000000000000000000000000000000006139f5613ab4565b167faff2afbf000000000000000000000000000000000000000000000000000000008114908115613a8a575b8115613a60575b8115613a36575b5015158152f35b7f01ffc9a70000000000000000000000000000000000000000000000000000000091501483613a2f565b7f0e64dd290000000000000000000000000000000000000000000000000000000081149150613a28565b7f940a15420000000000000000000000000000000000000000000000000000000081149150613a21565b600435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613ae357565b600080fd5b602435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613ae357565b604435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613ae357565b6004359073ffffffffffffffffffffffffffffffffffffffff82168203613ae357565b6064359073ffffffffffffffffffffffffffffffffffffffff82168203613ae357565b6024359073ffffffffffffffffffffffffffffffffffffffff82168203613ae357565b359073ffffffffffffffffffffffffffffffffffffffff82168203613ae357565b6024359067ffffffffffffffff82168203613ae357565b6004359067ffffffffffffffff82168203613ae357565b359067ffffffffffffffff82168203613ae357565b9181601f84011215613ae35782359167ffffffffffffffff8311613ae35760208381860195010111613ae357565b919082519283825260005b848110613c8b5750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8460006020809697860101520116010190565b80602080928401015182828601015201613c4c565b35908115158203613ae357565b6020810190811067ffffffffffffffff821117613cc957604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040810190811067ffffffffffffffff821117613cc957604052565b60a0810190811067ffffffffffffffff821117613cc957604052565b60e0810190811067ffffffffffffffff821117613cc957604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117613cc957604052565b92919267ffffffffffffffff8211613cc95760405191613dd5601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200184613d4c565b829481845281830111613ae3578281602093846000960137010152565b9080601f83011215613ae357816020613e0d93359101613d8d565b90565b906040600319830112613ae35760043567ffffffffffffffff81168103613ae357916024359067ffffffffffffffff8211613ae357613e5191600401613c13565b9091565b613e0d916020613e6e8351604084526040840190613c41565b920151906020818403910152613c41565b9181601f84011215613ae35782359167ffffffffffffffff8311613ae3576020808501948460051b010111613ae357565b9181601f84011215613ae35782359167ffffffffffffffff8311613ae3576020808501948460081b010111613ae357565b67ffffffffffffffff8111613cc95760051b60200190565b81810292918115918404141715613f0c57565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b8115613f45570490565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b91908203918211613f0c57565b519073ffffffffffffffffffffffffffffffffffffffff82168203613ae357565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0938186528686013760008582860101520116010190565b92959390919473ffffffffffffffffffffffffffffffffffffffff600354169586156142b757809760028710156142885773ffffffffffffffffffffffffffffffffffffffff98614142957fffffffff00000000000000000000000000000000000000000000000000000000938961425e5767ffffffffffffffff8216600052600b6020526040600020906040519161407983613d30565b549163ffffffff8316815263ffffffff8360201c16602082015263ffffffff8360401c16604082015263ffffffff8360601c16606082015260c061ffff8460801c169182608082015260ff60a082019561ffff8160901c16875260a01c161515918291015261420a575b50505067ffffffffffffffff905b6040519b8c997f06b859ef000000000000000000000000000000000000000000000000000000008b521660048a0152166024880152604487015216606485015260c0608485015260c4840191613fa2565b928180600095869560a483015203915afa9182156141fd57819261416557505090565b9091503d8083833e6141778183613d4c565b810190602081830312610ada5780519067ffffffffffffffff8211610705570181601f82011215610ada578051906141ae82613ee1565b936141bc6040519586613d4c565b82855260208086019360051b8301019384116105d15750602001905b8282106141e55750505090565b602080916141f284613f81565b8152019101906141d8565b50604051903d90823e3d90fd5b92935067ffffffffffffffff9285871615614246575061271061423561ffff61423c94511683613ef9565b0490613f74565b915b9038806140e3565b61425892506142356127109183613ef9565b9161423e565b67ffffffffffffffff9192506142829061427c612a6f36898b613d8d565b9061495b565b916140f1565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b50505050505050506040516142cd602082613d4c565b60008152600036813790565b67ffffffffffffffff909291926143177fffffffff0000000000000000000000000000000000000000000000000000000060025460401b1685614a68565b16600052600b60205260406000206040519061433282613d30565b549163ffffffff83169384835263ffffffff8460201c169384602085015263ffffffff8160401c169182604086015263ffffffff8260601c169081606087015261ffff8360801c169586608082015260ff61ffff8560901c16948560a084015260a01c16159060c082159101526143df577fffffffff00000000000000000000000000000000000000000000000000000000166143d457505093929190600190565b959493509160019150565b5050505092505050600090600090600090600090600090565b6040519061440582613d14565b60006080838281528260208201528260408201528260608201520152565b9060405161443081613d14565b60806001829460ff81546fffffffffffffffffffffffffffffffff8116865263ffffffff81861c16602087015260a01c161515604085015201546fffffffffffffffffffffffffffffffff81166060840152811c910152565b67ffffffffffffffff9161449b6143f8565b506144a46143f8565b506144d857166000526008602052604060002090613e0d6144cc60026144d16144cc86614423565b614b4f565b9401614423565b16908160005260046020526144f36144cc6040600020614423565b916000526005602052613e0d6144cc6040600020614423565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215613ae3570180359067ffffffffffffffff8211613ae357602001918136038313613ae357565b3573ffffffffffffffffffffffffffffffffffffffff81168103613ae35790565b3567ffffffffffffffff81168103613ae35790565b9067ffffffffffffffff613e0d92166000526008602052600560406000200190602081519101209060019160005201602052604060002054151590565b604051906145dd82613cf8565b60606020838281520152565b80518210156145fd5760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b90600182811c92168015614675575b602083101461464657565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b91607f169161463b565b90604051918260008254926146938461462c565b808452936001811690811561470157506001146146ba575b506146b892500383613d4c565b565b90506000929192526020600020906000915b8183106146e55750509060206146b892820101386146ab565b60209193508060019154838589010152019101909184926146cc565b602093506146b89592507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0091501682840152151560051b820101386146ab565b67ffffffffffffffff166000526008602052613e0d600460406000200161467f565b91908110156145fd5760081b0190565b358015158103613ae35790565b3561ffff81168103613ae35790565b3563ffffffff81168103613ae35790565b359063ffffffff82168203613ae357565b359061ffff82168203613ae357565b91908110156145fd5760051b0190565b35906fffffffffffffffffffffffffffffffff82168203613ae357565b9190826060910312613ae3576040516060810181811067ffffffffffffffff821117613cc957604052604061484081839561482781613ca0565b8552614835602082016147d0565b6020860152016147d0565b910152565b6fffffffffffffffffffffffffffffffff6148836040809361486681613ca0565b1515865283614877602083016147d0565b166020870152016147d0565b16910152565b818110614894575050565b60008155600101614889565b80518015614910576020036148d2578051602082810191830183900312613ae357519060ff82116148d2575060ff1690565b612082906040519182917f953576f7000000000000000000000000000000000000000000000000000000008352602060048401526024830190613c41565b50507f000000000000000000000000000000000000000000000000000000000000000090565b9060ff8091169116039060ff8211613f0c57565b60ff16604d8111613f0c57600a0a90565b907f00000000000000000000000000000000000000000000000000000000000000009060ff82169060ff811692828414614a6157828411614a3757906149a091614936565b91604d60ff84161180156149fe575b6149c8575050906149c2613e0d9261494a565b90613ef9565b9091507fa9cb113d0000000000000000000000000000000000000000000000000000000060005260045260245260445260646000fd5b50614a088361494a565b8015613f45577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0484116149af565b614a4091614936565b91604d60ff8416116149c857505090614a5b613e0d9261494a565b90613f3b565b5050505090565b7fffffffff000000000000000000000000000000000000000000000000000000008116908115614b4a57614a9b8161519a565b7dffff00000000000000000000000000000000000000000000000000000000601082811c9085901c1616614b4a5761ffff8360e01c168015918215614b39575b5050614ae5575050565b7fffffffff0000000000000000000000000000000000000000000000000000000092507fdf63778f000000000000000000000000000000000000000000000000000000006000526004521660245260446000fd5b60e01c61ffff161090503880614adb565b505050565b614b576143f8565b506fffffffffffffffffffffffffffffffff6060820151166fffffffffffffffffffffffffffffffff8083511691614bb46020850193614bae614ba163ffffffff87511642613f74565b8560808901511690613ef9565b9061518d565b80821015614bcd57505b16825263ffffffff4216905290565b9050614bbe565b90816020910312613ae357518015158103613ae35790565b73ffffffffffffffffffffffffffffffffffffffff600154163303614c0d57565b7f2b5c74de0000000000000000000000000000000000000000000000000000000060005260046000fd5b90805115614e775767ffffffffffffffff81516020830120921691826000526008602052614c6c8160056040600020016157f1565b15614e335760005260096020526040600020815167ffffffffffffffff8111613cc957614c99825461462c565b601f8111614e01575b506020601f8211600114614d3b5791614d15827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea9593614d2b95600091614d30575b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b9055604051918291602083526020830190613c41565b0390a2565b905084015138614ce4565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe082169083600052806000209160005b818110614de9575092614d2b9492600192827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea989610614db2575b5050811b019055611d30565b8501517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690553880614da6565b9192602060018192868a015181550194019201614d6b565b614e2d90836000526020600020601f840160051c81019160208510610f2a57601f0160051c0190614889565b38614ca2565b50906120826040519283927f393b8ad20000000000000000000000000000000000000000000000000000000084526004840152604060248401526044830190613c41565b7f14c880ca0000000000000000000000000000000000000000000000000000000060005260046000fd5b60405160ff7f000000000000000000000000000000000000000000000000000000000000000016602082015260208152613e0d604082613d4c565b81519192911561505e576fffffffffffffffffffffffffffffffff6040840151166fffffffffffffffffffffffffffffffff60208501511610614ffb576146b891925b805182547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff0000000000000000000000000000000000000000161782556020810151825460409290920151608090811b7fffffffffffffffffffffffffffffffff00000000000000000000000000000000166fffffffffffffffffffffffffffffffff9290921691821760018501557fffffffffffffffffffffffff0000000000000000000000000000000000000000909216174290911b73ffffffff0000000000000000000000000000000016179055565b60648361505c604051917f8020d12400000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565bfd5b6fffffffffffffffffffffffffffffffff604084015116158015906150ed575b61508c576146b89192614f1f565b60648361505c604051917fd68af9cc00000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b506fffffffffffffffffffffffffffffffff602084015116151561507e565b906127109167ffffffffffffffff6151266020830161457e565b166000908152600b60205260409020917fffffffff00000000000000000000000000000000000000000000000000000000161561517757606061ffff615173935460901c16910135613ef9565b0490565b606061ffff615173935460801c16910135613ef9565b91908201809211613f0c57565b7fffffffff000000000000000000000000000000000000000000000000000000008116908115615271577dffff000000000000000000000000000000000000000000000000000000008116156152685760ff60015b169060f01c80615232575b506001036152055750565b7fc512f96c0000000000000000000000000000000000000000000000000000000060005260045260246000fd5b60005b6010811061524357506151fa565b6001811b8216615256575b600101615235565b9160018101809111613f0c579161524e565b60ff60006151ef565b5050565b9167ffffffffffffffff7f50f6fbee3ceedce6b7fd7eaef18244487867e6718aec7208187efb6b7908c14c9216928360005260086020526152be81836002604060002001615846565b6040805173ffffffffffffffffffffffffffffffffffffffff909216825260208201929092529081908101614d2b565b91909167ffffffffffffffff83169283600052600560205260ff60406000205460a01c16156153535750907fc6735cd4fa2bbe7b203b1682936e6ee61bc1702464bbbd12abb6630229d9a5f9918360005260056020526152be81836040600020615846565b906146b89350615275565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe182360301811215613ae357016020813591019167ffffffffffffffff8211613ae3578136038313613ae357565b9167ffffffffffffffff7fff0133389f9bb82d5b9385826160eaf2328039f6fa950eeb8cf0836da81789449216928360005260086020526152be81836040600020615846565b91909167ffffffffffffffff83169283600052600460205260ff60406000205460a01c16156154595750907f28d6c52e2b0b7587b0d195539fbe6af984b28791aca4d2cc0844244e38bce29e918360005260046020526152be81836040600020615846565b906146b893506153ae565b906040519182815491828252602082019060005260206000209260005b8181106154965750506146b892500383613d4c565b8454835260019485019487945060209093019201615481565b80548210156145fd5760005260206000200190600090565b6000818152600760205260409020548015615656577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111613f0c57600654907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211613f0c578181036155e7575b50505060065480156155b8577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff016155758160066154af565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b19169055600655600052600760205260006040812055600190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b61563e6155f86156099360066154af565b90549060031b1c92839260066154af565b81939154907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060031b92831b921b19161790565b9055600052600760205260406000205538808061553c565b5050600090565b9060018201918160005282602052604060002054801515600014615788577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111613f0c578254907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211613f0c57818103615751575b505050805480156155b8577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff019061571282826154af565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b191690555560005260205260006040812055600190565b61577161576161560993866154af565b90549060031b1c928392866154af565b9055600052836020526040600020553880806156da565b50505050600090565b806000526007602052604060002054156000146157eb5760065468010000000000000000811015613cc9576157d261560982600185940160065560066154af565b9055600654906000526007602052604060002055600190565b50600090565b60008281526001820160205260409020546156565780549068010000000000000000821015613cc9578261582f6156098460018096018555846154af565b905580549260005201602052604060002055600190565b9182549060ff8260a01c16158015615afa575b615af4576fffffffffffffffffffffffffffffffff8216916001850190815461589e63ffffffff6fffffffffffffffffffffffffffffffff83169360801c1642613f74565b9081615a56575b5050848110615a0a57508383106158ff5750506158d46fffffffffffffffffffffffffffffffff928392613f74565b16167fffffffffffffffffffffffffffffffff00000000000000000000000000000000825416179055565b9190915460801c92831561599e578161591791613f74565b927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff810190808211613f0c5761596561596a9273ffffffffffffffffffffffffffffffffffffffff9661518d565b613f3b565b7fd0c8d23a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b73ffffffffffffffffffffffffffffffffffffffff83837fd0c8d23a000000000000000000000000000000000000000000000000000000006000527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6004526024521660445260646000fd5b828573ffffffffffffffffffffffffffffffffffffffff927f1a76572a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b828692939611615aca57615a7192614bae9160801c90613ef9565b80841015615ac55750825b85547fffffffffffffffffffffffff00000000ffffffffffffffffffffffffffffffff164260801b73ffffffff00000000000000000000000000000000161786559238806158a5565b615a7c565b7f9725942a0000000000000000000000000000000000000000000000000000000060005260046000fd5b50505050565b50821561585956fea164736f6c634300081a000a' diff --git a/ccip-sdk/src/cct/evm/token-pool/bytecodes/ERC20LockBox.ts b/ccip-sdk/src/cct/evm/token-pool/bytecodes/ERC20LockBox.ts new file mode 100644 index 00000000..f23e3c66 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/bytecodes/ERC20LockBox.ts @@ -0,0 +1,8 @@ +/** + * ERC20LockBox (v2.0.0) deployment bytecode. Lazy-loaded via dynamic import(). + * + * Source: chainlink-ccip (refs/heads/main) + * chains/evm/gobindings/generated/v2_0_0/erc20_lock_box/erc20_lock_box.go + */ +export const ERC20_LOCK_BOX_BYTECODE = + '0x60a0604052346101d9576113cf6020813803918261001c816101de565b9384928339810103126101d957516001600160a01b038116908190036101d957602090610048826101de565b9160008352600036813733156101c857600180546001600160a01b03191633179055610073816101de565b60008152600036813760408051949085016001600160401b038111868210176101b2576040528452808285015260005b815181101561010a576001906001600160a01b036100c18285610203565b5116846100cd82610245565b6100da575b5050016100a3565b7fc3803387881faad271c47728894e3e36fac830ffc8602ca6fc07733cbda7758091604051908152a138846100d2565b5050915160005b8151811015610182576001600160a01b0361012c8284610203565b5116908115610171577feb1b9b92e50b7f88f9ff25d56765095ac6e91540eee214906f4036a908ffbdef8583610163600195610343565b50604051908152a101610111565b6342bcdf7f60e11b60005260046000fd5b8280156101715760805260405161102b90816103a482396080518181816105f6015281816109960152610c060152f35b634e487b7160e01b600052604160045260246000fd5b639b15e16f60e01b60005260046000fd5b600080fd5b6040519190601f01601f191682016001600160401b038111838210176101b257604052565b80518210156102175760209160051b010190565b634e487b7160e01b600052603260045260246000fd5b80548210156102175760005260206000200190600090565b600081815260036020526040902054801561033c57600019810181811161032657600254600019810191908211610326578082036102d5575b50505060025480156102bf576000190161029981600261022d565b8154906000199060031b1b19169055600255600052600360205260006040812055600190565b634e487b7160e01b600052603160045260246000fd5b61030e6102e66102f793600261022d565b90549060031b1c928392600261022d565b819391549060031b91821b91600019901b19161790565b9055600052600360205260406000205538808061027e565b634e487b7160e01b600052601160045260246000fd5b5050600090565b8060005260036020526040600020541560001461039d57600254680100000000000000008110156101b2576103846102f7826001859401600255600261022d565b9055600254906000526003602052604060002055600190565b5060009056fe608080604052600436101561001357600080fd5b60003560e01c908163181f5a77146109ba5750806321df0da71461094b5780632451a6271461085d57806374fd18ac1461061b57806375151b631461058c57806379ba5097146104a35780638da5cb5b1461045157806391a2749a14610267578063a36a7fee146101825763f2fde38b1461008d57600080fd5b3461017d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d5773ffffffffffffffffffffffffffffffffffffffff6100d9610a89565b6100e1610cc8565b1633811461015357807fffffffffffffffffffffffff0000000000000000000000000000000000000000600054161760005573ffffffffffffffffffffffffffffffffffffffff600154167fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae1278600080a3005b7fdad89dca0000000000000000000000000000000000000000000000000000000060005260046000fd5b600080fd5b3461017d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d576101b9610a89565b6101c1610aac565b5073ffffffffffffffffffffffffffffffffffffffff604435916101e58382610bd3565b166102396040517f23b872dd00000000000000000000000000000000000000000000000000000000602082015233602482015230604482015283606482015260648152610233608482610b0e565b82610d56565b6040519182527f5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f6260203393a3005b3461017d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d5760043567ffffffffffffffff811161017d5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc823603011261017d57604051906102e182610ac3565b806004013567ffffffffffffffff811161017d576103059060043691840101610b4f565b825260248101359067ffffffffffffffff821161017d57600461032b9236920101610b4f565b6020820190815261033a610cc8565b519060005b82518110156103b2578073ffffffffffffffffffffffffffffffffffffffff61036a60019386610d13565b511661037581610df9565b610381575b500161033f565b60207fc3803387881faad271c47728894e3e36fac830ffc8602ca6fc07733cbda7758091604051908152a18461037a565b505160005b815181101561044f5773ffffffffffffffffffffffffffffffffffffffff6103df8284610d13565b5116908115610425577feb1b9b92e50b7f88f9ff25d56765095ac6e91540eee214906f4036a908ffbdef602083610417600195610fbe565b50604051908152a1016103b7565b7f8579befe0000000000000000000000000000000000000000000000000000000060005260046000fd5b005b3461017d5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d57602073ffffffffffffffffffffffffffffffffffffffff60015416604051908152f35b3461017d5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d5760005473ffffffffffffffffffffffffffffffffffffffff81163303610562577fffffffffffffffffffffffff00000000000000000000000000000000000000006001549133828416176001551660005573ffffffffffffffffffffffffffffffffffffffff3391167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0600080a3005b7f02b543c60000000000000000000000000000000000000000000000000000000060005260046000fd5b3461017d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d5760206105c5610a89565b73ffffffffffffffffffffffffffffffffffffffff604051911673ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016148152f35b3461017d5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d57610652610a89565b61065a610aac565b506044356064359173ffffffffffffffffffffffffffffffffffffffff831680930361017d57819061068c8382610bd3565b83156108335773ffffffffffffffffffffffffffffffffffffffff1691604051907f70a08231000000000000000000000000000000000000000000000000000000008252306004830152602082602481875afa918215610827576000926107d0575b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146107c8575b808211610797575060207f2717ead6b9200dd235aad468c9809ea400fe33ac69b5bfaa6d3e90fc922b63989161078e6040517fa9059cbb000000000000000000000000000000000000000000000000000000008482015286602482015282604482015260448152610788606482610b0e565b85610d56565b604051908152a3005b907fcf4791810000000000000000000000000000000000000000000000000000000060005260045260245260446000fd5b905080610716565b90916020823d60201161081f575b816107eb60209383610b0e565b8101031261081c575051907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6106ee565b80fd5b3d91506107de565b6040513d6000823e3d90fd5b7fd87070520000000000000000000000000000000000000000000000000000000060005260046000fd5b3461017d5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d576040518060206002549283815201809260026000527f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace9060005b81811061093557505050816108dc910382610b0e565b6040519182916020830190602084525180915260408301919060005b818110610906575050500390f35b825173ffffffffffffffffffffffffffffffffffffffff168452859450602093840193909201916001016108f8565b82548452602090930192600192830192016108c6565b3461017d5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b3461017d5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d576109f281610ac3565b601281527f45524332304c6f636b426f7820322e302e300000000000000000000000000000602082015260405190602082528181519182602083015260005b838110610a715750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f836000604080968601015201168101030190f35b60208282018101516040878401015285935001610a31565b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361017d57565b6024359067ffffffffffffffff8216820361017d57565b6040810190811067ffffffffffffffff821117610adf57604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117610adf57604052565b81601f8201121561017d5780359167ffffffffffffffff8311610adf578260051b9160405193610b826020850186610b0e565b845260208085019382010191821161017d57602001915b818310610ba65750505090565b823573ffffffffffffffffffffffffffffffffffffffff8116810361017d57815260209283019201610b99565b9015610c9e5773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168103610c71575033600052600360205260406000205415610c4357565b7fd86ad9cf000000000000000000000000000000000000000000000000000000006000523360045260246000fd5b7fbf16aab60000000000000000000000000000000000000000000000000000000060005260045260246000fd5b7f8b1fa9dd0000000000000000000000000000000000000000000000000000000060005260046000fd5b73ffffffffffffffffffffffffffffffffffffffff600154163303610ce957565b7f2b5c74de0000000000000000000000000000000000000000000000000000000060005260046000fd5b8051821015610d275760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b906000602091828151910182855af115610827576000513d610dd8575073ffffffffffffffffffffffffffffffffffffffff81163b155b610d945750565b73ffffffffffffffffffffffffffffffffffffffff907f5274afe7000000000000000000000000000000000000000000000000000000006000521660045260246000fd5b60011415610d8d565b8054821015610d275760005260206000200190600090565b6000818152600360205260409020548015610fb7577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111610f8857600254907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211610f8857808203610f19575b5050506002548015610eea577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01610ea7816002610de1565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b19169055600255600052600360205260006040812055600190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b610f70610f2a610f3b936002610de1565b90549060031b1c9283926002610de1565b81939154907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060031b92831b921b19161790565b90556000526003602052604060002055388080610e6e565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b5050600090565b806000526003602052604060002054156000146110185760025468010000000000000000811015610adf57610fff610f3b8260018594016002556002610de1565b9055600254906000526003602052604060002055600190565b5060009056fea164736f6c634300081a000a' diff --git a/ccip-sdk/src/cct/evm/token-pool/bytecodes/LockReleaseTokenPool.ts b/ccip-sdk/src/cct/evm/token-pool/bytecodes/LockReleaseTokenPool.ts new file mode 100644 index 00000000..8c37fd28 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/bytecodes/LockReleaseTokenPool.ts @@ -0,0 +1,8 @@ +/** + * LockReleaseTokenPool (v2.0.0) deployment bytecode. Lazy-loaded via dynamic import(). + * + * Source: chainlink-ccip (refs/heads/main) + * chains/evm/gobindings/generated/v2_0_0/lock_release_token_pool/lock_release_token_pool.go + */ +export const LOCK_RELEASE_TOKEN_POOL_BYTECODE = + '0x610100806040523461037a5760c081616038803803809161002082856103ba565b83398101031261037a5780516001600160a01b0381169182820361037a5761004a602082016103f3565b9061005760408201610401565b61006360608301610401565b9261007c60a061007560808601610401565b9401610401565b9333156103a957600180546001600160a01b0319163317905586158015610398575b8015610387575b6102df578560805260c0523086036102f0575b60a052600380546001600160a01b03199081166001600160a01b03938416179091556002805490911692821692909217909155169182156102df576040516375151b6360e01b815260048101829052602081602481875afa9081156102d357600091610291575b501561027d57604051906020600081840163095ea7b360e01b815286602486015281196044860152604485526101566064866103ba565b84519082875af1903d600051908361025e575b50505015610219575b8260e052604051615bc79081610471823960805181818161024a015281816121d3015281816129c701528181612c3a015281816130e8015281816137140152818161376e0152614f0c015260a0518181816135da015281816148ed015281816149370152614f60015260c0518181816102e50152818161134d0152818161226d01528181612a620152613183015260e0518181816126cf01528181612bc10152614e910152f35b6102579161025260405163095ea7b360e01b6020820152856024820152600060448201526044815261024c6064826103ba565b82610415565b610415565b3880610172565b9192509061027357503b15155b388080610169565b600191501461026b565b63961c9a4f60e01b60005260045260246000fd5b6020813d6020116102cb575b816102aa602093836103ba565b810103126102c757519081151582036102c457503861011f565b80fd5b5080fd5b3d915061029d565b6040513d6000823e3d90fd5b630a64406560e11b60005260046000fd5b60405163313ce56760e01b81526020816004818a5afa60009181610346575b5061031b575b506100b8565b60ff1660ff821681810361032f5750610315565b6332ad3e0760e11b60005260045260245260446000fd5b9091506020813d60201161037f575b81610362602093836103ba565b8101031261037a57610373906103f3565b903861030f565b600080fd5b3d9150610355565b506001600160a01b038116156100a5565b506001600160a01b0384161561009e565b639b15e16f60e01b60005260046000fd5b601f909101601f19168101906001600160401b038211908210176103dd57604052565b634e487b7160e01b600052604160045260246000fd5b519060ff8216820361037a57565b51906001600160a01b038216820361037a57565b906000602091828151910182855af1156102d3576000513d61046757506001600160a01b0381163b155b6104465750565b635274afe760e01b60009081526001600160a01b0391909116600452602490fd5b6001141561043f56fe608080604052600436101561001357600080fd5b600090813560e01c90816301ffc9a71461398f5750806306b859ef146138aa578063181f5a77146138495780631826b1e71461379257806321df0da714613741578063240028e8146136dd5780632422ac45146135fe57806324f65ee7146135c05780632cab0fb61461304d57806337a3210d14613019578063390775371461291c5780634c5ef0ed146128d557806362ddd3c41461284e5780637437ff9f1461280057806379ba5097146127395780638926f54f146126f35780638c6894fb146126a25780638da5cb5b1461266e5780639a4575b91461215a578063a42a7b8b14611ff3578063acfecf9114611efb578063ae39a25714611d70578063b6cfa3b714611cb5578063b794658014611c7d578063bfeffd3f14611bd1578063c4bffe2b14611aa6578063c7230a60146117f5578063dc04fa1f14611371578063dc0bd97114611320578063dcbd41bc1461111c578063e8a1da1714610a44578063ea6396db14610906578063ec6ae7a7146108c3578063f2fde38b146107f45763fbc801a7146101a257600080fd5b34610668576060600319360112610668576004359067ffffffffffffffff8211610668578160040160a060031984360301126107f0576101e0613ac1565b9160443567ffffffffffffffff81116107f0579061020661022393923690600401613bec565b93906102106145a9565b5061021b86856151c4565b943691613d66565b93608486019461023286614536565b73ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116036107a657602487019677ffffffffffffffff0000000000000000000000000000000061029889614557565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115610719578591610777575b5061074f5767ffffffffffffffff61032c89614557565b16610344816000526007602052604060002054151590565b1561072457602073ffffffffffffffffffffffffffffffffffffffff60025416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa80156107195785906106c8575b73ffffffffffffffffffffffffffffffffffffffff915016330361069c576064810135946103d38787613f4d565b7fffffffff00000000000000000000000000000000000000000000000000000000851694851561067a5761042f907fffffffff0000000000000000000000000000000000000000000000000000000060025460401b1690614a41565b61044b8161043c8b614536565b6104458d614557565b906154ac565b73ffffffffffffffffffffffffffffffffffffffff600354169384610549575b61053f8a61050e6105098e6104808e8e613f4d565b936104938561048e84614557565b614e7a565b7ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff6104cf6104c985614557565b93614536565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252336020830152810188905292169180606081015b0390a2614557565b61471a565b90610517614f59565b6040519261052484613cd1565b83526020830152604051928392604084526040840190613e2e565b9060208301520390f35b843b15610676578694928a949286928d604051998a98899788967fa8027c0f00000000000000000000000000000000000000000000000000000000885260048801608090528061059891615416565b6084890160a090526101248901906105af92613f7b565b936105b990613bd7565b67ffffffffffffffff1660a48801526044016105d490613b88565b73ffffffffffffffffffffffffffffffffffffffff1660c48701528d60e48701526105fe90613b88565b73ffffffffffffffffffffffffffffffffffffffff16610104860152602485015283810360031901604485015261063491613c1a565b90606483015203925af1801561066b57610653575b808080808061046b565b61065e828092613d25565b6106685780610649565b80fd5b6040513d84823e3d90fd5b8680fd5b50610697816106888b614536565b6106918d614557565b90615466565b61044b565b6024847f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b506020813d602011610711575b816106e260209383613d25565b8101031261070d5761070873ffffffffffffffffffffffffffffffffffffffff91613f5a565b6103a5565b8480fd5b3d91506106d5565b6040513d87823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008552600452602484fd5b6004847f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b610799915060203d60201161079f575b6107918183613d25565b810190614bad565b38610315565b503d610787565b60248373ffffffffffffffffffffffffffffffffffffffff6107c789614536565b7f961c9a4f00000000000000000000000000000000000000000000000000000000835216600452fd5b5080fd5b50346106685760206003193601126106685773ffffffffffffffffffffffffffffffffffffffff610823613b1f565b61082b614bc5565b1633811461089b57807fffffffffffffffffffffffff000000000000000000000000000000000000000083541617825573ffffffffffffffffffffffffffffffffffffffff600154167fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12788380a380f35b6004827fdad89dca000000000000000000000000000000000000000000000000000000008152fd5b503461066857806003193601126106685760207fffffffff0000000000000000000000000000000000000000000000000000000060025460401b16604051908152f35b503461066857608060031936011261066857610920613b1f565b50610929613ba9565b610931613af0565b5060643567ffffffffffffffff8111610a40579167ffffffffffffffff60409261096160e0953690600401613bec565b50508260c0855161097181613d09565b82815282602082015282878201528260608201528260808201528260a08201520152168152600b60205220604051906109a982613d09565b5461ffff818163ffffffff82169485815263ffffffff60208201818560201c1681528160408401818760401c168152816060860193818960601c16855260ff60c060808901988a8c60801c168a528a60a082019c60901c168c52019b60a01c1615158b526040519b8c52511660208b0152511660408901525116606087015251166080850152511660a083015251151560c0820152f35b8280fd5b50346106685760406003193601126106685760043567ffffffffffffffff81116107f057610a76903690600401613e58565b9060243567ffffffffffffffff81116111185790610a9984923690600401613e58565b939091610aa4614bc5565b83905b828210610f595750505081927ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee182360301935b81811015610f55578060051b8301358581121561070d5783016101208136031261070d5760405194610b0b86613ced565b610b1482613bd7565b8652602082013567ffffffffffffffff81116107f05782019436601f870112156107f057853595610b4487613eba565b96610b526040519889613d25565b80885260208089019160051b8301019036821161070d5760208301905b828210610f26575050505060208701958652604083013567ffffffffffffffff8111610a4057610ba29036908501613dcb565b9160408801928352610bcc610bba36606087016147c6565b9460608a0195865260c03691016147c6565b956080890196875283515115610efe57610bf067ffffffffffffffff8a5116615849565b15610ec75767ffffffffffffffff8951168252600860205260408220610c17865182614f94565b610c25885160028301614f94565b6004855191019080519067ffffffffffffffff8211610e9a57610c488354614605565b601f8111610e5f575b50602090601f8311600114610dc057610c9f9291869183610db5575b50507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b90555b815b88518051821015610cd95790610cd3600192610ccc8367ffffffffffffffff8f5116926145c2565b5190614c10565b01610ca4565b5050977f8d340f17e19058004c20453540862a9c62778504476f6756755cb33bcd6c38c2939199975095610da767ffffffffffffffff6001979694985116925193519151610d73610d3e60405196879687526101006020880152610100870190613c1a565b9360408601906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b60a08401906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b0390a1019392909193610ada565b015190508e80610c6d565b83865281862091907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08416875b818110610e475750908460019594939210610e10575b505050811b019055610ca2565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690558d8080610e03565b92936020600181928786015181550195019301610ded565b610e8a9084875260208720601f850160051c81019160208610610e90575b601f0160051c0190614862565b8d610c51565b9091508190610e7d565b6024857f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b60248267ffffffffffffffff8b51167f1d5ad3c5000000000000000000000000000000000000000000000000000000008252600452fd5b6004827f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b813567ffffffffffffffff811161067657602091610f4a8392833691890101613dcb565b815201910190610b6f565b8380f35b9267ffffffffffffffff610f7b610f768486889a9699979a614799565b614557565b1691610f868361557f565b156110ec578284526008602052610fa26005604086200161551c565b94845b8651811015610fdb576001908587526008602052610fd460056040892001610fcd838b6145c2565b5190615715565b5001610fa5565b50939692909450949094808752600860205260056040882088815588600182015588600282015588600382015588600482016110178154614605565b806110ab575b505050018054908881558161108d575b5050907f5204aec90a3c794d8e90fded8b46ae9c7c552803e7e832e0c1d358396d8599166020836001948a52600482528985604082208281550155808a52600582528985604082208281550155604051908152a101909194939294610aa7565b885260208820908101905b8181101561102d57888155600101611098565b601f81116001146110c15750555b888a8061101d565b818352602083206110dc91601f01861c810190600101614862565b80825281602081209155556110b9565b602484847f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b8380fd5b50346106685760206003193601126106685760043567ffffffffffffffff81116107f05761114e903690600401613e89565b73ffffffffffffffffffffffffffffffffffffffff600a5416331415806112fe575b6112d257825b818110611181578380f35b61118c81838561473c565b67ffffffffffffffff61119e82614557565b16906111b7826000526007602052604060002054151590565b156112a657907f41f7c8f7cfdad9350aa495e6c54cbbf750a07ab38a9098aed1256e30dd1682bb60e083611266611240602060019897018b6111f88261474c565b1561126d57879052600460205261121f60408d2061121936604088016147c6565b90614f94565b868c52600560205261123b60408d206112193660a088016147c6565b61474c565b916040519215158352611259602084016040830161481e565b60a060808401910161481e565ba201611176565b60026040828a61123b9452600860205261128f82822061121936858c016147c6565b8a8152600860205220016112193660a088016147c6565b602486837f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b6024837f8e4a23d600000000000000000000000000000000000000000000000000000000815233600452fd5b5073ffffffffffffffffffffffffffffffffffffffff60015416331415611170565b5034610668578060031936011261066857602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346106685760406003193601126106685760043567ffffffffffffffff81116107f0576113a3903690600401613e89565b60243567ffffffffffffffff8111611118576113c3903690600401613e58565b9190926113ce614bc5565b845b82811061143a57505050825b8181106113e7578380f35b8067ffffffffffffffff611401610f766001948688614799565b16808652600b6020528560408120557f5479bbc0288b7eaeaf2ace0943b88016cc648964fcd42919a86fd93b15fdbee88680a2016113dc565b67ffffffffffffffff611451610f7683868661473c565b16611469816000526007602052604060002054151590565b156117ca5761147982858561473c565b602081019060e081019061148c8261474c565b1561179e5760a0810161271061ffff6114a483614759565b16101561178f5760c082019161271061ffff6114bf85614759565b1610156117575763ffffffff6114d486614768565b161561172b57858c52600b60205260408c206114ef86614768565b63ffffffff1690805490604084019161150783614768565b60201b67ffffffff000000001693606086019461152386614768565b60401b6bffffffff000000000000000016966080019661154288614768565b60601b6fffffffff00000000000000000000000016916115618a614759565b60801b71ffff0000000000000000000000000000000016936115828c614759565b60901b73ffff00000000000000000000000000000000000016957fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016177fffffffffffffffffffffffffffffffff00000000ffffffffffffffffffffffff16177fffffffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffff16177fffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffffffff1617171781556116358761474c565b81547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff0000000000000000000000000000000000000000161790556040519661168690614779565b63ffffffff16875261169790614779565b63ffffffff1660208701526116ab90614779565b63ffffffff1660408601526116bf90614779565b63ffffffff1660608501526116d39061478a565b61ffff1660808401526116e59061478a565b61ffff1660a08301526116f790613c79565b151560c082015260e07ffae1e296719dac5269c3886fb5002bb29bf17ae403060c6eb063a55abaaa104191a26001016113d0565b60248c877f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b60248c61ffff61176686614759565b7f95f3517a00000000000000000000000000000000000000000000000000000000835216600452fd5b8a61ffff611766602493614759565b60248a857f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b7f1e670e4b000000000000000000000000000000000000000000000000000000008752600452602486fd5b50346106685760406003193601126106685760043567ffffffffffffffff81116107f057611827903690600401613e58565b90611830613b65565b9173ffffffffffffffffffffffffffffffffffffffff6001541633141580611a84575b611a585773ffffffffffffffffffffffffffffffffffffffff8316908115611a3057845b818110611882578580f35b73ffffffffffffffffffffffffffffffffffffffff6118aa6118a5838588614799565b614536565b166040517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152602081602481855afa908115611a255788916119f2575b50806118ff575b5050600101611877565b6040517fa9059cbb00000000000000000000000000000000000000000000000000000000602080830191825273ffffffffffffffffffffffffffffffffffffffff8a16602484015260448084018590528352918a9190611960606482613d25565b519082865af1156119e75787513d6119de5750813b155b6119b25790847f508d7d183612c18fc339b42618912b9fa3239f631dd7ec0671f950200a0fa66e602060019594604051908152a390386118f5565b602488837f5274afe7000000000000000000000000000000000000000000000000000000008252600452fd5b60011415611977565b6040513d89823e3d90fd5b905060203d8111611a1e575b611a088183613d25565b60208260009281010312610668575051386118ee565b503d6119fe565b6040513d8a823e3d90fd5b6004857f8579befe000000000000000000000000000000000000000000000000000000008152fd5b6024847fcb1afbd700000000000000000000000000000000000000000000000000000000815233600452fd5b5073ffffffffffffffffffffffffffffffffffffffff600c5416331415611853565b5034610668578060031936011261066857604051906006548083528260208101600684526020842092845b818110611bb8575050611ae692500383613d25565b8151611b0a611af482613eba565b91611b026040519384613d25565b808352613eba565b917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0602083019301368437805b8451811015611b69578067ffffffffffffffff611b56600193886145c2565b5116611b6282866145c2565b5201611b37565b50925090604051928392602084019060208552518091526040840192915b818110611b95575050500390f35b825167ffffffffffffffff16845285945060209384019390920191600101611b87565b8454835260019485019487945060209093019201611ad1565b50346106685760206003193601126106685760043573ffffffffffffffffffffffffffffffffffffffff81168091036107f057611c0c614bc5565b7fffffffffffffffffffffffff00000000000000000000000000000000000000006003547fbaff46844acf36d6ee996f489a1a288709c4542bd33cd557770afd267d5812096040805173ffffffffffffffffffffffffffffffffffffffff84168152856020820152a1161760035580f35b503461066857602060031936011261066857611cb1611c9d610509613bc0565b604051918291602083526020830190613c1a565b0390f35b5034610668576020600319360112610668577f307cf716eade81675bea3ccb6917b0f91baa2160056765d9a83d76f819caf06a6020611cf2613a8d565b611cfa614bc5565b6002547fffffffffffffffff00000000ffffffffffffffffffffffffffffffffffffffff77ffffffff0000000000000000000000000000000000000000808460401c16169116176002557fffffffff0000000000000000000000000000000000000000000000000000000060405191168152a180f35b503461066857606060031936011261066857611d8a613b1f565b90611d93613b65565b6044359273ffffffffffffffffffffffffffffffffffffffff841680850361111857611dbd614bc5565b73ffffffffffffffffffffffffffffffffffffffff82168015611ed35794611ecd917f3f1036e85d016a93254a0b1415844f79b85424959d90ae5ad51ce8f4533fe70195967fffffffffffffffffffffffff0000000000000000000000000000000000000000600254161760025573ffffffffffffffffffffffffffffffffffffffff85167fffffffffffffffffffffffff0000000000000000000000000000000000000000600a541617600a557fffffffffffffffffffffffff0000000000000000000000000000000000000000600c541617600c556040519384938491604091949373ffffffffffffffffffffffffffffffffffffffff809281606087019816865216602085015216910152565b0390a180f35b6004857f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b50346106685767ffffffffffffffff611f1336613de9565b929091611f1e614bc5565b1691611f37836000526007602052604060002054151590565b156110ec578284526008602052611f6660056040862001611f59368486613d66565b6020815191012090615715565b15611fab57907f52d00ee4d9bd51b40168f2afc5848837288ce258784ad914278791464b3f4d7691611fa5604051928392602084526020840191613f7b565b0390a280f35b82611fef836040519384937f74f23c7c0000000000000000000000000000000000000000000000000000000085526004850152604060248501526044840191613f7b565b0390fd5b50346106685760206003193601126106685767ffffffffffffffff612016613bc0565b168152600860205261202d6005604083200161551c565b80517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe061207261205c83613eba565b9261206a6040519485613d25565b808452613eba565b01835b818110612149575050825b82518110156120c65780612096600192856145c2565b51855260096020526120aa60408620614658565b6120b482856145c2565b526120bf81846145c2565b5001612080565b81846040519182916020830160208452825180915260408401602060408360051b870101940192905b8282106120fe57505050500390f35b91936020612139827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc060019597998495030186528851613c1a565b96019201920185949391926120ef565b806060602080938601015201612075565b50346106685760206003193601126106685760043567ffffffffffffffff81116107f057806004019060a06003198236030112610a40576121996145a9565b506040516020936121aa8583613d25565b80825260848301916121bb83614536565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361264d57602484019477ffffffffffffffff0000000000000000000000000000000061222187614557565b60801b16604051907f2cbc26bb0000000000000000000000000000000000000000000000000000000082526004820152878160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156125d2578491612630575b506126085767ffffffffffffffff6122b487614557565b166122cc816000526007602052604060002054151590565b156125dd578773ffffffffffffffffffffffffffffffffffffffff60025416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa80156125d257849061258a575b73ffffffffffffffffffffffffffffffffffffffff915016330361255e576064850135946123668661235d87614536565b6106918a614557565b73ffffffffffffffffffffffffffffffffffffffff600354169182612443575b886124136105098a8a7ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff8c6123c78461048e87614557565b6105016123dc6123d687614557565b92614536565b6040805173ffffffffffffffffffffffffffffffffffffffff90921682523360208301528101959095529116929081906060820190565b9061241c614f59565b6040519261242984613cd1565b835281830152611cb1604051928284938452830190613e2e565b823b1561070d57918791858094604051968795869485937fa8027c0f00000000000000000000000000000000000000000000000000000000855260048501608090528061248f91615416565b6084860160a090526101248601906124a692613f7b565b916124b090613bd7565b67ffffffffffffffff1660a48501526044016124cb90613b88565b73ffffffffffffffffffffffffffffffffffffffff1660c48401528b60e48401526124f58b613b88565b73ffffffffffffffffffffffffffffffffffffffff1661010484015283602484015282810360031901604484015261252c91613c1a565b8a606483015203925af1801561066b57612549575b808080612386565b612554828092613d25565b6106685780612541565b6024837f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b508781813d83116125cb575b6125a08183613d25565b81010312611118576125c673ffffffffffffffffffffffffffffffffffffffff91613f5a565b61232c565b503d612596565b6040513d86823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008452600452602483fd5b6004837f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b6126479150883d8a1161079f576107918183613d25565b3861229d565b5073ffffffffffffffffffffffffffffffffffffffff6107c7602493614536565b5034610668578060031936011261066857602073ffffffffffffffffffffffffffffffffffffffff60015416604051908152f35b5034610668578060031936011261066857602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b503461066857602060031936011261066857602061272f67ffffffffffffffff61271b613bc0565b166000526007602052604060002054151590565b6040519015158152f35b5034610668578060031936011261066857805473ffffffffffffffffffffffffffffffffffffffff811633036127d8577fffffffffffffffffffffffff000000000000000000000000000000000000000060015491338284161760015516825573ffffffffffffffffffffffffffffffffffffffff3391167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08380a380f35b6004827f02b543c6000000000000000000000000000000000000000000000000000000008152fd5b5034610668578060031936011261066857600254600a54600c546040805173ffffffffffffffffffffffffffffffffffffffff94851681529284166020840152921691810191909152606090f35b50346106685761285d36613de9565b61286993929193614bc5565b67ffffffffffffffff821661288b816000526007602052604060002054151590565b156128aa57506128a792936128a1913691613d66565b90614c10565b80f35b7f1e670e4b000000000000000000000000000000000000000000000000000000008452600452602483fd5b5034610668576040600319360112610668576128ef613bc0565b906024359067ffffffffffffffff821161066857602061272f846129163660048701613dcb565b9061456c565b5034610668576020600319360112610668576004359067ffffffffffffffff82116106685781600401906101006003198436030112610668578060405161296281613c86565b528060405161297081613c86565b52606483013560c48401936129a061299a61299561298e88886144e5565b3691613d66565b614879565b83614934565b9360848201956129af87614536565b73ffffffffffffffffffffffffffffffffffffffff807f000000000000000000000000000000000000000000000000000000000000000016911603612ff857602483019377ffffffffffffffff00000000000000000000000000000000612a1586614557565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156119e7578791612fd9575b50612fb15767ffffffffffffffff612aa986614557565b16612ac1816000526007602052604060002054151590565b15612f8657602073ffffffffffffffffffffffffffffffffffffffff60025416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa9081156119e7578791612f67575b5015612f3b57612b3885614557565b92612b4e60a486019461291661298e87856144e5565b15612ef457612b6f88612b608b614536565b612b6989614557565b9061532d565b73ffffffffffffffffffffffffffffffffffffffff600354169283612d22575b505050505060440191612ba183614536565b612baa83614557565b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803b1561111857608484928367ffffffffffffffff9373ffffffffffffffffffffffffffffffffffffffff60405197889687957f74fd18ac000000000000000000000000000000000000000000000000000000008752837f00000000000000000000000000000000000000000000000000000000000000001660048801521660248601528c60448601521660648401525af1801561066b57612d0d575b5050608067ffffffffffffffff60209573ffffffffffffffffffffffffffffffffffffffff612cd9612cd36104c97ffc5e3a5bddc11d92c2dc20fae6f7d5eb989f056be35239f7de7e86150609abc097614557565b96614536565b816040519716875233898801521660408601528560608601521692a260405190612d0282613c86565b815260405190518152f35b612d18828092613d25565b6106685780612c7e565b833b15612ef057878795938195938c93604051988997889687957f6371157400000000000000000000000000000000000000000000000000000000875260048701606090528d612d728780615416565b60648a0161010090526101648a0190612d8a92613f7b565b94612d9490613bd7565b67ffffffffffffffff166084890152604401612daf90613b88565b73ffffffffffffffffffffffffffffffffffffffff1660a488015260c4870152612dd890613b88565b73ffffffffffffffffffffffffffffffffffffffff1660e4860152612dfd9084615416565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c86840301610104870152612e329291613f7b565b90612e3d9083615416565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c85840301610124860152612e729291613f7b565b9060e48a01612e8091615416565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c84840301610144850152612eb59291613f7b565b8b602483015282604483015203925af180156125d257908491612edb575b808080612b8f565b81612ee591613d25565b610a40578238612ed3565b8780fd5b83612efe916144e5565b611fef6040519283927f24eb47e5000000000000000000000000000000000000000000000000000000008452602060048501526024840191613f7b565b6024867f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b612f80915060203d60201161079f576107918183613d25565b38612b29565b7fa9902c7e000000000000000000000000000000000000000000000000000000008752600452602486fd5b6004867f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b612ff2915060203d60201161079f576107918183613d25565b38612a92565b60248573ffffffffffffffffffffffffffffffffffffffff6107c78a614536565b5034610668578060031936011261066857602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b5034610668576040600319360112610668576004359067ffffffffffffffff821161066857816004019061010060031984360301126106685761308e613ac1565b918160405161309c81613c86565b5260648401359360c48101936130c16130bb61299561298e88876144e5565b87614934565b9460848301966130d088614536565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361359f57602484019477ffffffffffffffff0000000000000000000000000000000061313687614557565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115611a25578891613580575b506135585767ffffffffffffffff6131ca87614557565b166131e2816000526007602052604060002054151590565b1561352d57602073ffffffffffffffffffffffffffffffffffffffff60025416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa908115611a2557889161350e575b50156134e25761325986614557565b9361326f60a487019561291661298e88866144e5565b156134d8577fffffffff00000000000000000000000000000000000000000000000000000000169081156134bd576132b9896132aa8c614536565b6132b38a614557565b906153a6565b73ffffffffffffffffffffffffffffffffffffffff6003541693846132ec575b50505050505060440191612ba183614536565b843b156134b957868995938c959387938b6040519a8b998a9889977f63711574000000000000000000000000000000000000000000000000000000008952600489016060905261333c8780615416565b60648b0161010090526101648b019061335492613f7b565b9461335e90613bd7565b67ffffffffffffffff1660848a015260440161337990613b88565b73ffffffffffffffffffffffffffffffffffffffff1660a489015260c48801526133a290613b88565b73ffffffffffffffffffffffffffffffffffffffff1660e48701526133c79084615416565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c878403016101048801526133fc9291613f7b565b906134079083615416565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c8684030161012487015261343c9291613f7b565b9060e48b0161344a91615416565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c8584030161014486015261347f9291613f7b565b908c6024840152604483015203925af180156125d2576134a4575b80808080806132d9565b926134b28160449395613d25565b929061349a565b8880fd5b6134d3896134ca8c614536565b612b698a614557565b6132b9565b612efe85836144e5565b6024877f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b613527915060203d60201161079f576107918183613d25565b3861324a565b7fa9902c7e000000000000000000000000000000000000000000000000000000008852600452602487fd5b6004877f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b613599915060203d60201161079f576107918183613d25565b386131b3565b60248673ffffffffffffffffffffffffffffffffffffffff6107c78b614536565b5034610668578060031936011261066857602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b503461066857604060031936011261066857613618613bc0565b602435918215158303610668576101406136db6136358585614462565b61368b60409392935180946fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565b60a08301906fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565bf35b5034610668576020600319360112610668576020906136fa613b1f565b905073ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116146040519015158152f35b5034610668578060031936011261066857602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346106685760c0600319360112610668576137ac613b1f565b506137b5613ba9565b6137bd613b42565b50608435917fffffffff00000000000000000000000000000000000000000000000000000000831683036106685760a4359067ffffffffffffffff82116106685760a063ffffffff8061ffff613822888861381b3660048b01613bec565b50506142b2565b95926040979194975197885216602087015216604085015216606083015215156080820152f35b503461066857806003193601126106685750611cb160405161386c604082613d25565b601a81527f4c6f636b52656c65617365546f6b656e506f6f6c20322e302e300000000000006020820152604051918291602083526020830190613c1a565b50346106685760c0600319360112610668576138c4613b1f565b6138cc613ba9565b906064357fffffffff00000000000000000000000000000000000000000000000000000000811681036111185760843567ffffffffffffffff811161070d57613919903690600401613bec565b9160a435936002851015610676576139349560443591613fba565b90604051918291602083016020845282518091526020604085019301915b818110613960575050500390f35b825173ffffffffffffffffffffffffffffffffffffffff16845285945060209384019390920191600101613952565b9050346107f05760206003193601126107f0576020907fffffffff000000000000000000000000000000000000000000000000000000006139ce613a8d565b167faff2afbf000000000000000000000000000000000000000000000000000000008114908115613a63575b8115613a39575b8115613a0f575b5015158152f35b7f01ffc9a70000000000000000000000000000000000000000000000000000000091501483613a08565b7f0e64dd290000000000000000000000000000000000000000000000000000000081149150613a01565b7f940a154200000000000000000000000000000000000000000000000000000000811491506139fa565b600435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613abc57565b600080fd5b602435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613abc57565b604435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613abc57565b6004359073ffffffffffffffffffffffffffffffffffffffff82168203613abc57565b6064359073ffffffffffffffffffffffffffffffffffffffff82168203613abc57565b6024359073ffffffffffffffffffffffffffffffffffffffff82168203613abc57565b359073ffffffffffffffffffffffffffffffffffffffff82168203613abc57565b6024359067ffffffffffffffff82168203613abc57565b6004359067ffffffffffffffff82168203613abc57565b359067ffffffffffffffff82168203613abc57565b9181601f84011215613abc5782359167ffffffffffffffff8311613abc5760208381860195010111613abc57565b919082519283825260005b848110613c645750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8460006020809697860101520116010190565b80602080928401015182828601015201613c25565b35908115158203613abc57565b6020810190811067ffffffffffffffff821117613ca257604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040810190811067ffffffffffffffff821117613ca257604052565b60a0810190811067ffffffffffffffff821117613ca257604052565b60e0810190811067ffffffffffffffff821117613ca257604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117613ca257604052565b92919267ffffffffffffffff8211613ca25760405191613dae601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200184613d25565b829481845281830111613abc578281602093846000960137010152565b9080601f83011215613abc57816020613de693359101613d66565b90565b906040600319830112613abc5760043567ffffffffffffffff81168103613abc57916024359067ffffffffffffffff8211613abc57613e2a91600401613bec565b9091565b613de6916020613e478351604084526040840190613c1a565b920151906020818403910152613c1a565b9181601f84011215613abc5782359167ffffffffffffffff8311613abc576020808501948460051b010111613abc57565b9181601f84011215613abc5782359167ffffffffffffffff8311613abc576020808501948460081b010111613abc57565b67ffffffffffffffff8111613ca25760051b60200190565b81810292918115918404141715613ee557565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b8115613f1e570490565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b91908203918211613ee557565b519073ffffffffffffffffffffffffffffffffffffffff82168203613abc57565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0938186528686013760008582860101520116010190565b92959390919473ffffffffffffffffffffffffffffffffffffffff6003541695861561429057809760028710156142615773ffffffffffffffffffffffffffffffffffffffff9861411b957fffffffff0000000000000000000000000000000000000000000000000000000093896142375767ffffffffffffffff8216600052600b6020526040600020906040519161405283613d09565b549163ffffffff8316815263ffffffff8360201c16602082015263ffffffff8360401c16604082015263ffffffff8360601c16606082015260c061ffff8460801c169182608082015260ff60a082019561ffff8160901c16875260a01c16151591829101526141e3575b50505067ffffffffffffffff905b6040519b8c997f06b859ef000000000000000000000000000000000000000000000000000000008b521660048a0152166024880152604487015216606485015260c0608485015260c4840191613f7b565b928180600095869560a483015203915afa9182156141d657819261413e57505090565b9091503d8083833e6141508183613d25565b810190602081830312610a405780519067ffffffffffffffff8211611118570181601f82011215610a405780519061418782613eba565b936141956040519586613d25565b82855260208086019360051b8301019384116106685750602001905b8282106141be5750505090565b602080916141cb84613f5a565b8152019101906141b1565b50604051903d90823e3d90fd5b92935067ffffffffffffffff928587161561421f575061271061420e61ffff61421594511683613ed2565b0490613f4d565b915b9038806140bc565b614231925061420e6127109183613ed2565b91614217565b67ffffffffffffffff91925061425b9061425561299536898b613d66565b90614934565b916140ca565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b50505050505050506040516142a6602082613d25565b60008152600036813790565b67ffffffffffffffff909291926142f07fffffffff0000000000000000000000000000000000000000000000000000000060025460401b1685614a41565b16600052600b60205260406000206040519061430b82613d09565b549163ffffffff83169384835263ffffffff8460201c169384602085015263ffffffff8160401c169182604086015263ffffffff8260601c169081606087015261ffff8360801c169586608082015260ff61ffff8560901c16948560a084015260a01c16159060c082159101526143b8577fffffffff00000000000000000000000000000000000000000000000000000000166143ad57505093929190600190565b959493509160019150565b5050505092505050600090600090600090600090600090565b604051906143de82613ced565b60006080838281528260208201528260408201528260608201520152565b9060405161440981613ced565b60806001829460ff81546fffffffffffffffffffffffffffffffff8116865263ffffffff81861c16602087015260a01c161515604085015201546fffffffffffffffffffffffffffffffff81166060840152811c910152565b67ffffffffffffffff916144746143d1565b5061447d6143d1565b506144b157166000526008602052604060002090613de66144a560026144aa6144a5866143fc565b614b28565b94016143fc565b16908160005260046020526144cc6144a560406000206143fc565b916000526005602052613de66144a560406000206143fc565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215613abc570180359067ffffffffffffffff8211613abc57602001918136038313613abc57565b3573ffffffffffffffffffffffffffffffffffffffff81168103613abc5790565b3567ffffffffffffffff81168103613abc5790565b9067ffffffffffffffff613de692166000526008602052600560406000200190602081519101209060019160005201602052604060002054151590565b604051906145b682613cd1565b60606020838281520152565b80518210156145d65760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b90600182811c9216801561464e575b602083101461461f57565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b91607f1691614614565b906040519182600082549261466c84614605565b80845293600181169081156146da5750600114614693575b5061469192500383613d25565b565b90506000929192526020600020906000915b8183106146be5750509060206146919282010138614684565b60209193508060019154838589010152019101909184926146a5565b602093506146919592507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0091501682840152151560051b82010138614684565b67ffffffffffffffff166000526008602052613de66004604060002001614658565b91908110156145d65760081b0190565b358015158103613abc5790565b3561ffff81168103613abc5790565b3563ffffffff81168103613abc5790565b359063ffffffff82168203613abc57565b359061ffff82168203613abc57565b91908110156145d65760051b0190565b35906fffffffffffffffffffffffffffffffff82168203613abc57565b9190826060910312613abc576040516060810181811067ffffffffffffffff821117613ca257604052604061481981839561480081613c79565b855261480e602082016147a9565b6020860152016147a9565b910152565b6fffffffffffffffffffffffffffffffff61485c6040809361483f81613c79565b1515865283614850602083016147a9565b166020870152016147a9565b16910152565b81811061486d575050565b60008155600101614862565b805180156148e9576020036148ab578051602082810191830183900312613abc57519060ff82116148ab575060ff1690565b611fef906040519182917f953576f7000000000000000000000000000000000000000000000000000000008352602060048401526024830190613c1a565b50507f000000000000000000000000000000000000000000000000000000000000000090565b9060ff8091169116039060ff8211613ee557565b60ff16604d8111613ee557600a0a90565b907f00000000000000000000000000000000000000000000000000000000000000009060ff82169060ff811692828414614a3a57828411614a1057906149799161490f565b91604d60ff84161180156149d7575b6149a15750509061499b613de692614923565b90613ed2565b9091507fa9cb113d0000000000000000000000000000000000000000000000000000000060005260045260245260445260646000fd5b506149e183614923565b8015613f1e577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048411614988565b614a199161490f565b91604d60ff8416116149a157505090614a34613de692614923565b90613f14565b5050505090565b7fffffffff000000000000000000000000000000000000000000000000000000008116908115614b2357614a7481615252565b7dffff00000000000000000000000000000000000000000000000000000000601082811c9085901c1616614b235761ffff8360e01c168015918215614b12575b5050614abe575050565b7fffffffff0000000000000000000000000000000000000000000000000000000092507fdf63778f000000000000000000000000000000000000000000000000000000006000526004521660245260446000fd5b60e01c61ffff161090503880614ab4565b505050565b614b306143d1565b506fffffffffffffffffffffffffffffffff6060820151166fffffffffffffffffffffffffffffffff8083511691614b8d6020850193614b87614b7a63ffffffff87511642613f4d565b8560808901511690613ed2565b90615245565b80821015614ba657505b16825263ffffffff4216905290565b9050614b97565b90816020910312613abc57518015158103613abc5790565b73ffffffffffffffffffffffffffffffffffffffff600154163303614be657565b7f2b5c74de0000000000000000000000000000000000000000000000000000000060005260046000fd5b90805115614e505767ffffffffffffffff81516020830120921691826000526008602052614c458160056040600020016158a9565b15614e0c5760005260096020526040600020815167ffffffffffffffff8111613ca257614c728254614605565b601f8111614dda575b506020601f8211600114614d145791614cee827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea9593614d0495600091614d09575b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b9055604051918291602083526020830190613c1a565b0390a2565b905084015138614cbd565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe082169083600052806000209160005b818110614dc2575092614d049492600192827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea989610614d8b575b5050811b019055611c9d565b8501517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690553880614d7f565b9192602060018192868a015181550194019201614d44565b614e0690836000526020600020601f840160051c81019160208510610e9057601f0160051c0190614862565b38614c7b565b5090611fef6040519283927f393b8ad20000000000000000000000000000000000000000000000000000000084526004840152604060248401526044830190613c1a565b7f14c880ca0000000000000000000000000000000000000000000000000000000060005260046000fd5b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803b15613abc5767ffffffffffffffff906064604051809481937fa36a7fee0000000000000000000000000000000000000000000000000000000083526000978896879373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016600487015216602485015260448401525af1801561066b57614f4c575050565b81614f5691613d25565b50565b60405160ff7f000000000000000000000000000000000000000000000000000000000000000016602082015260208152613de6604082613d25565b815191929115615116576fffffffffffffffffffffffffffffffff6040840151166fffffffffffffffffffffffffffffffff602085015116106150b35761469191925b805182547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff0000000000000000000000000000000000000000161782556020810151825460409290920151608090811b7fffffffffffffffffffffffffffffffff00000000000000000000000000000000166fffffffffffffffffffffffffffffffff9290921691821760018501557fffffffffffffffffffffffff0000000000000000000000000000000000000000909216174290911b73ffffffff0000000000000000000000000000000016179055565b606483615114604051917f8020d12400000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565bfd5b6fffffffffffffffffffffffffffffffff604084015116158015906151a5575b615144576146919192614fd7565b606483615114604051917fd68af9cc00000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b506fffffffffffffffffffffffffffffffff6020840151161515615136565b906127109167ffffffffffffffff6151de60208301614557565b166000908152600b60205260409020917fffffffff00000000000000000000000000000000000000000000000000000000161561522f57606061ffff61522b935460901c16910135613ed2565b0490565b606061ffff61522b935460801c16910135613ed2565b91908201809211613ee557565b7fffffffff000000000000000000000000000000000000000000000000000000008116908115615329577dffff000000000000000000000000000000000000000000000000000000008116156153205760ff60015b169060f01c806152ea575b506001036152bd5750565b7fc512f96c0000000000000000000000000000000000000000000000000000000060005260045260246000fd5b60005b601081106152fb57506152b2565b6001811b821661530e575b6001016152ed565b9160018101809111613ee55791615306565b60ff60006152a7565b5050565b9167ffffffffffffffff7f50f6fbee3ceedce6b7fd7eaef18244487867e6718aec7208187efb6b7908c14c921692836000526008602052615376818360026040600020016158fe565b6040805173ffffffffffffffffffffffffffffffffffffffff909216825260208201929092529081908101614d04565b91909167ffffffffffffffff83169283600052600560205260ff60406000205460a01c161561540b5750907fc6735cd4fa2bbe7b203b1682936e6ee61bc1702464bbbd12abb6630229d9a5f991836000526005602052615376818360406000206158fe565b90614691935061532d565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe182360301811215613abc57016020813591019167ffffffffffffffff8211613abc578136038313613abc57565b9167ffffffffffffffff7fff0133389f9bb82d5b9385826160eaf2328039f6fa950eeb8cf0836da8178944921692836000526008602052615376818360406000206158fe565b91909167ffffffffffffffff83169283600052600460205260ff60406000205460a01c16156155115750907f28d6c52e2b0b7587b0d195539fbe6af984b28791aca4d2cc0844244e38bce29e91836000526004602052615376818360406000206158fe565b906146919350615466565b906040519182815491828252602082019060005260206000209260005b81811061554e57505061469192500383613d25565b8454835260019485019487945060209093019201615539565b80548210156145d65760005260206000200190600090565b600081815260076020526040902054801561570e577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111613ee557600654907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211613ee55781810361569f575b5050506006548015615670577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0161562d816006615567565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b19169055600655600052600760205260006040812055600190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b6156f66156b06156c1936006615567565b90549060031b1c9283926006615567565b81939154907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060031b92831b921b19161790565b905560005260076020526040600020553880806155f4565b5050600090565b9060018201918160005282602052604060002054801515600014615840577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111613ee5578254907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211613ee557818103615809575b50505080548015615670577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01906157ca8282615567565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b191690555560005260205260006040812055600190565b6158296158196156c19386615567565b90549060031b1c92839286615567565b905560005283602052604060002055388080615792565b50505050600090565b806000526007602052604060002054156000146158a35760065468010000000000000000811015613ca25761588a6156c18260018594016006556006615567565b9055600654906000526007602052604060002055600190565b50600090565b600082815260018201602052604090205461570e5780549068010000000000000000821015613ca257826158e76156c1846001809601855584615567565b905580549260005201602052604060002055600190565b9182549060ff8260a01c16158015615bb2575b615bac576fffffffffffffffffffffffffffffffff8216916001850190815461595663ffffffff6fffffffffffffffffffffffffffffffff83169360801c1642613f4d565b9081615b0e575b5050848110615ac257508383106159b757505061598c6fffffffffffffffffffffffffffffffff928392613f4d565b16167fffffffffffffffffffffffffffffffff00000000000000000000000000000000825416179055565b9190915460801c928315615a5657816159cf91613f4d565b927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff810190808211613ee557615a1d615a229273ffffffffffffffffffffffffffffffffffffffff96615245565b613f14565b7fd0c8d23a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b73ffffffffffffffffffffffffffffffffffffffff83837fd0c8d23a000000000000000000000000000000000000000000000000000000006000527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6004526024521660445260646000fd5b828573ffffffffffffffffffffffffffffffffffffffff927f1a76572a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b828692939611615b8257615b2992614b879160801c90613ed2565b80841015615b7d5750825b85547fffffffffffffffffffffffff00000000ffffffffffffffffffffffffffffffff164260801b73ffffffff000000000000000000000000000000001617865592388061595d565b615b34565b7f9725942a0000000000000000000000000000000000000000000000000000000060005260046000fd5b50505050565b50821561591156fea164736f6c634300081a000a' diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool-via-factory.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool-via-factory.test.ts new file mode 100644 index 00000000..3103ee78 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool-via-factory.test.ts @@ -0,0 +1,147 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { ZeroAddress } from 'ethers' + +import { DeployPoolViaFactory } from './deploy-pool-via-factory.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { BURN_MINT_TOKEN_POOL_BYTECODE } from '../bytecodes/BurnMintTokenPool.ts' +import { LOCK_RELEASE_TOKEN_POOL_BYTECODE } from '../bytecodes/LockReleaseTokenPool.ts' +import { FACTORY_POOL_TYPE, tokenPoolFactoryInterface } from '../token-pool-factory-abi.ts' + +const FACTORY = '0x1111111111111111111111111111111111111111' +const TOKEN = '0xa42ba090720aee0602ad4381fadcc9380ad3d888' +const LOCKBOX = '0xccccc17d24393eb02ecd2b1f2a0e78d5b1f0aa11' +const OWNER = '0xdddddddddddddddddddddddddddddddddddddddd' +const SALT = '0x' + '11'.repeat(32) + +/** Minimal stub chain — buildUnsigned is pure encoding (no RPC / staticCall). */ +function stubChain(): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + } as unknown as EVMChain +} + +describe('EVM cct deployPoolViaFactory', () => { + const op = new DeployPoolViaFactory() + + it('burn-mint: byte-identical factory-call calldata with a fixed salt', async () => { + const unsigned = await op.generate(stubChain(), { + factoryAddress: FACTORY, + tokenAddress: TOKEN, + decimals: 18, + poolType: 'burn-mint', + salt: SALT, + futureOwner: OWNER, + }) + const expected = tokenPoolFactoryInterface.encodeFunctionData( + 'deployTokenPoolWithExistingToken', + [ + TOKEN, + 18, + FACTORY_POOL_TYPE['burn-mint'], + [], + BURN_MINT_TOKEN_POOL_BYTECODE, + ZeroAddress, + SALT, + OWNER, + ], + ) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, FACTORY) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('lock-release: passes the supplied lockBox and lock-release bytecode', async () => { + const unsigned = await op.generate(stubChain(), { + factoryAddress: FACTORY, + tokenAddress: TOKEN, + decimals: 8, + poolType: 'lock-release', + lockBoxAddress: LOCKBOX, + salt: SALT, + futureOwner: OWNER, + }) + const expected = tokenPoolFactoryInterface.encodeFunctionData( + 'deployTokenPoolWithExistingToken', + [ + TOKEN, + 8, + FACTORY_POOL_TYPE['lock-release'], + [], + LOCK_RELEASE_TOKEN_POOL_BYTECODE, + LOCKBOX, + SALT, + OWNER, + ], + ) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('arg assembly round-trips through the factory ABI decoder', async () => { + const unsigned = await op.generate(stubChain(), { + factoryAddress: FACTORY, + tokenAddress: TOKEN, + decimals: 6, + poolType: 'burn-mint', + salt: SALT, + futureOwner: OWNER, + }) + const decoded = tokenPoolFactoryInterface.decodeFunctionData( + 'deployTokenPoolWithExistingToken', + unsigned.transactions[0]!.data!, + ) + assert.equal((decoded[0] as string).toLowerCase(), TOKEN) + assert.equal(Number(decoded[1]), 6) + assert.equal(Number(decoded[2]), FACTORY_POOL_TYPE['burn-mint']) + assert.equal((decoded[3] as unknown[]).length, 0) + assert.equal(decoded[4] as string, BURN_MINT_TOKEN_POOL_BYTECODE) + assert.equal(decoded[5] as string, ZeroAddress) + assert.equal(decoded[6] as string, SALT) + assert.equal((decoded[7] as string).toLowerCase(), OWNER) + }) + + it('requires futureOwner on the unsigned path', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + factoryAddress: FACTORY, + tokenAddress: TOKEN, + decimals: 18, + poolType: 'burn-mint', + salt: SALT, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'futureOwner', + ) + }) + + it('rejects an invalid poolType', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + factoryAddress: FACTORY, + tokenAddress: TOKEN, + decimals: 18, + poolType: 'nonsense' as never, + futureOwner: OWNER, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolType', + ) + }) + + it('rejects a missing tokenAddress', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + factoryAddress: FACTORY, + tokenAddress: '', + decimals: 18, + poolType: 'burn-mint', + futureOwner: OWNER, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'tokenAddress', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool-via-factory.ts b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool-via-factory.ts new file mode 100644 index 00000000..b4d91221 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool-via-factory.ts @@ -0,0 +1,211 @@ +/** + * deployPoolViaFactory — deploys a CCIP token pool for an **existing** token through a + * `TokenPoolFactory 2.0.0` (CREATE2). + * + * The signed `execute` path resolves the pool address by `staticCall`-ing the factory's + * `deployTokenPoolWithExistingToken(...)` first, then broadcasts the identical call; the + * pool address is therefore known before the tx is mined. `futureOwner` is auto-filled from + * the signer when omitted, and the CREATE2 `salt` defaults to a random 32-byte value. + * + * The unsigned `generate` path builds only the factory-call tx (to: factory) and requires an + * explicit `futureOwner` (there is no signer to derive it from). + * + * Unlike a token+pool factory deploy, the factory is not the token's `ccipAdmin` here, so the + * caller must wire the TokenAdminRegistry (propose/accept-admin, set-pool) and, for burn-mint, + * grant the pool mint/burn roles separately. + * + * @packageDocumentation + */ + +import { Contract, ZeroAddress, hexlify, randomBytes } from 'ethers' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { interfaces } from '../../../../evm/const.ts' +import { type EVMChain, isSigner } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type DeployVerificationTarget, + buildFactoryPoolVerification, +} from '../../deploy-verification.ts' +import { EVMOperation } from '../../operation.ts' +import { submitForReceipt } from '../../submit.ts' +import { BURN_MINT_TOKEN_POOL_BYTECODE } from '../bytecodes/BurnMintTokenPool.ts' +import { LOCK_RELEASE_TOKEN_POOL_BYTECODE } from '../bytecodes/LockReleaseTokenPool.ts' +import { + FACTORY_POOL_TYPE, + TOKEN_POOL_FACTORY_ABI, + tokenPoolFactoryInterface, +} from '../token-pool-factory-abi.ts' + +/** Which pool contract the factory should deploy. */ +export type FactoryPoolType = 'burn-mint' | 'lock-release' + +/** Parameters for `deployPoolViaFactory`. */ +export type DeployPoolViaFactoryParams = { + /** The `TokenPoolFactory 2.0.0` address on this chain. */ + factoryAddress: string + /** The existing token the pool will serve. */ + tokenAddress: string + /** Token decimals on this chain (must match the deployed token). */ + decimals: number + poolType: FactoryPoolType + /** Existing `ERC20LockBox` for lock-release; the factory auto-deploys one when omitted. */ + lockBoxAddress?: string + /** CREATE2 salt. A random 32-byte value is used when omitted (non-deterministic address). */ + salt?: string + /** + * Final owner of the pool. Required on the unsigned path; auto-filled from the signer on + * the signed path. + */ + futureOwner?: string + sender?: string +} + +/** Result of a signed `deployPoolViaFactory`: tx hash plus the CREATE2 pool address. */ +export type DeployPoolViaFactoryResult = TransactionHash & { + poolAddress: string + /** + * EVM lock-release only: the `ERC20LockBox` bound to the token. Read from + * `pool.getLockBox()` after the deploy when the caller did not supply one (the factory + * auto-deploys it). Omitted for burn-mint pools. + */ + lockBoxAddress?: string + /** + * Block-explorer verification handles for every contract the factory deployed (the pool, + * plus the auto-deployed `ERC20LockBox` for lock-release). The factory creates these in + * internal CREATE2 calls, so each carries its address alongside its constructor args. + */ + verifications: DeployVerificationTarget[] +} + +/** Deploys a CCIP token pool for an existing token via `TokenPoolFactory 2.0.0`. */ +export class DeployPoolViaFactory extends EVMOperation< + DeployPoolViaFactoryParams, + DeployPoolViaFactoryResult +> { + readonly name = 'deployPoolViaFactory' + + /** Validates factory-deploy params (`futureOwner` required on the unsigned path). */ + protected validate(p: DeployPoolViaFactoryParams): void { + if (p.poolType !== 'burn-mint' && p.poolType !== 'lock-release') + throw new CCTParamsInvalidError( + this.name, + 'poolType', + "must be 'burn-mint' or 'lock-release'", + ) + if (!p.factoryAddress || p.factoryAddress.trim().length === 0) + throw new CCTParamsInvalidError(this.name, 'factoryAddress', 'must be non-empty') + if (!p.tokenAddress || p.tokenAddress.trim().length === 0) + throw new CCTParamsInvalidError(this.name, 'tokenAddress', 'must be non-empty') + if (p.decimals < 0 || p.decimals > 255) + throw new CCTParamsInvalidError(this.name, 'decimals', 'must be 0-255') + if (!p.futureOwner || p.futureOwner.trim().length === 0) + throw new CCTParamsInvalidError( + this.name, + 'futureOwner', + 'required (the signed deployPoolViaFactory path auto-fills it from the signer)', + ) + } + + /** + * Assembles the `deployTokenPoolWithExistingToken` argument tuple. `futureOwner` and `salt` + * are read from `p` (already resolved by {@link execute} on the signed path; supplied by the + * caller on the unsigned path, where `salt` defaults to a fresh random value). + */ + private assembleArgs(p: DeployPoolViaFactoryParams): unknown[] { + const poolBytecode = + p.poolType === 'burn-mint' ? BURN_MINT_TOKEN_POOL_BYTECODE : LOCK_RELEASE_TOKEN_POOL_BYTECODE + return [ + p.tokenAddress, + p.decimals, + FACTORY_POOL_TYPE[p.poolType], + [], + poolBytecode, + p.lockBoxAddress ?? ZeroAddress, + p.salt ?? hexlify(randomBytes(32)), + p.futureOwner!, + ] + } + + /** Builds the factory-call tx (`to: factory`, `data: deployTokenPoolWithExistingToken(...)`). */ + protected buildUnsigned(_chain: EVMChain, p: DeployPoolViaFactoryParams): UnsignedEVMTx { + const data = tokenPoolFactoryInterface.encodeFunctionData( + 'deployTokenPoolWithExistingToken', + this.assembleArgs(p), + ) + return { family: ChainFamily.EVM, transactions: [{ to: p.factoryAddress, data }] } + } + + /** + * Signed factory deploy: auto-fills `futureOwner` from the signer, fixes the CREATE2 salt, + * `staticCall`s the factory to resolve the pool address, then broadcasts the identical call. + */ + override async execute( + chain: EVMChain, + params: DeployPoolViaFactoryParams & { wallet: unknown }, + ): Promise { + const { wallet } = params + if (!isSigner(wallet)) throw new CCIPWalletInvalidError(wallet) + + // Resolve owner + salt once so the staticCall and the broadcast deploy to the same address. + const futureOwner = params.futureOwner ?? (await wallet.getAddress()) + const effective: DeployPoolViaFactoryParams = { + ...params, + futureOwner, + salt: params.salt ?? hexlify(randomBytes(32)), + } + const deployArgs = this.assembleArgs(effective) + + const factory = new Contract(effective.factoryAddress, TOKEN_POOL_FACTORY_ABI, wallet) + const deployFn = factory.getFunction('deployTokenPoolWithExistingToken') + chain.logger.debug(`${this.name}: simulating to resolve pool address...`) + const poolAddress = (await deployFn.staticCall(...deployArgs)) as string + + // The factory appends the pool ctor args from its own immutables; read them to rebuild + // the verification handles. + const { rmnProxy, ccipRouter } = (await factory.getFunction('getStaticConfig')()) as { + rmnProxy: string + ccipRouter: string + } + + const unsigned = await this.generate(chain, effective) + const { hash } = await submitForReceipt(chain, wallet, unsigned, this.name) + + // lock-release without a supplied lockbox: the factory auto-deploys one; surface it. + let lockBoxAddress = params.lockBoxAddress + if (params.poolType === 'lock-release' && !lockBoxAddress) { + const pool = new Contract(poolAddress, interfaces.TokenPool_v2_0, chain.provider) + lockBoxAddress = (await pool.getFunction('getLockBox')()) as string + } + + const { poolVerification, lockBoxVerification } = buildFactoryPoolVerification( + params.poolType === 'lock-release' + ? { + poolType: 'lock-release', + token: params.tokenAddress, + decimals: params.decimals, + rmnProxy, + router: ccipRouter, + poolAddress, + lockBoxAddress: lockBoxAddress!, + } + : { + poolType: 'burn-mint', + token: params.tokenAddress, + decimals: params.decimals, + rmnProxy, + router: ccipRouter, + poolAddress, + }, + ) + return { + hash, + poolAddress, + ...(lockBoxAddress ? { lockBoxAddress } : {}), + verifications: [poolVerification, ...(lockBoxVerification ? [lockBoxVerification] : [])], + } + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool.fork.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool.fork.test.ts new file mode 100644 index 00000000..f0f277ba --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool.fork.test.ts @@ -0,0 +1,230 @@ +import assert from 'node:assert/strict' +import { execSync } from 'node:child_process' +import { after, before, describe, it } from 'node:test' + +import { Contract, JsonRpcProvider, Wallet } from 'ethers' +import { Instance } from 'prool' + +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMTokenManager } from '../../index.ts' + +// ── Constants ── + +const SEPOLIA_RPC = process.env['RPC_SEPOLIA'] || 'https://ethereum-sepolia-rpc.publicnode.com' +const SEPOLIA_ROUTER = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' +const ANVIL_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + +// ── Helpers ── + +function isAnvilAvailable(): boolean { + try { + execSync('anvil --version', { stdio: 'ignore' }) + return true + } catch { + return false + } +} + +// Minimal ABI for verifying deployed pool state +const POOL_ABI = [ + { + inputs: [], + name: 'getToken', + outputs: [{ internalType: 'contract IERC20', name: 'token', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRouter', + outputs: [{ internalType: 'address', name: 'router', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, +] as const + +// ── Tests ── + +const skip = !!process.env.SKIP_INTEGRATION_TESTS || !isAnvilAvailable() + +const testLogger = process.env.VERBOSE + ? console + : { debug() {}, info() {}, warn: console.warn, error: console.error } + +describe('EVMTokenManager Pool Fork Tests', { skip, timeout: 120_000 }, () => { + let provider: JsonRpcProvider + let wallet: Wallet + let mgr: EVMTokenManager + let anvilInstance: ReturnType | undefined + let tokenAddress: string + + before(async () => { + // Fork Sepolia so we have a real Router with getArmProxy() + anvilInstance = Instance.anvil({ + port: 8748, + forkUrl: SEPOLIA_RPC, + forkBlockNumber: undefined, // latest + }) + await anvilInstance.start() + + const anvilUrl = `http://${anvilInstance.host}:${anvilInstance.port}` + provider = new JsonRpcProvider(anvilUrl, undefined, { cacheTimeout: -1 }) + wallet = new Wallet(ANVIL_PRIVATE_KEY, provider) + mgr = await EVMTokenManager.fromProvider(provider, { logger: testLogger, apiClient: null }) + + // Deploy a token first (needed by pool constructor) + const tokenResult = await mgr.deployToken({ + name: 'Pool Test Token', + symbol: 'PTT', + decimals: 18, + initialSupply: 1_000_000n * 10n ** 18n, + wallet, + }) + tokenAddress = tokenResult.tokenAddress + }) + + after(async () => { + provider.destroy() + await anvilInstance?.stop() + }) + + // =========================================================================== + // deployPool — BurnMint + // =========================================================================== + + it('should deploy BurnMintTokenPool and verify contract state', async () => { + const result = await mgr.deployPool({ + poolType: 'burn-mint', + tokenAddress, + localTokenDecimals: 18, + routerAddress: SEPOLIA_ROUTER, + wallet, + }) + + assert.ok(result.poolAddress, 'should return pool address') + assert.match(result.poolAddress, /^0x[0-9a-fA-F]{40}$/, 'should be valid address') + assert.ok(result.hash, 'should return tx hash') + assert.match(result.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify deployed contract state + const pool = new Contract(result.poolAddress, POOL_ABI, provider) + const token: string = await pool.getFunction('getToken')() + const router: string = await pool.getFunction('getRouter')() + const owner: string = await pool.getFunction('owner')() + + assert.equal(token.toLowerCase(), tokenAddress.toLowerCase(), 'pool token should match') + assert.equal(router.toLowerCase(), SEPOLIA_ROUTER.toLowerCase(), 'pool router should match') + assert.equal( + owner.toLowerCase(), + (await wallet.getAddress()).toLowerCase(), + 'deployer should be owner', + ) + }) + + // =========================================================================== + // deployPool — LockRelease + // =========================================================================== + + it('should deploy LockReleaseTokenPool, auto-deploy a LockBox and verify contract state', async () => { + const result = await mgr.deployPool({ + poolType: 'lock-release', + tokenAddress, + localTokenDecimals: 18, + routerAddress: SEPOLIA_ROUTER, + wallet, + }) + + assert.ok(result.poolAddress, 'should return pool address') + assert.match(result.poolAddress, /^0x[0-9a-fA-F]{40}$/) + assert.ok(result.hash, 'should return tx hash') + // signed deployPool auto-deploys an ERC20LockBox and returns its address + assert.ok(result.lockBoxAddress, 'should return auto-deployed lockBox address') + assert.match(result.lockBoxAddress, /^0x[0-9a-fA-F]{40}$/, 'lockBox should be valid address') + + const pool = new Contract(result.poolAddress, POOL_ABI, provider) + const token: string = await pool.getFunction('getToken')() + const router: string = await pool.getFunction('getRouter')() + + assert.equal(token.toLowerCase(), tokenAddress.toLowerCase()) + assert.equal(router.toLowerCase(), SEPOLIA_ROUTER.toLowerCase()) + }) + + it('should require lockBoxAddress on the unsigned lock-release path', async () => { + await assert.rejects( + () => + mgr.generateUnsignedDeployPool({ + poolType: 'lock-release', + tokenAddress, + localTokenDecimals: 18, + routerAddress: SEPOLIA_ROUTER, + }), + (err: unknown) => { + assert.ok(err instanceof CCTParamsInvalidError) + assert.equal(err.context.param, 'lockBoxAddress') + return true + }, + ) + }) + + // =========================================================================== + // generateUnsignedDeployPool — manual sign + // =========================================================================== + + it('should produce unsigned tx that deploys successfully when signed manually', async () => { + const unsigned = await mgr.generateUnsignedDeployPool({ + poolType: 'burn-mint', + tokenAddress, + localTokenDecimals: 18, + routerAddress: SEPOLIA_ROUTER, + }) + + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal(tx.to, null) + + const populated = await wallet.populateTransaction(tx) + populated.from = undefined + const response = await wallet.sendTransaction(populated) + const receipt = await response.wait(1, 30_000) + + assert.ok(receipt, 'should get receipt') + assert.equal(receipt.status, 1, 'tx should succeed') + assert.ok(receipt.contractAddress, 'should have contract address') + + const pool = new Contract(receipt.contractAddress, POOL_ABI, provider) + const token: string = await pool.getFunction('getToken')() + assert.equal(token.toLowerCase(), tokenAddress.toLowerCase()) + }) + + // =========================================================================== + // deployPool — with advancedPoolHooks (v2.0) + // =========================================================================== + + it('should deploy burn-mint pool with a custom advancedPoolHooks address', async () => { + const advancedPoolHooks = '0x0000000000000000000000000000000000000001' + + const result = await mgr.deployPool({ + poolType: 'burn-mint', + tokenAddress, + localTokenDecimals: 18, + routerAddress: SEPOLIA_ROUTER, + advancedPoolHooks, + wallet, + }) + + assert.ok(result.poolAddress) + assert.ok(result.hash) + + // Pool still binds to the configured token/router regardless of the hooks address. + const pool = new Contract(result.poolAddress, POOL_ABI, provider) + const token: string = await pool.getFunction('getToken')() + assert.equal(token.toLowerCase(), tokenAddress.toLowerCase()) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool.test.ts new file mode 100644 index 00000000..8e81a048 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool.test.ts @@ -0,0 +1,96 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { AbiCoder, ZeroAddress, concat } from 'ethers' + +import { DeployPool } from './deploy-pool.ts' +import { BURN_MINT_TOKEN_POOL_BYTECODE } from '../bytecodes/BurnMintTokenPool.ts' +import { LOCK_RELEASE_TOKEN_POOL_BYTECODE } from '../bytecodes/LockReleaseTokenPool.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const TOKEN = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const ROUTER = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' +const RMN = '0x411dd1e5c9b2a6ded3eb8b7edcab0b9ea9c8cde1' +const LOCKBOX = '0xccccc17d24393eb02ecd2b1f2a0e78d5b1f0aa11' +const coder = AbiCoder.defaultAbiCoder() + +/** Stub chain whose router.getArmProxy() returns a fixed rmnProxy. */ +function stubChain(): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + // ethers Contract.getArmProxy() → provider.call(); return the ABI-encoded address + call: async () => coder.encode(['address'], [RMN]), + }, + } as unknown as EVMChain +} + +describe('EVM cct deployPool', () => { + const op = new DeployPool() + + it('burn-mint: byte-identical creation data with derived rmnProxy', async () => { + const unsigned = await op.generate(stubChain(), { + poolType: 'burn-mint', + tokenAddress: TOKEN, + localTokenDecimals: 18, + routerAddress: ROUTER, + }) + const expected = concat([ + BURN_MINT_TOKEN_POOL_BYTECODE, + coder.encode( + ['address', 'uint8', 'address', 'address', 'address'], + [TOKEN, 18, ZeroAddress, RMN, ROUTER], + ), + ]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, null) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('lock-release: byte-identical creation data (with lockBox in constructor)', async () => { + const unsigned = await op.generate(stubChain(), { + poolType: 'lock-release', + tokenAddress: TOKEN, + localTokenDecimals: 8, + routerAddress: ROUTER, + lockBoxAddress: LOCKBOX, + advancedPoolHooks: ZeroAddress, + }) + const expected = concat([ + LOCK_RELEASE_TOKEN_POOL_BYTECODE, + coder.encode( + ['address', 'uint8', 'address', 'address', 'address', 'address'], + [TOKEN, 8, ZeroAddress, RMN, ROUTER, LOCKBOX], + ), + ]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('requires lockBoxAddress on the unsigned lock-release path', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + poolType: 'lock-release', + tokenAddress: TOKEN, + localTokenDecimals: 18, + routerAddress: ROUTER, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'lockBoxAddress', + ) + }) + + it('rejects an invalid poolType', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + poolType: 'nonsense' as never, + tokenAddress: TOKEN, + localTokenDecimals: 18, + routerAddress: ROUTER, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolType', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool.ts b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool.ts new file mode 100644 index 00000000..001ec39f --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-pool.ts @@ -0,0 +1,200 @@ +/** + * deployPool — deploys a canonical CCT v2.0 CCIP token pool (BurnMint or LockRelease). + * + * Signed `execute` (multi-step for lock-release): auto-deploys an `ERC20LockBox` bound + * to the token (if none supplied), deploys the pool, then authorizes the pool as a + * caller on the lockbox. Returns the deployed `poolAddress` (+ `lockBoxAddress`). + * + * The unsigned `generate` path builds only the pool-creation tx and requires an existing + * `lockBoxAddress` for lock-release (it cannot predict an auto-deployed lockbox address). + * + * @packageDocumentation + */ + +import { type TransactionReceipt, AbiCoder, Contract, ZeroAddress, concat } from 'ethers' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { interfaces } from '../../../../evm/const.ts' +import { type EVMChain, isSigner, submitTransaction } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { type DeployVerification, buildDeployVerification } from '../../deploy-verification.ts' +import { EVMOperation } from '../../operation.ts' +import { submitForReceipt } from '../../submit.ts' +import { BURN_MINT_TOKEN_POOL_BYTECODE } from '../bytecodes/BurnMintTokenPool.ts' +import { ERC20_LOCK_BOX_BYTECODE } from '../bytecodes/ERC20LockBox.ts' +import { LOCK_RELEASE_TOKEN_POOL_BYTECODE } from '../bytecodes/LockReleaseTokenPool.ts' + +/** Which pool contract to deploy. */ +export type EVMPoolType = 'burn-mint' | 'lock-release' + +/** Parameters for `deployPool`. */ +export type DeployPoolParams = { + poolType: EVMPoolType + tokenAddress: string + localTokenDecimals: number + /** CCIP Router; `rmnProxy` is derived from it via `Router.getArmProxy()`. */ + routerAddress: string + /** Advanced pool hooks contract. Defaults to the zero address. */ + advancedPoolHooks?: string + /** + * `lock-release` only: an existing `ERC20LockBox`. Required on the unsigned path; + * the signed path auto-deploys one when omitted. + */ + lockBoxAddress?: string + sender?: string +} + +/** Result of a signed `deployPool`: tx hash, deployed pool address, and (lock-release) lockbox. */ +export type DeployPoolResult = TransactionHash & { + poolAddress: string + /** EVM lock-release only: the `ERC20LockBox` bound to the token. */ + lockBoxAddress?: string + /** Block-explorer verification handle for the deployed pool. */ + verification: DeployVerification + /** Verification handle for the auto-deployed `ERC20LockBox` (lock-release only). */ + lockBoxVerification?: DeployVerification +} + +/** Deploys a CCIP token pool (BurnMint or LockRelease). */ +export class DeployPool extends EVMOperation { + readonly name = 'deployPool' + + /** Validates pool params (lockBoxAddress required on the unsigned lock-release path). */ + protected validate(p: DeployPoolParams): void { + if (p.poolType !== 'burn-mint' && p.poolType !== 'lock-release') + throw new CCTParamsInvalidError( + this.name, + 'poolType', + "must be 'burn-mint' or 'lock-release'", + ) + if (!p.tokenAddress || p.tokenAddress.trim().length === 0) + throw new CCTParamsInvalidError(this.name, 'tokenAddress', 'must be non-empty') + if (!p.routerAddress || p.routerAddress.trim().length === 0) + throw new CCTParamsInvalidError(this.name, 'routerAddress', 'must be non-empty') + if (p.localTokenDecimals < 0 || p.localTokenDecimals > 255) + throw new CCTParamsInvalidError(this.name, 'localTokenDecimals', 'must be 0-255') + if ( + p.poolType === 'lock-release' && + (!p.lockBoxAddress || p.lockBoxAddress.trim().length === 0) + ) + throw new CCTParamsInvalidError( + this.name, + 'lockBoxAddress', + 'required to build a lock-release pool deploy (the signed deployPool auto-deploys an ERC20LockBox)', + ) + } + + /** Reads `rmnProxy` from the router via `Router.getArmProxy()`. */ + private async deriveRmnProxy(chain: EVMChain, routerAddress: string): Promise { + const router = new Contract(routerAddress, interfaces.Router, chain.provider) + return (await router.getFunction('getArmProxy')()) as string + } + + /** Builds the pool contract-creation tx (constructor args + bytecode). */ + protected async buildUnsigned(chain: EVMChain, p: DeployPoolParams): Promise { + const rmnProxy = await this.deriveRmnProxy(chain, p.routerAddress) + const advancedPoolHooks = p.advancedPoolHooks ?? ZeroAddress + const coder = AbiCoder.defaultAbiCoder() + + let data: string + if (p.poolType === 'burn-mint') { + const encodedArgs = coder.encode( + ['address', 'uint8', 'address', 'address', 'address'], + [p.tokenAddress, p.localTokenDecimals, advancedPoolHooks, rmnProxy, p.routerAddress], + ) + data = concat([BURN_MINT_TOKEN_POOL_BYTECODE, encodedArgs]) + } else { + const encodedArgs = coder.encode( + ['address', 'uint8', 'address', 'address', 'address', 'address'], + [ + p.tokenAddress, + p.localTokenDecimals, + advancedPoolHooks, + rmnProxy, + p.routerAddress, + p.lockBoxAddress!, + ], + ) + data = concat([LOCK_RELEASE_TOKEN_POOL_BYTECODE, encodedArgs]) + } + return { family: ChainFamily.EVM, transactions: [{ to: null, data }] } + } + + /** + * Signed multi-step deploy: (1) auto-deploy an ERC20LockBox for lock-release pools + * without one, (2) deploy the pool, (3) authorize the pool on the auto-deployed lockbox. + */ + override async execute( + chain: EVMChain, + params: DeployPoolParams & { wallet: unknown }, + ): Promise { + const { wallet } = params + if (!isSigner(wallet)) throw new CCIPWalletInvalidError(wallet) + + // (1) auto-deploy the ERC20LockBox for lock-release pools lacking one. + let lockBoxAddress = params.lockBoxAddress + let autoDeployedLockBox = false + let lockBoxVerification: DeployVerification | undefined + if (params.poolType === 'lock-release' && !lockBoxAddress) { + const lockBoxArgs = AbiCoder.defaultAbiCoder().encode(['address'], [params.tokenAddress]) + const lockBoxData = concat([ERC20_LOCK_BOX_BYTECODE, lockBoxArgs]) + const { receipt } = await submitForReceipt( + chain, + wallet, + { family: ChainFamily.EVM, transactions: [{ to: null, data: lockBoxData }] }, + `${this.name}: ERC20LockBox`, + ) + lockBoxAddress = this.#contractAddress(receipt, 'ERC20LockBox') + autoDeployedLockBox = true + lockBoxVerification = buildDeployVerification( + 'ERC20LockBox', + lockBoxData, + ERC20_LOCK_BOX_BYTECODE, + ) + } + + // (2) deploy the pool. + const unsigned = await this.generate(chain, { ...params, lockBoxAddress }) + const { hash, receipt } = await submitForReceipt(chain, wallet, unsigned, this.name) + const poolAddress = this.#contractAddress(receipt, 'pool') + const verification = buildDeployVerification( + params.poolType === 'burn-mint' ? 'BurnMintTokenPool' : 'LockReleaseTokenPool', + unsigned.transactions[0]!.data!, + params.poolType === 'burn-mint' + ? BURN_MINT_TOKEN_POOL_BYTECODE + : LOCK_RELEASE_TOKEN_POOL_BYTECODE, + ) + + // (3) authorize the pool as a caller on the auto-deployed lockbox. + if (autoDeployedLockBox && lockBoxAddress) { + const authData = interfaces.ERC20LockBox_v2_0.encodeFunctionData( + 'applyAuthorizedCallerUpdates', + [{ addedCallers: [poolAddress], removedCallers: [] }], + ) + const authTx = await wallet.populateTransaction({ to: lockBoxAddress, data: authData }) + authTx.from = undefined + const response = await submitTransaction(wallet, authTx, chain.provider) + await response.wait(1, 60_000) + } + + return { + hash, + poolAddress, + ...(lockBoxAddress ? { lockBoxAddress } : {}), + verification, + ...(lockBoxVerification ? { lockBoxVerification } : {}), + } + } + + /** Returns the deployed contract address from a creation receipt, or throws. */ + #contractAddress(receipt: TransactionReceipt, what: string): string { + if (!receipt.contractAddress) + throw new CCTTxFailedError(this.name, `no ${what} address in deploy receipt`, { + context: { txHash: receipt.hash }, + }) + return receipt.contractAddress + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-and-pool-via-factory.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-and-pool-via-factory.test.ts new file mode 100644 index 00000000..7e2f4bf3 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-and-pool-via-factory.test.ts @@ -0,0 +1,184 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { AbiCoder, ZeroAddress, concat } from 'ethers' + +import { DeployTokenAndPoolViaFactory } from './deploy-token-and-pool-via-factory.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { CROSS_CHAIN_TOKEN_BYTECODE } from '../../token/bytecodes/CrossChainToken.ts' +import { BURN_MINT_TOKEN_POOL_BYTECODE } from '../bytecodes/BurnMintTokenPool.ts' +import { LOCK_RELEASE_TOKEN_POOL_BYTECODE } from '../bytecodes/LockReleaseTokenPool.ts' +import { FACTORY_POOL_TYPE, tokenPoolFactoryInterface } from '../token-pool-factory-abi.ts' + +const FACTORY = '0x1111111111111111111111111111111111111111' +const OWNER = '0xdddddddddddddddddddddddddddddddddddddddd' +const RECIPIENT = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' +const SALT = '0x' + '22'.repeat(32) +const TOKEN_TUPLE = + 'tuple(string name, string symbol, uint256 maxSupply, uint256 preMint, address preMintRecipient, uint8 decimals, address ccipAdmin)' + +/** Minimal stub chain — buildUnsigned is pure encoding (no RPC / staticCall). */ +function stubChain(): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + } as unknown as EVMChain +} + +/** Rebuilds the CrossChainToken init code the way the op does, for byte-parity checks. */ +function tokenInitCode( + ccipAdmin: string, + burnMintRoleAdmin: string, + futureOwner: string, + preMint: bigint, + preMintRecipient: string, +): string { + const tokenArgs = AbiCoder.defaultAbiCoder().encode( + [TOKEN_TUPLE, 'address', 'address'], + [ + { + name: 'My Token', + symbol: 'MTK', + maxSupply: 1000n, + preMint, + preMintRecipient, + decimals: 18, + ccipAdmin, + }, + burnMintRoleAdmin, + futureOwner, + ], + ) + return concat([CROSS_CHAIN_TOKEN_BYTECODE, tokenArgs]) +} + +describe('EVM cct deployTokenAndPoolViaFactory', () => { + const op = new DeployTokenAndPoolViaFactory() + + it('burn-mint: byte-identical factory-call calldata; factory is ccipAdmin + role admin', async () => { + const unsigned = await op.generate(stubChain(), { + factoryAddress: FACTORY, + name: 'My Token', + symbol: 'MTK', + decimals: 18, + maxSupply: 1000n, + poolType: 'burn-mint', + salt: SALT, + futureOwner: OWNER, + }) + const expected = tokenPoolFactoryInterface.encodeFunctionData('deployTokenAndTokenPool', [ + [], + 18, + FACTORY_POOL_TYPE['burn-mint'], + tokenInitCode(FACTORY, FACTORY, OWNER, 0n, ZeroAddress), + BURN_MINT_TOKEN_POOL_BYTECODE, + ZeroAddress, + SALT, + OWNER, + ]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, FACTORY) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('lock-release: role admin is futureOwner; pre-mint recipient defaults to futureOwner', async () => { + const unsigned = await op.generate(stubChain(), { + factoryAddress: FACTORY, + name: 'My Token', + symbol: 'MTK', + decimals: 18, + maxSupply: 1000n, + preMint: 500n, + poolType: 'lock-release', + salt: SALT, + futureOwner: OWNER, + }) + const expected = tokenPoolFactoryInterface.encodeFunctionData('deployTokenAndTokenPool', [ + [], + 18, + FACTORY_POOL_TYPE['lock-release'], + tokenInitCode(FACTORY, OWNER, OWNER, 500n, OWNER), + LOCK_RELEASE_TOKEN_POOL_BYTECODE, + ZeroAddress, + SALT, + OWNER, + ]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('honours an explicit preMintRecipient when preMint > 0', async () => { + const unsigned = await op.generate(stubChain(), { + factoryAddress: FACTORY, + name: 'My Token', + symbol: 'MTK', + decimals: 18, + maxSupply: 1000n, + preMint: 500n, + preMintRecipient: RECIPIENT, + poolType: 'burn-mint', + salt: SALT, + futureOwner: OWNER, + }) + const expected = tokenPoolFactoryInterface.encodeFunctionData('deployTokenAndTokenPool', [ + [], + 18, + FACTORY_POOL_TYPE['burn-mint'], + tokenInitCode(FACTORY, FACTORY, OWNER, 500n, RECIPIENT), + BURN_MINT_TOKEN_POOL_BYTECODE, + ZeroAddress, + SALT, + OWNER, + ]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('requires futureOwner on the unsigned path', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + factoryAddress: FACTORY, + name: 'My Token', + symbol: 'MTK', + decimals: 18, + maxSupply: 1000n, + poolType: 'burn-mint', + salt: SALT, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'futureOwner', + ) + }) + + it('rejects preMint exceeding maxSupply', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + factoryAddress: FACTORY, + name: 'My Token', + symbol: 'MTK', + decimals: 18, + maxSupply: 100n, + preMint: 200n, + poolType: 'burn-mint', + futureOwner: OWNER, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'preMint', + ) + }) + + it('rejects an empty symbol', async () => { + await assert.rejects( + () => + op.generate(stubChain(), { + factoryAddress: FACTORY, + name: 'My Token', + symbol: '', + decimals: 18, + maxSupply: 1000n, + poolType: 'burn-mint', + futureOwner: OWNER, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'symbol', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-and-pool-via-factory.ts b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-and-pool-via-factory.ts new file mode 100644 index 00000000..b98e6ede --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-and-pool-via-factory.ts @@ -0,0 +1,264 @@ +/** + * deployTokenAndPoolViaFactory — deploys a new `CrossChainToken` **and** its token pool in a + * single transaction through a `TokenPoolFactory 2.0.0` (CREATE2). + * + * The signed `execute` path resolves both addresses by `staticCall`-ing the factory's + * `deployTokenAndTokenPool(...)` first, then broadcasts the identical call. `futureOwner` is + * auto-filled from the signer when omitted; the CREATE2 `salt` defaults to a random 32-byte value. + * + * The unsigned `generate` path builds only the factory-call tx (to: factory) and requires an + * explicit `futureOwner` (it is baked into the token's constructor). + * + * The factory must be the token's `ccipAdmin` (and, for burn-mint, its burn/mint role admin) to + * wire the registry, so those are set to the factory address; final ownership of the token + pool + * goes to `futureOwner`. + * + * @packageDocumentation + */ + +import { AbiCoder, Contract, ZeroAddress, concat, hexlify, randomBytes } from 'ethers' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { interfaces } from '../../../../evm/const.ts' +import { type EVMChain, isSigner } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type DeployVerificationTarget, + buildFactoryPoolVerification, +} from '../../deploy-verification.ts' +import { EVMOperation } from '../../operation.ts' +import { submitForReceipt } from '../../submit.ts' +import { CROSS_CHAIN_TOKEN_BYTECODE } from '../../token/bytecodes/CrossChainToken.ts' +import { BURN_MINT_TOKEN_POOL_BYTECODE } from '../bytecodes/BurnMintTokenPool.ts' +import { LOCK_RELEASE_TOKEN_POOL_BYTECODE } from '../bytecodes/LockReleaseTokenPool.ts' +import { + FACTORY_POOL_TYPE, + TOKEN_POOL_FACTORY_ABI, + tokenPoolFactoryInterface, +} from '../token-pool-factory-abi.ts' +import type { FactoryPoolType } from './deploy-pool-via-factory.ts' + +/** Canonical CCT v2.0 constructor tuple for CrossChainToken: `ConstructorParams`. */ +const CROSS_CHAIN_TOKEN_PARAMS_TUPLE = + 'tuple(string name, string symbol, uint256 maxSupply, uint256 preMint, address preMintRecipient, uint8 decimals, address ccipAdmin)' + +/** Parameters for `deployTokenAndPoolViaFactory`. */ +export type DeployTokenAndPoolViaFactoryParams = { + /** The `TokenPoolFactory 2.0.0` address on this chain. */ + factoryAddress: string + name: string + symbol: string + decimals: number + maxSupply: bigint + /** Amount pre-minted at deploy. Defaults to `0n`. */ + preMint?: bigint + /** Recipient of the pre-mint. Defaults to `futureOwner`; ignored when `preMint` is `0n`. */ + preMintRecipient?: string + poolType: FactoryPoolType + /** Existing `ERC20LockBox` for lock-release; the factory auto-deploys one when omitted. */ + lockBoxAddress?: string + /** CREATE2 salt. A random 32-byte value is used when omitted (non-deterministic addresses). */ + salt?: string + /** + * Final owner of the token + pool. Required on the unsigned path; auto-filled from the signer + * on the signed path. + */ + futureOwner?: string + sender?: string +} + +/** Result of a signed `deployTokenAndPoolViaFactory`: tx hash plus both CREATE2 addresses. */ +export type DeployTokenAndPoolViaFactoryResult = TransactionHash & { + tokenAddress: string + poolAddress: string + /** + * EVM lock-release only: the `ERC20LockBox` bound to the token. Read from + * `pool.getLockBox()` after the deploy when the caller did not supply one (the factory + * auto-deploys it). Omitted for burn-mint pools. + */ + lockBoxAddress?: string + /** + * Block-explorer verification handles for every contract the factory deployed (the token, + * the pool, plus the auto-deployed `ERC20LockBox` for lock-release). The factory creates + * these in internal CREATE2 calls, so each carries its address alongside its constructor args. + */ + verifications: DeployVerificationTarget[] +} + +/** Deploys a new CrossChainToken and its pool in one tx via `TokenPoolFactory 2.0.0`. */ +export class DeployTokenAndPoolViaFactory extends EVMOperation< + DeployTokenAndPoolViaFactoryParams, + DeployTokenAndPoolViaFactoryResult +> { + readonly name = 'deployTokenAndPoolViaFactory' + + /** Validates factory-deploy params (`futureOwner` required on the unsigned path). */ + protected validate(p: DeployTokenAndPoolViaFactoryParams): void { + if (p.poolType !== 'burn-mint' && p.poolType !== 'lock-release') + throw new CCTParamsInvalidError( + this.name, + 'poolType', + "must be 'burn-mint' or 'lock-release'", + ) + if (!p.factoryAddress || p.factoryAddress.trim().length === 0) + throw new CCTParamsInvalidError(this.name, 'factoryAddress', 'must be non-empty') + if (!p.name || p.name.trim().length === 0) + throw new CCTParamsInvalidError(this.name, 'name', 'must be non-empty') + if (!p.symbol || p.symbol.trim().length === 0) + throw new CCTParamsInvalidError(this.name, 'symbol', 'must be non-empty') + if (p.decimals < 0 || p.decimals > 255) + throw new CCTParamsInvalidError(this.name, 'decimals', 'must be 0-255') + if (p.maxSupply < 0n) + throw new CCTParamsInvalidError(this.name, 'maxSupply', 'must be non-negative') + if (p.preMint !== undefined && p.preMint < 0n) + throw new CCTParamsInvalidError(this.name, 'preMint', 'must be non-negative') + if (p.maxSupply > 0n && p.preMint !== undefined && p.preMint > p.maxSupply) + throw new CCTParamsInvalidError(this.name, 'preMint', 'exceeds maxSupply') + if (!p.futureOwner || p.futureOwner.trim().length === 0) + throw new CCTParamsInvalidError( + this.name, + 'futureOwner', + 'required (the signed deployTokenAndPoolViaFactory path auto-fills it from the signer)', + ) + } + + /** + * Assembles the `deployTokenAndTokenPool` argument tuple. The factory is set as the token's + * `ccipAdmin` (and burn/mint role admin for burn-mint) so it can wire the registry; final + * ownership goes to `futureOwner`. `salt` defaults to a fresh random value when omitted. + */ + private assembleArgs(p: DeployTokenAndPoolViaFactoryParams): { + deployArgs: unknown[] + tokenArgs: string + } { + const futureOwner = p.futureOwner! + const preMint = p.preMint ?? 0n + // CrossChainToken reverts unless preMintRecipient is zero exactly when preMint is zero. + const preMintRecipient = preMint > 0n ? (p.preMintRecipient ?? futureOwner) : ZeroAddress + const burnMintRoleAdmin = p.poolType === 'burn-mint' ? p.factoryAddress : futureOwner + + const tokenArgs = AbiCoder.defaultAbiCoder().encode( + [CROSS_CHAIN_TOKEN_PARAMS_TUPLE, 'address', 'address'], + [ + { + name: p.name, + symbol: p.symbol, + maxSupply: p.maxSupply, + preMint, + preMintRecipient, + decimals: p.decimals, + ccipAdmin: p.factoryAddress, + }, + burnMintRoleAdmin, + futureOwner, + ], + ) + const tokenInitCode = concat([CROSS_CHAIN_TOKEN_BYTECODE, tokenArgs]) + const poolBytecode = + p.poolType === 'burn-mint' ? BURN_MINT_TOKEN_POOL_BYTECODE : LOCK_RELEASE_TOKEN_POOL_BYTECODE + return { + deployArgs: [ + [], + p.decimals, + FACTORY_POOL_TYPE[p.poolType], + tokenInitCode, + poolBytecode, + p.lockBoxAddress ?? ZeroAddress, + p.salt ?? hexlify(randomBytes(32)), + futureOwner, + ], + tokenArgs, + } + } + + /** Builds the factory-call tx (`to: factory`, `data: deployTokenAndTokenPool(...)`). */ + protected buildUnsigned(_chain: EVMChain, p: DeployTokenAndPoolViaFactoryParams): UnsignedEVMTx { + const data = tokenPoolFactoryInterface.encodeFunctionData( + 'deployTokenAndTokenPool', + this.assembleArgs(p).deployArgs, + ) + return { family: ChainFamily.EVM, transactions: [{ to: p.factoryAddress, data }] } + } + + /** + * Signed factory deploy: auto-fills `futureOwner` from the signer, fixes the CREATE2 salt, + * `staticCall`s the factory to resolve token + pool addresses, then broadcasts the same call. + */ + override async execute( + chain: EVMChain, + params: DeployTokenAndPoolViaFactoryParams & { wallet: unknown }, + ): Promise { + const { wallet } = params + if (!isSigner(wallet)) throw new CCIPWalletInvalidError(wallet) + + // Resolve owner + salt once so the staticCall and the broadcast deploy to the same addresses. + const futureOwner = params.futureOwner ?? (await wallet.getAddress()) + const effective: DeployTokenAndPoolViaFactoryParams = { + ...params, + futureOwner, + salt: params.salt ?? hexlify(randomBytes(32)), + } + const { deployArgs, tokenArgs } = this.assembleArgs(effective) + + const factory = new Contract(effective.factoryAddress, TOKEN_POOL_FACTORY_ABI, wallet) + const deployFn = factory.getFunction('deployTokenAndTokenPool') + chain.logger.debug(`${this.name}: simulating to resolve addresses...`) + const [tokenAddress, poolAddress] = (await deployFn.staticCall(...deployArgs)) as [ + string, + string, + ] + + // The factory appends the pool ctor args from its own immutables; read them to rebuild + // the verification handles. + const { rmnProxy, ccipRouter } = (await factory.getFunction('getStaticConfig')()) as { + rmnProxy: string + ccipRouter: string + } + + const unsigned = await this.generate(chain, effective) + const { hash } = await submitForReceipt(chain, wallet, unsigned, this.name) + + // lock-release without a supplied lockbox: the factory auto-deploys one; surface it. + let lockBoxAddress = params.lockBoxAddress + if (params.poolType === 'lock-release' && !lockBoxAddress) { + const pool = new Contract(poolAddress, interfaces.TokenPool_v2_0, chain.provider) + lockBoxAddress = (await pool.getFunction('getLockBox')()) as string + } + + const { poolVerification, lockBoxVerification } = buildFactoryPoolVerification( + params.poolType === 'lock-release' + ? { + poolType: 'lock-release', + token: tokenAddress, + decimals: params.decimals, + rmnProxy, + router: ccipRouter, + poolAddress, + lockBoxAddress: lockBoxAddress!, + } + : { + poolType: 'burn-mint', + token: tokenAddress, + decimals: params.decimals, + rmnProxy, + router: ccipRouter, + poolAddress, + }, + ) + const verifications: DeployVerificationTarget[] = [ + { contract: 'CrossChainToken', address: tokenAddress, encodedConstructorArgs: tokenArgs }, + poolVerification, + ...(lockBoxVerification ? [lockBoxVerification] : []), + ] + return { + hash, + tokenAddress, + poolAddress, + ...(lockBoxAddress ? { lockBoxAddress } : {}), + verifications, + } + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/provide-liquidity.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/provide-liquidity.test.ts new file mode 100644 index 00000000..28f21464 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/provide-liquidity.test.ts @@ -0,0 +1,194 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { AbiCoder, Interface, getAddress } from 'ethers' + +import { ProvideLiquidity } from './provide-liquidity.ts' +import ERC20LockBox_ABI from '../../../../evm/abi/ERC20LockBox.ts' +import TokenPool_1_6_ABI from '../../../../evm/abi/LockReleaseTokenPool_1_6_1.ts' +import TokenPool_2_0_ABI from '../../../../evm/abi/TokenPool_2_0.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const POOL = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const TOKEN = '0xd7bf0e3d34b4c4f7d5f3c4c6b2a1e0f9c8b7a6d5' +const LOCK_BOX = '0xaabbccddeeff00112233445566778899aabbccdd' +const AMOUNT = 1_000n * 10n ** 18n + +const approveIface = new Interface(['function approve(address spender, uint256 amount)']) +const poolIface_1_6 = new Interface(TokenPool_1_6_ABI) +const poolIface_2_0 = new Interface(TokenPool_2_0_ABI) +const lockBoxIface = new Interface(ERC20LockBox_ABI) + +const WALLET = '0x00000000000000000000000000000000000000ab' + +const enc = (addr: string) => AbiCoder.defaultAbiCoder().encode(['address'], [addr]) +const encAddrs = (addrs: string[]) => AbiCoder.defaultAbiCoder().encode(['address[]'], [addrs]) +const selector = (iface: Interface, name: string) => iface.getFunction(name)!.selector + +/** + * Stub chain: `typeAndVersion` reports the given pool type + version; `provider.call` + * dispatches on the 4-byte selector to answer `getToken` / `getLockBox`. + */ +function stubChain(version: CCIPVersion, poolType = 'LockReleaseTokenPool'): EVMChain { + const iface = version >= CCIPVersion.V2_0 ? poolIface_2_0 : poolIface_1_6 + const getTokenSel = selector(iface, 'getToken') + const getLockBoxSel = version >= CCIPVersion.V2_0 ? selector(iface, 'getLockBox') : '' + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + typeAndVersion: () => Promise.resolve([poolType, version, '', undefined]), + provider: { + call: (tx: { data?: string }) => { + const sel = (tx.data ?? '').slice(0, 10) + if (sel === getTokenSel) return Promise.resolve(enc(TOKEN)) + if (sel === getLockBoxSel) return Promise.resolve(enc(LOCK_BOX)) + return Promise.reject(new Error(`unexpected call selector ${sel}`)) + }, + }, + } as unknown as EVMChain +} + +describe('EVM cct provideLiquidity', () => { + const op = new ProvideLiquidity() + + it('v1.6: [approve(pool), pool.provideLiquidity(amount)] — byte-identical', async () => { + const unsigned = await op.generate(stubChain(CCIPVersion.V1_6), { + poolAddress: POOL, + amount: AMOUNT, + }) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 2) + + const [approveTx, provideTx] = unsigned.transactions + assert.equal(approveTx!.to, getAddress(TOKEN)) + assert.equal(approveTx!.data, approveIface.encodeFunctionData('approve', [POOL, AMOUNT])) + assert.equal(provideTx!.to, POOL) + assert.equal(provideTx!.data, poolIface_1_6.encodeFunctionData('provideLiquidity', [AMOUNT])) + }) + + it('v2.0: [approve(lockBox), lockBox.deposit(token,0,amount)] — byte-identical', async () => { + const unsigned = await op.generate(stubChain(CCIPVersion.V2_0), { + poolAddress: POOL, + amount: AMOUNT, + }) + assert.equal(unsigned.transactions.length, 2) + + const [approveTx, provideTx] = unsigned.transactions + assert.equal(approveTx!.to, getAddress(TOKEN)) + assert.equal(approveTx!.data, approveIface.encodeFunctionData('approve', [LOCK_BOX, AMOUNT])) + assert.equal(provideTx!.to, getAddress(LOCK_BOX)) + assert.equal(provideTx!.data, lockBoxIface.encodeFunctionData('deposit', [TOKEN, 0n, AMOUNT])) + }) + + it('applies sender to from on the first tx', async () => { + const unsigned = await op.generate(stubChain(CCIPVersion.V1_6), { + poolAddress: POOL, + amount: AMOUNT, + sender: POOL, + }) + assert.equal(unsigned.transactions[0]!.from, POOL) + }) + + it('rejects an invalid pool address before RPC', async () => { + await assert.rejects( + () => op.generate(stubChain(CCIPVersion.V1_6), { poolAddress: 'nope', amount: AMOUNT }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) + + it('rejects a non-positive amount', async () => { + await assert.rejects( + () => op.generate(stubChain(CCIPVersion.V1_6), { poolAddress: POOL, amount: 0n }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'amount', + ) + }) + + it('rejects non-lock-release pools', async () => { + await assert.rejects( + () => + op.generate(stubChain(CCIPVersion.V2_0, 'BurnMintTokenPool'), { + poolAddress: POOL, + amount: AMOUNT, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'poolAddress', + ) + }) + + /** + * Execute stub: `getAllAuthorizedCallers` returns `authorized`; `getLockBox`/`getToken` + * answer the reads; every `sendTransaction` is recorded so tests can assert whether the + * lockbox pre-authorize tx was issued. Signer/chain plumbing is minimal. + */ + function executeStub( + version: CCIPVersion, + authorized: string[], + poolType = 'LockReleaseTokenPool', + ) { + const iface = version >= CCIPVersion.V2_0 ? poolIface_2_0 : poolIface_1_6 + const getTokenSel = selector(iface, 'getToken') + const getLockBoxSel = version >= CCIPVersion.V2_0 ? selector(iface, 'getLockBox') : '' + const getAuthSel = selector(lockBoxIface, 'getAllAuthorizedCallers') + const sent: { to?: string | null; data?: string }[] = [] + const wallet = { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(getAddress(WALLET)), + populateTransaction: (tx: { to?: string | null; data?: string }) => + Promise.resolve({ ...tx }), + sendTransaction: (tx: { to?: string | null; data?: string }) => { + sent.push(tx) + return Promise.resolve({ + hash: `0xhash${sent.length}`, + wait: () => Promise.resolve({ status: 1, hash: `0xhash${sent.length}` }), + }) + }, + } + const chain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + typeAndVersion: () => Promise.resolve([poolType, version, '', undefined]), + nextNonce: () => Promise.resolve(0), + rollbackNonce: () => {}, + provider: { + call: (tx: { data?: string }) => { + const sel = (tx.data ?? '').slice(0, 10) + if (sel === getTokenSel) return Promise.resolve(enc(TOKEN)) + if (sel === getLockBoxSel) return Promise.resolve(enc(LOCK_BOX)) + if (sel === getAuthSel) return Promise.resolve(encAddrs(authorized)) + return Promise.reject(new Error(`unexpected call selector ${sel}`)) + }, + }, + } as unknown as EVMChain + return { chain, wallet, sent } + } + + const applyAuthSel = selector(lockBoxIface, 'applyAuthorizedCallerUpdates') + + it('v2.0: pre-authorizes an unauthorized caller on the lockbox before approve+deposit', async () => { + const { chain, wallet, sent } = executeStub(CCIPVersion.V2_0, []) + await op.execute(chain, { poolAddress: POOL, amount: AMOUNT, wallet }) + assert.equal(sent.length, 3) + assert.equal(getAddress(sent[0]!.to as string), getAddress(LOCK_BOX)) + assert.equal((sent[0]!.data ?? '').slice(0, 10), applyAuthSel) + // the authorize update adds exactly the caller. + const decoded = lockBoxIface.decodeFunctionData('applyAuthorizedCallerUpdates', sent[0]!.data!) + assert.equal( + getAddress((decoded[0] as { addedCallers: string[] }).addedCallers[0]!), + getAddress(WALLET), + ) + }) + + it('v2.0: skips pre-authorize when the caller is already authorized', async () => { + const { chain, wallet, sent } = executeStub(CCIPVersion.V2_0, [getAddress(WALLET)]) + await op.execute(chain, { poolAddress: POOL, amount: AMOUNT, wallet }) + assert.equal(sent.length, 2) + assert.notEqual((sent[0]!.data ?? '').slice(0, 10), applyAuthSel) + }) + + it('v1.6: never issues a pre-authorize tx (no lockbox)', async () => { + const { chain, wallet, sent } = executeStub(CCIPVersion.V1_6, []) + await op.execute(chain, { poolAddress: POOL, amount: AMOUNT, wallet }) + assert.equal(sent.length, 2) + assert.notEqual((sent[0]!.data ?? '').slice(0, 10), applyAuthSel) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/provide-liquidity.ts b/ccip-sdk/src/cct/evm/token-pool/operations/provide-liquidity.ts new file mode 100644 index 00000000..a53ccaeb --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/provide-liquidity.ts @@ -0,0 +1,154 @@ +/** + * provideLiquidity — funds a lock-release token pool with liquidity so it can release + * tokens on inbound CCIP transfers. **EVM lock-release pools only** (burn-mint pools + * mint on demand and hold no liquidity). + * + * Emits **two** transactions, `[approveTx, provideTx]` (the shared submit runs them + * sequentially and returns the second, provide/deposit, hash): + * 1. ERC20 `approve(spender, amount)` on the pool's token. + * 2. The version-specific provide call: + * - **v1.5 / v1.6**: `pool.provideLiquidity(amount)` — liquidity held by the pool, + * spender is the pool (caller must be the pool's rebalancer). + * - **v2.0**: `lockBox.deposit(token, 0, amount)` on the pool's `ERC20LockBox` + * (resolved via `pool.getLockBox()`), spender is the lock box. The + * `remoteChainSelector` deposit arg is unused on-chain (passed as `0`). + * + * @packageDocumentation + */ + +import { Contract, Interface } from 'ethers' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { interfaces } from '../../../../evm/const.ts' +import { type EVMChain, isSigner } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCIPVersion } from '../../../../types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { EVMOperation } from '../../operation.ts' +import { submitForReceipt } from '../../submit.ts' +import { validateAddress } from '../../validate.ts' + +/** Minimal ERC20 approve interface — spender depends on the pool version. */ +const ERC20_APPROVE_IFACE = new Interface(['function approve(address spender, uint256 amount)']) + +/** Parameters for `provideLiquidity`. */ +export type ProvideLiquidityParams = { + /** Local lock-release pool address. */ + poolAddress: string + /** Amount of token (in smallest units) to provide as liquidity. Must be greater than 0. */ + amount: bigint + sender?: string +} + +/** Resolves the version-appropriate cached pool interface for reads/encoding. */ +function poolInterface(version: string): Interface { + if (version <= CCIPVersion.V1_5) return interfaces.TokenPool_v1_5 + if (version < CCIPVersion.V2_0) return interfaces.TokenPool_v1_6 + return interfaces.TokenPool_v2_0 +} + +/** Provides liquidity to an EVM lock-release token pool (approve + provide/deposit). */ +export class ProvideLiquidity extends EVMOperation { + readonly name = 'provideLiquidity' + + /** Validates the pool address and amount before any RPC. */ + protected validate(p: ProvideLiquidityParams): void { + validateAddress(this.name, 'poolAddress', p.poolAddress) + if (p.amount <= 0n) { + throw new CCTParamsInvalidError(this.name, 'amount', 'must be greater than 0') + } + } + + /** Builds `[approveTx, provideTx]`; rejects non-lock-release pools. */ + protected async buildUnsigned( + chain: EVMChain, + p: ProvideLiquidityParams, + ): Promise { + // DX guard: provide-liquidity is meaningful only for lock-release pools. + const [poolType, version] = await chain.typeAndVersion(p.poolAddress) + if (!poolType.includes('LockRelease')) { + throw new CCTParamsInvalidError( + this.name, + 'poolAddress', + `provide-liquidity is only supported for lock-release pools (got ${poolType})`, + ) + } + + const iface = poolInterface(version) + const poolContract = new Contract(p.poolAddress, iface, chain.provider) + const token = (await poolContract.getFunction('getToken')()) as string + + let spender: string + let provideTx: { to: string; data: string } + if (version >= CCIPVersion.V2_0) { + // v2.0: liquidity lives in a separate ERC20LockBox whose deposit() is gated on + // authorized callers; the signed `execute` path pre-authorizes the caller when needed. + const lockBox = (await poolContract.getFunction('getLockBox')()) as string + spender = lockBox + // deposit(address token, uint64 remoteChainSelector, uint256 amount) — selector arg unused. + provideTx = { + to: lockBox, + data: interfaces.ERC20LockBox_v2_0.encodeFunctionData('deposit', [token, 0n, p.amount]), + } + } else { + // v1.5/v1.6: liquidity held by the pool itself; caller must be the rebalancer. + spender = p.poolAddress + provideTx = { + to: p.poolAddress, + data: iface.encodeFunctionData('provideLiquidity', [p.amount]), + } + } + + const approveTx = { + to: token, + data: ERC20_APPROVE_IFACE.encodeFunctionData('approve', [spender, p.amount]), + } + + return { family: ChainFamily.EVM, transactions: [approveTx, provideTx] } + } + + /** + * Signed flow with a pre-step for v2.0 lock-release pools: `ERC20LockBox.deposit()` is + * gated on authorized callers, so a first-time provider's deposit would revert. Before the + * normal approve+deposit, this reads the lockbox's `getAllAuthorizedCallers()` and, if the + * caller is not yet authorized, submits `applyAuthorizedCallerUpdates` to add it (requires + * the caller to be the lockbox owner). Non-lock-release / already-authorized callers and + * v1.5/v1.6 pools take no extra transaction. + */ + override async execute( + chain: EVMChain, + params: ProvideLiquidityParams & { wallet: unknown }, + ): Promise { + const { wallet } = params + const [poolType, version] = await chain.typeAndVersion(params.poolAddress) + if (version >= CCIPVersion.V2_0 && poolType.includes('LockRelease')) { + if (!isSigner(wallet)) throw new CCIPWalletInvalidError(wallet) + const poolContract = new Contract( + params.poolAddress, + interfaces.TokenPool_v2_0, + chain.provider, + ) + const lockBox = (await poolContract.getFunction('getLockBox')()) as string + const lockBoxContract = new Contract(lockBox, interfaces.ERC20LockBox_v2_0, chain.provider) + const caller = params.sender ?? (await wallet.getAddress()) + const authorized = (await lockBoxContract + .getFunction('getAllAuthorizedCallers')() + .catch(() => [] as string[])) as string[] + if (!authorized.some((a) => a.toLowerCase() === caller.toLowerCase())) { + chain.logger.debug(`${this.name}: authorizing caller on lockbox`, lockBox) + const authData = interfaces.ERC20LockBox_v2_0.encodeFunctionData( + 'applyAuthorizedCallerUpdates', + [{ addedCallers: [caller], removedCallers: [] }], + ) + const authUnsigned: UnsignedEVMTx = { + family: ChainFamily.EVM, + transactions: [{ to: lockBox, data: authData }], + } + await submitForReceipt(chain, wallet, authUnsigned, `${this.name}: authorizeCaller`) + } + } + return super.execute(chain, params) + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/token-pool-factory-abi.ts b/ccip-sdk/src/cct/evm/token-pool/token-pool-factory-abi.ts new file mode 100644 index 00000000..dc159281 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/token-pool-factory-abi.ts @@ -0,0 +1,28 @@ +/** + * Minimal human-readable ABI (and a cached {@link Interface}) for `TokenPoolFactory 2.0.0` + * (chainlink-ccip `chains/evm/contracts/TokenPoolFactory.sol`). + * + * Only the members the factory-deploy operations need are declared. `RemoteTokenPoolInfo` + * is spelled out so the empty remote-pools array type-checks; the same-chain deploys here + * always pass `[]` for it. + * + * @packageDocumentation + */ + +import { Interface } from 'ethers' + +const REMOTE_TOKEN_POOL_INFO = + 'tuple(uint64 remoteChainSelector, bytes remotePoolAddress, bytes remotePoolInitCode, tuple(address remotePoolFactory, address remoteRouter, address remoteRMNProxy, address remoteLockBox, uint8 remoteTokenDecimals) remoteChainConfig, uint8 poolType, bytes remoteTokenAddress, bytes remoteTokenInitCode, tuple(bool isEnabled, uint128 capacity, uint128 rate) rateLimiterConfig)[] remoteTokenPools' + +/** Human-readable ABI fragments for `TokenPoolFactory 2.0.0`. */ +export const TOKEN_POOL_FACTORY_ABI = [ + 'function getStaticConfig() view returns (address rmnProxy, address tokenAdminRegistry, address registryModuleOwnerCustom, address ccipRouter)', + `function deployTokenAndTokenPool(${REMOTE_TOKEN_POOL_INFO}, uint8 localTokenDecimals, uint8 localPoolType, bytes tokenInitCode, bytes tokenPoolInitCode, address lockBox, bytes32 salt, address futureOwner) returns (address token, address pool)`, + `function deployTokenPoolWithExistingToken(address token, uint8 localTokenDecimals, uint8 localPoolType, ${REMOTE_TOKEN_POOL_INFO}, bytes tokenPoolInitCode, address lockBox, bytes32 salt, address futureOwner) returns (address pool)`, +] as const + +/** Cached {@link Interface} built from {@link TOKEN_POOL_FACTORY_ABI}. */ +export const tokenPoolFactoryInterface = new Interface(TOKEN_POOL_FACTORY_ABI) + +/** `TokenPoolFactory` `PoolType` enum: `BURN_MINT = 0`, `LOCK_RELEASE = 1`. */ +export const FACTORY_POOL_TYPE = { 'burn-mint': 0, 'lock-release': 1 } as const diff --git a/ccip-sdk/src/cct/evm/token/bytecodes/CrossChainPoolToken.ts b/ccip-sdk/src/cct/evm/token/bytecodes/CrossChainPoolToken.ts new file mode 100644 index 00000000..cbdfc2bc --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/bytecodes/CrossChainPoolToken.ts @@ -0,0 +1,8 @@ +/** + * CrossChainPoolToken (v2.0.0) — combined token + pool deployment bytecode. Lazy-loaded via dynamic import(). + * + * Source: chainlink-ccip (refs/heads/main) + * chains/evm/gobindings/generated/v2_0_0/cross_chain_pool_token/cross_chain_pool_token.go + */ +export const CROSS_CHAIN_POOL_TOKEN_BYTECODE = + '0x61012080604052346106c5576165da803803809161001d82856106ca565b83398101906080818303126106c55780516001600160401b0381116106c55781019160e0838203126106c5576040519060e082016001600160401b038111838210176105c25760405283516001600160401b0381116106c557816100829186016106ed565b82526020840151906001600160401b0382116106c5576100a39185016106ed565b9182602083015260408401519060408301918252606085015193606084019485526100d06080870161075c565b936080810194855260a08701519160ff8316978884036106c55760c06100fd9160a085019586520161075c565b9760c083019889526101116020860161075c565b9161012a60606101236040890161075c565b970161075c565b93518051906001600160401b0382116105c25760035490600182811c921680156106bb575b60208310146105a25781601f84931161064b575b50602090601f83116001146105e3576000926105d8575b50508160011b916000199060031b1c1916176003555b8051906001600160401b0382116105c25760045490600182811c921680156105b8575b60208310146105a25781601f849311610532575b50602090601f83116001146104ca576000926104bf575b50508160011b916000199060031b1c1916176004555b33156104ae57600680546001600160a01b031916331790553015801561049d575b801561048c575b61047b573060805260c09490945260a093909352600880546001600160a01b039485166001600160a01b03199182161790915560078054929094169116179091555160ff1660e05251610100528151156104505780516001600160a01b03161561043f57519051906001600160a01b031680156104295760025491808301809311610413576020926002557fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef600093849284845283825260408420818154019055604051908152a361010051806103ed575b50505b516001600160a01b0316806103e85750335b601280546001600160a01b039283166001600160a01b03198216811790925560405192167f9524c9e4b0b61eb018dd58a1cd856e3e74009528328ab4a613b434fa631d7242600080a3615e69908161077182396080518181816102b50152818161223c01528181612a94015281816130d20152818161362401526137d7015260a0518181816134f701528181614b6301528181614bad01526151a0015260c0518181816103430152818161135e015281816122c901528181612b220152613160015260e0518161302001526101005181818161181e015281816155cf01526157110152f35b610303565b6002548181116103fd57506102ee565b637502c12360e11b835260045260245260449150fd5b634e487b7160e01b600052601160045260246000fd5b63ec442f0560e01b600052600060045260246000fd5b634dd371db60e11b60005260046000fd5b516001600160a01b031690508061046757506102f1565b63f5c8f5a160e01b60005260045260246000fd5b630a64406560e11b60005260046000fd5b506001600160a01b0385161561021c565b506001600160a01b03831615610215565b639b15e16f60e01b60005260046000fd5b0151905038806101de565b600460009081528281209350601f198516905b81811061051a5750908460019594939210610501575b505050811b016004556101f4565b015160001960f88460031b161c191690553880806104f3565b929360206001819287860151815501950193016104dd565b60046000529091507f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b601f840160051c81019160208510610598575b90601f859493920160051c01905b81811061058957506101c7565b6000815584935060010161057c565b909150819061056e565b634e487b7160e01b600052602260045260246000fd5b91607f16916101b3565b634e487b7160e01b600052604160045260246000fd5b01519050388061017a565b600360009081528281209350601f198516905b818110610633575090846001959493921061061a575b505050811b01600355610190565b015160001960f88460031b161c1916905538808061060c565b929360206001819287860151815501950193016105f6565b60036000529091507fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b601f840160051c810191602085106106b1575b90601f859493920160051c01905b8181106106a25750610163565b60008155849350600101610695565b9091508190610687565b91607f169161014f565b600080fd5b601f909101601f19168101906001600160401b038211908210176105c257604052565b81601f820112156106c5578051906001600160401b0382116105c25760405192610721601f8401601f1916602001856106ca565b828452602083830101116106c55760005b82811061074757505060206000918301015290565b80602080928401015182828701015201610732565b51906001600160a01b03821682036106c55756fe608080604052600436101561001357600080fd5b600090813560e01c90816301ffc9a714613bbd5750806306b859ef14613ae557806306fdde0314613a3e578063095ea7b31461393157806318160ddd14613913578063181f5a77146138b25780631826b1e7146137fb57806321df0da7146137b757806323b872dd14613651578063240028e8146135fa5780632422ac451461351b57806324f65ee7146134dd5780632cab0fb614613044578063313ce5671461300657806337a3210d14612fdf57806339077537146129f65780634c5ef0ed146129af57806362ddd3c41461292857806370a08231146128f15780637437ff9f146128b057806379ba5097146128015780638926f54f146127bb5780638da5cb5b146127945780638fd6a6ac1461276d57806395d89b41146126805780639a4575b9146121d0578063a42a7b8b14612058578063a8fa343c14611fd7578063a9059cbb14611fa5578063acfecf9114611ead578063ae39a25714611d56578063b6cfa3b714611c9b578063b794658014611c63578063bfeffd3f14611bd1578063c4bffe2b14611aa6578063c7230a6014611841578063d5abeb0114611806578063dc04fa1f14611382578063dc0bd9711461133e578063dcbd41bc14611154578063dd62ed3e14611104578063e8a1da1714610a64578063ea6396db14610926578063ec6ae7a7146108e3578063f2fde38b1461082c5763fbc801a71461021b57600080fd5b34610829576060600319360112610829576004359067ffffffffffffffff8211610829578160040160a0600319843603011261082557610259613db5565b9060443567ffffffffffffffff81116106b6579061027e61029b923690600401613eac565b929061028861492a565b506102938584615406565b933691614026565b9260848601936102aa856148c4565b6001600160a01b03807f0000000000000000000000000000000000000000000000000000000000000000169116036107e857602487019677ffffffffffffffff00000000000000000000000000000000610303896148d8565b60801b16604051907f2cbc26bb00000000000000000000000000000000000000000000000000000000825260048201526020816024816001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000165afa90811561075b5788916107b9575b506107915767ffffffffffffffff61038a896148d8565b166103a281600052600c602052604060002054151590565b156107665760206001600160a01b0360075416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa801561075b578890610717575b6001600160a01b0391501633036106eb57606481013593610417868661420d565b7fffffffff0000000000000000000000000000000000000000000000000000000085169485156106c957610473907fffffffff0000000000000000000000000000000000000000000000000000000060075460401b1690614c99565b61048f816104808a6148c4565b6104898d6148d8565b906158bf565b6001600160a01b036008541693846105ae575b505050505050906104b29161420d565b916104bc846148d8565b503015610582575061054261057893610547926104d9853061556f565b7ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff61051561050f856148d8565b936148c4565b604080516001600160a01b039092168252336020830152810188905292169180606081015b0390a26148d8565b614986565b90610550615199565b6040519261055d84613f91565b835260208301526040519283926040845260408401906140ee565b9060208301520390f35b807f96c6fd1e000000000000000000000000000000000000000000000000000000006024925280600452fd5b843b156106c5578994928b9694928692604051988997889687957fa8027c0f0000000000000000000000000000000000000000000000000000000087526004870160809052806105fd91615829565b6084880160a090526101248801906106149261422e565b9261061e90613e97565b67ffffffffffffffff1660a487015260440161063990613e55565b6001600160a01b031660c48601528d8c60e487015261065790613e55565b6001600160a01b0316610104860152602485015283810360031901604485015261068091613eda565b90606483015203925af180156106ba579085916106a1575b808080806104a2565b816106ab91613fe5565b6106b6578338610698565b8380fd5b6040513d87823e3d90fd5b8980fd5b506106e6816106d78a6148c4565b6106e08d6148d8565b90615879565b61048f565b6024877f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b506020813d602011610753575b8161073160209383613fe5565b8101031261074f5761074a6001600160a01b039161421a565b6103f6565b8780fd5b3d9150610724565b6040513d8a823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008852600452602487fd5b6004877f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b6107db915060203d6020116107e1575b6107d38183613fe5565b810190614f15565b38610373565b503d6107c9565b6024866001600160a01b036107fc886148c4565b7f961c9a4f00000000000000000000000000000000000000000000000000000000835216600452fd5b5080fd5b80fd5b5034610829576020600319360112610829576001600160a01b0361084e613e13565b610856614f2d565b163381146108bb57807fffffffffffffffffffffffff000000000000000000000000000000000000000060055416176005556001600160a01b03600654167fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12788380a380f35b6004827fdad89dca000000000000000000000000000000000000000000000000000000008152fd5b503461082957806003193601126108295760207fffffffff0000000000000000000000000000000000000000000000000000000060075460401b16604051908152f35b503461082957608060031936011261082957610940613e13565b50610949613e69565b610951613de4565b5060643567ffffffffffffffff8111610a60579167ffffffffffffffff60409261098160e0953690600401613eac565b50508260c0855161099181613fc9565b82815282602082015282878201528260608201528260808201528260a08201520152168152601060205220604051906109c982613fc9565b5461ffff818163ffffffff82169485815263ffffffff60208201818560201c1681528160408401818760401c168152816060860193818960601c16855260ff60c060808901988a8c60801c168a528a60a082019c60901c168c52019b60a01c1615158b526040519b8c52511660208b0152511660408901525116606087015251166080850152511660a083015251151560c0820152f35b8280fd5b50346108295760406003193601126108295760043567ffffffffffffffff811161082557610a96903690600401614118565b9060243567ffffffffffffffff81116106b65790610ab984923690600401614118565b939091610ac4614f2d565b83905b828210610f455750505081927ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee182360301935b81811015610f41578060051b83013585811215610f3d57830161012081360312610f3d5760405194610b2b86613fad565b610b3482613e97565b8652602082013567ffffffffffffffff81116108255782019436601f8701121561082557853595610b648761417a565b96610b726040519889613fe5565b80885260208089019160051b83010190368211610f3d5760208301905b828210610f0a575050505060208701958652604083013567ffffffffffffffff8111610a6057610bc2903690850161408b565b9160408801928352610bec610bda3660608701614a3c565b9460608a0195865260c0369101614a3c565b956080890196875283515115610ee257610c1067ffffffffffffffff8a5116615b4e565b15610eab5767ffffffffffffffff8951168252600d60205260408220610c378651826151d4565b610c458851600283016151d4565b6004855191019080519067ffffffffffffffff8211610e7e57610c68835461454b565b601f8111610e43575b50602090601f8311600114610dc257610ca19291869183610db7575b50506000198260011b9260031b1c19161790565b90555b815b88518051821015610cdb5790610cd5600192610cce8367ffffffffffffffff8f511692614943565b5190614f6b565b01610ca6565b5050977f8d340f17e19058004c20453540862a9c62778504476f6756755cb33bcd6c38c2939199975095610da967ffffffffffffffff6001979694985116925193519151610d75610d4060405196879687526101006020880152610100870190613eda565b9360408601906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b60a08401906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b0390a1019392909193610afa565b015190508e80610c8d565b83865281862091907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08416875b818110610e2b5750908460019594939210610e12575b505050811b019055610ca4565b015160001960f88460031b161c191690558d8080610e05565b92936020600181928786015181550195019301610def565b610e6e9084875260208720601f850160051c81019160208610610e74575b601f0160051c0190614ad8565b8d610c71565b9091508190610e61565b6024857f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b60248267ffffffffffffffff8b51167f1d5ad3c5000000000000000000000000000000000000000000000000000000008252600452fd5b6004827f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b813567ffffffffffffffff8111610f3957602091610f2e839283369189010161408b565b815201910190610b8f565b8680fd5b8480fd5b8380f35b9267ffffffffffffffff610f67610f628486889a9699979a614a0f565b6148d8565b1691610f7283615992565b156110d857828452600d602052610f8e6005604086200161592f565b94845b8651811015610fc757600190858752600d602052610fc060056040892001610fb9838b614943565b5190615a92565b5001610f91565b50939692909450949094808752600d6020526005604088208881558860018201558860028201558860038201558860048201611003815461454b565b80611097575b5050500180549088815581611079575b5050907f5204aec90a3c794d8e90fded8b46ae9c7c552803e7e832e0c1d358396d8599166020836001948a52600982528985604082208281550155808a52600a82528985604082208281550155604051908152a101909194939294610ac7565b885260208820908101905b8181101561101957888155600101611084565b601f81116001146110ad5750555b888a80611009565b818352602083206110c891601f01861c810190600101614ad8565b80825281602081209155556110a5565b602484847f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b5034610829576040600319360112610829576001600160a01b036040611128613e13565b9282611132613e3f565b9416815260016020522091166000526020526020604060002054604051908152f35b50346108295760206003193601126108295760043567ffffffffffffffff811161082557611186903690600401614149565b6001600160a01b03600f541633141580611329575b6112fd57825b8181106111ac578380f35b6111b78183856149b2565b67ffffffffffffffff6111c9826148d8565b16906111e282600052600c602052604060002054151590565b156112d157907f41f7c8f7cfdad9350aa495e6c54cbbf750a07ab38a9098aed1256e30dd1682bb60e08361129161126b602060019897018b611223826149c2565b1561129857879052600960205261124a60408d206112443660408801614a3c565b906151d4565b868c52600a60205261126660408d206112443660a08801614a3c565b6149c2565b9160405192151583526112846020840160408301614a94565b60a0608084019101614a94565ba2016111a1565b60026040828a6112669452600d6020526112ba82822061124436858c01614a3c565b8a8152600d60205220016112443660a08801614a3c565b602486837f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b6024837f8e4a23d600000000000000000000000000000000000000000000000000000000815233600452fd5b506001600160a01b036006541633141561119b565b503461082957806003193601126108295760206040516001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346108295760406003193601126108295760043567ffffffffffffffff8111610825576113b4903690600401614149565b60243567ffffffffffffffff81116106b6576113d4903690600401614118565b9190926113df614f2d565b845b82811061144b57505050825b8181106113f8578380f35b8067ffffffffffffffff611412610f626001948688614a0f565b1680865260106020528560408120557f5479bbc0288b7eaeaf2ace0943b88016cc648964fcd42919a86fd93b15fdbee88680a2016113ed565b67ffffffffffffffff611462610f628386866149b2565b1661147a81600052600c602052604060002054151590565b156117db5761148a8285856149b2565b602081019060e081019061149d826149c2565b156117af5760a0810161271061ffff6114b5836149cf565b1610156117a05760c082019161271061ffff6114d0856149cf565b1610156117685763ffffffff6114e5866149de565b161561173c57858c52601060205260408c20611500866149de565b63ffffffff16908054906040840191611518836149de565b60201b67ffffffff0000000016936060860194611534866149de565b60401b6bffffffff0000000000000000169660800196611553886149de565b60601b6fffffffff00000000000000000000000016916115728a6149cf565b60801b71ffff0000000000000000000000000000000016936115938c6149cf565b60901b73ffff00000000000000000000000000000000000016957fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016177fffffffffffffffffffffffffffffffff00000000ffffffffffffffffffffffff16177fffffffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffff16177fffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffffffff161717178155611646876149c2565b81547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff00000000000000000000000000000000000000001617905560405196611697906149ef565b63ffffffff1687526116a8906149ef565b63ffffffff1660208701526116bc906149ef565b63ffffffff1660408601526116d0906149ef565b63ffffffff1660608501526116e490614a00565b61ffff1660808401526116f690614a00565b61ffff1660a083015261170890613f39565b151560c082015260e07ffae1e296719dac5269c3886fb5002bb29bf17ae403060c6eb063a55abaaa104191a26001016113e1565b60248c877f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b60248c61ffff611777866149cf565b7f95f3517a00000000000000000000000000000000000000000000000000000000835216600452fd5b8a61ffff6117776024936149cf565b60248a857f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b7f1e670e4b000000000000000000000000000000000000000000000000000000008752600452602486fd5b503461082957806003193601126108295760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b50346108295760406003193601126108295760043567ffffffffffffffff811161082557611873903690600401614118565b9061187c613e3f565b916001600160a01b036006541633141580611a91575b611a65576001600160a01b038316908115611a3d57845b8181106118b4578580f35b6001600160a01b036118cf6118ca838588614a0f565b6148c4565b166040517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152602081602481855afa90811561075b578891611a0a575b5080611924575b50506001016118a9565b6040517fa9059cbb0000000000000000000000000000000000000000000000000000000060208083019182526001600160a01b038a16602484015260448084018590528352918a9190611978606482613fe5565b519082865af1156119ff5787513d6119f65750813b155b6119ca5790847f508d7d183612c18fc339b42618912b9fa3239f631dd7ec0671f950200a0fa66e602060019594604051908152a3903861191a565b602488837f5274afe7000000000000000000000000000000000000000000000000000000008252600452fd5b6001141561198f565b6040513d89823e3d90fd5b905060203d8111611a36575b611a208183613fe5565b6020826000928101031261082957505138611913565b503d611a16565b6004857f8579befe000000000000000000000000000000000000000000000000000000008152fd5b6024847fcb1afbd700000000000000000000000000000000000000000000000000000000815233600452fd5b506001600160a01b0360115416331415611892565b503461082957806003193601126108295760405190600b548083528260208101600b84526020842092845b818110611bb8575050611ae692500383613fe5565b8151611b0a611af48261417a565b91611b026040519384613fe5565b80835261417a565b917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0602083019301368437805b8451811015611b69578067ffffffffffffffff611b5660019388614943565b5116611b628286614943565b5201611b37565b50925090604051928392602084019060208552518091526040840192915b818110611b95575050500390f35b825167ffffffffffffffff16845285945060209384019390920191600101611b87565b8454835260019485019487945060209093019201611ad1565b5034610829576020600319360112610829576004356001600160a01b03811680910361082557611bff614f2d565b7fffffffffffffffffffffffff00000000000000000000000000000000000000006008547fbaff46844acf36d6ee996f489a1a288709c4542bd33cd557770afd267d581209604080516001600160a01b0384168152856020820152a1161760085580f35b503461082957602060031936011261082957611c97611c83610542613e80565b604051918291602083526020830190613eda565b0390f35b5034610829576020600319360112610829577f307cf716eade81675bea3ccb6917b0f91baa2160056765d9a83d76f819caf06a6020611cd8613d81565b611ce0614f2d565b6007547fffffffffffffffff00000000ffffffffffffffffffffffffffffffffffffffff77ffffffff0000000000000000000000000000000000000000808460401c16169116176007557fffffffff0000000000000000000000000000000000000000000000000000000060405191168152a180f35b503461082957606060031936011261082957611d70613e13565b90611d79613e3f565b604435926001600160a01b0384168085036106b657611d96614f2d565b6001600160a01b0382168015611e855794611e7f917f3f1036e85d016a93254a0b1415844f79b85424959d90ae5ad51ce8f4533fe70195967fffffffffffffffffffffffff000000000000000000000000000000000000000060075416176007556001600160a01b0385167fffffffffffffffffffffffff0000000000000000000000000000000000000000600f541617600f557fffffffffffffffffffffffff00000000000000000000000000000000000000006011541617601155604051938493849160409194936001600160a01b03809281606087019816865216602085015216910152565b0390a180f35b6004857f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b50346108295767ffffffffffffffff611ec5366140a9565b929091611ed0614f2d565b1691611ee983600052600c602052604060002054151590565b156110d857828452600d602052611f1860056040862001611f0b368486614026565b6020815191012090615a92565b15611f5d57907f52d00ee4d9bd51b40168f2afc5848837288ce258784ad914278791464b3f4d7691611f5760405192839260208452602084019161422e565b0390a280f35b82611fa1836040519384937f74f23c7c000000000000000000000000000000000000000000000000000000008552600485015260406024850152604484019161422e565b0390fd5b503461082957604060031936011261082957611fcc611fc2613e13565b6024359033614d80565b602060405160018152f35b503461082957602060031936011261082957611ff1613e13565b611ff9614f2d565b6001600160a01b0380601254921691827fffffffffffffffffffffffff0000000000000000000000000000000000000000821617601255167f9524c9e4b0b61eb018dd58a1cd856e3e74009528328ab4a613b434fa631d72428380a380f35b50346108295760206003193601126108295767ffffffffffffffff61207b613e80565b168152600d6020526120926005604083200161592f565b80517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06120d76120c18361417a565b926120cf6040519485613fe5565b80845261417a565b01835b8181106121bf575050825b825181101561213c57806120fb60019285614943565b518552600e602052612119612120604087206040519283809261459e565b0382613fe5565b61212a8285614943565b526121358184614943565b50016120e5565b81846040519182916020830160208452825180915260408401602060408360051b870101940192905b82821061217457505050500390f35b919360206121af827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc060019597998495030186528851613eda565b9601920192018594939192612165565b8060606020809386010152016120da565b50346108295760206003193601126108295760043567ffffffffffffffff811161082557806004019060a06003198236030112610a605761220f61492a565b506040516020936122208583613fe5565b8082526084830191612231836148c4565b6001600160a01b03807f00000000000000000000000000000000000000000000000000000000000000001691160361266c57602484019477ffffffffffffffff0000000000000000000000000000000061228a876148d8565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015287816024816001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000165afa9081156125f157849161264f575b506126275767ffffffffffffffff612310876148d8565b1661232881600052600c602052604060002054151590565b156125fc57876001600160a01b0360075416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa80156125f15784906125b6575b6001600160a01b03915016330361258a576064850135946123a88661239f876148c4565b6106e08a6148d8565b6001600160a01b0360085416918261247c575b505050506123c8846148d8565b503015610582575091817ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff61244c9561240d610542963061556f565b61053a61242261241c876148d8565b926148c4565b604080516001600160a01b0390921682523360208301528101959095529116929081906060820190565b90612455615199565b6040519261246284613f91565b835281830152611c976040519282849384528301906140ee565b823b15610f3d57918791858094604051968795869485937fa8027c0f0000000000000000000000000000000000000000000000000000000085526004850160809052806124c891615829565b6084860160a090526101248601906124df9261422e565b916124e990613e97565b67ffffffffffffffff1660a485015260440161250490613e55565b6001600160a01b031660c48401528b60e48401526125218b613e55565b6001600160a01b031661010484015283602484015282810360031901604484015261254b91613eda565b8a606483015203925af1801561257f5790829161256a575b80806123bb565b8161257491613fe5565b610829578038612563565b6040513d84823e3d90fd5b6024837f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b508781813d83116125ea575b6125cc8183613fe5565b810103126106b6576125e56001600160a01b039161421a565b61237b565b503d6125c2565b6040513d86823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008452600452602483fd5b6004837f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b6126669150883d8a116107e1576107d38183613fe5565b386122f9565b506001600160a01b036107fc6024936148c4565b50346108295780600319360112610829576040519080600454906126a38261454b565b808552916001811690811561272857506001146126cb575b611c9784611c8381860382613fe5565b600481527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b939250905b80821061270e57509091508101602001611c83826126bb565b9192600181602092548385880101520191019092916126f5565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660208087019190915292151560051b85019092019250611c8391508390506126bb565b503461082957806003193601126108295760206001600160a01b0360125416604051908152f35b503461082957806003193601126108295760206001600160a01b0360065416604051908152f35b50346108295760206003193601126108295760206127f767ffffffffffffffff6127e3613e80565b16600052600c602052604060002054151590565b6040519015158152f35b50346108295780600319360112610829576005546001600160a01b0381163303612888577fffffffffffffffffffffffff0000000000000000000000000000000000000000600654913382841617600655166005556001600160a01b033391167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08380a380f35b6004827f02b543c6000000000000000000000000000000000000000000000000000000008152fd5b5034610829578060031936011261082957600754600f54601154604080516001600160a01b0394851681529284166020840152921691810191909152606090f35b50346108295760206003193601126108295760406020916001600160a01b03612918613e13565b1681528083522054604051908152f35b503461082957612937366140a9565b61294393929193614f2d565b67ffffffffffffffff821661296581600052600c602052604060002054151590565b156129845750612981929361297b913691614026565b90614f6b565b80f35b7f1e670e4b000000000000000000000000000000000000000000000000000000008452600452602483fd5b5034610829576040600319360112610829576129c9613e80565b906024359067ffffffffffffffff82116108295760206127f7846129f0366004870161408b565b906148ed565b5034610829576020600319360112610829576004359067ffffffffffffffff821161082957816004019061010060031984360301126108295780604051612a3c81613f46565b5280604051612a4a81613f46565b52606483013560c4840193612a7a612a74612a6f612a688888614873565b3691614026565b614aef565b83614baa565b936084820195612a89876148c4565b6001600160a01b03807f000000000000000000000000000000000000000000000000000000000000000016911603612fcb57602483019377ffffffffffffffff00000000000000000000000000000000612ae2866148d8565b60801b16604051907f2cbc26bb00000000000000000000000000000000000000000000000000000000825260048201526020816024816001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000165afa9081156119ff578791612fac575b50612f845767ffffffffffffffff612b69866148d8565b16612b8181600052600c602052604060002054151590565b15612f595760206001600160a01b0360075416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa9081156119ff578791612f3a575b5015612f0e57612beb856148d8565b92612c0160a48601946129f0612a688785614873565b15612ec757612c2288612c138b6148c4565b612c1c896148d8565b9061574d565b6001600160a01b03600854169283612d13575b505050505060440191612c47836148c4565b90612c51836148d8565b506001600160a01b03821615612ce7575067ffffffffffffffff6020956001600160a01b03612cb3612cad61050f7ffc5e3a5bddc11d92c2dc20fae6f7d5eb989f056be35239f7de7e86150609abc097610f628b6080996156b8565b966148c4565b816040519716875233898801521660408601528560608601521692a260405190612cdc82613f46565b815260405190518152f35b807fec442f05000000000000000000000000000000000000000000000000000000006024925280600452fd5b833b1561074f57878795938195938c93604051988997889687957f6371157400000000000000000000000000000000000000000000000000000000875260048701606090528d612d638780615829565b60648a0161010090526101648a0190612d7b9261422e565b94612d8590613e97565b67ffffffffffffffff166084890152604401612da090613e55565b6001600160a01b031660a488015260c4870152612dbc90613e55565b6001600160a01b031660e4860152612dd49084615829565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c86840301610104870152612e09929161422e565b90612e149083615829565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c85840301610124860152612e49929161422e565b9060e48a01612e5791615829565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c84840301610144850152612e8c929161422e565b8b602483015282604483015203925af180156125f157908491612eb2575b808080612c35565b81612ebc91613fe5565b610a60578238612eaa565b83612ed191614873565b611fa16040519283927f24eb47e500000000000000000000000000000000000000000000000000000000845260206004850152602484019161422e565b6024867f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b612f53915060203d6020116107e1576107d38183613fe5565b38612bdc565b7fa9902c7e000000000000000000000000000000000000000000000000000000008752600452602486fd5b6004867f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b612fc5915060203d6020116107e1576107d38183613fe5565b38612b52565b6024856001600160a01b036107fc8a6148c4565b503461082957806003193601126108295760206001600160a01b0360085416604051908152f35b5034610829578060031936011261082957602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5034610829576040600319360112610829576004359067ffffffffffffffff8211610829578160040190610100600319843603011261082957613085613db5565b918160405161309381613f46565b5260648401359360c48101936130b86130b2612a6f612a688887614873565b87614baa565b9460848301966130c7886148c4565b6001600160a01b03807f0000000000000000000000000000000000000000000000000000000000000000169116036134c957602484019477ffffffffffffffff00000000000000000000000000000000613120876148d8565b60801b16604051907f2cbc26bb00000000000000000000000000000000000000000000000000000000825260048201526020816024816001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000165afa90811561075b5788916134aa575b506107915767ffffffffffffffff6131a7876148d8565b166131bf81600052600c602052604060002054151590565b156107665760206001600160a01b0360075416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa90811561075b57889161348b575b50156106eb57613229866148d8565b9361323f60a48701956129f0612a688886614873565b15613481577fffffffff0000000000000000000000000000000000000000000000000000000016908115613466576132898961327a8c6148c4565b6132838a6148d8565b906157b9565b6001600160a01b036008541693846132af575b50505050505060440191612c47836148c4565b843b1561346257868995938c959387938b6040519a8b998a9889977f6371157400000000000000000000000000000000000000000000000000000000895260048901606090526132ff8780615829565b60648b0161010090526101648b01906133179261422e565b9461332190613e97565b67ffffffffffffffff1660848a015260440161333c90613e55565b6001600160a01b031660a489015260c488015261335890613e55565b6001600160a01b031660e48701526133709084615829565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c878403016101048801526133a5929161422e565b906133b09083615829565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c868403016101248701526133e5929161422e565b9060e48b016133f391615829565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c85840301610144860152613428929161422e565b908c6024840152604483015203925af180156125f15761344d575b808080808061329c565b9261345b8160449395613fe5565b9290613443565b8880fd5b61347c896134738c6148c4565b612c1c8a6148d8565b613289565b612ed18583614873565b6134a4915060203d6020116107e1576107d38183613fe5565b3861321a565b6134c3915060203d6020116107e1576107d38183613fe5565b38613190565b6024866001600160a01b036107fc8b6148c4565b5034610829578060031936011261082957602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b503461082957604060031936011261082957613535613e80565b602435918215158303610829576101406135f861355285856147f0565b6135a860409392935180946fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565b60a08301906fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565bf35b503461082957602060031936011261082957602090613617613e13565b90506001600160a01b03807f0000000000000000000000000000000000000000000000000000000000000000169116146040519015158152f35b50346108295760606003193601126108295761366b613e13565b613673613e3f565b604435916001600160a01b0381168085526001602052604085206001600160a01b033316865260205260408520549060001982106136b8575b5050611fcc9350614d80565b8482106137835730331461375757801561372b5733156136ff576040868692611fcc985260016020528181206001600160a01b0333168252602052209103905538806136ac565b6024867f94280d6200000000000000000000000000000000000000000000000000000000815280600452fd5b6024867fe602df0500000000000000000000000000000000000000000000000000000000815280600452fd5b6024867f94280d6200000000000000000000000000000000000000000000000000000000815233600452fd5b60648686847ffb8f41b200000000000000000000000000000000000000000000000000000000835233600452602452604452fd5b503461082957806003193601126108295760206040516001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346108295760c060031936011261082957613815613e13565b5061381e613e69565b613826613e29565b50608435917fffffffff00000000000000000000000000000000000000000000000000000000831683036108295760a4359067ffffffffffffffff82116108295760a063ffffffff8061ffff61388b88886138843660048b01613eac565b5050614640565b95926040979194975197885216602087015216604085015216606083015215156080820152f35b503461082957806003193601126108295750611c976040516138d5604082613fe5565b601981527f43726f7373436861696e506f6f6c546f6b656e20322e302e30000000000000006020820152604051918291602083526020830190613eda565b50346108295780600319360112610829576020600254604051908152f35b50346108295760406003193601126108295761394b613e13565b6001600160a01b03602435911691308314613a125733156139e65782156139ba5760408291338152600160205281812085825260205220556040519081527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560203392a3602060405160018152f35b807f94280d62000000000000000000000000000000000000000000000000000000006024925280600452fd5b807fe602df05000000000000000000000000000000000000000000000000000000006024925280600452fd5b80837f94280d620000000000000000000000000000000000000000000000000000000060249352600452fd5b5034610829578060031936011261082957604051908060035490613a618261454b565b80855291600181169081156127285750600114613a8857611c9784611c8381860382613fe5565b600381527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b939250905b808210613acb57509091508101602001611c83826126bb565b919260018160209254838588010152019101909291613ab2565b50346108295760c060031936011261082957613aff613e13565b613b07613e69565b906064357fffffffff00000000000000000000000000000000000000000000000000000000811681036106b65760843567ffffffffffffffff8111610f3d57613b54903690600401613eac565b9160a435936002851015610f3957613b6f956044359161426d565b90604051918291602083016020845282518091526020604085019301915b818110613b9b575050500390f35b82516001600160a01b0316845285945060209384019390920191600101613b8d565b905034610825576020600319360112610825576020907fffffffff00000000000000000000000000000000000000000000000000000000613bfc613d81565b167f36372b07000000000000000000000000000000000000000000000000000000008114908115613d57575b8115613d2d575b8115613d03575b8115613c44575b5015158152f35b7faff2afbf00000000000000000000000000000000000000000000000000000000811491508115613cd9575b8115613caf575b8115613c85575b5083613c3d565b7f01ffc9a70000000000000000000000000000000000000000000000000000000091501483613c7e565b7f0e64dd290000000000000000000000000000000000000000000000000000000081149150613c77565b7f940a15420000000000000000000000000000000000000000000000000000000081149150613c70565b7f01ffc9a70000000000000000000000000000000000000000000000000000000081149150613c36565b7fa219a0250000000000000000000000000000000000000000000000000000000081149150613c2f565b7f8fd6a6ac0000000000000000000000000000000000000000000000000000000081149150613c28565b600435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613db057565b600080fd5b602435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613db057565b604435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613db057565b600435906001600160a01b0382168203613db057565b606435906001600160a01b0382168203613db057565b602435906001600160a01b0382168203613db057565b35906001600160a01b0382168203613db057565b6024359067ffffffffffffffff82168203613db057565b6004359067ffffffffffffffff82168203613db057565b359067ffffffffffffffff82168203613db057565b9181601f84011215613db05782359167ffffffffffffffff8311613db05760208381860195010111613db057565b919082519283825260005b848110613f245750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8460006020809697860101520116010190565b80602080928401015182828601015201613ee5565b35908115158203613db057565b6020810190811067ffffffffffffffff821117613f6257604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040810190811067ffffffffffffffff821117613f6257604052565b60a0810190811067ffffffffffffffff821117613f6257604052565b60e0810190811067ffffffffffffffff821117613f6257604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117613f6257604052565b92919267ffffffffffffffff8211613f62576040519161406e601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200184613fe5565b829481845281830111613db0578281602093846000960137010152565b9080601f83011215613db0578160206140a693359101614026565b90565b906040600319830112613db05760043567ffffffffffffffff81168103613db057916024359067ffffffffffffffff8211613db0576140ea91600401613eac565b9091565b6140a69160206141078351604084526040840190613eda565b920151906020818403910152613eda565b9181601f84011215613db05782359167ffffffffffffffff8311613db0576020808501948460051b010111613db057565b9181601f84011215613db05782359167ffffffffffffffff8311613db0576020808501948460081b010111613db057565b67ffffffffffffffff8111613f625760051b60200190565b818102929181159184041417156141a557565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b81156141de570490565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b919082039182116141a557565b51906001600160a01b0382168203613db057565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0938186528686013760008582860101520116010190565b9295939091946001600160a01b036008541695861561452957809760028710156144fa576001600160a01b03986143b4957fffffffff0000000000000000000000000000000000000000000000000000000093896144d05767ffffffffffffffff82166000526010602052604060002090604051916142eb83613fc9565b549163ffffffff8316815263ffffffff8360201c16602082015263ffffffff8360401c16604082015263ffffffff8360601c16606082015260c061ffff8460801c169182608082015260ff60a082019561ffff8160901c16875260a01c161515918291015261447c575b50505067ffffffffffffffff905b6040519b8c997f06b859ef000000000000000000000000000000000000000000000000000000008b521660048a0152166024880152604487015216606485015260c0608485015260c484019161422e565b928180600095869560a483015203915afa91821561446f5781926143d757505090565b9091503d8083833e6143e98183613fe5565b810190602081830312610a605780519067ffffffffffffffff82116106b6570181601f82011215610a60578051906144208261417a565b9361442e6040519586613fe5565b82855260208086019360051b8301019384116108295750602001905b8282106144575750505090565b602080916144648461421a565b81520191019061444a565b50604051903d90823e3d90fd5b92935067ffffffffffffffff92858716156144b857506127106144a761ffff6144ae94511683614192565b049061420d565b915b903880614355565b6144ca92506144a76127109183614192565b916144b0565b67ffffffffffffffff9192506144f4906144ee612a6f36898b614026565b90614baa565b91614363565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b505050505050505060405161453f602082613fe5565b60008152600036813790565b90600182811c92168015614594575b602083101461456557565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b91607f169161455a565b600092918154916145ae8361454b565b808352926001811690811561460457506001146145ca57505050565b60009081526020812093945091925b8383106145ea575060209250010190565b6001816020929493945483858701015201910191906145d9565b905060209495507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0091509291921683830152151560051b010190565b67ffffffffffffffff9092919261467e7fffffffff0000000000000000000000000000000000000000000000000000000060075460401b1685614c99565b16600052601060205260406000206040519061469982613fc9565b549163ffffffff83169384835263ffffffff8460201c169384602085015263ffffffff8160401c169182604086015263ffffffff8260601c169081606087015261ffff8360801c169586608082015260ff61ffff8560901c16948560a084015260a01c16159060c08215910152614746577fffffffff000000000000000000000000000000000000000000000000000000001661473b57505093929190600190565b959493509160019150565b5050505092505050600090600090600090600090600090565b6040519061476c82613fad565b60006080838281528260208201528260408201528260608201520152565b9060405161479781613fad565b60806001829460ff81546fffffffffffffffffffffffffffffffff8116865263ffffffff81861c16602087015260a01c161515604085015201546fffffffffffffffffffffffffffffffff81166060840152811c910152565b67ffffffffffffffff9161480261475f565b5061480b61475f565b5061483f5716600052600d6020526040600020906140a661483360026148386148338661478a565b614e90565b940161478a565b169081600052600960205261485a614833604060002061478a565b91600052600a6020526140a6614833604060002061478a565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215613db0570180359067ffffffffffffffff8211613db057602001918136038313613db057565b356001600160a01b0381168103613db05790565b3567ffffffffffffffff81168103613db05790565b9067ffffffffffffffff6140a69216600052600d602052600560406000200190602081519101209060019160005201602052604060002054151590565b6040519061493782613f91565b60606020838281520152565b80518210156149575760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b67ffffffffffffffff16600052600d6020526121196140a660046040600020016040519283809261459e565b91908110156149575760081b0190565b358015158103613db05790565b3561ffff81168103613db05790565b3563ffffffff81168103613db05790565b359063ffffffff82168203613db057565b359061ffff82168203613db057565b91908110156149575760051b0190565b35906fffffffffffffffffffffffffffffffff82168203613db057565b9190826060910312613db0576040516060810181811067ffffffffffffffff821117613f62576040526040614a8f818395614a7681613f39565b8552614a8460208201614a1f565b602086015201614a1f565b910152565b6fffffffffffffffffffffffffffffffff614ad260408093614ab581613f39565b1515865283614ac660208301614a1f565b16602087015201614a1f565b16910152565b818110614ae3575050565b60008155600101614ad8565b80518015614b5f57602003614b21578051602082810191830183900312613db057519060ff8211614b21575060ff1690565b611fa1906040519182917f953576f7000000000000000000000000000000000000000000000000000000008352602060048401526024830190613eda565b50507f000000000000000000000000000000000000000000000000000000000000000090565b9060ff8091169116039060ff82116141a557565b60ff16604d81116141a557600a0a90565b907f00000000000000000000000000000000000000000000000000000000000000009060ff82169060ff811692828414614c9257828411614c685790614bef91614b85565b91604d60ff8416118015614c4d575b614c1757505090614c116140a692614b99565b90614192565b9091507fa9cb113d0000000000000000000000000000000000000000000000000000000060005260045260245260445260646000fd5b50614c5783614b99565b80156141de57600019048411614bfe565b614c7191614b85565b91604d60ff841611614c1757505090614c8c6140a692614b99565b906141d4565b5050505090565b7fffffffff000000000000000000000000000000000000000000000000000000008116908115614d7b57614ccc81615494565b7dffff00000000000000000000000000000000000000000000000000000000601082811c9085901c1616614d7b5761ffff8360e01c168015918215614d6a575b5050614d16575050565b7fffffffff0000000000000000000000000000000000000000000000000000000092507fdf63778f000000000000000000000000000000000000000000000000000000006000526004521660245260446000fd5b60e01c61ffff161090503880614d0c565b505050565b6001600160a01b0316908115614e61576001600160a01b0316918215614e32576000828152806020526040812054828110614dff5791604082827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef958760209652828652038282205586815280845220818154019055604051908152a3565b6064937fe450d38c0000000000000000000000000000000000000000000000000000000083949352600452602452604452fd5b7fec442f0500000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b7f96c6fd1e00000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b614e9861475f565b506fffffffffffffffffffffffffffffffff6060820151166fffffffffffffffffffffffffffffffff8083511691614ef56020850193614eef614ee263ffffffff8751164261420d565b8560808901511690614192565b90615487565b80821015614f0e57505b16825263ffffffff4216905290565b9050614eff565b90816020910312613db057518015158103613db05790565b6001600160a01b03600654163303614f4157565b7f2b5c74de0000000000000000000000000000000000000000000000000000000060005260046000fd5b9080511561516f5767ffffffffffffffff8151602083012092169182600052600d602052614fa0816005604060002001615bae565b1561512b57600052600e6020526040600020815167ffffffffffffffff8111613f6257614fcd825461454b565b601f81116150f9575b506020601f8211600114615051579161502b827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea959361504195600091615046575b506000198260011b9260031b1c19161790565b9055604051918291602083526020830190613eda565b0390a2565b905084015138615018565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe082169083600052806000209160005b8181106150e15750926150419492600192827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea9896106150c8575b5050811b019055611c83565b85015160001960f88460031b161c1916905538806150bc565b9192602060018192868a015181550194019201615081565b61512590836000526020600020601f840160051c81019160208510610e7457601f0160051c0190614ad8565b38614fd6565b5090611fa16040519283927f393b8ad20000000000000000000000000000000000000000000000000000000084526004840152604060248401526044830190613eda565b7f14c880ca0000000000000000000000000000000000000000000000000000000060005260046000fd5b60405160ff7f0000000000000000000000000000000000000000000000000000000000000000166020820152602081526140a6604082613fe5565b815191929115615358576fffffffffffffffffffffffffffffffff6040840151166fffffffffffffffffffffffffffffffff602085015116106152f5576152f391925b805182547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff0000000000000000000000000000000000000000161782556020810151825460409290920151608090811b7fffffffffffffffffffffffffffffffff00000000000000000000000000000000166fffffffffffffffffffffffffffffffff9290921691821760018501557fffffffffffffffffffffffff0000000000000000000000000000000000000000909216174290911b73ffffffff0000000000000000000000000000000016179055565b565b606483615356604051917f8020d12400000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565bfd5b6fffffffffffffffffffffffffffffffff604084015116158015906153e7575b615386576152f39192615217565b606483615356604051917fd68af9cc00000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b506fffffffffffffffffffffffffffffffff6020840151161515615378565b906127109167ffffffffffffffff615420602083016148d8565b166000908152601060205260409020917fffffffff00000000000000000000000000000000000000000000000000000000161561547157606061ffff61546d935460901c16910135614192565b0490565b606061ffff61546d935460801c16910135614192565b919082018092116141a557565b7fffffffff00000000000000000000000000000000000000000000000000000000811690811561556b577dffff000000000000000000000000000000000000000000000000000000008116156155625760ff60015b169060f01c8061552c575b506001036154ff5750565b7fc512f96c0000000000000000000000000000000000000000000000000000000060005260045260246000fd5b60005b6010811061553d57506154f4565b6001811b8216615550575b60010161552f565b91600181018091116141a55791615548565b60ff60006154e9565b5050565b6001600160a01b0316801591821561563457907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef6020836155b4600095600254615487565b6002555b8060025403600255604051908152a36155cd57565b7f0000000000000000000000000000000000000000000000000000000000000000806155f65750565b600254818111615604575050565b7fea0582460000000000000000000000000000000000000000000000000000000060005260045260245260446000fd5b8160005260006020526040600020548181106156845760208284937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9360009687528684520360408620556155b8565b827fe450d38c0000000000000000000000000000000000000000000000000000000060005260045260245260445260646000fd5b7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60206001600160a01b036000936156f286600254615487565b6002551693841584146157385780600254036002555b604051908152a37f0000000000000000000000000000000000000000000000000000000000000000806155f65750565b84845283825260408420818154019055615708565b9167ffffffffffffffff7f50f6fbee3ceedce6b7fd7eaef18244487867e6718aec7208187efb6b7908c14c92169283600052600d60205261579681836002604060002001615c03565b604080516001600160a01b03909216825260208201929092529081908101615041565b91909167ffffffffffffffff83169283600052600a60205260ff60406000205460a01c161561581e5750907fc6735cd4fa2bbe7b203b1682936e6ee61bc1702464bbbd12abb6630229d9a5f99183600052600a60205261579681836040600020615c03565b906152f3935061574d565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe182360301811215613db057016020813591019167ffffffffffffffff8211613db0578136038313613db057565b9167ffffffffffffffff7fff0133389f9bb82d5b9385826160eaf2328039f6fa950eeb8cf0836da817894492169283600052600d60205261579681836040600020615c03565b91909167ffffffffffffffff83169283600052600960205260ff60406000205460a01c16156159245750907f28d6c52e2b0b7587b0d195539fbe6af984b28791aca4d2cc0844244e38bce29e9183600052600960205261579681836040600020615c03565b906152f39350615879565b906040519182815491828252602082019060005260206000209260005b8181106159615750506152f392500383613fe5565b845483526001948501948794506020909301920161594c565b80548210156149575760005260206000200190600090565b6000818152600c60205260409020548015615a8b5760001981018181116141a557600b549060001982019182116141a557818103615a3a575b505050600b548015615a0b57600019016159e681600b61597a565b60001982549160031b1b19169055600b55600052600c60205260006040812055600190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b615a73615a4b615a5c93600b61597a565b90549060031b1c928392600b61597a565b81939154906000199060031b92831b921b19161790565b9055600052600c6020526040600020553880806159cb565b5050600090565b9060018201918160005282602052604060002054801515600014615b455760001981018181116141a55782549060001982019182116141a557818103615b0e575b50505080548015615a0b576000190190615aed828261597a565b60001982549160031b1b191690555560005260205260006040812055600190565b615b2e615b1e615a5c938661597a565b90549060031b1c9283928661597a565b905560005283602052604060002055388080615ad3565b50505050600090565b80600052600c60205260406000205415600014615ba857600b5468010000000000000000811015613f6257615b8f615a5c826001859401600b55600b61597a565b9055600b5490600052600c602052604060002055600190565b50600090565b6000828152600182016020526040902054615a8b5780549068010000000000000000821015613f625782615bec615a5c84600180960185558461597a565b905580549260005201602052604060002055600190565b9182549060ff8260a01c16158015615e54575b615e4e576fffffffffffffffffffffffffffffffff82169160018501908154615c5b63ffffffff6fffffffffffffffffffffffffffffffff83169360801c164261420d565b9081615db0575b5050848110615d715750838310615cbc575050615c916fffffffffffffffffffffffffffffffff92839261420d565b16167fffffffffffffffffffffffffffffffff00000000000000000000000000000000825416179055565b9190915460801c928315615d305781615cd49161420d565b926000198101908082116141a557615cf7615cfc926001600160a01b0396615487565b6141d4565b7fd0c8d23a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b6001600160a01b0383837fd0c8d23a000000000000000000000000000000000000000000000000000000006000526000196004526024521660445260646000fd5b82856001600160a01b03927f1a76572a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b828692939611615e2457615dcb92614eef9160801c90614192565b80841015615e1f5750825b85547fffffffffffffffffffffffff00000000ffffffffffffffffffffffffffffffff164260801b73ffffffff0000000000000000000000000000000016178655923880615c62565b615dd6565b7f9725942a0000000000000000000000000000000000000000000000000000000060005260046000fd5b50505050565b508215615c1656fea164736f6c634300081a000a' diff --git a/ccip-sdk/src/cct/evm/token/bytecodes/CrossChainToken.ts b/ccip-sdk/src/cct/evm/token/bytecodes/CrossChainToken.ts new file mode 100644 index 00000000..9f7cbff1 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/bytecodes/CrossChainToken.ts @@ -0,0 +1,8 @@ +/** + * CrossChainToken (v2.0.0) deployment bytecode. Lazy-loaded via dynamic import(). + * + * Source: chainlink-ccip (refs/heads/main) + * chains/evm/gobindings/generated/v2_0_0/cross_chain_token/cross_chain_token.go + */ +export const CROSS_CHAIN_TOKEN_BYTECODE = + '0x60c06040523461072757612e58803803806100198161072c565b92833981016060828203126107275781516001600160401b03811161072757820160e081830312610727576040519160e083016001600160401b0381118482101761061e5760405281516001600160401b038111610727578161007d918401610751565b83526020820151906001600160401b0382116107275761009e918301610751565b9081602084015260408101519060408401918252606081015191606085019283526100cb608083016107bc565b916080860192835260a08101519060ff821682036107275760c06100f69160a08901938452016107bc565b9460c08701958652610116604061010f60208b016107bc565b99016107bc565b6001600160a01b038116610721575033965b518051906001600160401b03821161061e5760035490600182811c92168015610717575b60208310146105fe5781601f8493116106a7575b50602090601f831160011461063f57600092610634575b50508160011b916000199060031b1c1916176003555b8051906001600160401b03821161061e57600454600181811c91168015610614575b60208210146105fe57601f8111610599575b50602090601f831160011461052d5760ff93929160009183610522575b50508160011b916000199060031b1c1916176004555b51166080525160a0528151156104f75780516001600160a01b0316156104e657519051906001600160a01b031680156104d0573081146104bc57600254918083018093116104a6576020926002557fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef600093849284845283825260408420818154019055604051908152a360a05180610480575b50505b516001600160a01b03168061047b5750335b600580546001600160a01b039283166001600160a01b0319821681179092559091167f9524c9e4b0b61eb018dd58a1cd856e3e74009528328ab4a613b434fa631d7242600080a36001600160a01b0381161561046557600780546001600160d01b0316905561030b906107d0565b506001600160a01b038116610455575b7f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6600081815260066020527f3195c024b2ddd6d9b8f6c836aa52f67fe69376c8903d009b80229b3ce4425f528054600080516020612e3883398151915291829055909290917fbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff9080a47f3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a848600081815260066020527f42d20fd6db25ea5a8e33f43724ad72f2ebd9488257fa78c86176b8175fc383fb8054600080516020612e3883398151915291829055909290917fbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff9080a460405161257990816108bf823960805181611417015260a051818181610330015261113a0152f35b61045e9061081b565b503861031b565b636116401160e11b600052600060045260246000fd5b61029d565b6002548181116104905750610288565b637502c12360e11b835260045260245260449150fd5b634e487b7160e01b600052601160045260246000fd5b63ec442f0560e01b60005260045260246000fd5b63ec442f0560e01b600052600060045260246000fd5b634dd371db60e11b60005260046000fd5b516001600160a01b031690508061050e575061028b565b63f5c8f5a160e01b60005260045260246000fd5b0151905038806101de565b90601f198316916004600052816000209260005b818110610581575091600193918560ff97969410610568575b505050811b016004556101f4565b015160001960f88460031b161c1916905538808061055a565b92936020600181928786015181550195019301610541565b60046000527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b601f840160051c810191602085106105f4575b601f0160051c01905b8181106105e857506101c1565b600081556001016105db565b90915081906105d2565b634e487b7160e01b600052602260045260246000fd5b90607f16906101af565b634e487b7160e01b600052604160045260246000fd5b015190503880610177565b600360009081528281209350601f198516905b81811061068f5750908460019594939210610676575b505050811b0160035561018d565b015160001960f88460031b161c19169055388080610668565b92936020600181928786015181550195019301610652565b60036000529091507fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b601f840160051c8101916020851061070d575b90601f859493920160051c01905b8181106106fe5750610160565b600081558493506001016106f1565b90915081906106e3565b91607f169161014c565b96610128565b600080fd5b6040519190601f01601f191682016001600160401b0381118382101761061e57604052565b81601f82011215610727578051906001600160401b03821161061e57610780601f8301601f191660200161072c565b92828452602083830101116107275760005b8281106107a757505060206000918301015290565b80602080928401015182828701015201610792565b51906001600160a01b038216820361072757565b600854906001600160a01b03821661080a576001600160a01b03199091166001600160a01b0382161760085561080790600061082f565b90565b631fe1e13d60e11b60005260046000fd5b61080790600080516020612e388339815191525b60008181526006602090815260408083206001600160a01b038616845290915290205460ff166108b75760008181526006602090815260408083206001600160a01b0395909516808452949091528120805460ff19166001179055339291907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d9080a4600190565b505060009056fe608080604052600436101561001357600080fd5b60003560e01c90816301ffc9a7146119d457508063022d63fb1461199857806306fdde03146118bb578063095ea7b3146117795780630aa6220b1461169357806318160ddd14611657578063181f5a77146115a157806323b872dd1461154b578063248a9ca3146114f8578063282c51f31461149f5780632f2ff15d1461143b578063313ce567146113df57806336568abe1461125057806340c10f191461105157806342966c681461100e578063634e93da14610eb7578063649a5ec714610c8757806370a0823114610c2257806379cc67901461095657806384ef8ffc14610bd05780638da5cb5b14610bd05780638fd6a6ac14610b7e57806391d1485414610b0557806395d89b41146109ac5780639dc29fac14610956578063a1eda53c146108d1578063a217fddf14610897578063a8fa343c146107ec578063a9059cbb1461079d578063c630948d146106ac578063c91ddc2014610653578063cc8463c81461060a578063cefc1429146104cc578063cf6eefb714610441578063d5391393146103e8578063d547741f14610353578063d5abeb01146102fa578063d602b9fd146102615763dd62ed3e146101cc57600080fd5b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57610203611c1c565b73ffffffffffffffffffffffffffffffffffffffff610220611c3f565b9116600052600160205273ffffffffffffffffffffffffffffffffffffffff604060002091166000526020526020604060002054604051908152f35b600080fd5b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57610298611cdc565b600780547fffffffffffff0000000000000000000000000000000000000000000000000000811690915560a01c65ffffffffffff166102d357005b7f8886ebfc4259abdbc16601dd8fb5678e54878f47b3c34836cfc51154a9605109600080a1005b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760043561038d611c3f565b81156103be57816103b76103b26103bc94600052600660205260016040600020015490565b611dd3565b6122f9565b005b7f3fc3c27a0000000000000000000000000000000000000000000000000000000060005260046000fd5b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760206040517f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a68152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57604065ffffffffffff6104a66007549065ffffffffffff73ffffffffffffffffffffffffffffffffffffffff83169260a01c1690565b73ffffffffffffffffffffffffffffffffffffffff849392935193168352166020820152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760075473ffffffffffffffffffffffffffffffffffffffff1633036105dc5760075460a081901c65ffffffffffff169073ffffffffffffffffffffffffffffffffffffffff16811580156105d2575b6105a4576105799061057373ffffffffffffffffffffffffffffffffffffffff6008541661228b565b506121af565b50600780547fffffffffffff0000000000000000000000000000000000000000000000000000169055005b507f19ca5ebb0000000000000000000000000000000000000000000000000000000060005260045260246000fd5b504282101561054a565b7fc22c8022000000000000000000000000000000000000000000000000000000006000523360045260246000fd5b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576020610643611ca3565b65ffffffffffff60405191168152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760206040517fcfd2b420c3d2b6ebd6af82f6e29c095b45a072b8d1b5d9eda2a56dcb850acaa68152f35b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576103bc6106e6611c1c565b7f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a660005260066020527f3195c024b2ddd6d9b8f6c836aa52f67fe69376c8903d009b80229b3ce4425f525461073a90611dd3565b61074381612158565b507f3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a84860005260066020527f42d20fd6db25ea5a8e33f43724ad72f2ebd9488257fa78c86176b8175fc383fb5461079890611dd3565b612185565b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576107e16107d7611c1c565b6024359033611f5d565b602060405160018152f35b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57610823611c1c565b61082b611cdc565b73ffffffffffffffffffffffffffffffffffffffff80600554921691827fffffffffffffffffffffffff0000000000000000000000000000000000000000821617600555167f9524c9e4b0b61eb018dd58a1cd856e3e74009528328ab4a613b434fa631d7242600080a3005b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57602060405160008152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576008548060d01c908115158061094c575b156109425760a01c65ffffffffffff165b6040805165ffffffffffff928316815292909116602083015290f35b0390f35b5050600080610922565b5042821015610911565b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576103bc610990611c1c565b6024359061099c611d48565b6109a7823383611e40565b61208d565b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760405160006004548060011c90600181168015610afb575b602083108114610ace57828552908115610a8c5750600114610a2c575b61093e83610a2081850382611c62565b60405191829182611bb4565b91905060046000527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b916000905b808210610a7257509091508101602001610a20610a10565b919260018160209254838588010152019101909291610a5a565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660208086019190915291151560051b84019091019150610a209050610a10565b6024847f4e487b710000000000000000000000000000000000000000000000000000000081526022600452fd5b91607f16916109f3565b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57610b3c611c3f565b600435600052600660205273ffffffffffffffffffffffffffffffffffffffff60406000209116600052602052602060ff604060002054166040519015158152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57602073ffffffffffffffffffffffffffffffffffffffff60055416604051908152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57602073ffffffffffffffffffffffffffffffffffffffff60085416604051908152f35b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5773ffffffffffffffffffffffffffffffffffffffff610c6e611c1c565b1660005260006020526020604060002054604051908152f35b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760043565ffffffffffff81169081810361025c57610cd2611cdc565b610cdb4261236f565b9165ffffffffffff610ceb611ca3565b1680821115610e4e57507ff1038c18cf84a56e432fdbfaf746924b7ea511dfe03a6506a0ceba4888788d9b9265ffffffffffff826206978080610d3895109118026206978018169061213a565b906008548060d01c80610dca575b50506008805473ffffffffffffffffffffffffffffffffffffffff1660a083901b79ffffffffffff0000000000000000000000000000000000000000161760d084901b7fffffffffffff0000000000000000000000000000000000000000000000000000161790556040805165ffffffffffff9283168152919092166020820152a1005b421115610e235779ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff00000000000000000000000000000000000000000000000000006007549260301b169116176007555b8380610d46565b507f2b1fa2edafe6f7b9e97c1a9e0c3660e645beb2dcaa2d45bdbf9beaf5472e1ec5600080a1610e1c565b0365ffffffffffff8111610e88577ff1038c18cf84a56e432fdbfaf746924b7ea511dfe03a6506a0ceba4888788d9b92610d38919061213a565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57610eee611c1c565b610ef6611cdc565b7f3377dc44241e779dd06afab5b788a35ca5f3b778836e2990bdb26a2a4b2e5ed66020610f33610f254261236f565b610f2d611ca3565b9061213a565b65ffffffffffff73ffffffffffffffffffffffffffffffffffffffff610f7c6007549065ffffffffffff73ffffffffffffffffffffffffffffffffffffffff83169260a01c1690565b9690501694600754867fffffffffffff000000000000000000000000000000000000000000000000000079ffffffffffff00000000000000000000000000000000000000008660a01b169216171760075516610fe4575b65ffffffffffff60405191168152a2005b7f8886ebfc4259abdbc16601dd8fb5678e54878f47b3c34836cfc51154a9605109600080a1610fd3565b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57611045611d48565b6103bc6004353361208d565b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57611088611c1c565b3360009081527f3195c024b2ddd6d9b8f6c836aa52f67fe69376c8903d009b80229b3ce4425f516020526040902054602435919060ff16156111fe5773ffffffffffffffffffffffffffffffffffffffff1680156111cf573081146111a25760025491808301809311610e88576020926002557fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef600093849284845283825260408420818154019055604051908152a37f000000000000000000000000000000000000000000000000000000000000000080611162575080f35b90600254918083116111745750905080f35b6044927fea058246000000000000000000000000000000000000000000000000000000008352600452602452fd5b7fec442f050000000000000000000000000000000000000000000000000000000060005260045260246000fd5b7fec442f0500000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b7fe2517d3f00000000000000000000000000000000000000000000000000000000600052336004527f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a660245260446000fd5b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760043561128a611c3f565b8115806113a8575b6112e7575b3373ffffffffffffffffffffffffffffffffffffffff8216036112bd576103bc916122f9565b7f6697b2320000000000000000000000000000000000000000000000000000000060005260046000fd5b60075465ffffffffffff60a082901c169073ffffffffffffffffffffffffffffffffffffffff1615801590611398575b8015611386575b61135057507fffffffffffff000000000000ffffffffffffffffffffffffffffffffffffffff60075416600755611297565b65ffffffffffff907f19ca5ebb000000000000000000000000000000000000000000000000000000006000521660045260246000fd5b504265ffffffffffff8216101561131e565b5065ffffffffffff811615611317565b5073ffffffffffffffffffffffffffffffffffffffff6008541673ffffffffffffffffffffffffffffffffffffffff821614611292565b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57600435611475611c3f565b81156103be578161149a6103b26103bc94600052600660205260016040600020015490565b612217565b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760206040517f3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a8488152f35b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576020611543600435600052600660205260016040600020015490565b604051908152f35b3461025c5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576107e1611585611c1c565b61158d611c3f565b6044359161159c833383611e40565b611f5d565b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57604051604081019080821067ffffffffffffffff8311176116285761093e91604052601581527f43726f7373436861696e546f6b656e20322e302e300000000000000000000000602082015260405191829182611bb4565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576020600254604051908152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576116ca611cdc565b6008548060d01c806116f5575b6008805473ffffffffffffffffffffffffffffffffffffffff169055005b42111561174e5779ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff00000000000000000000000000000000000000000000000000006007549260301b169116176007555b80806116d7565b507f2b1fa2edafe6f7b9e97c1a9e0c3660e645beb2dcaa2d45bdbf9beaf5472e1ec5600080a1611747565b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576117b0611c1c565b73ffffffffffffffffffffffffffffffffffffffff1660243530821461188d57331561185e57811561182f57336000526001602052604060002082600052602052806040600020556040519081527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560203392a3602060405160018152f35b7f94280d6200000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b7fe602df0500000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b507f94280d620000000000000000000000000000000000000000000000000000000060005260045260246000fd5b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760405160006003548060011c9060018116801561198e575b602083108114610ace57828552908115610a8c575060011461192e5761093e83610a2081850382611c62565b91905060036000527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b916000905b80821061197457509091508101602001610a20610a10565b91926001816020925483858801015201910190929161195c565b91607f1691611902565b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576020604051620697808152f35b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57600435907fffffffff00000000000000000000000000000000000000000000000000000000821680920361025c57817f314987860000000000000000000000000000000000000000000000000000000060209314908115611b59575b8115611a9e575b8115611a74575b5015158152f35b7fe6599b4d0000000000000000000000000000000000000000000000000000000091501483611a6d565b90507f36372b070000000000000000000000000000000000000000000000000000000081148015611b30575b8015611b07575b8015611ade575b90611a66565b507f01ffc9a7000000000000000000000000000000000000000000000000000000008114611ad8565b507fa219a025000000000000000000000000000000000000000000000000000000008114611ad1565b507f8fd6a6ac000000000000000000000000000000000000000000000000000000008114611aca565b90507f7965db0b0000000000000000000000000000000000000000000000000000000081148015611b8b575b90611a5f565b507f01ffc9a7000000000000000000000000000000000000000000000000000000008114611b85565b9190916020815282519283602083015260005b848110611c065750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8460006040809697860101520116010190565b8060208092840101516040828601015201611bc7565b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361025c57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361025c57565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761162857604052565b6008548060d01c8015159081611cd2575b5015611cc85760a01c65ffffffffffff1690565b5060075460d01c90565b9050421138611cb4565b3360009081527f54cdd369e4e8a8515e52ca72ec816c2101831ad1f18bf44102ed171459c9b4f8602052604090205460ff1615611d1557565b7fe2517d3f0000000000000000000000000000000000000000000000000000000060005233600452600060245260446000fd5b3360009081527f42d20fd6db25ea5a8e33f43724ad72f2ebd9488257fa78c86176b8175fc383fa602052604090205460ff1615611d8157565b7fe2517d3f00000000000000000000000000000000000000000000000000000000600052336004527f3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a84860245260446000fd5b806000526006602052604060002073ffffffffffffffffffffffffffffffffffffffff331660005260205260ff6040600020541615611e0f5750565b7fe2517d3f000000000000000000000000000000000000000000000000000000006000523360045260245260446000fd5b73ffffffffffffffffffffffffffffffffffffffff9092919216806000526001602052604060002073ffffffffffffffffffffffffffffffffffffffff8416600052602052604060002054927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8410611eba575b50505050565b828410611f115773ffffffffffffffffffffffffffffffffffffffff169030821461188d57801561185e57811561182f57600052600160205260406000209060005260205260406000209103905538808080611eb4565b8373ffffffffffffffffffffffffffffffffffffffff84927ffb8f41b2000000000000000000000000000000000000000000000000000000006000521660045260245260445260646000fd5b73ffffffffffffffffffffffffffffffffffffffff1690811561205e5773ffffffffffffffffffffffffffffffffffffffff169182156111cf57308314612030576000828152806020526040812054828110611ffd5791604082827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef958760209652828652038282205586815280845220818154019055604051908152a3565b6064937fe450d38c0000000000000000000000000000000000000000000000000000000083949352600452602452604452fd5b827fec442f050000000000000000000000000000000000000000000000000000000060005260045260246000fd5b7f96c6fd1e00000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b73ffffffffffffffffffffffffffffffffffffffff16801561205e5730156111cf5760009181835282602052604083205481811061210857817fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef926020928587528684520360408620558060025403600255604051908152a3565b83927fe450d38c0000000000000000000000000000000000000000000000000000000060649552600452602452604452fd5b9065ffffffffffff8091169116019065ffffffffffff8211610e8857565b612182907f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a66123b9565b90565b612182907f3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a8486123b9565b6008549073ffffffffffffffffffffffffffffffffffffffff82166103be57612182917fffffffffffffffffffffffff000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff831691161760085560006123b9565b908115612228575b612182916123b9565b6008549173ffffffffffffffffffffffffffffffffffffffff83166103be577fffffffffffffffffffffffff000000000000000000000000000000000000000090921673ffffffffffffffffffffffffffffffffffffffff82161760085561221f565b6121829073ffffffffffffffffffffffffffffffffffffffff6008541673ffffffffffffffffffffffffffffffffffffffff8216146122cc575b6000612498565b7fffffffffffffffffffffffff0000000000000000000000000000000000000000600854166008556122c5565b9061218291801580612338575b15612498577fffffffffffffffffffffffff000000000000000000000000000000000000000060085416600855612498565b5073ffffffffffffffffffffffffffffffffffffffff6008541673ffffffffffffffffffffffffffffffffffffffff831614612306565b65ffffffffffff81116123875765ffffffffffff1690565b7f6dfcc65000000000000000000000000000000000000000000000000000000000600052603060045260245260446000fd5b806000526006602052604060002073ffffffffffffffffffffffffffffffffffffffff831660005260205260ff604060002054161560001461249157806000526006602052604060002073ffffffffffffffffffffffffffffffffffffffff8316600052602052604060002060017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0082541617905573ffffffffffffffffffffffffffffffffffffffff339216907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d600080a4600190565b5050600090565b806000526006602052604060002073ffffffffffffffffffffffffffffffffffffffff831660005260205260ff6040600020541660001461249157806000526006602052604060002073ffffffffffffffffffffffffffffffffffffffff831660005260205260406000207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00815416905573ffffffffffffffffffffffffffffffffffffffff339216907ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b600080a460019056fea164736f6c634300081a000acfd2b420c3d2b6ebd6af82f6e29c095b45a072b8d1b5d9eda2a56dcb850acaa6' diff --git a/ccip-sdk/src/cct/evm/token/get-mint-burn-roles.ts b/ccip-sdk/src/cct/evm/token/get-mint-burn-roles.ts new file mode 100644 index 00000000..35e40b68 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/get-mint-burn-roles.ts @@ -0,0 +1,78 @@ +/** + * getMintBurnRoles — reads the addresses currently holding MINTER/BURNER roles on + * a CrossChainToken. + * + * CrossChainToken uses non-enumerable OZ AccessControl (no `getRoleMember`), so we + * scan `RoleGranted` events and confirm each candidate still holds the role via + * `hasRole`. Read-only — not a write {@link Operation}. + * + * @packageDocumentation + */ + +import { AbiCoder, Contract, id } from 'ethers' + +import { interfaces } from '../../../evm/const.ts' +import type { EVMChain } from '../../../evm/index.ts' +import { getEvmLogs } from '../../../evm/logs.ts' +import type { ChainLog } from '../../../types.ts' + +/** Addresses holding the mint/burn roles on a token. */ +export type MintBurnRolesResult = { + /** Addresses with the MINTER_ROLE. */ + minters: string[] + /** Addresses with the BURNER_ROLE. */ + burners: string[] +} + +const MINTER_ROLE = id('MINTER_ROLE') +const BURNER_ROLE = id('BURNER_ROLE') + +/** Reads the current MINTER/BURNER role holders on a CrossChainToken. */ +export async function getMintBurnRoles( + chain: EVMChain, + tokenAddress: string, +): Promise { + const contract = new Contract(tokenAddress, interfaces.CrossChainToken, chain.provider) + const roleGrantedTopic = interfaces.CrossChainToken.getEvent('RoleGranted')!.topicHash + + const scanCandidates = async (roleTopic: string): Promise => { + const logs: ChainLog[] = [] + for await (const log of getEvmLogs( + { address: tokenAddress, topics: [[roleGrantedTopic], roleTopic], startBlock: 1 }, + chain, + )) { + logs.push(log) + } + // indexed `account` is topic[2] + return [ + ...new Set( + logs.map((l) => AbiCoder.defaultAbiCoder().decode(['address'], l.topics[2]!)[0] as string), + ), + ] + } + + const verify = async (candidates: string[], roleHash: string): Promise => { + const checks = await Promise.all( + candidates.map((addr) => + (contract.getFunction('hasRole')(roleHash, addr) as Promise).then((has) => + has ? addr : null, + ), + ), + ) + return checks.filter((a): a is string => a !== null) + } + + const [minterCandidates, burnerCandidates] = await Promise.all([ + scanCandidates(MINTER_ROLE), + scanCandidates(BURNER_ROLE), + ]) + const [minters, burners] = await Promise.all([ + verify(minterCandidates, MINTER_ROLE), + verify(burnerCandidates, BURNER_ROLE), + ]) + + chain.logger.debug( + `getMintBurnRoles: token=${tokenAddress}, minters=${minters.length}, burners=${burners.length}`, + ) + return { minters, burners } +} diff --git a/ccip-sdk/src/cct/evm/token/operations/deploy-cross-chain-pool-token.test.ts b/ccip-sdk/src/cct/evm/token/operations/deploy-cross-chain-pool-token.test.ts new file mode 100644 index 00000000..eacfd09a --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/deploy-cross-chain-pool-token.test.ts @@ -0,0 +1,151 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { AbiCoder, ZeroAddress, concat, dataLength } from 'ethers' + +import { DeployCrossChainPoolToken } from './deploy-cross-chain-pool-token.ts' +import { CROSS_CHAIN_POOL_TOKEN_BYTECODE } from '../bytecodes/CrossChainPoolToken.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const CCIP_ADMIN = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const ROUTER = '0xd7bf0e3d34b4c4f7d5f3c4c6b2a1e0f9c8b7a6d5' +const RMN_PROXY = '0xaabbccddeeff00112233445566778899aabbccdd' +const HOOKS = '0x1111111111111111111111111111111111111111' +const TUPLE = + 'tuple(string name, string symbol, uint256 maxSupply, uint256 preMint, address preMintRecipient, uint8 decimals, address ccipAdmin)' + +// Stub chain whose provider answers Router.getArmProxy() with RMN_PROXY. +const stubChain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: () => Promise.resolve(AbiCoder.defaultAbiCoder().encode(['address'], [RMN_PROXY])), + }, +} as unknown as EVMChain + +function expectedData(args: { + name: string + symbol: string + maxSupply: bigint + preMint: bigint + preMintRecipient: string + decimals: number + ccipAdmin: string + advancedPoolHooks: string + rmnProxy: string + router: string +}) { + const encoded = AbiCoder.defaultAbiCoder().encode( + [TUPLE, 'address', 'address', 'address'], + [ + { + name: args.name, + symbol: args.symbol, + maxSupply: args.maxSupply, + preMint: args.preMint, + preMintRecipient: args.preMintRecipient, + decimals: args.decimals, + ccipAdmin: args.ccipAdmin, + }, + args.advancedPoolHooks, + args.rmnProxy, + args.router, + ], + ) + return concat([CROSS_CHAIN_POOL_TOKEN_BYTECODE, encoded]) +} + +describe('EVM cct deployCrossChainPoolToken', () => { + const op = new DeployCrossChainPoolToken() + + it('encodes a no-premint deploy — byte-identical, to=null (creation)', async () => { + const unsigned = await op.generate(stubChain, { + name: 'My Token', + symbol: 'MTK', + decimals: 18, + routerAddress: ROUTER, + ccipAdmin: CCIP_ADMIN, + }) + const expected = expectedData({ + name: 'My Token', + symbol: 'MTK', + maxSupply: 0n, + preMint: 0n, + preMintRecipient: ZeroAddress, // zero exactly when preMint is 0 + decimals: 18, + ccipAdmin: CCIP_ADMIN, + advancedPoolHooks: ZeroAddress, // defaults to zero + rmnProxy: RMN_PROXY, + router: ROUTER, + }) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(unsigned.transactions[0]!.to, null) + assert.equal(unsigned.transactions[0]!.data, expected) + assert.ok(dataLength(unsigned.transactions[0]!.data) > 0) + }) + + it('defaults preMintRecipient to ccipAdmin when preMint > 0 and honors advancedPoolHooks', async () => { + const unsigned = await op.generate(stubChain, { + name: 'T', + symbol: 'T', + decimals: 8, + initialSupply: 1000n, + maxSupply: 5000n, + routerAddress: ROUTER, + ccipAdmin: CCIP_ADMIN, + advancedPoolHooks: HOOKS, + }) + const expected = expectedData({ + name: 'T', + symbol: 'T', + maxSupply: 5000n, + preMint: 1000n, + preMintRecipient: CCIP_ADMIN, + decimals: 8, + ccipAdmin: CCIP_ADMIN, + advancedPoolHooks: HOOKS, + rmnProxy: RMN_PROXY, + router: ROUTER, + }) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('requires ccipAdmin on the unsigned path', async () => { + await assert.rejects( + () => op.generate(stubChain, { name: 'T', symbol: 'T', decimals: 18, routerAddress: ROUTER }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'ccipAdmin', + ) + }) + + it('rejects an invalid routerAddress', async () => { + await assert.rejects( + () => + op.generate(stubChain, { + name: 'T', + symbol: 'T', + decimals: 18, + routerAddress: 'nope', + ccipAdmin: CCIP_ADMIN, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'routerAddress', + ) + }) + + it('rejects initialSupply > maxSupply', async () => { + await assert.rejects( + () => + op.generate(stubChain, { + name: 'T', + symbol: 'T', + decimals: 18, + maxSupply: 100n, + initialSupply: 200n, + routerAddress: ROUTER, + ccipAdmin: CCIP_ADMIN, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'initialSupply', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/deploy-cross-chain-pool-token.ts b/ccip-sdk/src/cct/evm/token/operations/deploy-cross-chain-pool-token.ts new file mode 100644 index 00000000..44ade867 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/deploy-cross-chain-pool-token.ts @@ -0,0 +1,195 @@ +/** + * deployCrossChainPoolToken — deploys a `CrossChainPoolToken` (v2.0.0) via contract + * creation. The contract is simultaneously an ERC20 token and its own CCIP token pool, + * so the single deployed address is both the token and the pool. + * + * Constructor args: ConstructorParams tokenParams, address advancedPoolHooks, + * address rmnProxy, address router. `rmnProxy` is derived from the router via + * `Router.getArmProxy()`; `advancedPoolHooks` defaults to the zero address. + * + * The signed `execute` path auto-fills `ccipAdmin` from the wallet and returns the deployed + * address (from the mined receipt). The unsigned `generate` path requires `ccipAdmin` + * explicitly (no signer to derive it from). + * + * @packageDocumentation + */ + +import { type TransactionReceipt, AbiCoder, Contract, ZeroAddress, concat } from 'ethers' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { interfaces } from '../../../../evm/const.ts' +import { type EVMChain, isSigner } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { type DeployVerification, buildDeployVerification } from '../../deploy-verification.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' +import { CROSS_CHAIN_POOL_TOKEN_BYTECODE } from '../bytecodes/CrossChainPoolToken.ts' + +/** Canonical CCT v2.0 constructor tuple for BaseERC20/CrossChainToken: `ConstructorParams`. */ +const CROSS_CHAIN_TOKEN_PARAMS_TUPLE = + 'tuple(string name, string symbol, uint256 maxSupply, uint256 preMint, address preMintRecipient, uint8 decimals, address ccipAdmin)' + +/** Parameters for `deployCrossChainPoolToken`. */ +export type DeployCrossChainPoolTokenParams = { + name: string + symbol: string + decimals: number + maxSupply?: bigint + /** Amount pre-minted at deploy. `undefined`/`0n` = none. */ + initialSupply?: bigint + /** CCIP Router address (used to derive `rmnProxy` via `getArmProxy()`). */ + routerAddress: string + /** Advanced pool hooks contract. Defaults to the zero address (no hooks). */ + advancedPoolHooks?: string + /** + * CCIP admin (`getCCIPAdmin()`). Required on the unsigned path; auto-filled from the + * signer on the signed path. Defaults `preMintRecipient`. + */ + ccipAdmin?: string + /** Recipient of the pre-mint. Defaults to `ccipAdmin`; ignored when `initialSupply` is `0n`. */ + preMintRecipient?: string + sender?: string +} + +/** + * Result of a signed `deployCrossChainPoolToken`: the tx hash plus the deployed address. + * The single contract is both the token and its pool, so `tokenAddress` and `poolAddress` + * are equal to `address`. + */ +export type DeployCrossChainPoolTokenResult = TransactionHash & { + /** Deployed contract address (token == pool). */ + address: string + /** Same as `address` (the contract is its own token). */ + tokenAddress: string + /** Same as `address` (the contract is its own pool). */ + poolAddress: string + /** Block-explorer verification handle (contract key + ABI-encoded constructor args). */ + verification: DeployVerification +} + +/** Deploys a `CrossChainPoolToken` (combined token + pool) via contract creation. */ +export class DeployCrossChainPoolToken extends EVMOperation< + DeployCrossChainPoolTokenParams, + DeployCrossChainPoolTokenResult +> { + readonly name = 'deployCrossChainPoolToken' + + /** Validates token params (ccipAdmin required only on the unsigned path). */ + protected validate(p: DeployCrossChainPoolTokenParams): void { + if (!p.name || p.name.trim().length === 0) + throw new CCTParamsInvalidError(this.name, 'name', 'must be non-empty') + if (!p.symbol || p.symbol.trim().length === 0) + throw new CCTParamsInvalidError(this.name, 'symbol', 'must be non-empty') + if (p.maxSupply !== undefined && p.maxSupply < 0n) + throw new CCTParamsInvalidError(this.name, 'maxSupply', 'must be non-negative') + if (p.initialSupply !== undefined && p.initialSupply < 0n) + throw new CCTParamsInvalidError(this.name, 'initialSupply', 'must be non-negative') + if ( + p.maxSupply !== undefined && + p.maxSupply > 0n && + p.initialSupply !== undefined && + p.initialSupply > p.maxSupply + ) + throw new CCTParamsInvalidError(this.name, 'initialSupply', 'exceeds maxSupply') + validateAddress(this.name, 'routerAddress', p.routerAddress) + if (!p.ccipAdmin || p.ccipAdmin.trim().length === 0) + throw new CCTParamsInvalidError( + this.name, + 'ccipAdmin', + 'required (the signed deployCrossChainPoolToken path auto-fills it from the signer)', + ) + } + + /** Builds the CrossChainPoolToken contract-creation tx (constructor args + bytecode). */ + protected async buildUnsigned( + chain: EVMChain, + p: DeployCrossChainPoolTokenParams, + ): Promise { + const ccipAdmin = p.ccipAdmin! + const maxSupply = p.maxSupply ?? 0n + const preMint = p.initialSupply ?? 0n + const advancedPoolHooks = p.advancedPoolHooks ?? ZeroAddress + // CrossChainPoolToken reverts unless preMintRecipient is zero exactly when preMint is zero. + const preMintRecipient = preMint > 0n ? (p.preMintRecipient ?? ccipAdmin) : ZeroAddress + const rmnProxy = await this.deriveRmnProxy(chain, p.routerAddress) + + // CrossChainPoolToken constructor: (ConstructorParams tokenParams, advancedPoolHooks, rmnProxy, router) + const encodedArgs = AbiCoder.defaultAbiCoder().encode( + [CROSS_CHAIN_TOKEN_PARAMS_TUPLE, 'address', 'address', 'address'], + [ + { + name: p.name, + symbol: p.symbol, + maxSupply, + preMint, + preMintRecipient, + decimals: p.decimals, + ccipAdmin, + }, + advancedPoolHooks, + rmnProxy, + p.routerAddress, + ], + ) + const data = concat([CROSS_CHAIN_POOL_TOKEN_BYTECODE, encodedArgs]) + return { family: ChainFamily.EVM, transactions: [{ to: null, data }] } + } + + /** Derives the RMN proxy address from a CCIP Router via `Router.getArmProxy()`. */ + private async deriveRmnProxy(chain: EVMChain, routerAddress: string): Promise { + const router = new Contract(routerAddress, interfaces.Router, chain.provider) + try { + return (await router.getFunction('getArmProxy')()) as string + } catch (error) { + throw new CCTTxFailedError( + this.name, + `failed to derive rmnProxy from router ${routerAddress}`, + { cause: error instanceof Error ? error : undefined }, + ) + } + } + + /** Signed deploy: auto-fills `ccipAdmin` from the wallet, then deploys. */ + override async execute( + chain: EVMChain, + params: DeployCrossChainPoolTokenParams & { wallet: unknown }, + ): Promise { + if (!isSigner(params.wallet)) throw new CCIPWalletInvalidError(params.wallet) + const ccipAdmin = params.ccipAdmin ?? (await params.wallet.getAddress()) + return super.execute(chain, { ...params, ccipAdmin }) + } + + /** + * Extracts the deployed address from the creation receipt (token == pool == address) and + * rebuilds the block-explorer verification handle from the submitted creation calldata. + */ + protected override resultFromReceipt( + hash: TransactionHash, + receipt: TransactionReceipt, + unsigned: UnsignedEVMTx, + ): DeployCrossChainPoolTokenResult { + if (!receipt.contractAddress) + throw new CCTTxFailedError(this.name, 'no contract address in deploy receipt', { + context: { txHash: hash.hash }, + }) + const data = unsigned.transactions[0]?.data + if (!data) + throw new CCTTxFailedError(this.name, 'missing deploy calldata for verification', { + context: { txHash: hash.hash }, + }) + return { + ...hash, + address: receipt.contractAddress, + tokenAddress: receipt.contractAddress, + poolAddress: receipt.contractAddress, + verification: buildDeployVerification( + 'CrossChainPoolToken', + data, + CROSS_CHAIN_POOL_TOKEN_BYTECODE, + ), + } + } +} diff --git a/ccip-sdk/src/cct/evm/token/operations/deploy-token.fork.test.ts b/ccip-sdk/src/cct/evm/token/operations/deploy-token.fork.test.ts new file mode 100644 index 00000000..638ae5f6 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/deploy-token.fork.test.ts @@ -0,0 +1,154 @@ +import assert from 'node:assert/strict' +import { execSync } from 'node:child_process' +import { after, before, describe, it } from 'node:test' + +import { Contract, JsonRpcProvider, Wallet } from 'ethers' +import { Instance } from 'prool' + +import CrossChainTokenABI from '../../../../evm/abi/CrossChainToken.ts' +import { EVMTokenManager } from '../../index.ts' + +// ── Constants ── + +const ANVIL_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + +// ── Helpers ── + +function isAnvilAvailable(): boolean { + try { + execSync('anvil --version', { stdio: 'ignore' }) + return true + } catch { + return false + } +} + +// ── Tests ── + +const skip = !!process.env.SKIP_INTEGRATION_TESTS || !isAnvilAvailable() + +const testLogger = process.env.VERBOSE + ? console + : { debug() {}, info() {}, warn: console.warn, error: console.error } + +describe('EVMTokenManager Fork Tests', { skip, timeout: 60_000 }, () => { + let provider: JsonRpcProvider + let wallet: Wallet + let mgr: EVMTokenManager + let anvilInstance: ReturnType | undefined + + before(async () => { + anvilInstance = Instance.anvil({ port: 8747 }) + await anvilInstance.start() + + const anvilUrl = `http://${anvilInstance.host}:${anvilInstance.port}` + provider = new JsonRpcProvider(anvilUrl, undefined, { cacheTimeout: -1 }) + wallet = new Wallet(ANVIL_PRIVATE_KEY, provider) + mgr = await EVMTokenManager.fromProvider(provider, { logger: testLogger, apiClient: null }) + }) + + after(async () => { + provider.destroy() + await anvilInstance?.stop() + }) + + // =========================================================================== + // deployToken — Full integration + // =========================================================================== + + it('should deploy CrossChainToken and verify all contract state', async () => { + const maxSupply = 1_000_000n * 10n ** 18n + const initialSupply = 10_000n * 10n ** 18n + + const result = await mgr.deployToken({ + name: 'Test Token', + symbol: 'TT', + decimals: 18, + maxSupply, + initialSupply, + wallet, + }) + + // Verify result shape + assert.ok(result.tokenAddress, 'should return token address') + assert.match(result.tokenAddress, /^0x[0-9a-fA-F]{40}$/, 'should be valid address') + assert.ok(result.hash, 'should return tx hash') + assert.match(result.hash, /^0x[0-9a-fA-F]{64}$/, 'should be valid tx hash') + + // Verify all deployed contract state + const token = new Contract(result.tokenAddress, CrossChainTokenABI, provider) + const name: string = await token.getFunction('name')() + const symbol: string = await token.getFunction('symbol')() + const decimals: bigint = await token.getFunction('decimals')() + const supply: bigint = await token.getFunction('totalSupply')() + const max: bigint = await token.getFunction('maxSupply')() + const balance: bigint = await token.getFunction('balanceOf')(await wallet.getAddress()) + const ccipAdmin: string = await token.getFunction('getCCIPAdmin')() + + assert.equal(name, 'Test Token') + assert.equal(symbol, 'TT') + assert.equal(decimals, 18n) + assert.equal(supply, initialSupply) + assert.equal(max, maxSupply) + assert.equal(balance, initialSupply, 'deployer should receive initial supply') + assert.equal( + ccipAdmin.toLowerCase(), + (await wallet.getAddress()).toLowerCase(), + 'deployer should be CCIP admin', + ) + }) + + it('should deploy with 0 decimals and unlimited supply', async () => { + const result = await mgr.deployToken({ + name: 'Zero Decimal', + symbol: 'ZD', + decimals: 0, + maxSupply: 0n, + initialSupply: 100n, + wallet, + }) + + const token = new Contract(result.tokenAddress, CrossChainTokenABI, provider) + const decimals: bigint = await token.getFunction('decimals')() + const supply: bigint = await token.getFunction('totalSupply')() + const max: bigint = await token.getFunction('maxSupply')() + + assert.equal(decimals, 0n) + assert.equal(supply, 100n) + assert.equal(max, 0n, 'maxSupply 0 means unlimited') + }) + + // =========================================================================== + // generateUnsignedDeployToken — Verify unsigned tx can be signed manually + // =========================================================================== + + it('should produce unsigned tx that deploys successfully when signed manually', async () => { + const unsigned = await mgr.generateUnsignedDeployToken({ + name: 'Manual Token', + symbol: 'MAN', + decimals: 8, + initialSupply: 500n, + // Unsigned path requires an explicit owner (no signer to derive it from). + ownerAddress: await wallet.getAddress(), + }) + + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal(tx.to, null) + + // Sign and send manually + const populated = await wallet.populateTransaction(tx) + populated.from = undefined + const response = await wallet.sendTransaction(populated) + const receipt = await response.wait(1, 30_000) + + assert.ok(receipt, 'should get receipt') + assert.equal(receipt.status, 1, 'tx should succeed') + assert.ok(receipt.contractAddress, 'should have contract address') + + // Verify the deployed contract + const token = new Contract(receipt.contractAddress, CrossChainTokenABI, provider) + const name: string = await token.getFunction('name')() + assert.equal(name, 'Manual Token') + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/deploy-token.test.ts b/ccip-sdk/src/cct/evm/token/operations/deploy-token.test.ts new file mode 100644 index 00000000..292a585a --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/deploy-token.test.ts @@ -0,0 +1,167 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { type TransactionReceipt, AbiCoder, ZeroAddress, concat, dataLength } from 'ethers' + +import { DeployToken } from './deploy-token.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { CROSS_CHAIN_TOKEN_BYTECODE } from '../bytecodes/CrossChainToken.ts' + +const OWNER = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const TUPLE = + 'tuple(string name, string symbol, uint256 maxSupply, uint256 preMint, address preMintRecipient, uint8 decimals, address ccipAdmin)' +const stubChain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, +} as unknown as EVMChain + +function expectedData(args: { + name: string + symbol: string + maxSupply: bigint + preMint: bigint + preMintRecipient: string + decimals: number + ccipAdmin: string + burnMintRoleAdmin: string + owner: string +}) { + const encoded = AbiCoder.defaultAbiCoder().encode( + [TUPLE, 'address', 'address'], + [ + { + name: args.name, + symbol: args.symbol, + maxSupply: args.maxSupply, + preMint: args.preMint, + preMintRecipient: args.preMintRecipient, + decimals: args.decimals, + ccipAdmin: args.ccipAdmin, + }, + args.burnMintRoleAdmin, + args.owner, + ], + ) + return concat([CROSS_CHAIN_TOKEN_BYTECODE, encoded]) +} + +describe('EVM cct deployToken', () => { + const op = new DeployToken() + + it('encodes a no-premint deploy — byte-identical, to=null (creation)', async () => { + const unsigned = await op.generate(stubChain, { + name: 'My Token', + symbol: 'MTK', + decimals: 18, + ownerAddress: OWNER, + }) + const expected = expectedData({ + name: 'My Token', + symbol: 'MTK', + maxSupply: 0n, + preMint: 0n, + preMintRecipient: ZeroAddress, // zero exactly when preMint is 0 + decimals: 18, + ccipAdmin: OWNER, + burnMintRoleAdmin: OWNER, + owner: OWNER, + }) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, null) + assert.equal(unsigned.transactions[0]!.data, expected) + assert.ok(dataLength(unsigned.transactions[0]!.data) > 0) + }) + + it('defaults preMintRecipient to owner when preMint > 0', async () => { + const unsigned = await op.generate(stubChain, { + name: 'T', + symbol: 'T', + decimals: 8, + initialSupply: 1000n, + maxSupply: 5000n, + ownerAddress: OWNER, + }) + const expected = expectedData({ + name: 'T', + symbol: 'T', + maxSupply: 5000n, + preMint: 1000n, + preMintRecipient: OWNER, + decimals: 8, + ccipAdmin: OWNER, + burnMintRoleAdmin: OWNER, + owner: OWNER, + }) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('requires ownerAddress on the unsigned path', async () => { + await assert.rejects( + () => op.generate(stubChain, { name: 'T', symbol: 'T', decimals: 18 }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'ownerAddress', + ) + }) + + it('signed deploy returns a verification handle with the exact encoded ctor args', async () => { + const DEPLOYED = '0xbEEF000000000000000000000000000000000000' + const receipt = { contractAddress: DEPLOYED, status: 1 } as unknown as TransactionReceipt + const wallet = { + signTransaction() {}, + getAddress: async () => OWNER, + populateTransaction: async (tx: unknown) => tx, + sendTransaction: async () => ({ hash: '0xhash', wait: async () => receipt }), + } + const chain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + nextNonce: async () => 0, + rollbackNonce() {}, + provider: {}, + } as unknown as EVMChain + + const result = await op.execute(chain, { + name: 'My Token', + symbol: 'MTK', + decimals: 18, + ownerAddress: OWNER, + wallet, + }) + + const encodedArgs = AbiCoder.defaultAbiCoder().encode( + [TUPLE, 'address', 'address'], + [ + { + name: 'My Token', + symbol: 'MTK', + maxSupply: 0n, + preMint: 0n, + preMintRecipient: ZeroAddress, + decimals: 18, + ccipAdmin: OWNER, + }, + OWNER, + OWNER, + ], + ) + assert.equal(result.tokenAddress, DEPLOYED) + assert.deepEqual(result.verification, { + contract: 'CrossChainToken', + encodedConstructorArgs: encodedArgs, + }) + }) + + it('rejects initialSupply > maxSupply', async () => { + await assert.rejects( + () => + op.generate(stubChain, { + name: 'T', + symbol: 'T', + decimals: 18, + maxSupply: 100n, + initialSupply: 200n, + ownerAddress: OWNER, + }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'initialSupply', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts b/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts new file mode 100644 index 00000000..d918e659 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts @@ -0,0 +1,148 @@ +/** + * deployToken — deploys a CrossChainToken (v2.0.0) via contract creation. + * + * The signed `execute` path auto-fills `ownerAddress` from the wallet and returns + * the deployed `tokenAddress` (from the mined receipt). The unsigned `generate` + * path requires `ownerAddress` explicitly (no signer to derive it from). + * + * @packageDocumentation + */ + +import { type TransactionReceipt, AbiCoder, ZeroAddress, concat } from 'ethers' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { type EVMChain, isSigner } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { type DeployVerification, buildDeployVerification } from '../../deploy-verification.ts' +import { EVMOperation } from '../../operation.ts' +import { CROSS_CHAIN_TOKEN_BYTECODE } from '../bytecodes/CrossChainToken.ts' + +const CROSS_CHAIN_TOKEN_PARAMS_TUPLE = + 'tuple(string name, string symbol, uint256 maxSupply, uint256 preMint, address preMintRecipient, uint8 decimals, address ccipAdmin)' + +/** Parameters for `deployToken`. */ +export type DeployTokenParams = { + name: string + symbol: string + decimals: number + maxSupply?: bigint + /** Amount pre-minted at deploy. `undefined`/`0n` = none. */ + initialSupply?: bigint + /** + * Owner (2-step AccessControl admin). Required on the unsigned path; auto-filled + * from the signer on the signed path. Defaults the other address fields. + */ + ownerAddress?: string + /** CCIP admin (`getCCIPAdmin()`). Defaults to `ownerAddress`. */ + ccipAdmin?: string + /** Admin that may grant/revoke MINTER/BURNER roles. Defaults to `ownerAddress`. */ + burnMintRoleAdmin?: string + /** Recipient of the pre-mint. Defaults to `ownerAddress`; ignored when `initialSupply` is `0n`. */ + preMintRecipient?: string + sender?: string +} + +/** Result of a signed `deployToken`: the tx hash plus the deployed token address. */ +export type DeployTokenResult = TransactionHash & { + tokenAddress: string + /** Block-explorer verification handle (contract key + ABI-encoded constructor args). */ + verification: DeployVerification +} + +/** Deploys a CrossChainToken via contract creation. */ +export class DeployToken extends EVMOperation { + readonly name = 'deployToken' + + /** Validates token params (owner required only on the unsigned path). */ + protected validate(p: DeployTokenParams): void { + if (!p.name || p.name.trim().length === 0) + throw new CCTParamsInvalidError(this.name, 'name', 'must be non-empty') + if (!p.symbol || p.symbol.trim().length === 0) + throw new CCTParamsInvalidError(this.name, 'symbol', 'must be non-empty') + if (p.maxSupply !== undefined && p.maxSupply < 0n) + throw new CCTParamsInvalidError(this.name, 'maxSupply', 'must be non-negative') + if (p.initialSupply !== undefined && p.initialSupply < 0n) + throw new CCTParamsInvalidError(this.name, 'initialSupply', 'must be non-negative') + if ( + p.maxSupply !== undefined && + p.maxSupply > 0n && + p.initialSupply !== undefined && + p.initialSupply > p.maxSupply + ) + throw new CCTParamsInvalidError(this.name, 'initialSupply', 'exceeds maxSupply') + if (!p.ownerAddress || p.ownerAddress.trim().length === 0) + throw new CCTParamsInvalidError( + this.name, + 'ownerAddress', + 'required (the signed deployToken path auto-fills it from the signer)', + ) + } + + /** Builds the CrossChainToken contract-creation tx (constructor args + bytecode). */ + protected buildUnsigned(_chain: EVMChain, p: DeployTokenParams): UnsignedEVMTx { + const owner = p.ownerAddress! + const maxSupply = p.maxSupply ?? 0n + const preMint = p.initialSupply ?? 0n + const ccipAdmin = p.ccipAdmin ?? owner + const burnMintRoleAdmin = p.burnMintRoleAdmin ?? owner + // CrossChainToken reverts unless preMintRecipient is zero exactly when preMint is zero. + const preMintRecipient = preMint > 0n ? (p.preMintRecipient ?? owner) : ZeroAddress + + const encodedArgs = AbiCoder.defaultAbiCoder().encode( + [CROSS_CHAIN_TOKEN_PARAMS_TUPLE, 'address', 'address'], + [ + { + name: p.name, + symbol: p.symbol, + maxSupply, + preMint, + preMintRecipient, + decimals: p.decimals, + ccipAdmin, + }, + burnMintRoleAdmin, + owner, + ], + ) + const data = concat([CROSS_CHAIN_TOKEN_BYTECODE, encodedArgs]) + return { family: ChainFamily.EVM, transactions: [{ to: null, data }] } + } + + /** Signed deploy: auto-fills `ownerAddress` from the wallet, then deploys. */ + override async execute( + chain: EVMChain, + params: DeployTokenParams & { wallet: unknown }, + ): Promise { + if (!isSigner(params.wallet)) throw new CCIPWalletInvalidError(params.wallet) + const ownerAddress = params.ownerAddress ?? (await params.wallet.getAddress()) + return super.execute(chain, { ...params, ownerAddress }) + } + + /** + * Extracts the deployed token address from the creation receipt and rebuilds the + * block-explorer verification handle from the submitted creation calldata. + */ + protected override resultFromReceipt( + hash: TransactionHash, + receipt: TransactionReceipt, + unsigned: UnsignedEVMTx, + ): DeployTokenResult { + if (!receipt.contractAddress) + throw new CCTTxFailedError(this.name, 'no contract address in deploy receipt', { + context: { txHash: hash.hash }, + }) + const data = unsigned.transactions[0]?.data + if (!data) + throw new CCTTxFailedError(this.name, 'missing deploy calldata for verification', { + context: { txHash: hash.hash }, + }) + return { + ...hash, + tokenAddress: receipt.contractAddress, + verification: buildDeployVerification('CrossChainToken', data, CROSS_CHAIN_TOKEN_BYTECODE), + } + } +} diff --git a/ccip-sdk/src/cct/evm/token/operations/grant-mint-burn-access.test.ts b/ccip-sdk/src/cct/evm/token/operations/grant-mint-burn-access.test.ts new file mode 100644 index 00000000..e7e79493 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/grant-mint-burn-access.test.ts @@ -0,0 +1,74 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, id } from 'ethers' + +import { GrantMintBurnAccess } from './grant-mint-burn-access.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import CrossChainTokenABI from '../../../../evm/abi/CrossChainToken.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const TOKEN = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const AUTHORITY = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const MINTER_ROLE = id('MINTER_ROLE') +const BURNER_ROLE = id('BURNER_ROLE') +const canonical = new Interface(CrossChainTokenABI) +const stubChain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, +} as unknown as EVMChain + +describe('EVM cct grantMintBurnAccess', () => { + const op = new GrantMintBurnAccess() + + it('defaults to grantMintAndBurnRoles — byte-identical to a direct ethers encode', async () => { + const unsigned = await op.generate(stubChain, { tokenAddress: TOKEN, authority: AUTHORITY }) + const expected = canonical.encodeFunctionData('grantMintAndBurnRoles', [AUTHORITY]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, TOKEN) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('encodes grantRole(MINTER_ROLE, authority) for role=mint', async () => { + const unsigned = await op.generate(stubChain, { + tokenAddress: TOKEN, + authority: AUTHORITY, + role: 'mint', + }) + const expected = canonical.encodeFunctionData('grantRole', [MINTER_ROLE, AUTHORITY]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('encodes grantRole(BURNER_ROLE, authority) for role=burn', async () => { + const unsigned = await op.generate(stubChain, { + tokenAddress: TOKEN, + authority: AUTHORITY, + role: 'burn', + }) + const expected = canonical.encodeFunctionData('grantRole', [BURNER_ROLE, AUTHORITY]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('applies sender to from', async () => { + const unsigned = await op.generate(stubChain, { + tokenAddress: TOKEN, + authority: AUTHORITY, + sender: AUTHORITY, + }) + assert.equal(unsigned.transactions[0]!.from, AUTHORITY) + }) + + it('rejects invalid tokenAddress before RPC', async () => { + await assert.rejects( + () => op.generate(stubChain, { tokenAddress: 'nope', authority: AUTHORITY }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'tokenAddress', + ) + }) + + it('rejects invalid authority before RPC', async () => { + await assert.rejects( + () => op.generate(stubChain, { tokenAddress: TOKEN, authority: 'nope' }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'authority', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/grant-mint-burn-access.ts b/ccip-sdk/src/cct/evm/token/operations/grant-mint-burn-access.ts new file mode 100644 index 00000000..d7432d4a --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/grant-mint-burn-access.ts @@ -0,0 +1,80 @@ +/** + * grantMintBurnAccess — grants mint and/or burn roles on a CrossChainToken + * (BurnMintERC677-style, OpenZeppelin AccessControl) token to an authority. + * + * - `role: 'mintAndBurn'` (default) → `grantMintAndBurnRoles(authority)`. + * - `role: 'mint'` → `grantRole(MINTER_ROLE, authority)`. + * - `role: 'burn'` → `grantRole(BURNER_ROLE, authority)`. + * + * @packageDocumentation + */ + +import { id } from 'ethers' + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Which role(s) to grant. */ +export type GrantMintBurnRole = 'mint' | 'burn' | 'mintAndBurn' + +/** OZ AccessControl role hashes — `keccak256('MINTER_ROLE')` / `keccak256('BURNER_ROLE')`. */ +const MINTER_ROLE = id('MINTER_ROLE') +const BURNER_ROLE = id('BURNER_ROLE') + +const VALID_ROLES: readonly GrantMintBurnRole[] = ['mint', 'burn', 'mintAndBurn'] + +/** Parameters for `grantMintBurnAccess`. */ +export type GrantMintBurnAccessParams = { + /** CrossChainToken contract address. */ + tokenAddress: string + /** Address to grant mint/burn access to (pool, multisig, etc.). */ + authority: string + /** Which role(s) to grant. Defaults to `'mintAndBurn'`. */ + role?: GrantMintBurnRole + sender?: string +} + +/** Grants mint and/or burn roles on a CrossChainToken to an authority. */ +export class GrantMintBurnAccess extends EVMOperation { + readonly name = 'grantMintBurnAccess' + + /** Validates addresses and the requested role before any RPC. */ + protected validate(p: GrantMintBurnAccessParams): void { + validateAddress(this.name, 'tokenAddress', p.tokenAddress) + validateAddress(this.name, 'authority', p.authority) + if (p.role && !VALID_ROLES.includes(p.role)) { + throw new CCTParamsInvalidError(this.name, 'role', `must be one of ${VALID_ROLES.join(', ')}`) + } + } + + /** Encodes the AccessControl grant calldata for the requested role. */ + protected buildUnsigned(_chain: EVMChain, p: GrantMintBurnAccessParams): UnsignedEVMTx { + const role = p.role ?? 'mintAndBurn' + + let data: string + switch (role) { + case 'mint': + data = interfaces.CrossChainToken.encodeFunctionData('grantRole', [ + MINTER_ROLE, + p.authority, + ]) + break + case 'burn': + data = interfaces.CrossChainToken.encodeFunctionData('grantRole', [ + BURNER_ROLE, + p.authority, + ]) + break + case 'mintAndBurn': + data = interfaces.CrossChainToken.encodeFunctionData('grantMintAndBurnRoles', [p.authority]) + break + } + + return { family: ChainFamily.EVM, transactions: [{ to: p.tokenAddress, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/token/operations/revoke-mint-burn-access.test.ts b/ccip-sdk/src/cct/evm/token/operations/revoke-mint-burn-access.test.ts new file mode 100644 index 00000000..885749e5 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/revoke-mint-burn-access.test.ts @@ -0,0 +1,69 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, id } from 'ethers' + +import { RevokeMintBurnAccess } from './revoke-mint-burn-access.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import CrossChainTokenABI from '../../../../evm/abi/CrossChainToken.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const TOKEN = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const AUTHORITY = '0xa3c796d480638d7476792230da1E2ADa86e031b0' +const MINTER_ROLE = id('MINTER_ROLE') +const BURNER_ROLE = id('BURNER_ROLE') +const canonical = new Interface(CrossChainTokenABI) +const stubChain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, +} as unknown as EVMChain + +describe('EVM cct revokeMintBurnAccess', () => { + const op = new RevokeMintBurnAccess() + + it('encodes revokeRole(MINTER_ROLE, authority) for role=mint', async () => { + const unsigned = await op.generate(stubChain, { + tokenAddress: TOKEN, + authority: AUTHORITY, + role: 'mint', + }) + const expected = canonical.encodeFunctionData('revokeRole', [MINTER_ROLE, AUTHORITY]) + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions[0]!.to, TOKEN) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('encodes revokeRole(BURNER_ROLE, authority) for role=burn', async () => { + const unsigned = await op.generate(stubChain, { + tokenAddress: TOKEN, + authority: AUTHORITY, + role: 'burn', + }) + const expected = canonical.encodeFunctionData('revokeRole', [BURNER_ROLE, AUTHORITY]) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('applies sender to from', async () => { + const unsigned = await op.generate(stubChain, { + tokenAddress: TOKEN, + authority: AUTHORITY, + role: 'mint', + sender: AUTHORITY, + }) + assert.equal(unsigned.transactions[0]!.from, AUTHORITY) + }) + + it('rejects invalid tokenAddress before RPC', async () => { + await assert.rejects( + () => op.generate(stubChain, { tokenAddress: 'nope', authority: AUTHORITY, role: 'mint' }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'tokenAddress', + ) + }) + + it('rejects invalid authority before RPC', async () => { + await assert.rejects( + () => op.generate(stubChain, { tokenAddress: TOKEN, authority: 'nope', role: 'mint' }), + (e: unknown) => e instanceof CCTParamsInvalidError && e.context.param === 'authority', + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/revoke-mint-burn-access.ts b/ccip-sdk/src/cct/evm/token/operations/revoke-mint-burn-access.ts new file mode 100644 index 00000000..bfe14b36 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/revoke-mint-burn-access.ts @@ -0,0 +1,61 @@ +/** + * revokeMintBurnAccess — revokes the mint or burn role from an authority on a + * CrossChainToken (BurnMintERC677-style, OpenZeppelin AccessControl) token. + * + * - `role: 'mint'` → `revokeRole(MINTER_ROLE, authority)`. + * - `role: 'burn'` → `revokeRole(BURNER_ROLE, authority)`. + * + * @packageDocumentation + */ + +import { id } from 'ethers' + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Which role to revoke — must be specified explicitly. */ +export type RevokeMintBurnRole = 'mint' | 'burn' + +/** OZ AccessControl role hashes — `keccak256('MINTER_ROLE')` / `keccak256('BURNER_ROLE')`. */ +const MINTER_ROLE = id('MINTER_ROLE') +const BURNER_ROLE = id('BURNER_ROLE') + +/** Parameters for `revokeMintBurnAccess`. */ +export type RevokeMintBurnAccessParams = { + /** CrossChainToken contract address. */ + tokenAddress: string + /** Address to revoke mint/burn access from. */ + authority: string + /** Which role to revoke — must be `'mint'` or `'burn'`. */ + role: RevokeMintBurnRole + sender?: string +} + +/** Revokes the mint or burn role from an authority on a CrossChainToken. */ +export class RevokeMintBurnAccess extends EVMOperation { + readonly name = 'revokeMintBurnAccess' + + /** Validates addresses and the requested role before any RPC. */ + protected validate(p: RevokeMintBurnAccessParams): void { + validateAddress(this.name, 'tokenAddress', p.tokenAddress) + validateAddress(this.name, 'authority', p.authority) + if (p.role !== 'mint' && p.role !== 'burn') { + throw new CCTParamsInvalidError(this.name, 'role', "must be 'mint' or 'burn'") + } + } + + /** Encodes the AccessControl `revokeRole` calldata for the requested role. */ + protected buildUnsigned(_chain: EVMChain, p: RevokeMintBurnAccessParams): UnsignedEVMTx { + const roleHash = p.role === 'mint' ? MINTER_ROLE : BURNER_ROLE + const data = interfaces.CrossChainToken.encodeFunctionData('revokeRole', [ + roleHash, + p.authority, + ]) + return { family: ChainFamily.EVM, transactions: [{ to: p.tokenAddress, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/validate.ts b/ccip-sdk/src/cct/evm/validate.ts new file mode 100644 index 00000000..0ececb67 --- /dev/null +++ b/ccip-sdk/src/cct/evm/validate.ts @@ -0,0 +1,29 @@ +/** + * Shared parameter validators for EVM CCT ops. + * + * @packageDocumentation + */ + +import { isAddress } from 'ethers' + +import { CCIPAddressInvalidError } from '../../errors/index.ts' +import { ChainFamily } from '../../networks.ts' +import { CCTParamsInvalidError } from '../errors.ts' + +/** + * Asserts `value` is a valid EVM address. Links the canonical + * {@link CCIPAddressInvalidError} as the `cause`, keeping the + * {@link operation}/{@link param} context on top. + * @throws {@link CCTParamsInvalidError} if `value` is not a valid address + */ +export function validateAddress(operation: string, param: string, value: unknown): void { + if (typeof value === 'string' && isAddress(value)) return + throw new CCTParamsInvalidError( + operation, + param, + `must be a valid address, got ${String(value)}`, + { + cause: new CCIPAddressInvalidError(String(value), ChainFamily.EVM), + }, + ) +} diff --git a/ccip-sdk/src/cct/operation.ts b/ccip-sdk/src/cct/operation.ts new file mode 100644 index 00000000..d29e9b9d --- /dev/null +++ b/ccip-sdk/src/cct/operation.ts @@ -0,0 +1,26 @@ +/** + * Cross-family CCT write contract. {@link Operation} defines the shared + * generate/execute surface; each chain family supplies its own lifecycle base. + * + * @packageDocumentation + */ + +import type { ChainTransaction } from '../types.ts' + +/** Confirmed on-chain hash returned by a successful CCT write. */ +export type TransactionHash = Pick + +/** + * Abstract CCT write operation: build unsigned tx(s) with {@link generate}, or + * sign and submit with {@link execute}. + */ +export abstract class Operation { + /** camelCase id; matches the token-manager facade method and error context. */ + abstract readonly name: string + /** Reject invalid params before any chain RPC. */ + protected abstract validate(params: Params): void + /** Build unsigned transaction(s); no wallet required. */ + abstract generate(chain: Chain, params: Params): Promise + /** Sign and submit via `params.wallet`; returns once confirmed. */ + abstract execute(chain: Chain, params: Params & { wallet: unknown }): Promise +} diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts new file mode 100644 index 00000000..2a3617ff --- /dev/null +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Connection } from '@solana/web3.js' + +import { SolanaTokenManager } from './index.ts' +import { SolanaChain } from '../../solana/index.ts' + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + } as unknown as SolanaChain +} + +describe('SolanaTokenManager (cct/solana)', () => { + it('fromChain exposes flat Solana CCT operations', () => { + const chain = stubChain() + const cct = SolanaTokenManager.fromChain(chain) + assert.equal(cct.chain, chain) + assert.equal(cct.provider, chain.connection) + assert.equal(typeof cct.generateUnsignedDeployToken, 'function') + assert.equal(typeof cct.deployToken, 'function') + assert.equal(typeof cct.generateUnsignedDeployTokenPool, 'function') + assert.equal(typeof cct.deployTokenPool, 'function') + assert.equal(typeof cct.generateUnsignedCreateLookupTable, 'function') + assert.equal(typeof cct.createLookupTable, 'function') + assert.equal(typeof cct.generateUnsignedAppendToLookupTable, 'function') + assert.equal(typeof cct.appendToLookupTable, 'function') + assert.equal(typeof cct.generateUnsignedSetPool, 'function') + assert.equal(typeof cct.setPool, 'function') + }) + + it('creates from a connection provider', async (t) => { + const chain = stubChain() + const connection = new Connection('http://localhost:8899') + t.mock.method(SolanaChain, 'fromConnection', async (provider: Connection) => { + assert.equal(provider, connection) + return chain + }) + + const cct = await SolanaTokenManager.fromProvider(connection) + + assert.equal(cct.chain, chain) + }) + + it('creates from an RPC URL', async (t) => { + const chain = stubChain() + t.mock.method(SolanaChain, 'fromUrl', async (url: string) => { + assert.equal(url, 'http://localhost:8899') + return chain + }) + + const cct = await SolanaTokenManager.fromUrl('http://localhost:8899') + + assert.equal(cct.chain, chain) + }) +}) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts new file mode 100644 index 00000000..17e1b3f0 --- /dev/null +++ b/ccip-sdk/src/cct/solana/index.ts @@ -0,0 +1,614 @@ +/** + * Solana Cross-Chain Token (CCT) admin operations. + * + * @packageDocumentation + */ + +import type { Connection } from '@solana/web3.js' + +import type { ChainContext } from '../../chain.ts' +import type { ChainFamily } from '../../networks.ts' +import { SolanaChain } from '../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../solana/types.ts' +import { TokenManager } from '../token-manager.ts' +import { type SerializedSolanaTxEncoding, serializeUnsignedSolanaTx } from './serialize.ts' +import { type MintBurnRolesResult, getMintBurnRoles } from './token/get-mint-burn-roles.ts' +import { + type ExecuteDeployTokenParams, + type ExecuteDeployTokenResult, + type GenerateDeployTokenParams, + type GenerateDeployTokenResult, + type ExecuteCreatePoolMintAuthorityMultisigParams, + type ExecuteCreatePoolMintAuthorityMultisigResult, + type ExecuteCreatePoolTokenAccountParams, + type ExecuteCreatePoolTokenAccountResult, + type ExecuteGrantMintBurnAccessParams, + type ExecuteGrantMintBurnAccessResult, + type ExecuteRevokeMintBurnAccessParams, + type ExecuteRevokeMintBurnAccessResult, + type ExecuteTransferMintAuthorityParams, + type ExecuteTransferMintAuthorityResult, + type GenerateCreatePoolMintAuthorityMultisigParams, + type GenerateCreatePoolMintAuthorityMultisigResult, + type GenerateCreatePoolTokenAccountParams, + type GenerateCreatePoolTokenAccountResult, + type GenerateGrantMintBurnAccessParams, + type GenerateGrantMintBurnAccessResult, + type GenerateRevokeMintBurnAccessParams, + type GenerateRevokeMintBurnAccessResult, + type GenerateTransferMintAuthorityParams, + type GenerateTransferMintAuthorityResult, + CreatePoolMintAuthorityMultisig, + CreatePoolTokenAccount, + DeployToken, + GrantMintBurnAccess, + RevokeMintBurnAccess, + TransferMintAuthority, +} from './token/operations/index.ts' +import { + type ExecuteApplyChainUpdatesParams, + type ExecuteApplyChainUpdatesResult, + type ExecuteAppendRemotePoolAddressesParams, + type ExecuteAppendRemotePoolAddressesResult, + type ExecuteDeleteChainConfigParams, + type ExecuteDeleteChainConfigResult, + type ExecuteRemoveRemotePoolAddressesParams, + type ExecuteRemoveRemotePoolAddressesResult, + type GenerateApplyChainUpdatesParams, + type GenerateApplyChainUpdatesResult, + type GenerateAppendRemotePoolAddressesParams, + type GenerateAppendRemotePoolAddressesResult, + type GenerateDeleteChainConfigParams, + type GenerateDeleteChainConfigResult, + type GenerateRemoveRemotePoolAddressesParams, + type GenerateRemoveRemotePoolAddressesResult, + type ExecuteAcceptOwnershipParams, + type ExecuteAcceptOwnershipResult, + type ExecuteSetChainRateLimiterConfigParams, + type ExecuteSetChainRateLimiterConfigResult, + type ExecuteSetRateLimitAdminParams, + type ExecuteSetRateLimitAdminResult, + type ExecuteTransferOwnershipParams, + type ExecuteTransferOwnershipResult, + type GenerateAcceptOwnershipParams, + type GenerateAcceptOwnershipResult, + type GenerateSetChainRateLimiterConfigParams, + type GenerateSetChainRateLimiterConfigResult, + type GenerateSetRateLimitAdminParams, + type GenerateSetRateLimitAdminResult, + type GenerateTransferOwnershipParams, + type GenerateTransferOwnershipResult, + AcceptOwnership, + ApplyChainUpdates, + AppendRemotePoolAddresses, + DeleteChainConfig, + RemoveRemotePoolAddresses, + SetChainRateLimiterConfig, + SetRateLimitAdmin, + TransferOwnership, +} from './pool/operations/index.ts' +import { + type ExecuteAcceptAdminRoleParams, + type ExecuteAcceptAdminRoleResult, + type ExecuteAppendToLookupTableParams, + type ExecuteAppendToLookupTableResult, + type ExecuteCreateLookupTableParams, + type ExecuteCreateLookupTableResult, + type ExecuteProposeAdminRoleParams, + type ExecuteProposeAdminRoleResult, + type ExecuteSetPoolParams, + type ExecuteSetPoolResult, + type ExecuteTransferAdminRoleParams, + type ExecuteTransferAdminRoleResult, + type GenerateAcceptAdminRoleParams, + type GenerateAcceptAdminRoleResult, + type GenerateAppendToLookupTableParams, + type GenerateAppendToLookupTableResult, + type GenerateCreateLookupTableParams, + type GenerateCreateLookupTableResult, + type GenerateProposeAdminRoleParams, + type GenerateProposeAdminRoleResult, + type GenerateSetPoolParams, + type GenerateSetPoolResult, + type GenerateTransferAdminRoleParams, + type GenerateTransferAdminRoleResult, + AcceptAdminRole, + AppendToLookupTable, + CreateLookupTable, + ProposeAdminRole, + SetPool, + TransferAdminRole, +} from './token-admin-registry/operations/index.ts' +import { + type ExecuteDeployTokenPoolParams, + type ExecuteDeployTokenPoolResult, + type GenerateDeployTokenPoolParams, + type GenerateDeployTokenPoolResult, + DeployTokenPool, +} from './token-pool/operations/index.ts' + +/** CCT admin facade for Solana. */ +export class SolanaTokenManager extends TokenManager { + readonly chain: SolanaChain + readonly #appendToLookupTable = new AppendToLookupTable() + readonly #createLookupTable = new CreateLookupTable() + readonly #deployToken = new DeployToken() + readonly #deployTokenPool = new DeployTokenPool() + readonly #setPool = new SetPool() + readonly #proposeAdminRole = new ProposeAdminRole() + readonly #acceptAdminRole = new AcceptAdminRole() + readonly #transferAdminRole = new TransferAdminRole() + readonly #applyChainUpdates = new ApplyChainUpdates() + readonly #deleteChainConfig = new DeleteChainConfig() + readonly #appendRemotePoolAddresses = new AppendRemotePoolAddresses() + readonly #removeRemotePoolAddresses = new RemoveRemotePoolAddresses() + readonly #setChainRateLimiterConfig = new SetChainRateLimiterConfig() + readonly #setRateLimitAdmin = new SetRateLimitAdmin() + readonly #transferOwnership = new TransferOwnership() + readonly #acceptOwnership = new AcceptOwnership() + readonly #transferMintAuthority = new TransferMintAuthority() + readonly #grantMintBurnAccess = new GrantMintBurnAccess() + readonly #revokeMintBurnAccess = new RevokeMintBurnAccess() + readonly #createPoolMintAuthorityMultisig = new CreatePoolMintAuthorityMultisig() + readonly #createPoolTokenAccount = new CreatePoolTokenAccount() + + /** Creates a Solana CCT manager for an existing chain. */ + constructor(chain: SolanaChain) { + super() + this.chain = chain + } + + /** Wraps an existing {@link SolanaChain}. */ + static fromChain(chain: SolanaChain): SolanaTokenManager { + return new SolanaTokenManager(chain) + } + + /** Creates from a Solana web3.js connection. */ + static async fromProvider(provider: Connection, ctx?: ChainContext): Promise { + return new SolanaTokenManager(await SolanaChain.fromConnection(provider, ctx)) + } + + /** Creates from an RPC URL. */ + static async fromUrl(url: string, ctx?: ChainContext): Promise { + return new SolanaTokenManager(await SolanaChain.fromUrl(url, ctx)) + } + + /** Provider of the underlying chain. */ + get provider(): Connection { + return this.chain.connection + } + + /** + * Builds unsigned Solana mint creation instructions. Does not mint supply. + * + * The `payer` is also the mint, freeze, and metadata update authority. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedDeployToken({ + * payer, + * decimals: 9, + * tokenProgram: 'spl-token', + * withMetaplex: true, + * name: 'My Token', + * symbol: 'MTK', + * }) + * ``` + */ + generateUnsignedDeployToken(opts: GenerateDeployTokenParams): Promise { + return this.#deployToken.generate(this.chain, opts) + } + + /** + * Creates a Solana mint. Does not mint supply. + * + * The wallet public key is also the mint, freeze, and metadata update authority. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.deployToken({ + * wallet, + * decimals: 9, + * tokenProgram: 'spl-token', + * withMetaplex: false, + * }) + * ``` + */ + deployToken(opts: ExecuteDeployTokenParams): Promise { + return this.#deployToken.execute(this.chain, opts) + } + + /** + * Builds unsigned Solana pool lookup table instructions. + * + * Defaults to create+extend. Use `mode: 'createEmpty'` to create an empty ALT, e.g. with an + * EOA payer and vault authority, then populate it later through the authority. If `authority` + * is omitted, it defaults to `payer`. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedCreateLookupTable({ + * mode: 'createEmpty', + * payer: eoa, + * authority: squadsVault, + * }) + * ``` + */ + generateUnsignedCreateLookupTable( + opts: GenerateCreateLookupTableParams, + ): Promise { + return this.#createLookupTable.generate(this.chain, opts) + } + + /** + * Creates a Solana pool lookup table. Defaults to create+extend; pass `mode: 'createEmpty'` to + * create an empty ALT owned by `authority` and paid by `wallet`. If `authority` is omitted, it + * defaults to the wallet public key. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const { hash, lookupTableAddress } = await cct.createLookupTable({ + * mode: 'createEmpty', + * authority: squadsVault, + * wallet, + * }) + * ``` + */ + createLookupTable(opts: ExecuteCreateLookupTableParams): Promise { + return this.#createLookupTable.execute(this.chain, opts) + } + + /** + * Builds unsigned Solana token pool initialize instructions. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedDeployTokenPool({ + * tokenAddress: mint, + * poolProgramAddress: poolProgram, + * payer, + * authority, + * allowlist: [allowedSender], + * }) + * ``` + */ + generateUnsignedDeployTokenPool( + opts: GenerateDeployTokenPoolParams, + ): Promise { + return this.#deployTokenPool.generate(this.chain, opts) + } + + /** + * Initializes a Solana token pool. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.deployTokenPool({ + * tokenAddress: mint, + * poolProgramAddress: poolProgram, + * wallet, + * }) + * ``` + */ + deployTokenPool(opts: ExecuteDeployTokenPoolParams): Promise { + return this.#deployTokenPool.execute(this.chain, opts) + } + + /** + * Builds unsigned Solana lookup table extend instructions. + * + * Pass `tokenAddress` and `poolProgramAddress` to append the standard CCIP pool addresses; + * pass `additionalAddresses` to append manual addresses. `authority` defaults to `payer`. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedAppendToLookupTable({ + * lookupTableAddress, + * payer: squadsVault, + * authority: squadsVault, + * tokenAddress: mint, + * poolProgramAddress: poolProgram, + * additionalAddresses: [extraAccount], + * }) + * ``` + */ + generateUnsignedAppendToLookupTable( + opts: GenerateAppendToLookupTableParams, + ): Promise { + return this.#appendToLookupTable.generate(this.chain, opts) + } + + /** + * Extends a Solana lookup table. + * + * Pass `tokenAddress` and `poolProgramAddress` to append the standard CCIP pool addresses; + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.appendToLookupTable({ + * lookupTableAddress, + * wallet, + * tokenAddress: mint, + * poolProgramAddress: poolProgram, + * additionalAddresses: [extraAccount], + * }) + * ``` + */ + appendToLookupTable( + opts: ExecuteAppendToLookupTableParams, + ): Promise { + return this.#appendToLookupTable.execute(this.chain, opts) + } + + /** + * Builds unsigned Solana `setPool` instructions. + * + * The `payer` pays transaction fees. `authority` defaults to `payer`; Squads/multisig flows + * should pass the token admin/vault authority explicitly. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedSetPool({ + * tokenAddress: mint, + * address: router, + * poolLookupTableAddress: lookupTable, + * payer: squadsVault, + * authority: tokenAdmin, + * }) + * ``` + */ + generateUnsignedSetPool(opts: GenerateSetPoolParams): Promise { + return this.#setPool.generate(this.chain, opts) + } + + /** + * Registers a token pool. The wallet must be the token admin authority. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.setPool({ + * tokenAddress: mint, + * address: router, + * poolLookupTableAddress: lookupTable, + * wallet, + * }) + * ``` + */ + setPool(opts: ExecuteSetPoolParams): Promise { + return this.#setPool.execute(this.chain, opts) + } + + /** Builds unsigned Solana `proposeAdminRole` (ownerProposeAdministrator) instructions. */ + generateUnsignedProposeAdminRole( + opts: GenerateProposeAdminRoleParams, + ): Promise { + return this.#proposeAdminRole.generate(this.chain, opts) + } + /** Proposes a token administrator in the Router TokenAdminRegistry. */ + proposeAdminRole(opts: ExecuteProposeAdminRoleParams): Promise { + return this.#proposeAdminRole.execute(this.chain, opts) + } + + /** Builds unsigned Solana `acceptAdminRole` instructions (wallet must be the pending admin). */ + generateUnsignedAcceptAdminRole( + opts: GenerateAcceptAdminRoleParams, + ): Promise { + return this.#acceptAdminRole.generate(this.chain, opts) + } + /** Accepts a pending token administrator role. */ + acceptAdminRole(opts: ExecuteAcceptAdminRoleParams): Promise { + return this.#acceptAdminRole.execute(this.chain, opts) + } + + /** Builds unsigned Solana `transferAdminRole` instructions (wallet must be the current admin). */ + generateUnsignedTransferAdminRole( + opts: GenerateTransferAdminRoleParams, + ): Promise { + return this.#transferAdminRole.generate(this.chain, opts) + } + /** Transfers the token administrator role to a new admin. */ + transferAdminRole(opts: ExecuteTransferAdminRoleParams): Promise { + return this.#transferAdminRole.execute(this.chain, opts) + } + + /** Builds unsigned Solana pool `applyChainUpdates` instructions (add/remove remote chains). */ + generateUnsignedApplyChainUpdates( + opts: GenerateApplyChainUpdatesParams, + ): Promise { + return this.#applyChainUpdates.generate(this.chain, opts) + } + /** Applies remote-chain config updates to a Solana token pool. */ + applyChainUpdates(opts: ExecuteApplyChainUpdatesParams): Promise { + return this.#applyChainUpdates.execute(this.chain, opts) + } + + /** Builds unsigned Solana pool `deleteChainConfig` instructions. */ + generateUnsignedDeleteChainConfig( + opts: GenerateDeleteChainConfigParams, + ): Promise { + return this.#deleteChainConfig.generate(this.chain, opts) + } + /** Removes a remote-chain config from a Solana token pool. */ + deleteChainConfig(opts: ExecuteDeleteChainConfigParams): Promise { + return this.#deleteChainConfig.execute(this.chain, opts) + } + + /** Builds unsigned Solana pool `appendRemotePoolAddresses` instructions. */ + generateUnsignedAppendRemotePoolAddresses( + opts: GenerateAppendRemotePoolAddressesParams, + ): Promise { + return this.#appendRemotePoolAddresses.generate(this.chain, opts) + } + /** Appends remote pool addresses to a remote-chain config on a Solana token pool. */ + appendRemotePoolAddresses( + opts: ExecuteAppendRemotePoolAddressesParams, + ): Promise { + return this.#appendRemotePoolAddresses.execute(this.chain, opts) + } + + /** Builds unsigned Solana pool `removeRemotePoolAddresses` instructions. */ + generateUnsignedRemoveRemotePoolAddresses( + opts: GenerateRemoveRemotePoolAddressesParams, + ): Promise { + return this.#removeRemotePoolAddresses.generate(this.chain, opts) + } + /** Removes remote pool addresses from a remote-chain config on a Solana token pool. */ + removeRemotePoolAddresses( + opts: ExecuteRemoveRemotePoolAddressesParams, + ): Promise { + return this.#removeRemotePoolAddresses.execute(this.chain, opts) + } + + /** Builds unsigned Solana pool `setChainRateLimiterConfig` instructions (per-chain rate limits). */ + generateUnsignedSetChainRateLimiterConfig( + opts: GenerateSetChainRateLimiterConfigParams, + ): Promise { + return this.#setChainRateLimiterConfig.generate(this.chain, opts) + } + /** Sets per-chain rate limiter config on a Solana token pool. */ + setChainRateLimiterConfig( + opts: ExecuteSetChainRateLimiterConfigParams, + ): Promise { + return this.#setChainRateLimiterConfig.execute(this.chain, opts) + } + + /** Builds unsigned Solana pool `setRateLimitAdmin` instructions. */ + generateUnsignedSetRateLimitAdmin( + opts: GenerateSetRateLimitAdminParams, + ): Promise { + return this.#setRateLimitAdmin.generate(this.chain, opts) + } + /** Sets the rate-limit admin on a Solana token pool. */ + setRateLimitAdmin(opts: ExecuteSetRateLimitAdminParams): Promise { + return this.#setRateLimitAdmin.execute(this.chain, opts) + } + + /** Builds unsigned Solana pool `transferOwnership` instructions (propose new owner). */ + generateUnsignedTransferOwnership( + opts: GenerateTransferOwnershipParams, + ): Promise { + return this.#transferOwnership.generate(this.chain, opts) + } + /** Proposes a new owner for a Solana token pool. */ + transferOwnership(opts: ExecuteTransferOwnershipParams): Promise { + return this.#transferOwnership.execute(this.chain, opts) + } + + /** Builds unsigned Solana pool `acceptOwnership` instructions. */ + generateUnsignedAcceptOwnership( + opts: GenerateAcceptOwnershipParams, + ): Promise { + return this.#acceptOwnership.generate(this.chain, opts) + } + /** Accepts proposed ownership of a Solana token pool. */ + acceptOwnership(opts: ExecuteAcceptOwnershipParams): Promise { + return this.#acceptOwnership.execute(this.chain, opts) + } + + /** Builds unsigned Solana `transferMintAuthority` instructions (SPL setAuthority MintTokens). */ + generateUnsignedTransferMintAuthority( + opts: GenerateTransferMintAuthorityParams, + ): Promise { + return this.#transferMintAuthority.generate(this.chain, opts) + } + /** Transfers the SPL mint authority of a token to a new authority. */ + transferMintAuthority( + opts: ExecuteTransferMintAuthorityParams, + ): Promise { + return this.#transferMintAuthority.execute(this.chain, opts) + } + + /** Builds unsigned Solana `grantMintBurnAccess` instructions (grants the mint authority). */ + generateUnsignedGrantMintBurnAccess( + opts: GenerateGrantMintBurnAccessParams, + ): Promise { + return this.#grantMintBurnAccess.generate(this.chain, opts) + } + /** Grants mint/burn access on a token to an authority (Solana). */ + grantMintBurnAccess( + opts: ExecuteGrantMintBurnAccessParams, + ): Promise { + return this.#grantMintBurnAccess.execute(this.chain, opts) + } + + /** Builds unsigned Solana `revokeMintBurnAccess` (unsupported on Solana — always rejects). */ + generateUnsignedRevokeMintBurnAccess( + opts: GenerateRevokeMintBurnAccessParams, + ): Promise { + return this.#revokeMintBurnAccess.generate(this.chain, opts) + } + /** Revoke mint/burn access — unsupported on Solana; always rejects. */ + revokeMintBurnAccess( + opts: ExecuteRevokeMintBurnAccessParams, + ): Promise { + return this.#revokeMintBurnAccess.execute(this.chain, opts) + } + + /** Builds unsigned Solana `createPoolMintAuthorityMultisig` instructions. */ + generateUnsignedCreatePoolMintAuthorityMultisig( + opts: GenerateCreatePoolMintAuthorityMultisigParams, + ): Promise { + return this.#createPoolMintAuthorityMultisig.generate(this.chain, opts) + } + /** Creates an SPL multisig as a pool's mint authority; returns the multisig address. */ + createPoolMintAuthorityMultisig( + opts: ExecuteCreatePoolMintAuthorityMultisigParams, + ): Promise { + return this.#createPoolMintAuthorityMultisig.execute(this.chain, opts) + } + + /** Builds unsigned Solana `createPoolTokenAccount` instructions (pool signer PDA's ATA). */ + generateUnsignedCreatePoolTokenAccount( + opts: GenerateCreatePoolTokenAccountParams, + ): Promise { + return this.#createPoolTokenAccount.generate(this.chain, opts) + } + /** Creates the pool's associated token account; returns its address. */ + createPoolTokenAccount( + opts: ExecuteCreatePoolTokenAccountParams, + ): Promise { + return this.#createPoolTokenAccount.execute(this.chain, opts) + } + + /** Reads a mint's current mint authority (and multisig detail), read-only. */ + getMintBurnRoles(tokenAddress: string): Promise { + return getMintBurnRoles(this.chain, tokenAddress) + } + + /** + * Serializes an unsigned Solana CCT tx for external signing. + * + * @example + * ```ts + * const unsigned = await cct.generateUnsignedSetPool({ ...params, payer }) + * const base58 = await cct.serializeUnsignedTx(unsigned, payer) + * const base64 = await cct.serializeUnsignedTx(unsigned, payer, 'base64') + * ``` + */ + serializeUnsignedTx( + unsigned: Pick, + payer: string, + encoding?: SerializedSolanaTxEncoding, + ): Promise { + return serializeUnsignedSolanaTx(this.provider, unsigned, payer, encoding) + } +} + +export * from '../errors.ts' +export type { TransactionHash } from '../operation.ts' +export type { SerializedSolanaTxEncoding } from './serialize.ts' +export type { MintBurnRolesResult } from './token/get-mint-burn-roles.ts' +export type * from './pool/operations/index.ts' +export type * from './token/operations/index.ts' +export type * from './token-pool/operations/index.ts' +export type * from './token-admin-registry/operations/index.ts' diff --git a/ccip-sdk/src/cct/solana/operation.test.ts b/ccip-sdk/src/cct/solana/operation.test.ts new file mode 100644 index 00000000..c22342da --- /dev/null +++ b/ccip-sdk/src/cct/solana/operation.test.ts @@ -0,0 +1,103 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import { SolanaOperation } from './operation.ts' +import { CCIPWalletInvalidError } from '../../errors/index.ts' +import { ChainFamily } from '../../networks.ts' +import type { SolanaChain } from '../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../solana/types.ts' + +class TestOperation extends SolanaOperation<{ value: string }> { + readonly name = 'testOperation' + captured?: string + validated?: string + + protected validate(params: { payer: string }): void { + this.validated = params.payer + } + + protected buildUnsigned( + _chain: SolanaChain, + params: { payer: string; value: string }, + ): Promise { + this.captured = params.payer + return Promise.resolve({ family: ChainFamily.Solana, instructions: [] }) + } +} + +type TestTx = UnsignedSolanaTx & { lookupTableAddress: string } +type TestResult = { hash: string; lookupTableAddress: string } + +class TestResultOperation extends SolanaOperation<{ value: string }, TestTx, TestResult> { + readonly name = 'testResultOperation' + + protected validate(): void {} + + protected buildUnsigned(): Promise { + return Promise.resolve({ + family: ChainFamily.Solana, + instructions: [], + lookupTableAddress: 'lookup-table', + }) + } + + protected override resultFromGenerated(hash: { hash: string }, tx: TestTx): TestResult { + return { ...hash, lookupTableAddress: tx.lookupTableAddress } + } +} + +const chain = { logger: console, connection: {} } as unknown as SolanaChain + +describe('SolanaOperation', () => { + it('uses wallet public key as payer without mutating caller params', async () => { + const op = new TestOperation() + const wallet = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, + } + const params = { value: 'x', payer: PublicKey.default.toBase58(), wallet } + + await op.execute(chain, params) + + assert.equal(op.validated, wallet.publicKey.toBase58()) + assert.equal(op.captured, wallet.publicKey.toBase58()) + assert.equal(params.payer, PublicKey.default.toBase58()) + }) + + it('does not require payer on signed execution params', async () => { + const op = new TestOperation() + const wallet = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, + } + + await op.execute(chain, { value: 'x', wallet }) + + assert.equal(op.captured, wallet.publicKey.toBase58()) + }) + + it('lets operations add generated data to execute results', async () => { + const op = new TestResultOperation() + const wallet = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, + } + + const result = await op.execute(chain, { value: 'x', wallet }) + + assert.equal(result.lookupTableAddress, 'lookup-table') + }) + + it('rejects invalid wallets before validation or building unsigned txs', async () => { + const op = new TestOperation() + + await assert.rejects( + () => op.execute(chain, { value: 'x', wallet: {} }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + assert.equal(op.validated, undefined) + assert.equal(op.captured, undefined) + }) +}) diff --git a/ccip-sdk/src/cct/solana/operation.ts b/ccip-sdk/src/cct/solana/operation.ts new file mode 100644 index 00000000..28676dc6 --- /dev/null +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -0,0 +1,60 @@ +/** + * Solana {@link Operation} lifecycle: validate → build unsigned tx → submit. + * Default execution uses wallet.publicKey as payer; use generateUnsigned* for a custom payer. + * + * @packageDocumentation + */ + +import { CCIPWalletInvalidError } from '../../errors/index.ts' +import type { SolanaChain } from '../../solana/index.ts' +import { type UnsignedSolanaTx, isWallet } from '../../solana/types.ts' +import { type TransactionHash, Operation } from '../operation.ts' +import { submit } from './submit.ts' + +/** Unsigned Solana operation params include an explicit fee payer. */ +export type SolanaGenerateParams

= P & { payer: string } + +/** Signed Solana operation params derive payer from `wallet.publicKey`. */ +export type SolanaExecuteParams

= P & { + wallet: unknown + computeUnits?: number +} + +function withPayer

( + params: SolanaExecuteParams

, + payer: string, +): SolanaGenerateParams

{ + const { wallet: _wallet, computeUnits: _computeUnits, ...rest } = params + return { ...rest, payer } as SolanaGenerateParams

+} + +/** Solana CCT write base. Subclasses supply {@link validate} and {@link buildUnsigned}. */ +export abstract class SolanaOperation< + P extends object, + Tx extends UnsignedSolanaTx = UnsignedSolanaTx, + Result = TransactionHash, +> extends Operation, Tx, Result> { + /** Build instructions after params have been validated. */ + protected abstract buildUnsigned(chain: SolanaChain, params: SolanaGenerateParams

): Promise + + /** Adds generated operation metadata to the submit result. */ + protected resultFromGenerated(hash: TransactionHash, _tx: Tx): Result { + return hash as Result + } + + /** Run {@link validate} and {@link buildUnsigned}; no signing. */ + async generate(chain: SolanaChain, params: SolanaGenerateParams

): Promise { + this.validate(params) + return this.buildUnsigned(chain, params) + } + + /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ + async execute(chain: SolanaChain, params: SolanaExecuteParams

): Promise { + const { wallet, computeUnits } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const tx = await this.generate(chain, withPayer(params, wallet.publicKey.toBase58())) + const hash = await submit(chain, wallet, tx, this.name, computeUnits) + return this.resultFromGenerated(hash, tx) + } +} diff --git a/ccip-sdk/src/cct/solana/pool/operations/accept-ownership.test.ts b/ccip-sdk/src/cct/solana/pool/operations/accept-ownership.test.ts new file mode 100644 index 00000000..60a5897d --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/accept-ownership.test.ts @@ -0,0 +1,73 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { createTokenPoolProgram } from '../../programs/token-pool.ts' +import { AcceptOwnership } from './accept-ownership.ts' +import { + AUTHORITY, + MINT, + PAYER, + POOL_PROGRAM, + POOL_STATE, + anchorDiscriminator, + statePda, + stubChain, +} from './test-helpers.ts' + +describe('Solana token-pool acceptOwnership', () => { + it('builds an instruction that matches a direct anchor build', async () => { + const chain = stubChain() + const unsigned = await new AcceptOwnership().generate(chain, { + poolAddress: POOL_STATE.toBase58(), + payer: PAYER, + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + const [ix] = unsigned.instructions + assert.ok(ix) + + assert.equal(ix.programId.toBase58(), POOL_PROGRAM.toBase58()) + assert.equal( + ix.data.subarray(0, 8).toString('hex'), + anchorDiscriminator('accept_ownership').toString('hex'), + ) + + const ref = await createTokenPoolProgram(chain, POOL_PROGRAM, new PublicKey(PAYER)) + .methods.acceptOwnership() + .accountsStrict({ state: statePda(), mint: MINT, authority: new PublicKey(PAYER) }) + .instruction() + + assert.equal(ix.data.toString('hex'), ref.data.toString('hex')) + assert.deepEqual( + ix.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ref.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ) + }) + + it('uses caller-provided authority for the signer account', async () => { + const unsigned = await new AcceptOwnership().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + payer: PAYER, + authority: AUTHORITY, + }) + const authKey = unsigned.instructions[0]!.keys.find((k) => k.isSigner) + assert.equal(authKey!.pubkey.toBase58(), AUTHORITY) + }) + + it('rejects an invalid poolAddress before RPC', async () => { + await assert.rejects( + () => + new AcceptOwnership().generate(stubChain(), { + poolAddress: 'not-a-key', + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/pool/operations/accept-ownership.ts b/ccip-sdk/src/cct/solana/pool/operations/accept-ownership.ts new file mode 100644 index 00000000..a7a92086 --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/accept-ownership.ts @@ -0,0 +1,68 @@ +import { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { createTokenPoolProgram, deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' +import { validatePublicKey } from '../../validate.ts' +import { discoverPoolInfo } from './common.ts' + +/** Parameters shared by token-pool `acceptOwnership` generation and execution. */ +type AcceptOwnershipParams = { + /** Local pool state (config PDA) address. */ + poolAddress: string + /** Pool authority (proposed owner). Defaults to `payer`. */ + authority?: string +} + +/** Parameters for unsigned token-pool `acceptOwnership` generation. */ +export type GenerateAcceptOwnershipParams = SolanaGenerateParams + +/** Unsigned token-pool `acceptOwnership` result. */ +export type GenerateAcceptOwnershipResult = UnsignedSolanaTx + +/** Parameters for executing token-pool `acceptOwnership`. */ +export type ExecuteAcceptOwnershipParams = SolanaExecuteParams + +/** Result of executing token-pool `acceptOwnership`. */ +export type ExecuteAcceptOwnershipResult = TransactionHash + +/** Accepts a proposed ownership transfer on a token pool (step 2 of the 2-step transfer). */ +export class AcceptOwnership extends SolanaOperation { + readonly name = 'acceptOwnership' + + /** Validates addresses before any RPC. */ + protected validate(params: GenerateAcceptOwnershipParams): void { + validatePublicKey(this.name, 'poolAddress', params.poolAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + } + + /** Builds the unsigned token-pool `acceptOwnership` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateAcceptOwnershipParams, + ): Promise { + const authority = new PublicKey(opts.authority ?? opts.payer) + const { poolProgramId, mint } = await discoverPoolInfo(chain, opts.poolAddress) + + const state = deriveTokenPoolConfigPda(poolProgramId, mint) + const program = createTokenPoolProgram(chain, poolProgramId, authority) + + const instruction = await program.methods + .acceptOwnership() + .accountsStrict({ state, mint, authority }) + .instruction() + + chain.logger.debug( + `${this.name}: pool = ${opts.poolAddress}, poolProgram = ${poolProgramId.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/pool/operations/append-remote-pool-addresses.test.ts b/ccip-sdk/src/cct/solana/pool/operations/append-remote-pool-addresses.test.ts new file mode 100644 index 00000000..b4055304 --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/append-remote-pool-addresses.test.ts @@ -0,0 +1,96 @@ +import { Buffer } from 'buffer' + +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { PublicKey, SystemProgram } from '@solana/web3.js' +import BN from 'bn.js' + +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { createTokenPoolProgram } from '../../programs/token-pool.ts' +import { AppendRemotePoolAddresses } from './append-remote-pool-addresses.ts' +import { encodeRemotePoolAddressBytes } from './common.ts' +import { + MINT, + PAYER, + POOL_PROGRAM, + POOL_STATE, + SELECTOR, + anchorDiscriminator, + chainConfigPda, + statePda, + stubChain, +} from './test-helpers.ts' + +const EVM_POOL = '0xd7BF0d8E6C242b6Dde4490Ab3aFc8C1e811ec9aD' +const SOL_POOL = new PublicKey('11111111111111111111111111111112').toBase58() + +describe('Solana token-pool appendRemotePoolAddresses', () => { + it('builds a single instruction matching a direct anchor build', async () => { + const chain = stubChain() + const unsigned = await new AppendRemotePoolAddresses().generate(chain, { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelector: SELECTOR, + remotePoolAddresses: [EVM_POOL, SOL_POOL], + payer: PAYER, + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.instructions.length, 1) + const [ix] = unsigned.instructions + assert.ok(ix) + assert.equal(ix.programId.toBase58(), POOL_PROGRAM.toBase58()) + assert.equal( + ix.data.subarray(0, 8).toString('hex'), + anchorDiscriminator('append_remote_pool_addresses').toString('hex'), + ) + + const addresses = [EVM_POOL, SOL_POOL].map((a) => ({ + address: Buffer.from(encodeRemotePoolAddressBytes(a)), + })) + const ref = await createTokenPoolProgram(chain, POOL_PROGRAM, new PublicKey(PAYER)) + .methods.appendRemotePoolAddresses(new BN(SELECTOR.toString()), MINT, addresses) + .accountsStrict({ + state: statePda(), + chainConfig: chainConfigPda(), + authority: new PublicKey(PAYER), + systemProgram: SystemProgram.programId, + }) + .instruction() + + assert.equal(ix.data.toString('hex'), ref.data.toString('hex')) + assert.deepEqual( + ix.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ref.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ) + // EVM address is preserved as raw 20 bytes (no left-padding to 32). + assert.ok(ix.data.includes(Buffer.from(EVM_POOL.slice(2), 'hex'))) + }) + + it('rejects an empty remotePoolAddresses list before RPC', async () => { + await assert.rejects( + () => + new AppendRemotePoolAddresses().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelector: SELECTOR, + remotePoolAddresses: [], + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) + + it('rejects a zero selector before RPC', async () => { + await assert.rejects( + () => + new AppendRemotePoolAddresses().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelector: 0n, + remotePoolAddresses: [EVM_POOL], + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/pool/operations/append-remote-pool-addresses.ts b/ccip-sdk/src/cct/solana/pool/operations/append-remote-pool-addresses.ts new file mode 100644 index 00000000..5f83e39a --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/append-remote-pool-addresses.ts @@ -0,0 +1,98 @@ +import { Buffer } from 'buffer' + +import { PublicKey, SystemProgram } from '@solana/web3.js' +import BN from 'bn.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { createTokenPoolProgram, deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' +import { validatePublicKey } from '../../validate.ts' +import { + deriveTokenPoolChainConfigPda, + discoverPoolInfo, + encodeRemotePoolAddressBytes, + validateSelectorWithAddresses, +} from './common.ts' + +/** Parameters shared by token-pool `appendRemotePoolAddresses` generation and execution. */ +type AppendRemotePoolAddressesParams = { + /** Local pool state (config PDA) address. */ + poolAddress: string + /** Remote chain selector; must already be configured via applyChainUpdates. */ + remoteChainSelector: bigint + /** Remote pool addresses in native format to append. At least one required. */ + remotePoolAddresses: string[] + /** Pool authority. Defaults to `payer`; pass a vault/multisig authority explicitly. */ + authority?: string +} + +/** Parameters for unsigned token-pool `appendRemotePoolAddresses` generation. */ +export type GenerateAppendRemotePoolAddressesParams = + SolanaGenerateParams + +/** Unsigned token-pool `appendRemotePoolAddresses` result. */ +export type GenerateAppendRemotePoolAddressesResult = UnsignedSolanaTx + +/** Parameters for executing token-pool `appendRemotePoolAddresses`. */ +export type ExecuteAppendRemotePoolAddressesParams = + SolanaExecuteParams + +/** Result of executing token-pool `appendRemotePoolAddresses`. */ +export type ExecuteAppendRemotePoolAddressesResult = TransactionHash + +/** Appends remote pool addresses to an existing token-pool chain config. */ +export class AppendRemotePoolAddresses extends SolanaOperation { + readonly name = 'appendRemotePoolAddresses' + + /** Validates addresses and selector before any RPC. */ + protected validate(params: GenerateAppendRemotePoolAddressesParams): void { + validatePublicKey(this.name, 'poolAddress', params.poolAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + validateSelectorWithAddresses( + this.name, + params.poolAddress, + params.remoteChainSelector, + params.remotePoolAddresses, + ) + } + + /** Builds the unsigned token-pool `appendRemotePoolAddresses` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateAppendRemotePoolAddressesParams, + ): Promise { + const authority = new PublicKey(opts.authority ?? opts.payer) + const { poolProgramId, mint } = await discoverPoolInfo(chain, opts.poolAddress) + + const state = deriveTokenPoolConfigPda(poolProgramId, mint) + const chainConfig = deriveTokenPoolChainConfigPda(poolProgramId, opts.remoteChainSelector, mint) + const program = createTokenPoolProgram(chain, poolProgramId, authority) + + const addresses = opts.remotePoolAddresses.map((addr) => ({ + address: Buffer.from(encodeRemotePoolAddressBytes(addr)), + })) + + const instruction = await program.methods + .appendRemotePoolAddresses(new BN(opts.remoteChainSelector.toString()), mint, addresses) + .accountsStrict({ + state, + chainConfig, + authority, + systemProgram: SystemProgram.programId, + }) + .instruction() + + chain.logger.debug( + `${this.name}: pool = ${opts.poolAddress}, addresses = ${opts.remotePoolAddresses.length}, poolProgram = ${poolProgramId.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/pool/operations/apply-chain-updates.test.ts b/ccip-sdk/src/cct/solana/pool/operations/apply-chain-updates.test.ts new file mode 100644 index 00000000..b38db79d --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/apply-chain-updates.test.ts @@ -0,0 +1,165 @@ +import { Buffer } from 'buffer' + +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { PublicKey, SystemProgram } from '@solana/web3.js' +import BN from 'bn.js' + +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { createTokenPoolProgram } from '../../programs/token-pool.ts' +import { ApplyChainUpdates } from './apply-chain-updates.ts' +import type { RemoteChainConfig } from './common.ts' +import { encodeRemoteAddressBytes, encodeRemotePoolAddressBytes } from './common.ts' +import { + MINT, + PAYER, + POOL_PROGRAM, + POOL_STATE, + SELECTOR, + anchorDiscriminator, + chainConfigPda, + statePda, + stubChain, +} from './test-helpers.ts' + +const EVM_TOKEN = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const EVM_POOL = '0xd7BF0d8E6C242b6Dde4490Ab3aFc8C1e811ec9aD' +const REMOVE_SELECTOR = 14767482510784806043n + +const addChain: RemoteChainConfig = { + remoteChainSelector: SELECTOR, + remotePoolAddresses: [EVM_POOL], + remoteTokenAddress: EVM_TOKEN, + remoteTokenDecimals: 18, + outboundRateLimiterConfig: { isEnabled: true, capacity: '1000', rate: '5' }, + inboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, +} + +describe('Solana token-pool applyChainUpdates', () => { + it('emits delete + init + append + rateLimit in order for a fresh chain', async () => { + const chain = stubChain({ chainConfigExists: false }) + const unsigned = await new ApplyChainUpdates().generate(chain, { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelectorsToRemove: [REMOVE_SELECTOR], + chainsToAdd: [addChain], + payer: PAYER, + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 4) + + const discs = unsigned.instructions.map((ix) => ix.data.subarray(0, 8).toString('hex')) + assert.deepEqual(discs, [ + anchorDiscriminator('delete_chain_config').toString('hex'), + anchorDiscriminator('init_chain_remote_config').toString('hex'), + anchorDiscriminator('append_remote_pool_addresses').toString('hex'), + anchorDiscriminator('set_chain_rate_limit').toString('hex'), + ]) + + for (const ix of unsigned.instructions) { + assert.equal(ix.programId.toBase58(), POOL_PROGRAM.toBase58()) + } + + // Reference builds for the add-chain instructions. + const program = createTokenPoolProgram(chain, POOL_PROGRAM, new PublicKey(PAYER)) + const authority = new PublicKey(PAYER) + const state = statePda() + const cfg = chainConfigPda() + + const refInit = await program.methods + .initChainRemoteConfig(new BN(SELECTOR.toString()), MINT, { + poolAddresses: [], + tokenAddress: { address: Buffer.from(encodeRemoteAddressBytes(EVM_TOKEN)) }, + decimals: 18, + }) + .accountsStrict({ + state, + chainConfig: cfg, + authority, + systemProgram: SystemProgram.programId, + }) + .instruction() + + const refAppend = await program.methods + .appendRemotePoolAddresses(new BN(SELECTOR.toString()), MINT, [ + { address: Buffer.from(encodeRemotePoolAddressBytes(EVM_POOL)) }, + ]) + .accountsStrict({ + state, + chainConfig: cfg, + authority, + systemProgram: SystemProgram.programId, + }) + .instruction() + + const refRate = await program.methods + .setChainRateLimit( + new BN(SELECTOR.toString()), + MINT, + { enabled: false, capacity: new BN(0), rate: new BN(0) }, + { enabled: true, capacity: new BN(1000), rate: new BN(5) }, + ) + .accountsStrict({ state, chainConfig: cfg, authority }) + .instruction() + + assert.equal(unsigned.instructions[1]!.data.toString('hex'), refInit.data.toString('hex')) + assert.equal(unsigned.instructions[2]!.data.toString('hex'), refAppend.data.toString('hex')) + assert.equal(unsigned.instructions[3]!.data.toString('hex'), refRate.data.toString('hex')) + + // delete targets the REMOVE_SELECTOR chain config PDA + assert.equal( + unsigned.instructions[0]!.keys[1]!.pubkey.toBase58(), + chainConfigPda(REMOVE_SELECTOR).toBase58(), + ) + }) + + it('skips init when the chain config already exists and is not being removed', async () => { + const chain = stubChain({ chainConfigExists: true }) + const unsigned = await new ApplyChainUpdates().generate(chain, { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelectorsToRemove: [], + chainsToAdd: [addChain], + payer: PAYER, + }) + + const discs = unsigned.instructions.map((ix) => ix.data.subarray(0, 8).toString('hex')) + assert.deepEqual(discs, [ + anchorDiscriminator('append_remote_pool_addresses').toString('hex'), + anchorDiscriminator('set_chain_rate_limit').toString('hex'), + ]) + }) + + it('re-inits when the chain is being removed and re-added in the same tx', async () => { + const chain = stubChain({ chainConfigExists: true }) + const unsigned = await new ApplyChainUpdates().generate(chain, { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelectorsToRemove: [SELECTOR], + chainsToAdd: [addChain], + payer: PAYER, + }) + + const discs = unsigned.instructions.map((ix) => ix.data.subarray(0, 8).toString('hex')) + assert.deepEqual(discs, [ + anchorDiscriminator('delete_chain_config').toString('hex'), + anchorDiscriminator('init_chain_remote_config').toString('hex'), + anchorDiscriminator('append_remote_pool_addresses').toString('hex'), + anchorDiscriminator('set_chain_rate_limit').toString('hex'), + ]) + }) + + it('rejects an add-chain with no remote pool addresses before RPC', async () => { + await assert.rejects( + () => + new ApplyChainUpdates().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelectorsToRemove: [], + chainsToAdd: [{ ...addChain, remotePoolAddresses: [] }], + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/pool/operations/apply-chain-updates.ts b/ccip-sdk/src/cct/solana/pool/operations/apply-chain-updates.ts new file mode 100644 index 00000000..d0383f3f --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/apply-chain-updates.ts @@ -0,0 +1,74 @@ +import { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { validatePublicKey } from '../../validate.ts' +import { + type RemoteChainConfig, + buildApplyChainUpdatesInstructions, + validateApplyChainUpdates, +} from './common.ts' + +/** Parameters shared by token-pool `applyChainUpdates` generation and execution. */ +type ApplyChainUpdatesParams = { + /** Local pool state (config PDA) address. */ + poolAddress: string + /** Remote chain selectors to remove (can be empty). */ + remoteChainSelectorsToRemove: bigint[] + /** Remote chain configurations to add (can be empty). */ + chainsToAdd: RemoteChainConfig[] + /** Pool authority. Defaults to `payer`; pass a vault/multisig authority explicitly. */ + authority?: string +} + +/** Parameters for unsigned token-pool `applyChainUpdates` generation. */ +export type GenerateApplyChainUpdatesParams = SolanaGenerateParams + +/** Unsigned token-pool `applyChainUpdates` result. */ +export type GenerateApplyChainUpdatesResult = UnsignedSolanaTx + +/** Parameters for executing token-pool `applyChainUpdates`. */ +export type ExecuteApplyChainUpdatesParams = SolanaExecuteParams + +/** Result of executing token-pool `applyChainUpdates`. */ +export type ExecuteApplyChainUpdatesResult = TransactionHash + +/** Configures remote chains on a burn-mint/lock-release token pool. */ +export class ApplyChainUpdates extends SolanaOperation { + readonly name = 'applyChainUpdates' + + /** Validates addresses and update fields before any RPC. */ + protected validate(params: GenerateApplyChainUpdatesParams): void { + validatePublicKey(this.name, 'poolAddress', params.poolAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + validateApplyChainUpdates(this.name, params.poolAddress, params.chainsToAdd) + } + + /** Builds the unsigned token-pool `applyChainUpdates` instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateApplyChainUpdatesParams, + ): Promise { + const authority = new PublicKey(opts.authority ?? opts.payer) + const { instructions, poolProgramId } = await buildApplyChainUpdatesInstructions(chain, { + operation: this.name, + poolAddress: opts.poolAddress, + authority, + remoteChainSelectorsToRemove: opts.remoteChainSelectorsToRemove, + chainsToAdd: opts.chainsToAdd, + }) + + chain.logger.debug( + `${this.name}: pool = ${opts.poolAddress}, poolProgram = ${poolProgramId.toBase58()}, instructions = ${instructions.length}`, + ) + return { family: ChainFamily.Solana, instructions, mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/pool/operations/common.ts b/ccip-sdk/src/cct/solana/pool/operations/common.ts new file mode 100644 index 00000000..050231b5 --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/common.ts @@ -0,0 +1,314 @@ +/** + * Shared helpers for Solana token-pool CCT operations (applyChainUpdates, + * deleteChainConfig, appendRemotePoolAddresses, removeRemotePoolAddresses). + * + * Targets the token POOL program (burn-mint / lock-release), not the router: + * derives pool state/chain-config PDAs and builds pool-program instructions. + * + * @packageDocumentation + */ + +import { Buffer } from 'buffer' + +import { type TransactionInstruction, PublicKey, SystemProgram } from '@solana/web3.js' +import BN from 'bn.js' +import { hexlify, zeroPadValue } from 'ethers' + +import type { SolanaChain } from '../../../../solana/index.ts' +import { getAddressBytes } from '../../../../utils.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { createTokenPoolProgram, deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' + +/** Anchor seed prefix for a token pool per-chain config PDA. */ +const CCIP_TOKENPOOL_CHAINCONFIG_SEED = 'ccip_tokenpool_chainconfig' + +/** Rate limiter configuration for one direction of a remote chain lane. */ +export type RateLimiterConfig = { + /** Whether the rate limiter is enabled. */ + isEnabled: boolean + /** Maximum token bucket capacity (bigint as string). */ + capacity: string + /** Token refill rate per second (bigint as string). */ + rate: string +} + +/** Configuration for a single remote chain on a token pool. */ +export type RemoteChainConfig = { + /** Remote chain selector. */ + remoteChainSelector: bigint + /** Remote pool address(es) in native format. At least one required. */ + remotePoolAddresses: string[] + /** Remote token address in native format. */ + remoteTokenAddress: string + /** Remote token decimals (used in init_chain_remote_config). Defaults to 0. */ + remoteTokenDecimals?: number + /** Outbound rate limiter (local to remote). */ + outboundRateLimiterConfig: RateLimiterConfig + /** Inbound rate limiter (remote to local). */ + inboundRateLimiterConfig: RateLimiterConfig +} + +/** + * Encodes a remote address to a 32-byte left-padded hex string. + * + * Handles hex (EVM/Aptos), base58 (Solana) and base64 (Sui/TON) via + * {@link getAddressBytes}. Matches on-chain `LeftPadBytes(addr, 32)`. + * + * @param address - Address in native format + * @returns 32-byte left-padded hex string (0x-prefixed) + */ +export function encodeRemoteAddress(address: string): string { + return zeroPadValue(hexlify(getAddressBytes(address)), 32) +} + +/** + * Encodes a remote address to a 32-byte left-padded byte array. + * + * @param address - Address in native format + * @returns 32-byte left-padded bytes + */ +export function encodeRemoteAddressBytes(address: string): Uint8Array { + const hex = zeroPadValue(hexlify(getAddressBytes(address)), 32) + return Uint8Array.from(Buffer.from(hex.slice(2), 'hex')) +} + +/** + * Encodes a remote pool address to raw bytes (no padding). + * + * Pool addresses preserve their native byte length (20 bytes EVM, 32 bytes Solana) + * so the on-chain program can compare them during ReleaseOrMintTokens. + * + * @param address - Address in native format + * @returns Raw bytes (original length, no padding) + */ +export function encodeRemotePoolAddressBytes(address: string): Uint8Array { + return getAddressBytes(address) +} + +/** Little-endian 8-byte buffer for a u64 chain selector. */ +function selectorLeBuffer(selector: bigint): Buffer { + const buf = Buffer.alloc(8) + buf.writeBigUInt64LE(selector) + return buf +} + +/** Derives a token pool per-chain config PDA for a mint and remote selector. */ +export function deriveTokenPoolChainConfigPda( + poolProgram: PublicKey, + selector: bigint, + mint: PublicKey, +): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from(CCIP_TOKENPOOL_CHAINCONFIG_SEED), selectorLeBuffer(selector), mint.toBuffer()], + poolProgram, + )[0] +} + +/** + * Discovers the pool program id (owner of the pool state account) and the mint + * it manages from a pool state address. + * + * @param chain - Solana chain used for RPC + decoding + * @param poolAddress - Pool state (config PDA) address + * @returns The owning pool program id and managed mint + * @throws {@link CCTParamsInvalidError} if the pool account does not exist + */ +export async function discoverPoolInfo( + chain: SolanaChain, + poolAddress: string, +): Promise<{ poolProgramId: PublicKey; mint: PublicKey }> { + const poolPubkey = new PublicKey(poolAddress) + const accountInfo = await chain.connection.getAccountInfo(poolPubkey) + if (!accountInfo) { + throw new CCTParamsInvalidError( + 'poolAddress', + 'poolAddress', + `pool account not found: ${poolAddress}`, + ) + } + const mint = new PublicKey(await chain.getTokenForTokenPool(poolAddress)) + return { poolProgramId: accountInfo.owner, mint } +} + +/** Inputs for {@link buildApplyChainUpdatesInstructions}. */ +export type ApplyChainUpdatesBuild = { + operation: string + poolAddress: string + authority: PublicKey + remoteChainSelectorsToRemove: bigint[] + chainsToAdd: RemoteChainConfig[] +} + +/** + * Builds the burn-mint/lock-release pool instruction set for applying chain updates: + * a `deleteChainConfig` per removed selector, then per added chain an + * `initChainRemoteConfig` (skipped if the PDA already exists and is not being + * deleted), an `appendRemotePoolAddresses`, and a `setChainRateLimit`. + * + * @param chain - Solana chain used for RPC + program client construction + * @param opts - Discovery + update inputs + * @returns The ordered instruction list and discovered pool metadata + */ +export async function buildApplyChainUpdatesInstructions( + chain: SolanaChain, + opts: ApplyChainUpdatesBuild, +): Promise<{ instructions: TransactionInstruction[]; poolProgramId: PublicKey; mint: PublicKey }> { + const { poolProgramId, mint } = await discoverPoolInfo(chain, opts.poolAddress) + const state = deriveTokenPoolConfigPda(poolProgramId, mint) + const program = createTokenPoolProgram(chain, poolProgramId, opts.authority) + const instructions: TransactionInstruction[] = [] + + for (const selector of opts.remoteChainSelectorsToRemove) { + const chainConfig = deriveTokenPoolChainConfigPda(poolProgramId, selector, mint) + instructions.push( + await program.methods + .deleteChainConfig(new BN(selector.toString()), mint) + .accountsStrict({ state, chainConfig, authority: opts.authority }) + .instruction(), + ) + } + + const selectorsBeingRemoved = new Set(opts.remoteChainSelectorsToRemove) + + for (const remote of opts.chainsToAdd) { + const chainConfig = deriveTokenPoolChainConfigPda( + poolProgramId, + remote.remoteChainSelector, + mint, + ) + + const existingConfig = await chain.connection.getAccountInfo(chainConfig) + const beingDeleted = selectorsBeingRemoved.has(remote.remoteChainSelector) + const alreadyInitialized = existingConfig !== null && !beingDeleted + + if (!alreadyInitialized) { + instructions.push( + await program.methods + .initChainRemoteConfig(new BN(remote.remoteChainSelector.toString()), mint, { + poolAddresses: [], + tokenAddress: { + address: Buffer.from(encodeRemoteAddressBytes(remote.remoteTokenAddress)), + }, + decimals: remote.remoteTokenDecimals ?? 0, + }) + .accountsStrict({ + state, + chainConfig, + authority: opts.authority, + systemProgram: SystemProgram.programId, + }) + .instruction(), + ) + } + + if (remote.remotePoolAddresses.length > 0) { + const addresses = remote.remotePoolAddresses.map((addr) => ({ + address: Buffer.from(encodeRemotePoolAddressBytes(addr)), + })) + instructions.push( + await program.methods + .appendRemotePoolAddresses(new BN(remote.remoteChainSelector.toString()), mint, addresses) + .accountsStrict({ + state, + chainConfig, + authority: opts.authority, + systemProgram: SystemProgram.programId, + }) + .instruction(), + ) + } + + instructions.push( + await program.methods + .setChainRateLimit( + new BN(remote.remoteChainSelector.toString()), + mint, + { + enabled: remote.inboundRateLimiterConfig.isEnabled, + capacity: new BN(remote.inboundRateLimiterConfig.capacity), + rate: new BN(remote.inboundRateLimiterConfig.rate), + }, + { + enabled: remote.outboundRateLimiterConfig.isEnabled, + capacity: new BN(remote.outboundRateLimiterConfig.capacity), + rate: new BN(remote.outboundRateLimiterConfig.rate), + }, + ) + .accountsStrict({ state, chainConfig, authority: opts.authority }) + .instruction(), + ) + } + + return { instructions, poolProgramId, mint } +} + +// ── Validators (all throw CCTParamsInvalidError) ───────────────────────────── + +/** Asserts a string param is present and non-blank. */ +function assertNonEmpty(operation: string, param: string, value: string): void { + if (!value || value.trim().length === 0) { + throw new CCTParamsInvalidError(operation, param, 'must be non-empty') + } +} + +/** Asserts a chain selector is present and non-zero. */ +function assertNonZeroSelector(operation: string, param: string, selector: bigint): void { + if (selector == null || selector === 0n) { + throw new CCTParamsInvalidError(operation, param, 'must be non-zero') + } +} + +/** Validates applyChainUpdates params. */ +export function validateApplyChainUpdates( + operation: string, + poolAddress: string, + chainsToAdd: RemoteChainConfig[], +): void { + assertNonEmpty(operation, 'poolAddress', poolAddress) + for (const [i, remote] of chainsToAdd.entries()) { + assertNonZeroSelector( + operation, + `chainsToAdd[${i}].remoteChainSelector`, + remote.remoteChainSelector, + ) + if (remote.remotePoolAddresses.length === 0) { + throw new CCTParamsInvalidError( + operation, + `chainsToAdd[${i}].remotePoolAddresses`, + 'must have at least one address', + ) + } + assertNonEmpty(operation, `chainsToAdd[${i}].remoteTokenAddress`, remote.remoteTokenAddress) + } +} + +/** Validates params that reference a single chain selector with pool addresses. */ +export function validateSelectorWithAddresses( + operation: string, + poolAddress: string, + remoteChainSelector: bigint, + remotePoolAddresses: string[], +): void { + assertNonEmpty(operation, 'poolAddress', poolAddress) + assertNonZeroSelector(operation, 'remoteChainSelector', remoteChainSelector) + if (remotePoolAddresses.length === 0) { + throw new CCTParamsInvalidError( + operation, + 'remotePoolAddresses', + 'must have at least one address', + ) + } + for (const [i, addr] of remotePoolAddresses.entries()) { + assertNonEmpty(operation, `remotePoolAddresses[${i}]`, addr) + } +} + +/** Validates deleteChainConfig params. */ +export function validateDeleteChainConfig( + operation: string, + poolAddress: string, + remoteChainSelector: bigint, +): void { + assertNonEmpty(operation, 'poolAddress', poolAddress) + assertNonZeroSelector(operation, 'remoteChainSelector', remoteChainSelector) +} diff --git a/ccip-sdk/src/cct/solana/pool/operations/delete-chain-config.test.ts b/ccip-sdk/src/cct/solana/pool/operations/delete-chain-config.test.ts new file mode 100644 index 00000000..24300c2c --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/delete-chain-config.test.ts @@ -0,0 +1,106 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { PublicKey } from '@solana/web3.js' +import BN from 'bn.js' + +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { createTokenPoolProgram } from '../../programs/token-pool.ts' +import { DeleteChainConfig } from './delete-chain-config.ts' +import { + AUTHORITY, + MINT, + PAYER, + POOL_PROGRAM, + POOL_STATE, + SELECTOR, + anchorDiscriminator, + chainConfigPda, + statePda, + stubChain, +} from './test-helpers.ts' + +describe('Solana token-pool deleteChainConfig', () => { + it('builds an instruction that matches a direct anchor build', async () => { + const chain = stubChain() + const unsigned = await new DeleteChainConfig().generate(chain, { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelector: SELECTOR, + payer: PAYER, + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + const [ix] = unsigned.instructions + assert.ok(ix) + + // programId + instruction identity + assert.equal(ix.programId.toBase58(), POOL_PROGRAM.toBase58()) + assert.equal( + ix.data.subarray(0, 8).toString('hex'), + anchorDiscriminator('delete_chain_config').toString('hex'), + ) + + // Reference build via a fresh anchor Program client (independent of the op). + const ref = await createTokenPoolProgram(chain, POOL_PROGRAM, new PublicKey(PAYER)) + .methods.deleteChainConfig(new BN(SELECTOR.toString()), MINT) + .accountsStrict({ + state: statePda(), + chainConfig: chainConfigPda(), + authority: new PublicKey(PAYER), + }) + .instruction() + + // account order + pubkeys (state RO, chainConfig W, authority W+signer) + assert.equal(ix.keys.length, 3) + assert.equal(ix.keys[0]!.pubkey.toBase58(), statePda().toBase58()) + assert.equal(ix.keys[0]!.isWritable, false) + assert.equal(ix.keys[1]!.pubkey.toBase58(), chainConfigPda().toBase58()) + assert.equal(ix.keys[1]!.isWritable, true) + assert.equal(ix.keys[2]!.pubkey.toBase58(), PAYER) + assert.equal(ix.keys[2]!.isSigner, true) + assert.equal(ix.keys[2]!.isWritable, true) + + assert.equal(ix.data.toString('hex'), ref.data.toString('hex')) + assert.deepEqual( + ix.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ref.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ) + }) + + it('uses caller-provided authority for the signer account', async () => { + const unsigned = await new DeleteChainConfig().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelector: SELECTOR, + payer: PAYER, + authority: AUTHORITY, + }) + assert.equal(unsigned.instructions[0]!.keys[2]!.pubkey.toBase58(), AUTHORITY) + }) + + it('rejects a zero remoteChainSelector before RPC', async () => { + await assert.rejects( + () => + new DeleteChainConfig().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelector: 0n, + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) + + it('rejects an invalid poolAddress before RPC', async () => { + await assert.rejects( + () => + new DeleteChainConfig().generate(stubChain(), { + poolAddress: 'not-a-key', + remoteChainSelector: SELECTOR, + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/pool/operations/delete-chain-config.ts b/ccip-sdk/src/cct/solana/pool/operations/delete-chain-config.ts new file mode 100644 index 00000000..eb3f5d7b --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/delete-chain-config.ts @@ -0,0 +1,77 @@ +import { PublicKey } from '@solana/web3.js' +import BN from 'bn.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { createTokenPoolProgram, deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' +import { validatePublicKey } from '../../validate.ts' +import { + deriveTokenPoolChainConfigPda, + discoverPoolInfo, + validateDeleteChainConfig, +} from './common.ts' + +/** Parameters shared by token-pool `deleteChainConfig` generation and execution. */ +type DeleteChainConfigParams = { + /** Local pool state (config PDA) address. */ + poolAddress: string + /** Remote chain selector whose config PDA is closed. */ + remoteChainSelector: bigint + /** Pool authority. Defaults to `payer`; pass a vault/multisig authority explicitly. */ + authority?: string +} + +/** Parameters for unsigned token-pool `deleteChainConfig` generation. */ +export type GenerateDeleteChainConfigParams = SolanaGenerateParams + +/** Unsigned token-pool `deleteChainConfig` result. */ +export type GenerateDeleteChainConfigResult = UnsignedSolanaTx + +/** Parameters for executing token-pool `deleteChainConfig`. */ +export type ExecuteDeleteChainConfigParams = SolanaExecuteParams + +/** Result of executing token-pool `deleteChainConfig`. */ +export type ExecuteDeleteChainConfigResult = TransactionHash + +/** Removes an entire remote chain configuration from a token pool. */ +export class DeleteChainConfig extends SolanaOperation { + readonly name = 'deleteChainConfig' + + /** Validates addresses and selector before any RPC. */ + protected validate(params: GenerateDeleteChainConfigParams): void { + validatePublicKey(this.name, 'poolAddress', params.poolAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + validateDeleteChainConfig(this.name, params.poolAddress, params.remoteChainSelector) + } + + /** Builds the unsigned token-pool `deleteChainConfig` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateDeleteChainConfigParams, + ): Promise { + const authority = new PublicKey(opts.authority ?? opts.payer) + const { poolProgramId, mint } = await discoverPoolInfo(chain, opts.poolAddress) + + const state = deriveTokenPoolConfigPda(poolProgramId, mint) + const chainConfig = deriveTokenPoolChainConfigPda(poolProgramId, opts.remoteChainSelector, mint) + const program = createTokenPoolProgram(chain, poolProgramId, authority) + + const instruction = await program.methods + .deleteChainConfig(new BN(opts.remoteChainSelector.toString()), mint) + .accountsStrict({ state, chainConfig, authority }) + .instruction() + + chain.logger.debug( + `${this.name}: pool = ${opts.poolAddress}, remoteChainSelector = ${opts.remoteChainSelector}, poolProgram = ${poolProgramId.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/pool/operations/index.ts b/ccip-sdk/src/cct/solana/pool/operations/index.ts new file mode 100644 index 00000000..8cf11bc0 --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/index.ts @@ -0,0 +1,15 @@ +/** + * Solana token-pool CCT config operations (burn-mint / lock-release pool program). + * + * @packageDocumentation + */ + +export * from './accept-ownership.ts' +export * from './apply-chain-updates.ts' +export * from './append-remote-pool-addresses.ts' +export * from './delete-chain-config.ts' +export * from './remove-remote-pool-addresses.ts' +export * from './set-chain-rate-limiter-config.ts' +export * from './set-rate-limit-admin.ts' +export * from './transfer-ownership.ts' +export type { RateLimiterConfig, RemoteChainConfig } from './common.ts' diff --git a/ccip-sdk/src/cct/solana/pool/operations/remove-remote-pool-addresses.test.ts b/ccip-sdk/src/cct/solana/pool/operations/remove-remote-pool-addresses.test.ts new file mode 100644 index 00000000..9bdb7806 --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/remove-remote-pool-addresses.test.ts @@ -0,0 +1,124 @@ +import { Buffer } from 'buffer' + +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { RemoveRemotePoolAddresses } from './remove-remote-pool-addresses.ts' +import { encodeRemotePoolAddressBytes } from './common.ts' +import { + PAYER, + POOL_PROGRAM, + POOL_STATE, + SELECTOR, + anchorDiscriminator, + chainConfigPda, + stubChain, +} from './test-helpers.ts' + +const EVM_TOKEN = '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888' +const POOL_A = '0xd7BF0d8E6C242b6Dde4490Ab3aFc8C1e811ec9aD' +const POOL_B = '0x1111111111111111111111111111111111111111' + +function remotesWith(pools: string[]) { + return { + 'some-remote': { + remoteToken: EVM_TOKEN, + remotePools: pools, + inboundRateLimiterState: null, + outboundRateLimiterState: { tokens: 10n, capacity: 1000n, rate: 5n }, + }, + } +} + +describe('Solana token-pool removeRemotePoolAddresses', () => { + it('re-applies the chain with only the remaining pools', async () => { + const chain = stubChain({ remotes: remotesWith([POOL_A, POOL_B]) }) + const unsigned = await new RemoveRemotePoolAddresses().generate(chain, { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelector: SELECTOR, + remotePoolAddresses: [POOL_A], + payer: PAYER, + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + // delete + init + append + rateLimit + assert.equal(unsigned.instructions.length, 4) + const discs = unsigned.instructions.map((ix) => ix.data.subarray(0, 8).toString('hex')) + assert.deepEqual(discs, [ + anchorDiscriminator('delete_chain_config').toString('hex'), + anchorDiscriminator('init_chain_remote_config').toString('hex'), + anchorDiscriminator('append_remote_pool_addresses').toString('hex'), + anchorDiscriminator('set_chain_rate_limit').toString('hex'), + ]) + for (const ix of unsigned.instructions) { + assert.equal(ix.programId.toBase58(), POOL_PROGRAM.toBase58()) + } + + assert.equal( + unsigned.instructions[0]!.keys[1]!.pubkey.toBase58(), + chainConfigPda(SELECTOR).toBase58(), + ) + + // append keeps POOL_B, drops POOL_A + const appendData = unsigned.instructions[2]!.data + assert.ok(appendData.includes(Buffer.from(encodeRemotePoolAddressBytes(POOL_B)))) + assert.ok(!appendData.includes(Buffer.from(encodeRemotePoolAddressBytes(POOL_A)))) + }) + + it('rejects when none of the addresses match the current config', async () => { + const chain = stubChain({ remotes: remotesWith([POOL_A]) }) + await assert.rejects( + () => + new RemoveRemotePoolAddresses().generate(chain, { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelector: SELECTOR, + remotePoolAddresses: [POOL_B], + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) + + it('rejects removing all pool addresses', async () => { + const chain = stubChain({ remotes: remotesWith([POOL_A, POOL_B]) }) + await assert.rejects( + () => + new RemoveRemotePoolAddresses().generate(chain, { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelector: SELECTOR, + remotePoolAddresses: [POOL_A, POOL_B], + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) + + it('rejects when no chain config exists for the selector', async () => { + const chain = stubChain({ remotes: {} }) + await assert.rejects( + () => + new RemoveRemotePoolAddresses().generate(chain, { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelector: SELECTOR, + remotePoolAddresses: [POOL_A], + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) + + it('rejects invalid params before RPC', async () => { + await assert.rejects( + () => + new RemoveRemotePoolAddresses().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + remoteChainSelector: SELECTOR, + remotePoolAddresses: [], + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/pool/operations/remove-remote-pool-addresses.ts b/ccip-sdk/src/cct/solana/pool/operations/remove-remote-pool-addresses.ts new file mode 100644 index 00000000..05f13770 --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/remove-remote-pool-addresses.ts @@ -0,0 +1,139 @@ +import { PublicKey } from '@solana/web3.js' + +import type { RateLimiterState } from '../../../../chain.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { validatePublicKey } from '../../validate.ts' +import { + type RateLimiterConfig, + buildApplyChainUpdatesInstructions, + encodeRemoteAddress, + validateSelectorWithAddresses, +} from './common.ts' + +/** Parameters shared by token-pool `removeRemotePoolAddresses` generation and execution. */ +type RemoveRemotePoolAddressesParams = { + /** Local pool state (config PDA) address. */ + poolAddress: string + /** Remote chain selector; must already be configured via applyChainUpdates. */ + remoteChainSelector: bigint + /** Remote pool addresses in native format to remove. At least one required. */ + remotePoolAddresses: string[] + /** Pool authority. Defaults to `payer`; pass a vault/multisig authority explicitly. */ + authority?: string +} + +/** Parameters for unsigned token-pool `removeRemotePoolAddresses` generation. */ +export type GenerateRemoveRemotePoolAddressesParams = + SolanaGenerateParams + +/** Unsigned token-pool `removeRemotePoolAddresses` result. */ +export type GenerateRemoveRemotePoolAddressesResult = UnsignedSolanaTx + +/** Parameters for executing token-pool `removeRemotePoolAddresses`. */ +export type ExecuteRemoveRemotePoolAddressesParams = + SolanaExecuteParams + +/** Result of executing token-pool `removeRemotePoolAddresses`. */ +export type ExecuteRemoveRemotePoolAddressesResult = TransactionHash + +/** Converts an on-chain rate limiter state to an applyChainUpdates rate limiter config. */ +function toRateLimiterConfig(state: RateLimiterState): RateLimiterConfig { + if (!state || (state.capacity === 0n && state.rate === 0n)) { + return { isEnabled: false, capacity: '0', rate: '0' } + } + return { isEnabled: true, capacity: state.capacity.toString(), rate: state.rate.toString() } +} + +/** + * Removes specific remote pool addresses from an existing chain config. + * + * Solana has no on-chain `removeRemotePool` instruction, so this reads the current + * config, then re-applies the chain (delete + re-init with the remaining pools) via + * the same instruction set as {@link ApplyChainUpdates}. + */ +export class RemoveRemotePoolAddresses extends SolanaOperation { + readonly name = 'removeRemotePoolAddresses' + + /** Validates addresses and selector before any RPC. */ + protected validate(params: GenerateRemoveRemotePoolAddressesParams): void { + validatePublicKey(this.name, 'poolAddress', params.poolAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + validateSelectorWithAddresses( + this.name, + params.poolAddress, + params.remoteChainSelector, + params.remotePoolAddresses, + ) + } + + /** Reads the current config and builds the delete + re-apply instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateRemoveRemotePoolAddressesParams, + ): Promise { + const authority = new PublicKey(opts.authority ?? opts.payer) + + const remotes = await chain.getTokenPoolRemotes(opts.poolAddress, opts.remoteChainSelector) + const remoteConfig = Object.values(remotes)[0] + if (!remoteConfig) { + throw new CCTParamsInvalidError( + this.name, + 'remoteChainSelector', + `no chain config found for remote chain selector ${opts.remoteChainSelector}`, + ) + } + + const addressesToRemove = new Set( + opts.remotePoolAddresses.map((a) => encodeRemoteAddress(a).toLowerCase()), + ) + const remainingPools = remoteConfig.remotePools.filter( + (pool) => !addressesToRemove.has(encodeRemoteAddress(pool).toLowerCase()), + ) + + if (remainingPools.length === remoteConfig.remotePools.length) { + throw new CCTParamsInvalidError( + this.name, + 'remotePoolAddresses', + 'none of the specified pool addresses were found in the current chain config', + ) + } + if (remainingPools.length === 0) { + throw new CCTParamsInvalidError( + this.name, + 'remotePoolAddresses', + 'cannot remove all pool addresses — use deleteChainConfig instead to remove the entire chain config', + ) + } + + const { instructions, poolProgramId } = await buildApplyChainUpdatesInstructions(chain, { + operation: this.name, + poolAddress: opts.poolAddress, + authority, + remoteChainSelectorsToRemove: [opts.remoteChainSelector], + chainsToAdd: [ + { + remoteChainSelector: opts.remoteChainSelector, + remotePoolAddresses: remainingPools, + remoteTokenAddress: remoteConfig.remoteToken, + outboundRateLimiterConfig: toRateLimiterConfig(remoteConfig.outboundRateLimiterState), + inboundRateLimiterConfig: toRateLimiterConfig(remoteConfig.inboundRateLimiterState), + }, + ], + }) + + chain.logger.debug( + `${this.name}: pool = ${opts.poolAddress}, remaining = ${remainingPools.length}, poolProgram = ${poolProgramId.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions, mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/pool/operations/set-chain-rate-limiter-config.test.ts b/ccip-sdk/src/cct/solana/pool/operations/set-chain-rate-limiter-config.test.ts new file mode 100644 index 00000000..e9244652 --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/set-chain-rate-limiter-config.test.ts @@ -0,0 +1,139 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { PublicKey } from '@solana/web3.js' +import BN from 'bn.js' + +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { createTokenPoolProgram } from '../../programs/token-pool.ts' +import type { ChainRateLimiterConfig } from './set-chain-rate-limiter-config.ts' +import { SetChainRateLimiterConfig } from './set-chain-rate-limiter-config.ts' +import { + AUTHORITY, + MINT, + PAYER, + POOL_PROGRAM, + POOL_STATE, + SELECTOR, + anchorDiscriminator, + chainConfigPda, + statePda, + stubChain, +} from './test-helpers.ts' + +const SELECTOR_2 = 14767482510784806043n + +const config: ChainRateLimiterConfig = { + remoteChainSelector: SELECTOR, + outboundRateLimiterConfig: { isEnabled: true, capacity: '1000', rate: '5' }, + inboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, +} + +describe('Solana token-pool setChainRateLimiterConfig', () => { + it('builds an instruction that matches a direct anchor build', async () => { + const chain = stubChain() + const unsigned = await new SetChainRateLimiterConfig().generate(chain, { + poolAddress: POOL_STATE.toBase58(), + chainConfigs: [config], + payer: PAYER, + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + const [ix] = unsigned.instructions + assert.ok(ix) + + assert.equal(ix.programId.toBase58(), POOL_PROGRAM.toBase58()) + assert.equal( + ix.data.subarray(0, 8).toString('hex'), + anchorDiscriminator('set_chain_rate_limit').toString('hex'), + ) + + const ref = await createTokenPoolProgram(chain, POOL_PROGRAM, new PublicKey(PAYER)) + .methods.setChainRateLimit( + new BN(SELECTOR.toString()), + MINT, + { enabled: false, capacity: new BN(0), rate: new BN(0) }, + { enabled: true, capacity: new BN(1000), rate: new BN(5) }, + ) + .accountsStrict({ + state: statePda(), + chainConfig: chainConfigPda(), + authority: new PublicKey(PAYER), + }) + .instruction() + + assert.equal(ix.data.toString('hex'), ref.data.toString('hex')) + assert.deepEqual( + ix.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ref.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ) + }) + + it('emits one instruction per chain config, targeting each chainConfig PDA', async () => { + const unsigned = await new SetChainRateLimiterConfig().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + chainConfigs: [config, { ...config, remoteChainSelector: SELECTOR_2 }], + payer: PAYER, + }) + + assert.equal(unsigned.instructions.length, 2) + assert.equal( + unsigned.instructions[0]!.keys[1]!.pubkey.toBase58(), + chainConfigPda(SELECTOR).toBase58(), + ) + assert.equal( + unsigned.instructions[1]!.keys[1]!.pubkey.toBase58(), + chainConfigPda(SELECTOR_2).toBase58(), + ) + }) + + it('uses caller-provided authority for the signer account', async () => { + const unsigned = await new SetChainRateLimiterConfig().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + chainConfigs: [config], + payer: PAYER, + authority: AUTHORITY, + }) + const authKey = unsigned.instructions[0]!.keys.find((k) => k.isSigner) + assert.equal(authKey!.pubkey.toBase58(), AUTHORITY) + }) + + it('rejects an empty chainConfigs list before RPC', async () => { + await assert.rejects( + () => + new SetChainRateLimiterConfig().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + chainConfigs: [], + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) + + it('rejects a zero remoteChainSelector before RPC', async () => { + await assert.rejects( + () => + new SetChainRateLimiterConfig().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + chainConfigs: [{ ...config, remoteChainSelector: 0n }], + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) + + it('rejects an invalid poolAddress before RPC', async () => { + await assert.rejects( + () => + new SetChainRateLimiterConfig().generate(stubChain(), { + poolAddress: 'not-a-key', + chainConfigs: [config], + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/pool/operations/set-chain-rate-limiter-config.ts b/ccip-sdk/src/cct/solana/pool/operations/set-chain-rate-limiter-config.ts new file mode 100644 index 00000000..b0f39bd8 --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/set-chain-rate-limiter-config.ts @@ -0,0 +1,123 @@ +import { type TransactionInstruction, PublicKey } from '@solana/web3.js' +import BN from 'bn.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { createTokenPoolProgram, deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' +import { validatePublicKey } from '../../validate.ts' +import { + type RateLimiterConfig, + deriveTokenPoolChainConfigPda, + discoverPoolInfo, +} from './common.ts' + +/** Rate limiter update for a single already-configured remote chain. */ +export type ChainRateLimiterConfig = { + /** Remote chain selector (must already be configured on the pool). */ + remoteChainSelector: bigint + /** Outbound rate limiter (local to remote). */ + outboundRateLimiterConfig: RateLimiterConfig + /** Inbound rate limiter (remote to local). */ + inboundRateLimiterConfig: RateLimiterConfig +} + +/** Parameters shared by token-pool `setChainRateLimiterConfig` generation and execution. */ +type SetChainRateLimiterConfigParams = { + /** Local pool state (config PDA) address. */ + poolAddress: string + /** Rate limiter updates, one per remote chain (at least one required). */ + chainConfigs: ChainRateLimiterConfig[] + /** Pool authority (owner or rate-limit admin). Defaults to `payer`. */ + authority?: string +} + +/** Parameters for unsigned token-pool `setChainRateLimiterConfig` generation. */ +export type GenerateSetChainRateLimiterConfigParams = + SolanaGenerateParams + +/** Unsigned token-pool `setChainRateLimiterConfig` result. */ +export type GenerateSetChainRateLimiterConfigResult = UnsignedSolanaTx + +/** Parameters for executing token-pool `setChainRateLimiterConfig`. */ +export type ExecuteSetChainRateLimiterConfigParams = + SolanaExecuteParams + +/** Result of executing token-pool `setChainRateLimiterConfig`. */ +export type ExecuteSetChainRateLimiterConfigResult = TransactionHash + +/** Updates rate limiter configurations for already-configured remote chains on a token pool. */ +export class SetChainRateLimiterConfig extends SolanaOperation { + readonly name = 'setChainRateLimiterConfig' + + /** Validates addresses and rate limiter selectors before any RPC. */ + protected validate(params: GenerateSetChainRateLimiterConfigParams): void { + validatePublicKey(this.name, 'poolAddress', params.poolAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + if (params.chainConfigs.length === 0) { + throw new CCTParamsInvalidError(this.name, 'chainConfigs', 'must have at least one config') + } + for (const [i, config] of params.chainConfigs.entries()) { + if (config.remoteChainSelector == null || config.remoteChainSelector === 0n) { + throw new CCTParamsInvalidError( + this.name, + `chainConfigs[${i}].remoteChainSelector`, + 'must be non-zero', + ) + } + } + } + + /** Builds one `setChainRateLimit` instruction per remote chain config. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateSetChainRateLimiterConfigParams, + ): Promise { + const authority = new PublicKey(opts.authority ?? opts.payer) + const { poolProgramId, mint } = await discoverPoolInfo(chain, opts.poolAddress) + + const state = deriveTokenPoolConfigPda(poolProgramId, mint) + const program = createTokenPoolProgram(chain, poolProgramId, authority) + const instructions: TransactionInstruction[] = [] + + for (const config of opts.chainConfigs) { + const chainConfig = deriveTokenPoolChainConfigPda( + poolProgramId, + config.remoteChainSelector, + mint, + ) + instructions.push( + await program.methods + .setChainRateLimit( + new BN(config.remoteChainSelector.toString()), + mint, + { + enabled: config.inboundRateLimiterConfig.isEnabled, + capacity: new BN(config.inboundRateLimiterConfig.capacity), + rate: new BN(config.inboundRateLimiterConfig.rate), + }, + { + enabled: config.outboundRateLimiterConfig.isEnabled, + capacity: new BN(config.outboundRateLimiterConfig.capacity), + rate: new BN(config.outboundRateLimiterConfig.rate), + }, + ) + .accountsStrict({ state, chainConfig, authority }) + .instruction(), + ) + } + + chain.logger.debug( + `${this.name}: pool = ${opts.poolAddress}, poolProgram = ${poolProgramId.toBase58()}, instructions = ${instructions.length}`, + ) + return { family: ChainFamily.Solana, instructions, mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/pool/operations/set-rate-limit-admin.test.ts b/ccip-sdk/src/cct/solana/pool/operations/set-rate-limit-admin.test.ts new file mode 100644 index 00000000..4abebbe7 --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/set-rate-limit-admin.test.ts @@ -0,0 +1,90 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { createTokenPoolProgram } from '../../programs/token-pool.ts' +import { SetRateLimitAdmin } from './set-rate-limit-admin.ts' +import { + AUTHORITY, + MINT, + PAYER, + POOL_PROGRAM, + POOL_STATE, + anchorDiscriminator, + statePda, + stubChain, +} from './test-helpers.ts' + +const RATE_LIMIT_ADMIN = Keypair.generate().publicKey.toBase58() + +describe('Solana token-pool setRateLimitAdmin', () => { + it('builds an instruction that matches a direct anchor build', async () => { + const chain = stubChain() + const unsigned = await new SetRateLimitAdmin().generate(chain, { + poolAddress: POOL_STATE.toBase58(), + rateLimitAdmin: RATE_LIMIT_ADMIN, + payer: PAYER, + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + const [ix] = unsigned.instructions + assert.ok(ix) + + assert.equal(ix.programId.toBase58(), POOL_PROGRAM.toBase58()) + assert.equal( + ix.data.subarray(0, 8).toString('hex'), + anchorDiscriminator('set_rate_limit_admin').toString('hex'), + ) + + const ref = await createTokenPoolProgram(chain, POOL_PROGRAM, new PublicKey(PAYER)) + .methods.setRateLimitAdmin(MINT, new PublicKey(RATE_LIMIT_ADMIN)) + .accountsStrict({ state: statePda(), authority: new PublicKey(PAYER) }) + .instruction() + + assert.equal(ix.data.toString('hex'), ref.data.toString('hex')) + assert.deepEqual( + ix.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ref.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ) + }) + + it('uses caller-provided authority for the signer account', async () => { + const unsigned = await new SetRateLimitAdmin().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + rateLimitAdmin: RATE_LIMIT_ADMIN, + payer: PAYER, + authority: AUTHORITY, + }) + const authKey = unsigned.instructions[0]!.keys.find((k) => k.isSigner) + assert.equal(authKey!.pubkey.toBase58(), AUTHORITY) + }) + + it('rejects an invalid rateLimitAdmin before RPC', async () => { + await assert.rejects( + () => + new SetRateLimitAdmin().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + rateLimitAdmin: 'not-a-key', + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) + + it('rejects an invalid poolAddress before RPC', async () => { + await assert.rejects( + () => + new SetRateLimitAdmin().generate(stubChain(), { + poolAddress: 'not-a-key', + rateLimitAdmin: RATE_LIMIT_ADMIN, + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/pool/operations/set-rate-limit-admin.ts b/ccip-sdk/src/cct/solana/pool/operations/set-rate-limit-admin.ts new file mode 100644 index 00000000..f503e528 --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/set-rate-limit-admin.ts @@ -0,0 +1,72 @@ +import { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { createTokenPoolProgram, deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' +import { validatePublicKey } from '../../validate.ts' +import { discoverPoolInfo } from './common.ts' + +/** Parameters shared by token-pool `setRateLimitAdmin` generation and execution. */ +type SetRateLimitAdminParams = { + /** Local pool state (config PDA) address. */ + poolAddress: string + /** New rate limit admin address (base58). */ + rateLimitAdmin: string + /** Pool authority (current owner). Defaults to `payer`. */ + authority?: string +} + +/** Parameters for unsigned token-pool `setRateLimitAdmin` generation. */ +export type GenerateSetRateLimitAdminParams = SolanaGenerateParams + +/** Unsigned token-pool `setRateLimitAdmin` result. */ +export type GenerateSetRateLimitAdminResult = UnsignedSolanaTx + +/** Parameters for executing token-pool `setRateLimitAdmin`. */ +export type ExecuteSetRateLimitAdminParams = SolanaExecuteParams + +/** Result of executing token-pool `setRateLimitAdmin`. */ +export type ExecuteSetRateLimitAdminResult = TransactionHash + +/** Delegates rate-limit management on a token pool to a separate admin address. */ +export class SetRateLimitAdmin extends SolanaOperation { + readonly name = 'setRateLimitAdmin' + + /** Validates addresses before any RPC. */ + protected validate(params: GenerateSetRateLimitAdminParams): void { + validatePublicKey(this.name, 'poolAddress', params.poolAddress) + validatePublicKey(this.name, 'payer', params.payer) + validatePublicKey(this.name, 'rateLimitAdmin', params.rateLimitAdmin) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + } + + /** Builds the unsigned token-pool `setRateLimitAdmin` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateSetRateLimitAdminParams, + ): Promise { + const authority = new PublicKey(opts.authority ?? opts.payer) + const newRateLimitAdmin = new PublicKey(opts.rateLimitAdmin) + const { poolProgramId, mint } = await discoverPoolInfo(chain, opts.poolAddress) + + const state = deriveTokenPoolConfigPda(poolProgramId, mint) + const program = createTokenPoolProgram(chain, poolProgramId, authority) + + const instruction = await program.methods + .setRateLimitAdmin(mint, newRateLimitAdmin) + .accountsStrict({ state, authority }) + .instruction() + + chain.logger.debug( + `${this.name}: pool = ${opts.poolAddress}, admin = ${opts.rateLimitAdmin}, poolProgram = ${poolProgramId.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/pool/operations/test-helpers.ts b/ccip-sdk/src/cct/solana/pool/operations/test-helpers.ts new file mode 100644 index 00000000..8e6f3c02 --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/test-helpers.ts @@ -0,0 +1,63 @@ +import { Buffer } from 'buffer' + +import { Keypair, PublicKey } from '@solana/web3.js' +import { sha256, toUtf8Bytes } from 'ethers' + +import type { SolanaChain } from '../../../../solana/index.ts' + +/** Deterministic-ish test pubkeys. */ +export const POOL_PROGRAM = Keypair.generate().publicKey +export const POOL_STATE = Keypair.generate().publicKey +export const MINT = Keypair.generate().publicKey +export const PAYER = Keypair.generate().publicKey.toBase58() +export const AUTHORITY = Keypair.generate().publicKey.toBase58() +export const SELECTOR = 16015286601757825753n + +/** Anchor global-namespace discriminator: sha256('global:')[0..8]. */ +export function anchorDiscriminator(name: string): Buffer { + return Buffer.from(sha256(toUtf8Bytes(`global:${name}`)).slice(2, 18), 'hex') +} + +/** Independently derives the pool state (config) PDA. */ +export function statePda(mint = MINT, poolProgram = POOL_PROGRAM): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('ccip_tokenpool_config'), mint.toBuffer()], + poolProgram, + )[0] +} + +/** Independently derives a per-chain config PDA. */ +export function chainConfigPda( + selector = SELECTOR, + mint = MINT, + poolProgram = POOL_PROGRAM, +): PublicKey { + const sel = Buffer.alloc(8) + sel.writeBigUInt64LE(selector) + return PublicKey.findProgramAddressSync( + [Buffer.from('ccip_tokenpool_chainconfig'), sel, mint.toBuffer()], + poolProgram, + )[0] +} + +type StubOptions = { + /** Whether chain-config PDAs report as already existing (for applyChainUpdates idempotency). */ + chainConfigExists?: boolean + /** Optional override for getTokenPoolRemotes. */ + remotes?: Record +} + +/** Builds a minimal SolanaChain stub sufficient for `.instruction()` building. */ +export function stubChain(opts: StubOptions = {}): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async (key: PublicKey) => { + if (key.equals(POOL_STATE)) return { owner: POOL_PROGRAM, data: Buffer.alloc(0) } + return opts.chainConfigExists ? { owner: POOL_PROGRAM, data: Buffer.alloc(0) } : null + }, + }, + getTokenForTokenPool: async () => MINT.toBase58(), + getTokenPoolRemotes: async () => opts.remotes ?? {}, + } as unknown as SolanaChain +} diff --git a/ccip-sdk/src/cct/solana/pool/operations/transfer-ownership.test.ts b/ccip-sdk/src/cct/solana/pool/operations/transfer-ownership.test.ts new file mode 100644 index 00000000..98ee63b3 --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/transfer-ownership.test.ts @@ -0,0 +1,90 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { createTokenPoolProgram } from '../../programs/token-pool.ts' +import { TransferOwnership } from './transfer-ownership.ts' +import { + AUTHORITY, + MINT, + PAYER, + POOL_PROGRAM, + POOL_STATE, + anchorDiscriminator, + statePda, + stubChain, +} from './test-helpers.ts' + +const NEW_OWNER = Keypair.generate().publicKey.toBase58() + +describe('Solana token-pool transferOwnership', () => { + it('builds an instruction that matches a direct anchor build', async () => { + const chain = stubChain() + const unsigned = await new TransferOwnership().generate(chain, { + poolAddress: POOL_STATE.toBase58(), + newOwner: NEW_OWNER, + payer: PAYER, + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + const [ix] = unsigned.instructions + assert.ok(ix) + + assert.equal(ix.programId.toBase58(), POOL_PROGRAM.toBase58()) + assert.equal( + ix.data.subarray(0, 8).toString('hex'), + anchorDiscriminator('transfer_ownership').toString('hex'), + ) + + const ref = await createTokenPoolProgram(chain, POOL_PROGRAM, new PublicKey(PAYER)) + .methods.transferOwnership(new PublicKey(NEW_OWNER)) + .accountsStrict({ state: statePda(), mint: MINT, authority: new PublicKey(PAYER) }) + .instruction() + + assert.equal(ix.data.toString('hex'), ref.data.toString('hex')) + assert.deepEqual( + ix.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ref.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ) + }) + + it('uses caller-provided authority for the signer account', async () => { + const unsigned = await new TransferOwnership().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + newOwner: NEW_OWNER, + payer: PAYER, + authority: AUTHORITY, + }) + const authKey = unsigned.instructions[0]!.keys.find((k) => k.isSigner) + assert.equal(authKey!.pubkey.toBase58(), AUTHORITY) + }) + + it('rejects an invalid newOwner before RPC', async () => { + await assert.rejects( + () => + new TransferOwnership().generate(stubChain(), { + poolAddress: POOL_STATE.toBase58(), + newOwner: 'not-a-key', + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) + + it('rejects an invalid poolAddress before RPC', async () => { + await assert.rejects( + () => + new TransferOwnership().generate(stubChain(), { + poolAddress: 'not-a-key', + newOwner: NEW_OWNER, + payer: PAYER, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/pool/operations/transfer-ownership.ts b/ccip-sdk/src/cct/solana/pool/operations/transfer-ownership.ts new file mode 100644 index 00000000..459931fd --- /dev/null +++ b/ccip-sdk/src/cct/solana/pool/operations/transfer-ownership.ts @@ -0,0 +1,72 @@ +import { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { createTokenPoolProgram, deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' +import { validatePublicKey } from '../../validate.ts' +import { discoverPoolInfo } from './common.ts' + +/** Parameters shared by token-pool `transferOwnership` generation and execution. */ +type TransferOwnershipParams = { + /** Local pool state (config PDA) address. */ + poolAddress: string + /** Proposed new owner address (base58). */ + newOwner: string + /** Pool authority (current owner). Defaults to `payer`. */ + authority?: string +} + +/** Parameters for unsigned token-pool `transferOwnership` generation. */ +export type GenerateTransferOwnershipParams = SolanaGenerateParams + +/** Unsigned token-pool `transferOwnership` result. */ +export type GenerateTransferOwnershipResult = UnsignedSolanaTx + +/** Parameters for executing token-pool `transferOwnership`. */ +export type ExecuteTransferOwnershipParams = SolanaExecuteParams + +/** Result of executing token-pool `transferOwnership`. */ +export type ExecuteTransferOwnershipResult = TransactionHash + +/** Proposes a new owner for a token pool (step 1 of the 2-step ownership transfer). */ +export class TransferOwnership extends SolanaOperation { + readonly name = 'transferOwnership' + + /** Validates addresses before any RPC. */ + protected validate(params: GenerateTransferOwnershipParams): void { + validatePublicKey(this.name, 'poolAddress', params.poolAddress) + validatePublicKey(this.name, 'payer', params.payer) + validatePublicKey(this.name, 'newOwner', params.newOwner) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + } + + /** Builds the unsigned token-pool `transferOwnership` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateTransferOwnershipParams, + ): Promise { + const authority = new PublicKey(opts.authority ?? opts.payer) + const proposedOwner = new PublicKey(opts.newOwner) + const { poolProgramId, mint } = await discoverPoolInfo(chain, opts.poolAddress) + + const state = deriveTokenPoolConfigPda(poolProgramId, mint) + const program = createTokenPoolProgram(chain, poolProgramId, authority) + + const instruction = await program.methods + .transferOwnership(proposedOwner) + .accountsStrict({ state, mint, authority }) + .instruction() + + chain.logger.debug( + `${this.name}: pool = ${opts.poolAddress}, newOwner = ${opts.newOwner}, poolProgram = ${poolProgramId.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/programs/alt.ts b/ccip-sdk/src/cct/solana/programs/alt.ts new file mode 100644 index 00000000..debd0a8d --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/alt.ts @@ -0,0 +1,102 @@ +import { Buffer } from 'buffer' + +import { getAssociatedTokenAddressSync } from '@solana/spl-token' +import { + AddressLookupTableProgram, + PublicKey, + SystemProgram, + TransactionInstruction, +} from '@solana/web3.js' + +import { deriveFeeBillingTokenConfigPda } from './fee-quoter.ts' +import { deriveExternalTokenPoolsSignerPda, deriveTokenAdminRegistryPda } from './router.ts' +import { deriveTokenPoolConfigPda, deriveTokenPoolSignerPda } from './token-pool.ts' +import type { SolanaChain } from '../../../solana/index.ts' +import { resolveATA } from '../../../solana/utils.ts' + +const CREATE_LOOKUP_TABLE_DISCRIMINATOR = 0 +const CREATE_LOOKUP_TABLE_DATA_LENGTH = 13 + +type DeriveCcipLookupTableAddressesParams = { + lookupTableAddress: PublicKey + tokenMint: PublicKey + poolProgram: PublicKey + authority: PublicKey +} + +type BuildCreateLookupTableInstructionParams = { + authority: PublicKey + payer: PublicKey + recentSlot: number | bigint +} + +type BuildCreateLookupTableInstructionResult = { + instruction: TransactionInstruction + lookupTableAddress: PublicKey +} + +/** Builds an ALT create instruction without requiring the authority signature. */ +export function buildCreateLookupTableInstruction({ + authority, + payer, + recentSlot, +}: BuildCreateLookupTableInstructionParams): BuildCreateLookupTableInstructionResult { + const recentSlotBigInt = BigInt(recentSlot) + const recentSlotBuffer = Buffer.alloc(8) + recentSlotBuffer.writeBigUInt64LE(recentSlotBigInt) + + const [lookupTableAddress, bump] = PublicKey.findProgramAddressSync( + [authority.toBuffer(), recentSlotBuffer], + AddressLookupTableProgram.programId, + ) + + const data = Buffer.alloc(CREATE_LOOKUP_TABLE_DATA_LENGTH) + data.writeUInt32LE(CREATE_LOOKUP_TABLE_DISCRIMINATOR, 0) + data.writeBigUInt64LE(recentSlotBigInt, 4) + data.writeUInt8(bump, 12) + + return { + lookupTableAddress, + instruction: new TransactionInstruction({ + programId: AddressLookupTableProgram.programId, + keys: [ + { pubkey: lookupTableAddress, isSigner: false, isWritable: true }, + { pubkey: authority, isSigner: false, isWritable: false }, + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + data, + }), + } +} + +/** Derives the standard CCIP token pool addresses stored in a pool lookup table. */ +export async function deriveCcipLookupTableAddresses( + chain: SolanaChain, + { lookupTableAddress, tokenMint, poolProgram, authority }: DeriveCcipLookupTableAddressesParams, +): Promise { + const { tokenProgram } = await resolveATA(chain.connection, tokenMint, authority) + const poolConfig = deriveTokenPoolConfigPda(poolProgram, tokenMint) + const { router: routerAddress } = await chain.getTokenPoolConfig(poolConfig.toBase58()) + const router = new PublicKey(routerAddress) + const { feeQuoter } = await chain._getRouterConfig(routerAddress) + + const tokenAdminRegistry = deriveTokenAdminRegistryPda(router, tokenMint) + const poolSigner = deriveTokenPoolSignerPda(poolProgram, tokenMint) + const poolTokenAta = getAssociatedTokenAddressSync(tokenMint, poolSigner, true, tokenProgram) + const feeTokenConfig = deriveFeeBillingTokenConfigPda(feeQuoter, tokenMint) + const routerPoolSigner = deriveExternalTokenPoolsSignerPda(router, poolProgram) + + return [ + lookupTableAddress, + tokenAdminRegistry, + poolProgram, + poolConfig, + poolTokenAta, + poolSigner, + tokenProgram, + tokenMint, + feeTokenConfig, + routerPoolSigner, + ] +} diff --git a/ccip-sdk/src/cct/solana/programs/fee-quoter.ts b/ccip-sdk/src/cct/solana/programs/fee-quoter.ts new file mode 100644 index 00000000..71b1dd8a --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/fee-quoter.ts @@ -0,0 +1,11 @@ +import { Buffer } from 'buffer' + +import { PublicKey } from '@solana/web3.js' + +/** Derives the FeeQuoter billing token config PDA for a mint. */ +export function deriveFeeBillingTokenConfigPda(feeQuoter: PublicKey, mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('fee_billing_token_config'), mint.toBuffer()], + feeQuoter, + )[0] +} diff --git a/ccip-sdk/src/cct/solana/programs/router.ts b/ccip-sdk/src/cct/solana/programs/router.ts new file mode 100644 index 00000000..7df7abc3 --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/router.ts @@ -0,0 +1,37 @@ +import { Buffer } from 'buffer' + +import { Program } from '@coral-xyz/anchor' +import { PublicKey } from '@solana/web3.js' + +import { IDL as CCIP_ROUTER_IDL } from '../../../solana/idl/1.6.0/CCIP_ROUTER.ts' +import type { SolanaChain } from '../../../solana/index.ts' +import { simulationProvider } from '../../../solana/utils.ts' + +/** Creates an Anchor Program client for the CCIP Router program. */ +export function createRouterProgram(chain: SolanaChain, router: PublicKey, payer: PublicKey) { + return new Program(CCIP_ROUTER_IDL, router, simulationProvider(chain, payer)) +} + +/** Derives the Router config PDA. */ +export function deriveRouterConfigPda(router: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync([Buffer.from('config')], router)[0] +} + +/** Derives the Router token admin registry PDA for a mint. */ +export function deriveTokenAdminRegistryPda(router: PublicKey, mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('token_admin_registry'), mint.toBuffer()], + router, + )[0] +} + +/** Derives the Router external token pools signer PDA for a pool program. */ +export function deriveExternalTokenPoolsSignerPda( + router: PublicKey, + poolProgram: PublicKey, +): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('external_token_pools_signer'), poolProgram.toBuffer()], + router, + )[0] +} diff --git a/ccip-sdk/src/cct/solana/programs/token-pool.ts b/ccip-sdk/src/cct/solana/programs/token-pool.ts new file mode 100644 index 00000000..a351296b --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/token-pool.ts @@ -0,0 +1,52 @@ +import { Buffer } from 'buffer' + +import { Program } from '@coral-xyz/anchor' +import { PublicKey } from '@solana/web3.js' + +import { IDL as BASE_TOKEN_POOL_IDL } from '../../../solana/idl/1.6.0/BASE_TOKEN_POOL.ts' +import { IDL as BURN_MINT_TOKEN_POOL_IDL } from '../../../solana/idl/1.6.0/BURN_MINT_TOKEN_POOL.ts' +import type { SolanaChain } from '../../../solana/index.ts' +import { simulationProvider } from '../../../solana/utils.ts' + +const TOKEN_POOL_IDL = { + ...BURN_MINT_TOKEN_POOL_IDL, + types: BASE_TOKEN_POOL_IDL.types, +} + +/** Creates an Anchor Program client for a token pool program. */ +export function createTokenPoolProgram( + chain: SolanaChain, + poolProgram: PublicKey, + payer: PublicKey, +) { + return new Program(TOKEN_POOL_IDL, poolProgram, simulationProvider(chain, payer)) +} + +/** Derives the token pool global config PDA. */ +export function deriveTokenPoolGlobalConfigPda(poolProgram: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync([Buffer.from('config')], poolProgram)[0] +} + +/** Derives a token pool state/config PDA for a mint. */ +export function deriveTokenPoolConfigPda(poolProgram: PublicKey, mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('ccip_tokenpool_config'), mint.toBuffer()], + poolProgram, + )[0] +} + +/** Derives a token pool signer PDA for a mint. */ +export function deriveTokenPoolSignerPda(poolProgram: PublicKey, mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('ccip_tokenpool_signer'), mint.toBuffer()], + poolProgram, + )[0] +} + +/** Derives the token pool program data PDA. */ +export function deriveTokenPoolProgramDataPda(poolProgram: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [poolProgram.toBuffer()], + new PublicKey('BPFLoaderUpgradeab1e11111111111111111111111'), + )[0] +} diff --git a/ccip-sdk/src/cct/solana/serialize.test.ts b/ccip-sdk/src/cct/solana/serialize.test.ts new file mode 100644 index 00000000..97d78cb7 --- /dev/null +++ b/ccip-sdk/src/cct/solana/serialize.test.ts @@ -0,0 +1,56 @@ +import { Buffer } from 'buffer' +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Message, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js' +import bs58 from 'bs58' + +import { serializeUnsignedSolanaTx } from './serialize.ts' +import { CCTParamsInvalidError } from '../errors.ts' + +const KEY = PublicKey.default +const connection = { + getLatestBlockhash: async () => ({ blockhash: KEY.toBase58(), lastValidBlockHeight: 0 }), +} +const unsigned = { + instructions: [ + new TransactionInstruction({ + programId: SystemProgram.programId, + keys: [], + data: Buffer.alloc(0), + }), + ], +} + +describe('cct/solana serialize', () => { + it('serializes unsigned Solana txs as legacy messages in supported encodings', async () => { + const base58 = await serializeUnsignedSolanaTx(connection, unsigned, KEY) + const base64 = await serializeUnsignedSolanaTx(connection, unsigned, KEY, 'base64') + const hex = await serializeUnsignedSolanaTx(connection, unsigned, KEY, 'hex') + + assert.ok(Message.from(bs58.decode(base58))) + assert.ok(Message.from(Buffer.from(base64, 'base64'))) + assert.ok(Message.from(Buffer.from(hex, 'hex'))) + }) + + it('rejects lookup tables for legacy message serialization', async () => { + await assert.rejects( + () => + serializeUnsignedSolanaTx(connection, { ...unsigned, lookupTables: [{} as never] }, KEY), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'serializeUnsignedTx' && + err.context.param === 'lookupTables', + ) + }) + + it('rejects unsupported transaction encodings', async () => { + await assert.rejects( + () => serializeUnsignedSolanaTx(connection, unsigned, KEY, 'base32'), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'serializeUnsignedTx' && + err.context.param === 'encoding', + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/serialize.ts b/ccip-sdk/src/cct/solana/serialize.ts new file mode 100644 index 00000000..3bb81125 --- /dev/null +++ b/ccip-sdk/src/cct/solana/serialize.ts @@ -0,0 +1,48 @@ +import { Buffer } from 'buffer' + +import { PublicKey, TransactionMessage } from '@solana/web3.js' +import bs58 from 'bs58' + +import type { UnsignedSolanaTx } from '../../solana/types.ts' +import { CCTParamsInvalidError } from '../errors.ts' + +/** Supported serialized transaction encodings. */ +export type SerializedSolanaTxEncoding = 'base58' | 'base64' | 'hex' + +/** Serializes an unsigned Solana tx into one legacy message for external signing. */ +export async function serializeUnsignedSolanaTx( + connection: { getLatestBlockhash: () => Promise<{ blockhash: string }> }, + unsigned: Pick, + payer: PublicKey | string, + encoding = 'base58', +): Promise { + if (unsigned.lookupTables?.length) { + throw new CCTParamsInvalidError( + 'serializeUnsignedTx', + 'lookupTables', + 'legacy-message serialization does not support address lookup tables', + ) + } + + const payerKey = typeof payer === 'string' ? new PublicKey(payer) : payer + const { blockhash } = await connection.getLatestBlockhash() + const serialized = Buffer.from( + new TransactionMessage({ + payerKey, + recentBlockhash: blockhash, + instructions: unsigned.instructions, + }) + .compileToLegacyMessage() + .serialize(), + ) + + if (encoding === 'base58') return bs58.encode(serialized) + if (encoding === 'base64') return serialized.toString('base64') + if (encoding === 'hex') return serialized.toString('hex') + + throw new CCTParamsInvalidError( + 'serializeUnsignedTx', + 'encoding', + `unsupported Solana transaction encoding: ${String(encoding)}`, + ) +} diff --git a/ccip-sdk/src/cct/solana/submit.test.ts b/ccip-sdk/src/cct/solana/submit.test.ts new file mode 100644 index 00000000..703e9515 --- /dev/null +++ b/ccip-sdk/src/cct/solana/submit.test.ts @@ -0,0 +1,74 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { SendTransactionError, TransactionExpiredTimeoutError } from '@solana/web3.js' + +import { CCTTxFailedError, CCTTxNotConfirmedError } from '../errors.ts' +import { createCCTSubmitError } from './submit.ts' + +const OP = 'setPool' + +describe('cct/solana submit error mapping', () => { + it('maps post-broadcast confirmation errors with a signature to not-confirmed', () => { + const cause = Object.assign(new Error('transaction was not confirmed'), { signature: 'abc' }) + const err = createCCTSubmitError(OP, cause) + + assert.ok(err instanceof CCTTxNotConfirmedError) + assert.equal(err.isTransient, true) + assert.equal(err.context.txHash, 'abc') + }) + + it('maps web3.js transaction expiry errors to not-confirmed', () => { + const err = createCCTSubmitError(OP, new TransactionExpiredTimeoutError('def', 30)) + + assert.ok(err instanceof CCTTxNotConfirmedError) + assert.equal(err.context.txHash, 'def') + }) + + it('maps SendTransactionError with a signature to not-confirmed', () => { + const cause = new SendTransactionError({ + action: 'send', + signature: 'ghi', + transactionMessage: 'block height exceeded', + }) + const err = createCCTSubmitError(OP, cause) + + assert.ok(err instanceof CCTTxNotConfirmedError) + assert.equal(err.context.txHash, 'ghi') + }) + + it('maps signed on-chain failures to permanent tx failed', () => { + const cause = Object.assign(new Error('custom program error: 0x1'), { signature: 'jkl' }) + const err = createCCTSubmitError(OP, cause) + + assert.ok(err instanceof CCTTxFailedError) + assert.equal(err.isTransient, false) + assert.equal(err.context.txHash, undefined) + }) + + it('maps SendTransactionError with an empty signature to transient tx failed', () => { + const cause = new SendTransactionError({ + action: 'simulate', + signature: '', + transactionMessage: 'blockhash not found', + }) + const err = createCCTSubmitError(OP, cause) + + assert.ok(err instanceof CCTTxFailedError) + assert.equal(err.isTransient, true) + }) + + it('maps pre-broadcast transient errors to transient tx failed', () => { + const err = createCCTSubmitError(OP, new Error('blockhash not found')) + + assert.ok(err instanceof CCTTxFailedError) + assert.equal(err.isTransient, true) + }) + + it('maps program errors to permanent tx failed', () => { + const err = createCCTSubmitError(OP, new Error('custom program error: 0x1')) + + assert.ok(err instanceof CCTTxFailedError) + assert.equal(err.isTransient, false) + }) +}) diff --git a/ccip-sdk/src/cct/solana/submit.ts b/ccip-sdk/src/cct/solana/submit.ts new file mode 100644 index 00000000..664a80f8 --- /dev/null +++ b/ccip-sdk/src/cct/solana/submit.ts @@ -0,0 +1,80 @@ +/** + * Shared sign-and-submit pipeline for Solana CCT operations. Maps simulation/program + * failures to permanent {@link CCTTxFailedError}, pre-broadcast infra failures to + * transient {@link CCTTxFailedError}, and post-broadcast confirmation failures to + * {@link CCTTxNotConfirmedError}. + * + * @packageDocumentation + */ + +import { + TransactionExpiredBlockheightExceededError, + TransactionExpiredNonceInvalidError, + TransactionExpiredTimeoutError, +} from '@solana/web3.js' + +import { CCIPWalletInvalidError, shouldRetry } from '../../errors/index.ts' +import type { SolanaChain } from '../../solana/index.ts' +import { type UnsignedSolanaTx, isWallet } from '../../solana/types.ts' +import { simulateAndSendTxs } from '../../solana/utils.ts' +import { CCTTxFailedError, CCTTxNotConfirmedError } from '../errors.ts' +import type { TransactionHash } from '../operation.ts' + +/** Signs, simulates, sends, and confirms a Solana CCT transaction. */ +export async function submit( + chain: SolanaChain, + wallet: unknown, + unsigned: UnsignedSolanaTx, + operation: string, + computeUnits?: number, +): Promise { + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + try { + return { hash: await simulateAndSendTxs(chain, wallet, unsigned, computeUnits) } + } catch (error) { + throw createCCTSubmitError(operation, error) + } +} + +/** Maps Solana submit errors to permanent failed vs transient failed/not-confirmed CCT errors. */ +export function createCCTSubmitError( + operation: string, + error: unknown, +): CCTTxFailedError | CCTTxNotConfirmedError { + const signature = getSignature(error) + if (signature && isNotConfirmedError(error)) { + return new CCTTxNotConfirmedError(operation, signature, { + cause: error instanceof Error ? error : undefined, + }) + } + + return new CCTTxFailedError(operation, getReason(error), { + cause: error instanceof Error ? error : undefined, + isTransient: isTransientSubmitError(error), + }) +} + +function isTransientSubmitError(error: unknown): boolean { + return /blockhash|expired/i.test(getReason(error)) || shouldRetry(error) +} + +function isNotConfirmedError(error: unknown): boolean { + return ( + error instanceof TransactionExpiredBlockheightExceededError || + error instanceof TransactionExpiredNonceInvalidError || + error instanceof TransactionExpiredTimeoutError || + /not confirmed|unknown if it succeeded|block height exceeded/i.test(getReason(error)) + ) +} + +function getReason(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +function getSignature(error: unknown): string | undefined { + if (!error || typeof error !== 'object' || !('signature' in error)) return undefined + return typeof error.signature === 'string' && error.signature.length > 0 + ? error.signature + : undefined +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin-role.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin-role.test.ts new file mode 100644 index 00000000..0720fddc --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin-role.test.ts @@ -0,0 +1,88 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { + createRouterProgram, + deriveRouterConfigPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' +import { AcceptAdminRole } from './accept-admin-role.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: () => assert.fail('should not RPC before validation'), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return new AcceptAdminRole().generate(stubChain(), { + tokenAddress: TOKEN, + routerAddress: ROUTER, + payer: PAYER, + ...opts, + }) +} + +describe('Solana TokenAdminRegistry acceptAdminRole', () => { + it('matches a direct acceptAdminRoleTokenAdminRegistry build (instruction parity)', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + + assert.ok(instruction) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + assert.equal(instruction.programId.toBase58(), ROUTER) + + const router = new PublicKey(ROUTER) + const mint = new PublicKey(TOKEN) + const expected = await createRouterProgram(stubChain(), router, new PublicKey(PAYER)) + .methods.acceptAdminRoleTokenAdminRegistry() + .accountsStrict({ + config: deriveRouterConfigPda(router), + tokenAdminRegistry: deriveTokenAdminRegistryPda(router, mint), + mint, + authority: new PublicKey(PAYER), + }) + .instruction() + + assert.equal(instruction.data.toString('hex'), expected.data.toString('hex')) + assert.equal(instruction.keys.length, expected.keys.length) + for (const [i, key] of expected.keys.entries()) { + assert.equal(instruction.keys[i]!.pubkey.toBase58(), key.pubkey.toBase58()) + assert.equal(instruction.keys[i]!.isSigner, key.isSigner) + assert.equal(instruction.keys[i]!.isWritable, key.isWritable) + } + }) + + it('uses caller-provided authority', async () => { + const unsigned = await generate({ authority: AUTHORITY }) + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === AUTHORITY)) + assert.ok(!unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === PAYER)) + }) + + it('defaults authority to payer', async () => { + const unsigned = await generate() + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === PAYER)) + }) + + it('rejects an invalid public key before building', async () => { + await assert.rejects( + () => generate({ routerAddress: 'not-a-pubkey' }), + (error: unknown) => error instanceof CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin-role.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin-role.ts new file mode 100644 index 00000000..13c1b42e --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin-role.ts @@ -0,0 +1,83 @@ +import { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + createRouterProgram, + deriveRouterConfigPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' +import { validatePublicKey } from '../../validate.ts' + +/** Parameters shared by Solana TokenAdminRegistry `acceptAdminRole` generation and execution. */ +type AcceptAdminRoleParams = { + tokenAddress: string + routerAddress: string + /** + * Token admin authority (the pending administrator). Defaults to `payer` for single-signer + * transactions. Multisig/Squads flows should pass the pending admin/vault authority explicitly. + */ + authority?: string +} + +/** Parameters for unsigned Solana TokenAdminRegistry `acceptAdminRole` generation. */ +export type GenerateAcceptAdminRoleParams = SolanaGenerateParams + +/** Unsigned Solana TokenAdminRegistry `acceptAdminRole` result. */ +export type GenerateAcceptAdminRoleResult = UnsignedSolanaTx + +/** Parameters for executing Solana TokenAdminRegistry `acceptAdminRole`. */ +export type ExecuteAcceptAdminRoleParams = SolanaExecuteParams + +/** Result of executing Solana TokenAdminRegistry `acceptAdminRole`. */ +export type ExecuteAcceptAdminRoleResult = TransactionHash + +/** Solana TokenAdminRegistry `acceptAdminRole` operation. */ +export class AcceptAdminRole extends SolanaOperation { + readonly name = 'acceptAdminRole' + + /** Validates all public keys before any RPC. */ + protected validate(params: GenerateAcceptAdminRoleParams): void { + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'routerAddress', params.routerAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + } + + /** Builds the unsigned Solana `acceptAdminRoleTokenAdminRegistry` instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateAcceptAdminRoleParams, + ): Promise { + const router = new PublicKey(opts.routerAddress) + const mint = new PublicKey(opts.tokenAddress) + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + + const routerProgram = createRouterProgram(chain, router, payer) + const config = deriveRouterConfigPda(router) + const tokenAdminRegistry = deriveTokenAdminRegistryPda(router, mint) + + const instruction = await routerProgram.methods + .acceptAdminRoleTokenAdminRegistry() + .accountsStrict({ + config, + tokenAdminRegistry, + mint, + authority, + }) + .instruction() + + chain.logger.debug( + `${this.name}: router = ${router.toBase58()}, token = ${mint.toBase58()}, authority = ${authority.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.test.ts new file mode 100644 index 00000000..bab78caa --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.test.ts @@ -0,0 +1,165 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { AddressLookupTableProgram, Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveCcipLookupTableAddresses } from '../../programs/alt.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const POOL_PROGRAM = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const FEE_QUOTER = Keypair.generate().publicKey +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const LOOKUP_TABLE = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function stubChain(addresses: PublicKey[] = [], authority = AUTHORITY): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID }), + getAddressLookupTable: async () => ({ + value: { + state: { + authority: new PublicKey(authority), + addresses, + }, + }, + }), + }, + getTokenPoolConfig: async () => ({ + token: TOKEN, + router: ROUTER, + tokenPoolProgram: POOL_PROGRAM, + }), + _getRouterConfig: async () => ({ feeQuoter: FEE_QUOTER }), + } as unknown as SolanaChain +} + +function generate(opts = {}, chain = stubChain()) { + return SolanaTokenManager.fromChain(chain).generateUnsignedAppendToLookupTable({ + lookupTableAddress: LOOKUP_TABLE, + payer: PAYER, + authority: AUTHORITY, + additionalAddresses: [Keypair.generate().publicKey.toBase58()], + ...opts, + }) +} + +describe('Solana TokenAdminRegistry appendToLookupTable', () => { + it('builds extend ALT instructions', async () => { + const unsigned = await generate() + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + assert.equal( + unsigned.instructions[0]!.programId.toBase58(), + AddressLookupTableProgram.programId.toBase58(), + ) + }) + + it('chunks additional addresses into multiple extend instructions', async () => { + const additionalAddresses = Array.from({ length: 31 }, () => + Keypair.generate().publicKey.toBase58(), + ) + const unsigned = await generate({ additionalAddresses }) + + assert.equal(unsigned.instructions.length, 2) + }) + + it('appends derived CCIP addresses before manual addresses', async () => { + const unsigned = await generate({ tokenAddress: TOKEN, poolProgramAddress: POOL_PROGRAM }) + + assert.equal(unsigned.instructions.length, 1) + }) + + it('rejects auto-derived CCIP addresses when the canonical block already exists', async () => { + const chain = stubChain() + const ccipAddresses = await deriveCcipLookupTableAddresses(chain, { + lookupTableAddress: new PublicKey(LOOKUP_TABLE), + tokenMint: new PublicKey(TOKEN), + poolProgram: new PublicKey(POOL_PROGRAM), + authority: new PublicKey(AUTHORITY), + }) + + await assert.rejects( + () => + generate( + { tokenAddress: TOKEN, poolProgramAddress: POOL_PROGRAM }, + stubChain(ccipAddresses), + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'lookupTableAddress', + ) + }) + + it('rejects signed append when authority is not the wallet', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).appendToLookupTable({ + lookupTableAddress: LOOKUP_TABLE, + wallet: WALLET, + authority: AUTHORITY, + additionalAddresses: [Keypair.generate().publicKey.toBase58()], + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'authority', + ) + }) + + it('rejects authority mismatch', async () => { + await assert.rejects( + () => generate({}, stubChain([], Keypair.generate().publicKey.toBase58())), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'authority', + ) + }) + + it('rejects ALTs over 256 addresses', async () => { + const currentAddresses = Array.from({ length: 256 }, () => Keypair.generate().publicKey) + + await assert.rejects( + () => generate({}, stubChain(currentAddresses)), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'additionalAddresses', + ) + }) + + it('requires at least one address source', async () => { + await assert.rejects( + () => generate({ additionalAddresses: [] }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'additionalAddresses', + ) + }) + + it('requires token and pool program together', async () => { + await assert.rejects( + () => generate({ tokenAddress: TOKEN, poolProgramAddress: undefined }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'tokenAddress', + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.ts new file mode 100644 index 00000000..075251be --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.ts @@ -0,0 +1,183 @@ +import { type TransactionInstruction, AddressLookupTableProgram, PublicKey } from '@solana/web3.js' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { type UnsignedSolanaTx, isWallet } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { deriveCcipLookupTableAddresses } from '../../programs/alt.ts' +import { submit } from '../../submit.ts' +import { validatePublicKey } from '../../validate.ts' + +const MAX_ALT_ADDRESSES = 256 +const EXTEND_CHUNK_SIZE = 30 + +/** Parameters shared by Solana TokenAdminRegistry `appendToLookupTable` generation and execution. */ +type AppendToLookupTableParams = { + lookupTableAddress: string + tokenAddress?: string + poolProgramAddress?: string + additionalAddresses?: string[] + /** ALT authority. Defaults to payer for unsigned generation and wallet public key for execute. */ + authority?: string +} + +/** Parameters for unsigned Solana lookup table append generation. */ +export type GenerateAppendToLookupTableParams = SolanaGenerateParams + +/** Unsigned append lookup table result. */ +export type GenerateAppendToLookupTableResult = UnsignedSolanaTx + +/** Parameters for executing Solana TokenAdminRegistry `appendToLookupTable`. */ +export type ExecuteAppendToLookupTableParams = SolanaExecuteParams + +/** Result of executing Solana TokenAdminRegistry `appendToLookupTable`. */ +export type ExecuteAppendToLookupTableResult = TransactionHash + +/** Builds and submits Solana ALT extend instructions for token pool setup. */ +export class AppendToLookupTable extends SolanaOperation< + AppendToLookupTableParams, + GenerateAppendToLookupTableResult, + ExecuteAppendToLookupTableResult +> { + readonly name = 'appendToLookupTable' + + /** Validates all public keys before any RPC. */ + protected validate(params: GenerateAppendToLookupTableParams): void { + validatePublicKey(this.name, 'lookupTableAddress', params.lookupTableAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + if (params.tokenAddress) validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + if (params.poolProgramAddress) { + validatePublicKey(this.name, 'poolProgramAddress', params.poolProgramAddress) + } + for (const [i, address] of (params.additionalAddresses ?? []).entries()) { + validatePublicKey(this.name, `additionalAddresses[${i}]`, address) + } + + if (Boolean(params.tokenAddress) !== Boolean(params.poolProgramAddress)) { + throw new CCTParamsInvalidError( + this.name, + 'tokenAddress', + 'tokenAddress and poolProgramAddress must be provided together', + ) + } + if (!params.tokenAddress && !params.additionalAddresses?.length) { + throw new CCTParamsInvalidError( + this.name, + 'additionalAddresses', + 'must provide tokenAddress/poolProgramAddress or additionalAddresses', + ) + } + } + + /** Builds unsigned ALT extend instructions. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateAppendToLookupTableParams, + ): Promise { + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + const lookupTableAddress = new PublicKey(opts.lookupTableAddress) + const lookupTable = await chain.connection.getAddressLookupTable(lookupTableAddress) + + if (!lookupTable.value) { + throw new CCTParamsInvalidError( + this.name, + 'lookupTableAddress', + `lookup table not found: ${lookupTableAddress.toBase58()}`, + ) + } + + if (!lookupTable.value.state.authority?.equals(authority)) { + throw new CCTParamsInvalidError( + this.name, + 'authority', + `authority mismatch; ALT authority is ${lookupTable.value.state.authority?.toBase58() ?? 'none'}`, + ) + } + + const addresses = [...(opts.additionalAddresses ?? []).map((a) => new PublicKey(a))] + + if (opts.tokenAddress && opts.poolProgramAddress) { + const poolProgram = new PublicKey(opts.poolProgramAddress) + const tokenMint = new PublicKey(opts.tokenAddress) + const ccipAddresses = await deriveCcipLookupTableAddresses(chain, { + lookupTableAddress, + tokenMint, + poolProgram, + authority, + }) + const existingAddresses = new Set( + lookupTable.value.state.addresses.map((address) => address.toBase58()), + ) + + if (ccipAddresses.every((address) => existingAddresses.has(address.toBase58()))) { + throw new CCTParamsInvalidError( + this.name, + 'lookupTableAddress', + 'lookup table already contains the canonical CCIP address block; only append additionalAddresses or use an empty ALT', + ) + } + + addresses.unshift(...ccipAddresses) + } + + const totalAddressesAfterAppend = lookupTable.value.state.addresses.length + addresses.length + if (totalAddressesAfterAppend > MAX_ALT_ADDRESSES) { + throw new CCTParamsInvalidError( + this.name, + 'additionalAddresses', + `ALT cannot exceed ${MAX_ALT_ADDRESSES} addresses; requested ${totalAddressesAfterAppend}`, + ) + } + + const instructions: TransactionInstruction[] = [] + for (let i = 0; i < addresses.length; i += EXTEND_CHUNK_SIZE) { + instructions.push( + AddressLookupTableProgram.extendLookupTable({ + payer, + authority, + lookupTable: lookupTableAddress, + addresses: addresses.slice(i, i + EXTEND_CHUNK_SIZE), + }), + ) + } + + chain.logger.debug( + `${this.name}: lookupTable = ${lookupTableAddress.toBase58()}, appended = ${addresses.length}, total = ${totalAddressesAfterAppend}`, + ) + return { + family: ChainFamily.Solana, + instructions, + mainIndex: 0, + } + } + + /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ + override async execute( + chain: SolanaChain, + params: ExecuteAppendToLookupTableParams, + ): Promise { + const { wallet, computeUnits, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const payer = wallet.publicKey.toBase58() + if (params.authority && !new PublicKey(params.authority).equals(wallet.publicKey)) { + throw new CCTParamsInvalidError( + this.name, + 'authority', + 'appendToLookupTable requires authority to be the executing wallet. Use generateUnsignedAppendToLookupTable for vault-owned ALTs and have the vault sign/execute it.', + ) + } + + const tx = await this.generate(chain, { ...rest, payer }) + return submit(chain, wallet, tx, this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.test.ts new file mode 100644 index 00000000..50f92f5b --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.test.ts @@ -0,0 +1,148 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { AddressLookupTableProgram, Keypair } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const POOL_PROGRAM = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const FEE_QUOTER = Keypair.generate().publicKey +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getSlot: async () => 123, + getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID }), + }, + getTokenPoolConfig: async () => ({ + token: TOKEN, + router: ROUTER, + tokenPoolProgram: POOL_PROGRAM, + }), + _getRouterConfig: async () => ({ feeQuoter: FEE_QUOTER }), + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedCreateLookupTable({ + tokenAddress: TOKEN, + poolProgramAddress: POOL_PROGRAM, + payer: PAYER, + ...opts, + }) +} + +describe('Solana TokenAdminRegistry createLookupTable', () => { + it('builds create + extend ALT instructions', async () => { + const unsigned = await generate() + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 2) + assert.match(unsigned.lookupTableAddress, /^[1-9A-HJ-NP-Za-km-z]+$/) + assert.equal( + unsigned.instructions[0]!.programId.toBase58(), + AddressLookupTableProgram.programId.toBase58(), + ) + assert.equal( + unsigned.instructions[1]!.programId.toBase58(), + AddressLookupTableProgram.programId.toBase58(), + ) + assert.equal( + unsigned.instructions[0]!.keys.find((key) => key.pubkey.toBase58() === PAYER)?.isSigner, + false, + ) + }) + + it('builds create-only ALT instruction in createEmpty mode', async () => { + const unsigned = await SolanaTokenManager.fromChain( + stubChain(), + ).generateUnsignedCreateLookupTable({ + payer: PAYER, + authority: AUTHORITY, + mode: 'createEmpty', + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + assert.match(unsigned.lookupTableAddress, /^[1-9A-HJ-NP-Za-km-z]+$/) + assert.equal( + unsigned.instructions[0]!.programId.toBase58(), + AddressLookupTableProgram.programId.toBase58(), + ) + assert.equal( + unsigned.instructions[0]!.keys.find((key) => key.pubkey.toBase58() === AUTHORITY)?.isSigner, + false, + ) + }) + + it('defaults createEmpty authority to payer', async () => { + const unsigned = await SolanaTokenManager.fromChain( + stubChain(), + ).generateUnsignedCreateLookupTable({ + payer: PAYER, + mode: 'createEmpty', + }) + + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === PAYER)) + }) + + it('chunks additional addresses into multiple extend instructions', async () => { + const additionalAddresses = Array.from({ length: 21 }, () => + Keypair.generate().publicKey.toBase58(), + ) + const unsigned = await generate({ additionalAddresses }) + + assert.equal(unsigned.instructions.length, 3) + }) + + it('rejects signed create+extend when authority is not the wallet', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).createLookupTable({ + tokenAddress: TOKEN, + poolProgramAddress: POOL_PROGRAM, + wallet: WALLET, + authority: AUTHORITY, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'createLookupTable' && + err.context.param === 'authority', + ) + }) + + it('uses caller-provided authority', async () => { + const unsigned = await generate({ authority: AUTHORITY }) + + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === AUTHORITY)) + }) + + it('rejects ALTs over 256 addresses', async () => { + const additionalAddresses = Array.from({ length: 247 }, () => + Keypair.generate().publicKey.toBase58(), + ) + + await assert.rejects( + () => generate({ additionalAddresses }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'createLookupTable' && + err.context.param === 'additionalAddresses', + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.ts new file mode 100644 index 00000000..7a478e16 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.ts @@ -0,0 +1,181 @@ +import { type TransactionInstruction, AddressLookupTableProgram, PublicKey } from '@solana/web3.js' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { type UnsignedSolanaTx, isWallet } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + buildCreateLookupTableInstruction, + deriveCcipLookupTableAddresses, +} from '../../programs/alt.ts' +import { submit } from '../../submit.ts' +import { validatePublicKey } from '../../validate.ts' + +const MAX_ALT_ADDRESSES = 256 +const EXTEND_CHUNK_SIZE = 30 + +type CreateLookupTableMode = 'createAndExtend' | 'createEmpty' + +/** Parameters shared by Solana TokenAdminRegistry `createLookupTable` generation and execution. */ +type CreateLookupTableParams = + | { + /** Defaults to `createAndExtend`; use `createEmpty` to skip extending the ALT. */ + mode?: Extract + tokenAddress: string + poolProgramAddress: string + additionalAddresses?: string[] + /** ALT authority. Defaults to payer for unsigned generation and wallet public key for execute. */ + authority?: string + } + | { + /** Creates an empty ALT without extend instructions. */ + mode: Extract + /** ALT authority. Defaults to payer for unsigned generation and wallet public key for execute. */ + authority?: string + } + +/** Parameters for unsigned Solana lookup table generation. */ +export type GenerateCreateLookupTableParams = SolanaGenerateParams + +/** Unsigned create lookup table result, including the derived ALT address. */ +export type GenerateCreateLookupTableResult = UnsignedSolanaTx & { + lookupTableAddress: string +} + +/** Parameters for executing Solana TokenAdminRegistry `createLookupTable`. */ +export type ExecuteCreateLookupTableParams = SolanaExecuteParams + +/** Result of executing Solana TokenAdminRegistry `createLookupTable`. */ +export type ExecuteCreateLookupTableResult = TransactionHash & { lookupTableAddress: string } + +/** Builds and submits Solana ALT create instructions, optionally with extend instructions. */ +export class CreateLookupTable extends SolanaOperation< + CreateLookupTableParams, + GenerateCreateLookupTableResult, + ExecuteCreateLookupTableResult +> { + readonly name = 'createLookupTable' + + /** Validates all public keys before any RPC. */ + protected validate(params: GenerateCreateLookupTableParams): void { + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + if (params.mode === 'createEmpty') return + + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'poolProgramAddress', params.poolProgramAddress) + for (const [i, address] of (params.additionalAddresses ?? []).entries()) { + validatePublicKey(this.name, `additionalAddresses[${i}]`, address) + } + } + + /** Builds unsigned ALT create instructions, optionally with extend instructions. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateCreateLookupTableParams, + ): Promise { + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + + const { instruction: createIx, lookupTableAddress } = buildCreateLookupTableInstruction({ + authority, + payer, + recentSlot: await chain.connection.getSlot('finalized'), + }) + + if (opts.mode === 'createEmpty') { + chain.logger.debug( + `${this.name}: mode = createEmpty, lookupTable = ${lookupTableAddress.toBase58()}`, + ) + return { + family: ChainFamily.Solana, + instructions: [createIx], + mainIndex: 0, + lookupTableAddress: lookupTableAddress.toBase58(), + } + } + + const poolProgram = new PublicKey(opts.poolProgramAddress) + const tokenMint = new PublicKey(opts.tokenAddress) + const additionalAddresses = (opts.additionalAddresses ?? []).map((a) => new PublicKey(a)) + + const ccipAddresses = await deriveCcipLookupTableAddresses(chain, { + lookupTableAddress, + tokenMint, + poolProgram, + authority, + }) + const addresses = [...ccipAddresses, ...additionalAddresses] + + if (addresses.length > MAX_ALT_ADDRESSES) { + throw new CCTParamsInvalidError( + this.name, + 'additionalAddresses', + `ALT cannot exceed ${MAX_ALT_ADDRESSES} addresses; requested ${addresses.length}`, + ) + } + + const extendIxs: TransactionInstruction[] = [] + for (let i = 0; i < addresses.length; i += EXTEND_CHUNK_SIZE) { + extendIxs.push( + AddressLookupTableProgram.extendLookupTable({ + payer, + authority, + lookupTable: lookupTableAddress, + addresses: addresses.slice(i, i + EXTEND_CHUNK_SIZE), + }), + ) + } + + chain.logger.debug( + `${this.name}: token = ${tokenMint.toBase58()}, lookupTable = ${lookupTableAddress.toBase58()}`, + ) + return { + family: ChainFamily.Solana, + instructions: [createIx, ...extendIxs], + mainIndex: 0, + lookupTableAddress: lookupTableAddress.toBase58(), + } + } + + /** Adds the generated lookup table address to the execute result. */ + protected override resultFromGenerated( + hash: TransactionHash, + tx: GenerateCreateLookupTableResult, + ): ExecuteCreateLookupTableResult { + return { ...hash, lookupTableAddress: tx.lookupTableAddress } + } + + /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ + override async execute( + chain: SolanaChain, + params: ExecuteCreateLookupTableParams, + ): Promise { + const { wallet, computeUnits, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const payer = wallet.publicKey.toBase58() + if ( + params.mode !== 'createEmpty' && + params.authority && + !new PublicKey(params.authority).equals(wallet.publicKey) + ) { + throw new CCTParamsInvalidError( + this.name, + 'authority', + "createAndExtend requires authority to be the executing wallet. Use mode: 'createEmpty' for vault-owned ALTs.", + ) + } + + const tx = await this.generate(chain, { ...rest, payer }) + const hash = await submit(chain, wallet, tx, this.name, computeUnits) + return this.resultFromGenerated(hash, tx) + } +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts new file mode 100644 index 00000000..6f268947 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts @@ -0,0 +1,6 @@ +export * from './accept-admin-role.ts' +export * from './append-to-lookup-table.ts' +export * from './create-lookup-table.ts' +export * from './propose-admin-role.ts' +export * from './set-pool.ts' +export * from './transfer-admin-role.ts' diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/propose-admin-role.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/propose-admin-role.test.ts new file mode 100644 index 00000000..e574b5cd --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/propose-admin-role.test.ts @@ -0,0 +1,91 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { + createRouterProgram, + deriveRouterConfigPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' +import { ProposeAdminRole } from './propose-admin-role.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const ADMINISTRATOR = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: () => assert.fail('should not RPC before validation'), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return new ProposeAdminRole().generate(stubChain(), { + tokenAddress: TOKEN, + administrator: ADMINISTRATOR, + routerAddress: ROUTER, + payer: PAYER, + ...opts, + }) +} + +describe('Solana TokenAdminRegistry proposeAdminRole', () => { + it('matches a direct ownerProposeAdministrator build (instruction parity)', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + + assert.ok(instruction) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + assert.equal(instruction.programId.toBase58(), ROUTER) + + const router = new PublicKey(ROUTER) + const mint = new PublicKey(TOKEN) + const expected = await createRouterProgram(stubChain(), router, new PublicKey(PAYER)) + .methods.ownerProposeAdministrator(new PublicKey(ADMINISTRATOR)) + .accountsStrict({ + config: deriveRouterConfigPda(router), + tokenAdminRegistry: deriveTokenAdminRegistryPda(router, mint), + mint, + authority: new PublicKey(PAYER), + systemProgram: SystemProgram.programId, + }) + .instruction() + + assert.equal(instruction.data.toString('hex'), expected.data.toString('hex')) + assert.equal(instruction.keys.length, expected.keys.length) + for (const [i, key] of expected.keys.entries()) { + assert.equal(instruction.keys[i]!.pubkey.toBase58(), key.pubkey.toBase58()) + assert.equal(instruction.keys[i]!.isSigner, key.isSigner) + assert.equal(instruction.keys[i]!.isWritable, key.isWritable) + } + }) + + it('uses caller-provided authority', async () => { + const unsigned = await generate({ authority: AUTHORITY }) + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === AUTHORITY)) + assert.ok(!unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === PAYER)) + }) + + it('defaults authority to payer', async () => { + const unsigned = await generate() + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === PAYER)) + }) + + it('rejects an invalid public key before building', async () => { + await assert.rejects( + () => generate({ administrator: 'not-a-pubkey' }), + (error: unknown) => error instanceof CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/propose-admin-role.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/propose-admin-role.ts new file mode 100644 index 00000000..a2aab7e9 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/propose-admin-role.ts @@ -0,0 +1,87 @@ +import { PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + createRouterProgram, + deriveRouterConfigPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' +import { validatePublicKey } from '../../validate.ts' + +/** Parameters shared by Solana TokenAdminRegistry `proposeAdminRole` generation and execution. */ +type ProposeAdminRoleParams = { + tokenAddress: string + administrator: string + routerAddress: string + /** + * Token admin authority (the current mint authority). Defaults to `payer` for single-signer + * transactions. Multisig/Squads flows should pass the mint/vault authority explicitly. + */ + authority?: string +} + +/** Parameters for unsigned Solana TokenAdminRegistry `proposeAdminRole` generation. */ +export type GenerateProposeAdminRoleParams = SolanaGenerateParams + +/** Unsigned Solana TokenAdminRegistry `proposeAdminRole` result. */ +export type GenerateProposeAdminRoleResult = UnsignedSolanaTx + +/** Parameters for executing Solana TokenAdminRegistry `proposeAdminRole`. */ +export type ExecuteProposeAdminRoleParams = SolanaExecuteParams + +/** Result of executing Solana TokenAdminRegistry `proposeAdminRole`. */ +export type ExecuteProposeAdminRoleResult = TransactionHash + +/** Solana TokenAdminRegistry `proposeAdminRole` operation. */ +export class ProposeAdminRole extends SolanaOperation { + readonly name = 'proposeAdminRole' + + /** Validates all public keys before any RPC. */ + protected validate(params: GenerateProposeAdminRoleParams): void { + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'administrator', params.administrator) + validatePublicKey(this.name, 'routerAddress', params.routerAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + } + + /** Builds the unsigned Solana `ownerProposeAdministrator` instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateProposeAdminRoleParams, + ): Promise { + const router = new PublicKey(opts.routerAddress) + const mint = new PublicKey(opts.tokenAddress) + const administrator = new PublicKey(opts.administrator) + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + + const routerProgram = createRouterProgram(chain, router, payer) + const config = deriveRouterConfigPda(router) + const tokenAdminRegistry = deriveTokenAdminRegistryPda(router, mint) + + const instruction = await routerProgram.methods + .ownerProposeAdministrator(administrator) + .accountsStrict({ + config, + tokenAdminRegistry, + mint, + authority, + systemProgram: SystemProgram.programId, + }) + .instruction() + + chain.logger.debug( + `${this.name}: router = ${router.toBase58()}, token = ${mint.toBase58()}, administrator = ${administrator.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.test.ts new file mode 100644 index 00000000..df1412f9 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.test.ts @@ -0,0 +1,86 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { SolanaTokenManager } from '../../index.ts' + +const BLOCKHASH = PublicKey.default.toBase58() +const TOKEN = Keypair.generate().publicKey.toBase58() +const ADDRESS = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const POOL_LOOKUP_TABLE = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() + +function stubChain(router = ROUTER, onAddress?: (address: string) => void): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: () => assert.fail('should not RPC before validation'), + getLatestBlockhash: async () => ({ blockhash: BLOCKHASH, lastValidBlockHeight: 0 }), + }, + getTokenAdminRegistryFor: async (address: string) => { + onAddress?.(address) + return router + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedSetPool({ + tokenAddress: TOKEN, + address: ADDRESS, + poolLookupTableAddress: POOL_LOOKUP_TABLE, + payer: PAYER, + ...opts, + }) +} + +describe('Solana TokenAdminRegistry setPool', () => { + it('builds unsigned setPool instruction with default writable indexes and authority', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + + assert.ok(instruction) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + assert.equal(instruction.programId.toBase58(), ROUTER) + assert.equal(instruction.data.toString('hex'), '771e0eb473e1a7ee03000000030407') + assert.ok(instruction.keys.some((key) => key.pubkey.toBase58() === TOKEN)) + assert.ok(instruction.keys.some((key) => key.pubkey.toBase58() === POOL_LOOKUP_TABLE)) + assert.ok(instruction.keys.some((key) => key.pubkey.toBase58() === PAYER)) + }) + + it('uses caller-provided writable indexes', async () => { + const unsigned = await generate({ writableIndexes: [3, 4, 7, 9] }) + + assert.equal(unsigned.instructions[0]!.data.toString('hex'), '771e0eb473e1a7ee0400000003040709') + }) + + it('resolves the router from address', async () => { + let requestedAddress: string | undefined + const cct = SolanaTokenManager.fromChain( + stubChain(ROUTER, (address) => (requestedAddress = address)), + ) + + const unsigned = await cct.generateUnsignedSetPool({ + tokenAddress: TOKEN, + address: ADDRESS, + poolLookupTableAddress: POOL_LOOKUP_TABLE, + payer: PAYER, + }) + + assert.equal(requestedAddress, ADDRESS) + assert.equal(unsigned.instructions[0]!.programId.toBase58(), ROUTER) + }) + + it('uses caller-provided authority', async () => { + const unsigned = await generate({ authority: AUTHORITY }) + + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === AUTHORITY)) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.ts new file mode 100644 index 00000000..94c6a75b --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.ts @@ -0,0 +1,96 @@ +import { Buffer } from 'buffer' + +import { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + createRouterProgram, + deriveRouterConfigPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' +import { validatePublicKey, validateWritableIndexes } from '../../validate.ts' + +/** Standard BurnMint/LockRelease pool ALT writable positions. */ +export const DEFAULT_WRITABLE_INDEXES = [3, 4, 7] as const + +/** Parameters shared by Solana TokenAdminRegistry `setPool` generation and execution. */ +type SetPoolParams = { + tokenAddress: string + address: string + poolLookupTableAddress: string + writableIndexes?: number[] + /** + * Token admin authority. Defaults to `payer` for single-signer transactions. + * Multisig/Squads flows should pass the admin/vault authority explicitly. + */ + authority?: string +} + +/** Parameters for unsigned Solana TokenAdminRegistry `setPool` generation. */ +export type GenerateSetPoolParams = SolanaGenerateParams + +/** Unsigned Solana TokenAdminRegistry `setPool` result. */ +export type GenerateSetPoolResult = UnsignedSolanaTx + +/** Parameters for executing Solana TokenAdminRegistry `setPool`. */ +export type ExecuteSetPoolParams = SolanaExecuteParams + +/** Result of executing Solana TokenAdminRegistry `setPool`. */ +export type ExecuteSetPoolResult = TransactionHash + +/** Solana TokenAdminRegistry `setPool` operation. */ +export class SetPool extends SolanaOperation { + readonly name = 'setPool' + + /** Validates all public keys before any RPC. */ + protected validate(params: GenerateSetPoolParams): void { + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'address', params.address) + validatePublicKey(this.name, 'poolLookupTableAddress', params.poolLookupTableAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + validateWritableIndexes(this.name, 'writableIndexes', params.writableIndexes) + } + + /** Builds the unsigned Solana `setPool` instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateSetPoolParams, + ): Promise { + const routerAddress = await chain.getTokenAdminRegistryFor(opts.address) + const router = new PublicKey(routerAddress) + const tokenMint = new PublicKey(opts.tokenAddress) + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + const lookupTable = new PublicKey(opts.poolLookupTableAddress) + + const routerProgram = createRouterProgram(chain, router, payer) + const config = deriveRouterConfigPda(router) + const tokenAdminRegistry = deriveTokenAdminRegistryPda(router, tokenMint) + + const writableIndexes = opts.writableIndexes ?? [...DEFAULT_WRITABLE_INDEXES] + const instruction = await routerProgram.methods + .setPool(Buffer.from(writableIndexes)) + .accounts({ + config, + tokenAdminRegistry, + mint: tokenMint, + poolLookuptable: lookupTable, + authority, + }) + .instruction() + + chain.logger.debug( + `${this.name}: router = ${router.toBase58()}, token = ${tokenMint.toBase58()}, lookupTable = ${lookupTable.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin-role.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin-role.test.ts new file mode 100644 index 00000000..4e023f19 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin-role.test.ts @@ -0,0 +1,95 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { + createRouterProgram, + deriveRouterConfigPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' +import { TransferAdminRole } from './transfer-admin-role.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const NEW_ADMIN = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: () => assert.fail('should not RPC before validation'), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return new TransferAdminRole().generate(stubChain(), { + tokenAddress: TOKEN, + newAdmin: NEW_ADMIN, + routerAddress: ROUTER, + payer: PAYER, + ...opts, + }) +} + +describe('Solana TokenAdminRegistry transferAdminRole', () => { + it('matches a direct transferAdminRoleTokenAdminRegistry build (instruction parity)', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + + assert.ok(instruction) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + assert.equal(instruction.programId.toBase58(), ROUTER) + + const router = new PublicKey(ROUTER) + const mint = new PublicKey(TOKEN) + const expected = await createRouterProgram(stubChain(), router, new PublicKey(PAYER)) + .methods.transferAdminRoleTokenAdminRegistry(new PublicKey(NEW_ADMIN)) + .accountsStrict({ + config: deriveRouterConfigPda(router), + tokenAdminRegistry: deriveTokenAdminRegistryPda(router, mint), + mint, + authority: new PublicKey(PAYER), + }) + .instruction() + + assert.equal(instruction.data.toString('hex'), expected.data.toString('hex')) + assert.ok( + instruction.data + .toString('hex') + .includes(new PublicKey(NEW_ADMIN).toBuffer().toString('hex')), + ) + assert.equal(instruction.keys.length, expected.keys.length) + for (const [i, key] of expected.keys.entries()) { + assert.equal(instruction.keys[i]!.pubkey.toBase58(), key.pubkey.toBase58()) + assert.equal(instruction.keys[i]!.isSigner, key.isSigner) + assert.equal(instruction.keys[i]!.isWritable, key.isWritable) + } + }) + + it('uses caller-provided authority', async () => { + const unsigned = await generate({ authority: AUTHORITY }) + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === AUTHORITY)) + assert.ok(!unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === PAYER)) + }) + + it('defaults authority to payer', async () => { + const unsigned = await generate() + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === PAYER)) + }) + + it('rejects an invalid public key before building', async () => { + await assert.rejects( + () => generate({ newAdmin: 'not-a-pubkey' }), + (error: unknown) => error instanceof CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin-role.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin-role.ts new file mode 100644 index 00000000..aec5617c --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin-role.ts @@ -0,0 +1,86 @@ +import { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + createRouterProgram, + deriveRouterConfigPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' +import { validatePublicKey } from '../../validate.ts' + +/** Parameters shared by Solana TokenAdminRegistry `transferAdminRole` generation and execution. */ +type TransferAdminRoleParams = { + tokenAddress: string + newAdmin: string + routerAddress: string + /** + * Token admin authority (the current administrator). Defaults to `payer` for single-signer + * transactions. Multisig/Squads flows should pass the current admin/vault authority explicitly. + */ + authority?: string +} + +/** Parameters for unsigned Solana TokenAdminRegistry `transferAdminRole` generation. */ +export type GenerateTransferAdminRoleParams = SolanaGenerateParams + +/** Unsigned Solana TokenAdminRegistry `transferAdminRole` result. */ +export type GenerateTransferAdminRoleResult = UnsignedSolanaTx + +/** Parameters for executing Solana TokenAdminRegistry `transferAdminRole`. */ +export type ExecuteTransferAdminRoleParams = SolanaExecuteParams + +/** Result of executing Solana TokenAdminRegistry `transferAdminRole`. */ +export type ExecuteTransferAdminRoleResult = TransactionHash + +/** Solana TokenAdminRegistry `transferAdminRole` operation. */ +export class TransferAdminRole extends SolanaOperation { + readonly name = 'transferAdminRole' + + /** Validates all public keys before any RPC. */ + protected validate(params: GenerateTransferAdminRoleParams): void { + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'newAdmin', params.newAdmin) + validatePublicKey(this.name, 'routerAddress', params.routerAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + } + + /** Builds the unsigned Solana `transferAdminRoleTokenAdminRegistry` instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateTransferAdminRoleParams, + ): Promise { + const router = new PublicKey(opts.routerAddress) + const mint = new PublicKey(opts.tokenAddress) + const newAdmin = new PublicKey(opts.newAdmin) + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + + const routerProgram = createRouterProgram(chain, router, payer) + const config = deriveRouterConfigPda(router) + const tokenAdminRegistry = deriveTokenAdminRegistryPda(router, mint) + + const instruction = await routerProgram.methods + .transferAdminRoleTokenAdminRegistry(newAdmin) + .accountsStrict({ + config, + tokenAdminRegistry, + mint, + authority, + }) + .instruction() + + chain.logger.debug( + `${this.name}: router = ${router.toBase58()}, token = ${mint.toBase58()}, newAdmin = ${newAdmin.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.test.ts new file mode 100644 index 00000000..7d876d17 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.test.ts @@ -0,0 +1,129 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' + +const ASSOCIATED_TOKEN_PROGRAM = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL' +const BLOCKHASH = PublicKey.default.toBase58() +const TOKEN = Keypair.generate().publicKey.toBase58() +const POOL_PROGRAM = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + // detectMintTokenProgram reads the mint owner to choose the token program. + getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID }), + }, + } as unknown as SolanaChain +} + +/** A connection that satisfies the full simulate → send → confirm submit pipeline. */ +function stubExecuteChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID }), + simulateTransaction: async () => ({ + value: { unitsConsumed: 1000, err: null, logs: [], returnData: null }, + }), + getLatestBlockhash: async () => ({ blockhash: BLOCKHASH, lastValidBlockHeight: 0 }), + sendTransaction: async () => 'SIGNATURE', + confirmTransaction: async () => ({ value: { err: null } }), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedDeployTokenPool({ + tokenAddress: TOKEN, + poolProgramAddress: POOL_PROGRAM, + payer: PAYER, + authority: AUTHORITY, + ...opts, + }) +} + +describe('Solana token pool deployTokenPool', () => { + it('builds initialize + idempotent pool-ATA creation instructions', async () => { + const unsigned = await generate() + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + // POC parity: initialize + auto-created pool token ATA. + assert.equal(unsigned.instructions.length, 2) + assert.equal(unsigned.instructions[0]!.programId.toBase58(), POOL_PROGRAM) + assert.equal(unsigned.instructions[1]!.programId.toBase58(), ASSOCIATED_TOKEN_PROGRAM) + }) + + it('adds configure allowlist instruction when provided', async () => { + const unsigned = await generate({ + allowlist: [Keypair.generate().publicKey.toBase58()], + }) + + // initialize + configureAllowList + pool-ATA creation. + assert.equal(unsigned.instructions.length, 3) + assert.equal(unsigned.instructions[1]!.programId.toBase58(), POOL_PROGRAM) + assert.equal(unsigned.instructions[2]!.programId.toBase58(), ASSOCIATED_TOKEN_PROGRAM) + }) + + it('defaults authority to payer', async () => { + const unsigned = await generate({ authority: undefined }) + + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === PAYER)) + }) + + it('returns the pool state/config PDA from execute', async () => { + const result = await SolanaTokenManager.fromChain(stubExecuteChain()).deployTokenPool({ + tokenAddress: TOKEN, + poolProgramAddress: POOL_PROGRAM, + wallet: WALLET, + }) + + const expected = deriveTokenPoolConfigPda( + new PublicKey(POOL_PROGRAM), + new PublicKey(TOKEN), + ).toBase58() + assert.equal(result.hash, 'SIGNATURE') + assert.equal(result.poolAddress, expected) + }) + + it('rejects signed deploy when authority is not the wallet', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).deployTokenPool({ + tokenAddress: TOKEN, + poolProgramAddress: POOL_PROGRAM, + wallet: WALLET, + authority: AUTHORITY, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'deployTokenPool' && + err.context.param === 'authority', + ) + }) + + it('rejects invalid allowlist addresses', async () => { + await assert.rejects( + () => generate({ allowlist: ['not-a-pubkey'] }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'deployTokenPool' && + err.context.param === 'allowlist[0]', + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.ts b/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.ts new file mode 100644 index 00000000..e9ddc683 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.ts @@ -0,0 +1,174 @@ +import { + createAssociatedTokenAccountIdempotentInstruction, + getAssociatedTokenAddressSync, +} from '@solana/spl-token' +import { PublicKey, SystemProgram } from '@solana/web3.js' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { type UnsignedSolanaTx, isWallet } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + createTokenPoolProgram, + deriveTokenPoolConfigPda, + deriveTokenPoolGlobalConfigPda, + deriveTokenPoolProgramDataPda, + deriveTokenPoolSignerPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { detectMintTokenProgram } from '../../token/operations/spl.ts' +import { validatePublicKey } from '../../validate.ts' + +/** Parameters for initializing a Solana token pool, optionally with an allowlist. */ +type DeployTokenPoolParams = { + /** Token mint address this pool manages. */ + tokenAddress: string + /** Token pool program address, e.g. BurnMint or LockRelease token pool program. */ + poolProgramAddress: string + /** + * Optional addresses to enable in the pool allowlist during initialization. + * If omitted, the pool is initialized without configuring the allowlist. + */ + allowlist?: string[] + /** Pool authority. Defaults to payer for unsigned generation and wallet public key for execute. */ + authority?: string +} + +/** Parameters for unsigned Solana token pool deploy generation. */ +export type GenerateDeployTokenPoolParams = SolanaGenerateParams + +/** Unsigned Solana token pool deploy result. */ +export type GenerateDeployTokenPoolResult = UnsignedSolanaTx + +/** Parameters for executing Solana token pool deploy. */ +export type ExecuteDeployTokenPoolParams = SolanaExecuteParams + +/** Result of executing Solana token pool deploy, including the pool state/config PDA. */ +export type ExecuteDeployTokenPoolResult = TransactionHash & { + /** The pool state/config PDA (base58), derivable pre-submit. */ + poolAddress: string +} + +/** Initializes a Solana token pool, optionally configuring an allowlist. */ +export class DeployTokenPool extends SolanaOperation< + DeployTokenPoolParams, + GenerateDeployTokenPoolResult, + ExecuteDeployTokenPoolResult +> { + readonly name = 'deployTokenPool' + + /** Validates all public keys before any RPC. */ + protected validate(params: GenerateDeployTokenPoolParams): void { + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'poolProgramAddress', params.poolProgramAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + for (const [i, address] of (params.allowlist ?? []).entries()) { + validatePublicKey(this.name, `allowlist[${i}]`, address) + } + } + + /** Builds the unsigned Solana token pool initialize instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateDeployTokenPoolParams, + ): Promise { + const tokenMint = new PublicKey(opts.tokenAddress) + const poolProgram = new PublicKey(opts.poolProgramAddress) + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + const program = createTokenPoolProgram(chain, poolProgram, payer) + const state = deriveTokenPoolConfigPda(poolProgram, tokenMint) + + const instructions = [ + await program.methods + .initialize() + .accountsStrict({ + state, + mint: tokenMint, + authority, + systemProgram: SystemProgram.programId, + program: poolProgram, + programData: deriveTokenPoolProgramDataPda(poolProgram), + config: deriveTokenPoolGlobalConfigPda(poolProgram), + }) + .instruction(), + ] + + const allowlist = (opts.allowlist ?? []).map((a) => new PublicKey(a)) + if (allowlist.length) { + instructions.push( + await program.methods + .configureAllowList(allowlist, true) + .accountsStrict({ + state, + mint: tokenMint, + authority, + systemProgram: SystemProgram.programId, + }) + .instruction(), + ) + } + + // POC parity: auto-create the pool signer PDA's associated token account (not in DAPP-10507 + // deploy-token-pool). Idempotent so re-runs are safe. Reuses the same derivation as + // create-pool-token-account.ts. See legacy token-admin/solana/index.ts L816-847. + const tokenProgramId = await detectMintTokenProgram(chain, this.name, 'tokenAddress', tokenMint) + const poolSignerPda = deriveTokenPoolSignerPda(poolProgram, tokenMint) + const poolTokenAta = getAssociatedTokenAddressSync( + tokenMint, + poolSignerPda, + true, // allowOwnerOffCurve — PDAs are off-curve + tokenProgramId, + ) + instructions.push( + createAssociatedTokenAccountIdempotentInstruction( + payer, + poolTokenAta, + poolSignerPda, + tokenMint, + tokenProgramId, + ), + ) + + chain.logger.debug( + `${this.name}: token = ${tokenMint.toBase58()}, poolProgram = ${poolProgram.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions, mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ + override async execute( + chain: SolanaChain, + params: ExecuteDeployTokenPoolParams, + ): Promise { + const { wallet, computeUnits, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const payer = wallet.publicKey.toBase58() + if (params.authority && !new PublicKey(params.authority).equals(wallet.publicKey)) { + throw new CCTParamsInvalidError( + this.name, + 'authority', + 'deployTokenPool requires authority to be the executing wallet. Use generateUnsignedDeployTokenPool for vault-owned pools and have the vault sign/execute it.', + ) + } + + const tx = await this.generate(chain, { ...rest, payer }) + const hash = await submit(chain, wallet, tx, this.name, computeUnits) + // POC parity: surface the pool state/config PDA from execute (not in DAPP-10507 + // deploy-token-pool). Same derivation the op uses for the `initialize` state account. + const poolAddress = deriveTokenPoolConfigPda( + new PublicKey(rest.poolProgramAddress), + new PublicKey(rest.tokenAddress), + ).toBase58() + return { ...hash, poolAddress } + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts new file mode 100644 index 00000000..f7c1f26c --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -0,0 +1 @@ +export * from './deploy-token-pool.ts' diff --git a/ccip-sdk/src/cct/solana/token/get-mint-burn-roles.ts b/ccip-sdk/src/cct/solana/token/get-mint-burn-roles.ts new file mode 100644 index 00000000..14b911fb --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/get-mint-burn-roles.ts @@ -0,0 +1,87 @@ +/** + * getMintBurnRoles (Solana) — reads a mint's current mint authority and, if that + * authority is an SPL Token multisig, its threshold and members. + * + * On Solana "mint/burn access" is the single SPL mint authority (not a role set), + * optionally an m-of-n multisig. Read-only — not a write {@link Operation}. + * + * @packageDocumentation + */ + +import { + MULTISIG_SIZE, + MintLayout, + MultisigLayout, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import { PublicKey } from '@solana/web3.js' + +import type { SolanaChain } from '../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../errors.ts' + +/** A mint's mint-authority, with multisig detail when applicable. */ +export type MintBurnRolesResult = { + /** Current mint authority (base58), or `null` if disabled. */ + mintAuthority: string | null + /** Whether the mint authority is an SPL Token multisig. */ + isMultisig: boolean + /** Multisig threshold (m-of-n). Only set when `isMultisig` is true. */ + multisigThreshold?: number + /** Multisig members. Only set when `isMultisig` is true. */ + multisigMembers?: Array<{ address: string }> +} + +const SIGNER_KEYS = [ + 'signer1', + 'signer2', + 'signer3', + 'signer4', + 'signer5', + 'signer6', + 'signer7', + 'signer8', + 'signer9', + 'signer10', + 'signer11', +] as const + +/** Reads the mint authority (and multisig detail) for a Solana mint. */ +export async function getMintBurnRoles( + chain: SolanaChain, + tokenAddress: string, +): Promise { + const mintPubkey = new PublicKey(tokenAddress) + const mintAccountInfo = await chain.connection.getAccountInfo(mintPubkey) + if (!mintAccountInfo) { + throw new CCTParamsInvalidError( + 'getMintBurnRoles', + 'tokenAddress', + 'mint account not found on-chain', + ) + } + + const rawMint = MintLayout.decode(mintAccountInfo.data) + const mintAuthority = rawMint.mintAuthorityOption === 1 ? rawMint.mintAuthority.toBase58() : null + if (!mintAuthority) return { mintAuthority: null, isMultisig: false } + + const authorityInfo = await chain.connection.getAccountInfo(new PublicKey(mintAuthority)) + const isOwnedByTokenProgram = + authorityInfo?.owner.equals(TOKEN_PROGRAM_ID) || + authorityInfo?.owner.equals(TOKEN_2022_PROGRAM_ID) + if (!authorityInfo || authorityInfo.data.length !== MULTISIG_SIZE || !isOwnedByTokenProgram) { + return { mintAuthority, isMultisig: false } + } + + const rawMultisig = MultisigLayout.decode(authorityInfo.data) + if (!rawMultisig.isInitialized) return { mintAuthority, isMultisig: false } + + const multisigMembers: Array<{ address: string }> = [] + for (let i = 0; i < rawMultisig.n; i++) { + multisigMembers.push({ address: rawMultisig[SIGNER_KEYS[i]!].toBase58() }) + } + chain.logger.debug( + `getMintBurnRoles: token=${tokenAddress}, authority=${mintAuthority}, isMultisig=true, threshold=${rawMultisig.m}, members=${multisigMembers.length}`, + ) + return { mintAuthority, isMultisig: true, multisigThreshold: rawMultisig.m, multisigMembers } +} diff --git a/ccip-sdk/src/cct/solana/token/operations/create-pool-mint-authority-multisig.test.ts b/ccip-sdk/src/cct/solana/token/operations/create-pool-mint-authority-multisig.test.ts new file mode 100644 index 00000000..f69a89e2 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/create-pool-mint-authority-multisig.test.ts @@ -0,0 +1,138 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { + MULTISIG_SIZE, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + createInitializeMultisigInstruction, +} from '@solana/spl-token' +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { deriveTokenPoolSignerPda } from '../../programs/token-pool.ts' +import { CreatePoolMintAuthorityMultisig } from './create-pool-mint-authority-multisig.ts' + +const MINT = Keypair.generate().publicKey +const POOL_PROGRAM = Keypair.generate().publicKey +const PAYER = Keypair.generate().publicKey.toBase58() +const SIGNER_A = Keypair.generate().publicKey.toBase58() +const SIGNER_B = Keypair.generate().publicKey.toBase58() +const SEED = 'multisig-fixed-seed' +const RENT = 1_000_000 + +function stubChain(owner: PublicKey = TOKEN_PROGRAM_ID): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async (key: PublicKey) => (key.equals(MINT) ? { owner } : null), + getMinimumBalanceForRentExemption: async () => RENT, + }, + } as unknown as SolanaChain +} + +function noRpcChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: () => assert.fail('should not RPC before validation'), + getMinimumBalanceForRentExemption: () => assert.fail('should not RPC before validation'), + }, + } as unknown as SolanaChain +} + +describe('Solana token createPoolMintAuthorityMultisig', () => { + it('builds createAccountWithSeed + initializeMultisig with the pool signer PDA first', async () => { + const unsigned = await new CreatePoolMintAuthorityMultisig().generate(stubChain(), { + payer: PAYER, + mint: MINT.toBase58(), + poolProgramId: POOL_PROGRAM.toBase58(), + additionalSigners: [SIGNER_A, SIGNER_B], + threshold: 2, + seed: SEED, + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 1) + assert.equal(unsigned.instructions.length, 2) + + const poolSignerPda = deriveTokenPoolSignerPda(POOL_PROGRAM, MINT) + const multisig = await PublicKey.createWithSeed(new PublicKey(PAYER), SEED, TOKEN_PROGRAM_ID) + + assert.equal(unsigned.multisigAddress, multisig.toBase58()) + assert.equal(unsigned.poolSignerPda, poolSignerPda.toBase58()) + assert.deepEqual(unsigned.allSigners, [poolSignerPda.toBase58(), SIGNER_A, SIGNER_B]) + + // Parity: create-account-with-seed + const refCreate = SystemProgram.createAccountWithSeed({ + fromPubkey: new PublicKey(PAYER), + newAccountPubkey: multisig, + basePubkey: new PublicKey(PAYER), + seed: SEED, + lamports: RENT, + space: MULTISIG_SIZE, + programId: TOKEN_PROGRAM_ID, + }) + assert.equal(unsigned.instructions[0]!.programId.toBase58(), SystemProgram.programId.toBase58()) + assert.equal(unsigned.instructions[0]!.data.toString('hex'), refCreate.data.toString('hex')) + + // Parity: initialize-multisig + const refInit = createInitializeMultisigInstruction( + multisig, + [poolSignerPda, new PublicKey(SIGNER_A), new PublicKey(SIGNER_B)], + 2, + TOKEN_PROGRAM_ID, + ) + assert.equal(unsigned.instructions[1]!.programId.toBase58(), TOKEN_PROGRAM_ID.toBase58()) + assert.equal(unsigned.instructions[1]!.data.toString('hex'), refInit.data.toString('hex')) + assert.deepEqual( + unsigned.instructions[1]!.keys.map((k) => k.pubkey.toBase58()), + refInit.keys.map((k) => k.pubkey.toBase58()), + ) + }) + + it('uses the Token-2022 program when the mint is owned by it', async () => { + const unsigned = await new CreatePoolMintAuthorityMultisig().generate( + stubChain(TOKEN_2022_PROGRAM_ID), + { + payer: PAYER, + mint: MINT.toBase58(), + poolProgramId: POOL_PROGRAM.toBase58(), + additionalSigners: [SIGNER_A], + threshold: 1, + seed: SEED, + }, + ) + assert.equal(unsigned.instructions[1]!.programId.toBase58(), TOKEN_2022_PROGRAM_ID.toBase58()) + }) + + it('rejects empty additionalSigners before any RPC', async () => { + await assert.rejects( + () => + new CreatePoolMintAuthorityMultisig().generate(noRpcChain(), { + payer: PAYER, + mint: MINT.toBase58(), + poolProgramId: POOL_PROGRAM.toBase58(), + additionalSigners: [], + threshold: 1, + }), + CCTParamsInvalidError, + ) + }) + + it('rejects a threshold that exceeds total signers before any RPC', async () => { + await assert.rejects( + () => + new CreatePoolMintAuthorityMultisig().generate(noRpcChain(), { + payer: PAYER, + mint: MINT.toBase58(), + poolProgramId: POOL_PROGRAM.toBase58(), + additionalSigners: [SIGNER_A], + threshold: 3, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token/operations/create-pool-mint-authority-multisig.ts b/ccip-sdk/src/cct/solana/token/operations/create-pool-mint-authority-multisig.ts new file mode 100644 index 00000000..6e4edbe4 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/create-pool-mint-authority-multisig.ts @@ -0,0 +1,185 @@ +import { MULTISIG_SIZE, createInitializeMultisigInstruction } from '@solana/spl-token' +import { PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { deriveTokenPoolSignerPda } from '../../programs/token-pool.ts' +import { validatePublicKey } from '../../validate.ts' +import { detectMintTokenProgram } from './spl.ts' + +/** SPL Token multisig supports at most 11 signers. */ +const MAX_MULTISIG_SIGNERS = 11 + +/** + * Parameters for creating an SPL Token multisig whose first signer is the pool signer PDA. + * **Solana burn-mint pools only.** + */ +type CreatePoolMintAuthorityMultisigParams = { + /** SPL token mint (base58). */ + mint: string + /** Burn-mint pool program ID (base58) used to derive the pool signer PDA. */ + poolProgramId: string + /** Additional signers (base58), e.g. a Squads vault. The pool signer PDA is auto-prepended. */ + additionalSigners: string[] + /** Required number of signers (m-of-n). Must be a positive integer. */ + threshold: number + /** + * Optional seed for the multisig account, derived via `createAccountWithSeed`. When omitted a + * random seed is generated so only the payer must sign (matching the `deployToken` convention). + */ + seed?: string +} + +/** Parameters for unsigned Solana create-pool-mint-authority-multisig generation. */ +export type GenerateCreatePoolMintAuthorityMultisigParams = + SolanaGenerateParams + +/** Unsigned Solana create-pool-mint-authority-multisig result, including the derived addresses. */ +export type GenerateCreatePoolMintAuthorityMultisigResult = UnsignedSolanaTx & { + /** The multisig account address (base58), derivable pre-submit via `createAccountWithSeed`. */ + multisigAddress: string + /** The auto-derived pool signer PDA (base58). */ + poolSignerPda: string + /** All signers in order: `[poolSignerPda, ...additionalSigners]`. */ + allSigners: string[] +} + +/** Parameters for executing Solana create-pool-mint-authority-multisig. */ +export type ExecuteCreatePoolMintAuthorityMultisigParams = + SolanaExecuteParams + +/** Result of executing Solana create-pool-mint-authority-multisig. */ +export type ExecuteCreatePoolMintAuthorityMultisigResult = TransactionHash & { + /** The created multisig account address (base58). */ + multisigAddress: string + /** The auto-derived pool signer PDA (base58). */ + poolSignerPda: string + /** All signers in order: `[poolSignerPda, ...additionalSigners]`. */ + allSigners: string[] +} + +/** + * Creates an SPL Token multisig with the pool signer PDA as its first signer. The multisig + * account is created with `createAccountWithSeed` (seed provided or auto-generated) so the + * address is derivable pre-submit and only the payer must sign. **Solana burn-mint pools only.** + */ +export class CreatePoolMintAuthorityMultisig extends SolanaOperation< + CreatePoolMintAuthorityMultisigParams, + GenerateCreatePoolMintAuthorityMultisigResult, + ExecuteCreatePoolMintAuthorityMultisigResult +> { + readonly name = 'createPoolMintAuthorityMultisig' + + /** Validates public keys, signer count, and threshold before any RPC. */ + protected validate(params: GenerateCreatePoolMintAuthorityMultisigParams): void { + validatePublicKey(this.name, 'payer', params.payer) + validatePublicKey(this.name, 'mint', params.mint) + validatePublicKey(this.name, 'poolProgramId', params.poolProgramId) + + if (!Array.isArray(params.additionalSigners) || params.additionalSigners.length === 0) { + throw new CCTParamsInvalidError( + this.name, + 'additionalSigners', + 'must have at least one additional signer', + ) + } + params.additionalSigners.forEach((signer, i) => + validatePublicKey(this.name, `additionalSigners[${i}]`, signer), + ) + + // Total signers = 1 (pool signer PDA) + additionalSigners.length + const totalSigners = 1 + params.additionalSigners.length + if (totalSigners > MAX_MULTISIG_SIGNERS) { + throw new CCTParamsInvalidError( + this.name, + 'additionalSigners', + `total signers (${totalSigners}) exceeds SPL Token multisig limit of ${MAX_MULTISIG_SIGNERS}`, + ) + } + if (!Number.isInteger(params.threshold) || params.threshold < 1) { + throw new CCTParamsInvalidError(this.name, 'threshold', 'must be a positive integer') + } + if (params.threshold > totalSigners) { + throw new CCTParamsInvalidError( + this.name, + 'threshold', + `threshold (${params.threshold}) exceeds total signers (${totalSigners})`, + ) + } + } + + /** Builds the create-account + initialize-multisig instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + params: GenerateCreatePoolMintAuthorityMultisigParams, + ): Promise { + const payer = new PublicKey(params.payer) + const mint = new PublicKey(params.mint) + const poolProgram = new PublicKey(params.poolProgramId) + + const poolSignerPda = deriveTokenPoolSignerPda(poolProgram, mint) + const allSignerPubkeys = [ + poolSignerPda, + ...params.additionalSigners.map((s) => new PublicKey(s)), + ] + + const tokenProgramId = await detectMintTokenProgram(chain, this.name, 'mint', mint) + const lamports = await chain.connection.getMinimumBalanceForRentExemption(MULTISIG_SIZE) + + const seed = params.seed ?? `multisig_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` + const multisig = await PublicKey.createWithSeed(payer, seed, tokenProgramId) + + const instructions = [ + SystemProgram.createAccountWithSeed({ + fromPubkey: payer, + newAccountPubkey: multisig, + basePubkey: payer, + seed, + lamports, + space: MULTISIG_SIZE, + programId: tokenProgramId, + }), + createInitializeMultisigInstruction( + multisig, + allSignerPubkeys, + params.threshold, + tokenProgramId, + ), + ] + + const allSigners = allSignerPubkeys.map((pk) => pk.toBase58()) + chain.logger.debug( + `${this.name}: multisig = ${multisig.toBase58()}, poolSignerPda = ${poolSignerPda.toBase58()}, signers = ${allSigners.length}, threshold = ${params.threshold}`, + ) + + return { + family: ChainFamily.Solana, + instructions, + mainIndex: 1, + multisigAddress: multisig.toBase58(), + poolSignerPda: poolSignerPda.toBase58(), + allSigners, + } + } + + /** Adds the derived multisig metadata to the execute result. */ + protected override resultFromGenerated( + hash: TransactionHash, + tx: GenerateCreatePoolMintAuthorityMultisigResult, + ): ExecuteCreatePoolMintAuthorityMultisigResult { + return { + ...hash, + multisigAddress: tx.multisigAddress, + poolSignerPda: tx.poolSignerPda, + allSigners: tx.allSigners, + } + } +} diff --git a/ccip-sdk/src/cct/solana/token/operations/create-pool-token-account.test.ts b/ccip-sdk/src/cct/solana/token/operations/create-pool-token-account.test.ts new file mode 100644 index 00000000..4120c200 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/create-pool-token-account.test.ts @@ -0,0 +1,114 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + createAssociatedTokenAccountIdempotentInstruction, + getAssociatedTokenAddressSync, +} from '@solana/spl-token' +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { deriveTokenPoolSignerPda } from '../../programs/token-pool.ts' +import { CreatePoolTokenAccount } from './create-pool-token-account.ts' + +const MINT = Keypair.generate().publicKey +const POOL_PROGRAM = Keypair.generate().publicKey +const POOL_STATE = Keypair.generate().publicKey +const PAYER = Keypair.generate().publicKey.toBase58() + +function stubChain(mintOwner: PublicKey = TOKEN_PROGRAM_ID): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async (key: PublicKey) => { + if (key.equals(POOL_STATE)) return { owner: POOL_PROGRAM } + if (key.equals(MINT)) return { owner: mintOwner } + return null + }, + }, + } as unknown as SolanaChain +} + +function noRpcChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { getAccountInfo: () => assert.fail('should not RPC before validation') }, + } as unknown as SolanaChain +} + +describe('Solana token createPoolTokenAccount', () => { + it('matches createAssociatedTokenAccountIdempotentInstruction for the pool signer PDA', async () => { + const unsigned = await new CreatePoolTokenAccount().generate(stubChain(), { + payer: PAYER, + tokenAddress: MINT.toBase58(), + poolAddress: POOL_STATE.toBase58(), + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + + const poolSignerPda = deriveTokenPoolSignerPda(POOL_PROGRAM, MINT) + const poolTokenAta = getAssociatedTokenAddressSync(MINT, poolSignerPda, true, TOKEN_PROGRAM_ID) + + assert.equal(unsigned.poolSignerPda, poolSignerPda.toBase58()) + assert.equal(unsigned.poolTokenAccount, poolTokenAta.toBase58()) + + const ref = createAssociatedTokenAccountIdempotentInstruction( + new PublicKey(PAYER), + poolTokenAta, + poolSignerPda, + MINT, + TOKEN_PROGRAM_ID, + ) + assert.equal(unsigned.instructions[0]!.programId.toBase58(), ref.programId.toBase58()) + assert.equal(unsigned.instructions[0]!.data.toString('hex'), ref.data.toString('hex')) + assert.deepEqual( + unsigned.instructions[0]!.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ref.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ) + }) + + it('derives the Token-2022 ATA when the mint is owned by Token-2022', async () => { + const unsigned = await new CreatePoolTokenAccount().generate(stubChain(TOKEN_2022_PROGRAM_ID), { + payer: PAYER, + tokenAddress: MINT.toBase58(), + poolAddress: POOL_STATE.toBase58(), + }) + const poolSignerPda = deriveTokenPoolSignerPda(POOL_PROGRAM, MINT) + const ata = getAssociatedTokenAddressSync(MINT, poolSignerPda, true, TOKEN_2022_PROGRAM_ID) + assert.equal(unsigned.poolTokenAccount, ata.toBase58()) + }) + + it('rejects an invalid poolAddress before any RPC', async () => { + await assert.rejects( + () => + new CreatePoolTokenAccount().generate(noRpcChain(), { + payer: PAYER, + tokenAddress: MINT.toBase58(), + poolAddress: 'not-a-key', + }), + CCTParamsInvalidError, + ) + }) + + it('rejects when the pool state account is missing on-chain', async () => { + const chain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { getAccountInfo: async () => null }, + } as unknown as SolanaChain + await assert.rejects( + () => + new CreatePoolTokenAccount().generate(chain, { + payer: PAYER, + tokenAddress: MINT.toBase58(), + poolAddress: POOL_STATE.toBase58(), + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token/operations/create-pool-token-account.ts b/ccip-sdk/src/cct/solana/token/operations/create-pool-token-account.ts new file mode 100644 index 00000000..2e8adf3b --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/create-pool-token-account.ts @@ -0,0 +1,127 @@ +import { + createAssociatedTokenAccountIdempotentInstruction, + getAssociatedTokenAddressSync, +} from '@solana/spl-token' +import { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { deriveTokenPoolSignerPda } from '../../programs/token-pool.ts' +import { validatePublicKey } from '../../validate.ts' +import { detectMintTokenProgram } from './spl.ts' + +/** Parameters for creating the pool signer PDA's associated token account. **Solana only.** */ +type CreatePoolTokenAccountParams = { + /** SPL token mint (base58). */ + tokenAddress: string + /** Pool state PDA (base58). The pool program ID is derived from its on-chain owner. */ + poolAddress: string +} + +/** Parameters for unsigned Solana create-pool-token-account generation. */ +export type GenerateCreatePoolTokenAccountParams = + SolanaGenerateParams + +/** Unsigned Solana create-pool-token-account result, including the derived addresses. */ +export type GenerateCreatePoolTokenAccountResult = UnsignedSolanaTx & { + /** The pool token ATA (base58), derivable pre-submit. */ + poolTokenAccount: string + /** The pool signer PDA that owns the ATA (base58). */ + poolSignerPda: string +} + +/** Parameters for executing Solana create-pool-token-account. */ +export type ExecuteCreatePoolTokenAccountParams = SolanaExecuteParams + +/** Result of executing Solana create-pool-token-account. */ +export type ExecuteCreatePoolTokenAccountResult = TransactionHash & { + /** The created pool token ATA (base58). */ + poolTokenAccount: string + /** The pool signer PDA that owns the ATA (base58). */ + poolSignerPda: string +} + +/** + * Creates the pool signer PDA's associated token account (the pool "vault") via + * `createAssociatedTokenAccountIdempotentInstruction`, so it is safe to call even when the ATA + * already exists. The pool program ID is read from the pool state account's on-chain owner. + */ +export class CreatePoolTokenAccount extends SolanaOperation< + CreatePoolTokenAccountParams, + GenerateCreatePoolTokenAccountResult, + ExecuteCreatePoolTokenAccountResult +> { + readonly name = 'createPoolTokenAccount' + + /** Validates the mint, pool, and payer public keys before any RPC. */ + protected validate(params: GenerateCreatePoolTokenAccountParams): void { + validatePublicKey(this.name, 'payer', params.payer) + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'poolAddress', params.poolAddress) + } + + /** Builds the idempotent create-ATA instruction for the pool signer PDA. */ + protected async buildUnsigned( + chain: SolanaChain, + params: GenerateCreatePoolTokenAccountParams, + ): Promise { + const payer = new PublicKey(params.payer) + const mint = new PublicKey(params.tokenAddress) + + const poolStateInfo = await chain.connection.getAccountInfo(new PublicKey(params.poolAddress)) + if (!poolStateInfo) { + throw new CCTParamsInvalidError( + this.name, + 'poolAddress', + 'pool state account not found on-chain', + ) + } + const poolProgram = poolStateInfo.owner + + const tokenProgramId = await detectMintTokenProgram(chain, this.name, 'tokenAddress', mint) + + const poolSignerPda = deriveTokenPoolSignerPda(poolProgram, mint) + const poolTokenAta = getAssociatedTokenAddressSync( + mint, + poolSignerPda, + true, // allowOwnerOffCurve — PDAs are off-curve + tokenProgramId, + ) + + const createAtaIx = createAssociatedTokenAccountIdempotentInstruction( + payer, + poolTokenAta, + poolSignerPda, + mint, + tokenProgramId, + ) + + chain.logger.debug( + `${this.name}: poolTokenAta = ${poolTokenAta.toBase58()}, poolSignerPda = ${poolSignerPda.toBase58()}`, + ) + + return { + family: ChainFamily.Solana, + instructions: [createAtaIx], + mainIndex: 0, + poolTokenAccount: poolTokenAta.toBase58(), + poolSignerPda: poolSignerPda.toBase58(), + } + } + + /** Adds the derived ATA and owner to the execute result. */ + protected override resultFromGenerated( + hash: TransactionHash, + tx: GenerateCreatePoolTokenAccountResult, + ): ExecuteCreatePoolTokenAccountResult { + return { ...hash, poolTokenAccount: tx.poolTokenAccount, poolSignerPda: tx.poolSignerPda } + } +} diff --git a/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts b/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts new file mode 100644 index 00000000..0880e8b7 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts @@ -0,0 +1,101 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { SolanaTokenManager } from '../../index.ts' + +const BLOCKHASH = PublicKey.default.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const METAPLEX_PROGRAM = 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s' + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + rpcEndpoint: 'http://localhost:8899', + getAccountInfo: () => assert.fail('should not RPC before validation'), + getMinimumBalanceForRentExemption: async () => 123, + getLatestBlockhash: async () => ({ blockhash: BLOCKHASH, lastValidBlockHeight: 0 }), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedDeployToken({ + decimals: 9, + tokenProgram: 'spl-token', + withMetaplex: false, + payer: PAYER, + ...opts, + }) +} + +describe('Solana token deployToken', () => { + it('builds unsigned SPL mint create instructions without minting supply', async () => { + const unsigned = await generate() + const [createAccountIx, initializeMintIx] = unsigned.instructions + + assert.ok(createAccountIx) + assert.ok(initializeMintIx) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.match(unsigned.tokenAddress, /^[1-9A-HJ-NP-Za-km-z]+$/) + assert.equal('seed' in unsigned, false) + assert.equal(unsigned.instructions.length, 2) + assert.equal(createAccountIx.programId.toBase58(), SystemProgram.programId.toBase58()) + assert.equal(initializeMintIx.programId.toBase58(), TOKEN_PROGRAM_ID.toBase58()) + assert.equal(initializeMintIx.data[0], 0) // InitializeMint, not MintTo + }) + + it('adds Metaplex metadata when requested', async () => { + const unsigned = await generate({ + withMetaplex: true, + name: 'My Token', + symbol: 'MTK', + }) + + assert.equal(unsigned.instructions.length, 3) + assert.equal(unsigned.instructions[2]!.programId.toBase58(), METAPLEX_PROGRAM) + assert.equal(unsigned.instructions[2]!.data[0], 42) // createV1 + }) + + it('appends idempotent ATA + mintTo instructions when initialSupply is set', async () => { + const unsigned = await generate({ initialSupply: 1_000n }) + const [, , createAtaIx, mintToIx] = unsigned.instructions + + assert.equal(unsigned.instructions.length, 4) + assert.ok(createAtaIx) + assert.ok(mintToIx) + // Associated Token Program owns the idempotent create-ATA instruction. + assert.equal(createAtaIx.programId.toBase58(), 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL') + assert.equal(mintToIx.programId.toBase58(), TOKEN_PROGRAM_ID.toBase58()) + assert.equal(mintToIx.data[0], 7) // MintTo + // The created mint address (surfaced from execute via resultFromGenerated) is present. + assert.match(unsigned.tokenAddress, /^[1-9A-HJ-NP-Za-km-z]+$/) + }) + + it('does not mint when initialSupply is zero or omitted', async () => { + const unsigned = await generate({ initialSupply: 0n }) + assert.equal(unsigned.instructions.length, 2) + }) + + it('uses Token-2022 program for mint and metadata', async () => { + const unsigned = await generate({ + tokenProgram: 'token-2022', + withMetaplex: true, + name: 'My Token', + symbol: 'MTK', + }) + + assert.equal(unsigned.instructions[1]!.programId.toBase58(), TOKEN_2022_PROGRAM_ID.toBase58()) + assert.ok( + unsigned.instructions[2]!.keys.some( + (key) => key.pubkey.toBase58() === TOKEN_2022_PROGRAM_ID.toBase58(), + ), + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts new file mode 100644 index 00000000..bc3d086d --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts @@ -0,0 +1,240 @@ +import { TokenStandard, createV1, mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata' +import { + createNoopSigner, + percentAmount, + publicKey as umiPublicKey, + signerIdentity, +} from '@metaplex-foundation/umi' +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' +import { toWeb3JsInstruction } from '@metaplex-foundation/umi-web3js-adapters' +import { + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + createAssociatedTokenAccountIdempotentInstruction, + createInitializeMintInstruction, + createMintToInstruction, + getAssociatedTokenAddressSync, + getMintLen, +} from '@solana/spl-token' +import { PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { validatePublicKey } from '../../validate.ts' + +type BaseDeployTokenParams = { + /** Mint decimals. Must be an integer between 0 and 255. */ + decimals: number + /** Token program that owns the mint: classic SPL Token or Token-2022. */ + tokenProgram: 'spl-token' | 'token-2022' + /** + * Optional initial supply to mint (base units) after creating the mint. When set and greater + * than `0n`, the recipient's associated token account is created idempotently and the supply + * is minted to it with the payer as mint authority. Omit or set to `0n` to create a mint with + * no supply. + */ + initialSupply?: bigint + /** + * Optional base58 recipient of the initial supply. Only used when `initialSupply` is set and + * greater than `0n`. Defaults to the payer. + */ + recipient?: string +} + +/** + * Parameters for creating a Solana SPL mint. + * + * Set `withMetaplex: true` to create Metaplex metadata; `name` and `symbol` are required; + */ +type DeployTokenParams = BaseDeployTokenParams & + ( + | { withMetaplex: false } + | { + withMetaplex: true + /** Token display name for Metaplex metadata. Max 32 characters. */ + name: string + /** Token symbol for Metaplex metadata. Max 10 characters. */ + symbol: string + /** Metadata URI for Metaplex metadata JSON. Optional; defaults to an empty string when omitted. */ + uri?: string | undefined + } + ) + +/** Parameters for unsigned Solana token deploy generation. */ +export type GenerateDeployTokenParams = SolanaGenerateParams + +/** Unsigned token deploy transaction plus the created mint address. */ +export type GenerateDeployTokenResult = UnsignedSolanaTx & { tokenAddress: string } + +/** Parameters for executing Solana token deploy. */ +export type ExecuteDeployTokenParams = SolanaExecuteParams + +/** Result of executing Solana token deploy, including the created mint address. */ +export type ExecuteDeployTokenResult = TransactionHash & { + /** The created mint address (base58). */ + tokenAddress: string +} + +/** Creates a Solana SPL mint, optionally with Metaplex metadata. Does not mint supply. */ +export class DeployToken extends SolanaOperation< + DeployTokenParams, + GenerateDeployTokenResult, + ExecuteDeployTokenResult +> { + readonly name = 'deployToken' + + /** Validates mint and metadata params before any RPC. */ + protected validate(params: GenerateDeployTokenParams): void { + validatePublicKey(this.name, 'payer', params.payer) + if (!Number.isInteger(params.decimals) || params.decimals < 0 || params.decimals > 255) { + throw new CCTParamsInvalidError(this.name, 'decimals', 'must be an integer between 0 and 255') + } + if (!['spl-token', 'token-2022'].includes(params.tokenProgram)) { + throw new CCTParamsInvalidError(this.name, 'tokenProgram', 'must be spl-token or token-2022') + } + if (typeof params.withMetaplex !== 'boolean') { + throw new CCTParamsInvalidError(this.name, 'withMetaplex', 'must be a boolean') + } + // POC parity: initial-supply mint params (not in DAPP-10498 deploy-token) + if (params.initialSupply !== undefined) { + if (typeof params.initialSupply !== 'bigint' || params.initialSupply < 0n) { + throw new CCTParamsInvalidError( + this.name, + 'initialSupply', + 'must be a non-negative bigint when provided', + ) + } + if (params.recipient !== undefined) { + validatePublicKey(this.name, 'recipient', params.recipient) + } + } + if (!params.withMetaplex) return + if (!params.name || params.name.length > 32) { + throw new CCTParamsInvalidError( + this.name, + 'name', + 'is required and must be <= 32 characters when withMetaplex is true', + ) + } + if (!params.symbol || params.symbol.length > 10) { + throw new CCTParamsInvalidError( + this.name, + 'symbol', + 'is required and must be <= 10 characters when withMetaplex is true', + ) + } + if (params.uri !== undefined && typeof params.uri !== 'string') { + throw new CCTParamsInvalidError(this.name, 'uri', 'must be a string when provided') + } + } + + /** Builds the unsigned Solana mint creation instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + params: GenerateDeployTokenParams, + ): Promise { + const payer = new PublicKey(params.payer) + const tokenProgram = + params.tokenProgram === 'token-2022' ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID + + const seed = `mint_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` + const mint = await PublicKey.createWithSeed(payer, seed, tokenProgram) + const mintSpace = getMintLen([]) + const lamports = await chain.connection.getMinimumBalanceForRentExemption(mintSpace) + + const instructions = [ + SystemProgram.createAccountWithSeed({ + fromPubkey: payer, + newAccountPubkey: mint, + basePubkey: payer, + seed, + lamports, + space: mintSpace, + programId: tokenProgram, + }), + createInitializeMintInstruction(mint, params.decimals, payer, payer, tokenProgram), + ] + + if (params.withMetaplex) + instructions.push( + ...createMetadataInstructions(chain, mint, payer, tokenProgram, params.decimals, { + name: params.name, + symbol: params.symbol, + uri: params.uri ?? '', + }), + ) + + // POC parity: mint initial supply (not in DAPP-10498 deploy-token). Mint authority is the + // payer, matching the mint-init above. See legacy token-admin/solana/index.ts L697-736. + const initialSupply = params.initialSupply ?? 0n + if (initialSupply > 0n) { + const recipient = params.recipient ? new PublicKey(params.recipient) : payer + const ata = getAssociatedTokenAddressSync(mint, recipient, false, tokenProgram) + instructions.push( + createAssociatedTokenAccountIdempotentInstruction( + payer, + ata, + recipient, + mint, + tokenProgram, + ), + createMintToInstruction(mint, ata, payer, initialSupply, [], tokenProgram), + ) + } + + chain.logger.debug( + `${this.name}: mint = ${mint.toBase58()}, tokenProgram = ${tokenProgram.toBase58()}`, + ) + return { + family: ChainFamily.Solana, + instructions, + mainIndex: 0, + tokenAddress: mint.toBase58(), + } + } + + // POC parity: surface the created mint address from execute (not in DAPP-10498 deploy-token). + /** Adds the created mint address to the execute result. */ + protected override resultFromGenerated( + hash: TransactionHash, + tx: GenerateDeployTokenResult, + ): ExecuteDeployTokenResult { + return { ...hash, tokenAddress: tx.tokenAddress } + } +} + +function createMetadataInstructions( + chain: SolanaChain, + mint: PublicKey, + payer: PublicKey, + tokenProgram: PublicKey, + decimals: number, + params: { name: string; symbol: string; uri: string }, +) { + const payerSigner = createNoopSigner(umiPublicKey(payer.toBase58())) + const umi = createUmi(chain.connection).use(mplTokenMetadata()).use(signerIdentity(payerSigner)) + + return createV1(umi, { + mint: umiPublicKey(mint.toBase58()), + authority: payerSigner, + payer: payerSigner, + updateAuthority: payerSigner, + splTokenProgram: umiPublicKey(tokenProgram.toBase58()), + name: params.name, + symbol: params.symbol, + uri: params.uri, + sellerFeeBasisPoints: percentAmount(0), + decimals, + tokenStandard: TokenStandard.Fungible, + }) + .getInstructions() + .map(toWeb3JsInstruction) +} diff --git a/ccip-sdk/src/cct/solana/token/operations/grant-mint-burn-access.test.ts b/ccip-sdk/src/cct/solana/token/operations/grant-mint-burn-access.test.ts new file mode 100644 index 00000000..bb18c28f --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/grant-mint-burn-access.test.ts @@ -0,0 +1,95 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { AuthorityType, TOKEN_PROGRAM_ID, createSetAuthorityInstruction } from '@solana/spl-token' +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { GrantMintBurnAccess } from './grant-mint-burn-access.ts' + +const MINT = Keypair.generate().publicKey +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async (key: PublicKey) => + key.equals(MINT) ? { owner: TOKEN_PROGRAM_ID } : null, + }, + } as unknown as SolanaChain +} + +function noRpcChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { getAccountInfo: () => assert.fail('should not RPC before validation') }, + } as unknown as SolanaChain +} + +describe('Solana token grantMintBurnAccess', () => { + it('transfers mint authority to the grantee (matches setAuthority)', async () => { + const unsigned = await new GrantMintBurnAccess().generate(stubChain(), { + payer: PAYER, + tokenAddress: MINT.toBase58(), + authority: AUTHORITY, + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.instructions.length, 1) + + const ref = createSetAuthorityInstruction( + MINT, + new PublicKey(PAYER), + AuthorityType.MintTokens, + new PublicKey(AUTHORITY), + [], + TOKEN_PROGRAM_ID, + ) + assert.equal(unsigned.instructions[0]!.data.toString('hex'), ref.data.toString('hex')) + assert.deepEqual( + unsigned.instructions[0]!.keys.map((k) => k.pubkey.toBase58()), + ref.keys.map((k) => k.pubkey.toBase58()), + ) + }) + + it('accepts role mint and mintAndBurn', async () => { + for (const role of ['mint', 'mintAndBurn'] as const) { + const unsigned = await new GrantMintBurnAccess().generate(stubChain(), { + payer: PAYER, + tokenAddress: MINT.toBase58(), + authority: AUTHORITY, + role, + }) + assert.equal(unsigned.instructions.length, 1) + } + }) + + it("rejects role 'burn' before any RPC", async () => { + await assert.rejects( + () => + new GrantMintBurnAccess().generate(noRpcChain(), { + payer: PAYER, + tokenAddress: MINT.toBase58(), + authority: AUTHORITY, + role: 'burn', + }), + CCTParamsInvalidError, + ) + }) + + it('rejects an invalid tokenAddress before any RPC', async () => { + await assert.rejects( + () => + new GrantMintBurnAccess().generate(noRpcChain(), { + payer: PAYER, + tokenAddress: 'not-a-key', + authority: AUTHORITY, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token/operations/grant-mint-burn-access.ts b/ccip-sdk/src/cct/solana/token/operations/grant-mint-burn-access.ts new file mode 100644 index 00000000..e90e0e7b --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/grant-mint-burn-access.ts @@ -0,0 +1,75 @@ +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { validatePublicKey } from '../../validate.ts' +import { TransferMintAuthority } from './transfer-mint-authority.ts' + +/** + * Which role(s) to grant. On Solana, SPL tokens have a single mint authority and burn is + * implicit for any holder, so only `'mint'` and `'mintAndBurn'` are valid; `'burn'` is rejected. + */ +export type MintBurnRole = 'mint' | 'burn' | 'mintAndBurn' + +/** Parameters for granting mint/burn access on a Solana SPL token. */ +type GrantMintBurnAccessParams = { + /** SPL token mint (base58) whose mint authority is being reassigned. */ + tokenAddress: string + /** Address to receive mint authority (base58) — pool multisig or signer PDA. */ + authority: string + /** Which role(s) to grant. Defaults to `'mintAndBurn'`; `'burn'` is not supported on Solana. */ + role?: MintBurnRole +} + +/** Parameters for unsigned Solana grant-mint-burn-access generation. */ +export type GenerateGrantMintBurnAccessParams = SolanaGenerateParams + +/** Unsigned Solana grant-mint-burn-access result. */ +export type GenerateGrantMintBurnAccessResult = UnsignedSolanaTx + +/** Parameters for executing Solana grant-mint-burn-access. */ +export type ExecuteGrantMintBurnAccessParams = SolanaExecuteParams + +/** Result of executing Solana grant-mint-burn-access. */ +export type ExecuteGrantMintBurnAccessResult = TransactionHash + +/** + * Grants mint/burn access on a Solana SPL token by transferring the mint authority to + * `authority` (`setAuthority(MintTokens)`). Thin wrapper over {@link TransferMintAuthority} + * that maps `tokenAddress` → `mint` and `authority` → `newMintAuthority`. + */ +export class GrantMintBurnAccess extends SolanaOperation { + readonly name = 'grantMintBurnAccess' + readonly #transfer = new TransferMintAuthority() + + /** Validates public keys and rejects the unsupported `'burn'` role before any RPC. */ + protected validate(params: GenerateGrantMintBurnAccessParams): void { + validatePublicKey(this.name, 'payer', params.payer) + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'authority', params.authority) + if (params.role === 'burn') { + throw new CCTParamsInvalidError( + this.name, + 'role', + "Solana SPL tokens do not have a separate burn authority — any token holder can burn. Use 'mint' or 'mintAndBurn' instead", + ) + } + } + + /** Delegates to {@link TransferMintAuthority} to build the `setAuthority` instruction. */ + protected buildUnsigned( + chain: SolanaChain, + params: GenerateGrantMintBurnAccessParams, + ): Promise { + return this.#transfer.generate(chain, { + payer: params.payer, + mint: params.tokenAddress, + newMintAuthority: params.authority, + }) + } +} diff --git a/ccip-sdk/src/cct/solana/token/operations/index.ts b/ccip-sdk/src/cct/solana/token/operations/index.ts new file mode 100644 index 00000000..fbf48fad --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/index.ts @@ -0,0 +1,6 @@ +export * from './deploy-token.ts' +export * from './transfer-mint-authority.ts' +export * from './grant-mint-burn-access.ts' +export * from './revoke-mint-burn-access.ts' +export * from './create-pool-mint-authority-multisig.ts' +export * from './create-pool-token-account.ts' diff --git a/ccip-sdk/src/cct/solana/token/operations/revoke-mint-burn-access.test.ts b/ccip-sdk/src/cct/solana/token/operations/revoke-mint-burn-access.test.ts new file mode 100644 index 00000000..ed3aa757 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/revoke-mint-burn-access.test.ts @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair } from '@solana/web3.js' + +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { RevokeMintBurnAccess } from './revoke-mint-burn-access.ts' + +const PAYER = Keypair.generate().publicKey.toBase58() +const TOKEN = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() + +function noRpcChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { getAccountInfo: () => assert.fail('should not RPC — revoke is unsupported') }, + } as unknown as SolanaChain +} + +describe('Solana token revokeMintBurnAccess', () => { + it('is unsupported and always rejects before any RPC', async () => { + await assert.rejects( + () => + new RevokeMintBurnAccess().generate(noRpcChain(), { + payer: PAYER, + tokenAddress: TOKEN, + authority: AUTHORITY, + role: 'mint', + }), + (error: unknown) => { + assert.ok(error instanceof CCTParamsInvalidError) + assert.equal(error.context.operation, 'revokeMintBurnAccess') + return true + }, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token/operations/revoke-mint-burn-access.ts b/ccip-sdk/src/cct/solana/token/operations/revoke-mint-burn-access.ts new file mode 100644 index 00000000..f6d2f396 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/revoke-mint-burn-access.ts @@ -0,0 +1,64 @@ +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' + +/** Parameters for revoking mint/burn access — unsupported on Solana. */ +type RevokeMintBurnAccessParams = { + /** SPL token mint (base58). */ + tokenAddress: string + /** Address whose access would be revoked (base58). */ + authority: string + /** Which role to revoke. */ + role: 'mint' | 'burn' +} + +/** Parameters for unsigned Solana revoke-mint-burn-access generation. */ +export type GenerateRevokeMintBurnAccessParams = SolanaGenerateParams + +/** Unsigned Solana revoke-mint-burn-access result. */ +export type GenerateRevokeMintBurnAccessResult = UnsignedSolanaTx + +/** Parameters for executing Solana revoke-mint-burn-access. */ +export type ExecuteRevokeMintBurnAccessParams = SolanaExecuteParams + +/** Result of executing Solana revoke-mint-burn-access. */ +export type ExecuteRevokeMintBurnAccessResult = TransactionHash + +/** + * Not supported on Solana. SPL tokens use a single mint-authority model with no role-based + * grants to revoke — always throws {@link CCTParamsInvalidError}. Use `transferMintAuthority` + * to move the mint authority elsewhere instead. + */ +export class RevokeMintBurnAccess extends SolanaOperation { + readonly name = 'revokeMintBurnAccess' + + /** Always throws — role-based revoke is not supported on Solana. */ + protected validate(_params: GenerateRevokeMintBurnAccessParams): void { + throw new CCTParamsInvalidError( + this.name, + 'chain', + 'Solana SPL tokens do not support role-based revoke. Use transferMintAuthority to transfer mint authority instead', + ) + } + + /** Unreachable in practice — {@link validate} always throws first. */ + protected buildUnsigned( + _chain: SolanaChain, + params: GenerateRevokeMintBurnAccessParams, + ): Promise { + this.validate(params) + return Promise.reject( + new CCTParamsInvalidError( + this.name, + 'chain', + 'Solana SPL tokens do not support role-based revoke. Use transferMintAuthority to transfer mint authority instead', + ), + ) + } +} diff --git a/ccip-sdk/src/cct/solana/token/operations/spl.ts b/ccip-sdk/src/cct/solana/token/operations/spl.ts new file mode 100644 index 00000000..237515ca --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/spl.ts @@ -0,0 +1,32 @@ +import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token' +import type { PublicKey } from '@solana/web3.js' + +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +/** + * Resolves the SPL Token program (classic or Token-2022) that owns `mint` by + * reading the mint account on-chain. Throws {@link CCTParamsInvalidError} when the + * mint is missing or owned by an unexpected program. + */ +export async function detectMintTokenProgram( + chain: SolanaChain, + operation: string, + param: string, + mint: PublicKey, +): Promise { + const mintInfo = await chain.connection.getAccountInfo(mint) + if (!mintInfo) { + throw new CCTParamsInvalidError(operation, param, 'mint account not found on-chain') + } + const isToken2022 = mintInfo.owner.equals(TOKEN_2022_PROGRAM_ID) + const isTokenProgram = mintInfo.owner.equals(TOKEN_PROGRAM_ID) + if (!isToken2022 && !isTokenProgram) { + throw new CCTParamsInvalidError( + operation, + param, + `mint owned by ${mintInfo.owner.toBase58()}, expected SPL Token or Token-2022`, + ) + } + return mintInfo.owner +} diff --git a/ccip-sdk/src/cct/solana/token/operations/transfer-mint-authority.test.ts b/ccip-sdk/src/cct/solana/token/operations/transfer-mint-authority.test.ts new file mode 100644 index 00000000..019ea117 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/transfer-mint-authority.test.ts @@ -0,0 +1,102 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { + AuthorityType, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + createSetAuthorityInstruction, +} from '@solana/spl-token' +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { TransferMintAuthority } from './transfer-mint-authority.ts' + +const MINT = Keypair.generate().publicKey +const PAYER = Keypair.generate().publicKey.toBase58() +const NEW_AUTH = Keypair.generate().publicKey.toBase58() + +function stubChain(owner: PublicKey = TOKEN_PROGRAM_ID): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async (key: PublicKey) => (key.equals(MINT) ? { owner } : null), + }, + } as unknown as SolanaChain +} + +function noRpcChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { getAccountInfo: () => assert.fail('should not RPC before validation') }, + } as unknown as SolanaChain +} + +describe('Solana token transferMintAuthority', () => { + it('matches createSetAuthorityInstruction(MintTokens) for spl-token', async () => { + const unsigned = await new TransferMintAuthority().generate(stubChain(), { + payer: PAYER, + mint: MINT.toBase58(), + newMintAuthority: NEW_AUTH, + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + + const ix = unsigned.instructions[0]! + const ref = createSetAuthorityInstruction( + MINT, + new PublicKey(PAYER), + AuthorityType.MintTokens, + new PublicKey(NEW_AUTH), + [], + TOKEN_PROGRAM_ID, + ) + assert.equal(ix.programId.toBase58(), ref.programId.toBase58()) + assert.equal(ix.data.toString('hex'), ref.data.toString('hex')) + assert.deepEqual( + ix.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ref.keys.map((k) => [k.pubkey.toBase58(), k.isSigner, k.isWritable]), + ) + }) + + it('targets the Token-2022 program when the mint is owned by it', async () => { + const unsigned = await new TransferMintAuthority().generate(stubChain(TOKEN_2022_PROGRAM_ID), { + payer: PAYER, + mint: MINT.toBase58(), + newMintAuthority: NEW_AUTH, + }) + assert.equal(unsigned.instructions[0]!.programId.toBase58(), TOKEN_2022_PROGRAM_ID.toBase58()) + }) + + it('rejects an invalid mint before any RPC', async () => { + await assert.rejects( + () => + new TransferMintAuthority().generate(noRpcChain(), { + payer: PAYER, + mint: 'not-a-key', + newMintAuthority: NEW_AUTH, + }), + CCTParamsInvalidError, + ) + }) + + it('rejects when the mint account is missing on-chain', async () => { + const chain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { getAccountInfo: async () => null }, + } as unknown as SolanaChain + await assert.rejects( + () => + new TransferMintAuthority().generate(chain, { + payer: PAYER, + mint: MINT.toBase58(), + newMintAuthority: NEW_AUTH, + }), + CCTParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token/operations/transfer-mint-authority.ts b/ccip-sdk/src/cct/solana/token/operations/transfer-mint-authority.ts new file mode 100644 index 00000000..49a4eb89 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/transfer-mint-authority.ts @@ -0,0 +1,75 @@ +import { AuthorityType, createSetAuthorityInstruction } from '@solana/spl-token' +import { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { validatePublicKey } from '../../validate.ts' +import { detectMintTokenProgram } from './spl.ts' + +/** Parameters for transferring SPL mint authority. The `payer` must be the current mint authority. */ +type TransferMintAuthorityParams = { + /** SPL token mint to reassign. */ + mint: string + /** New mint authority (base58) — typically a multisig or pool signer PDA. */ + newMintAuthority: string +} + +/** Parameters for unsigned Solana transfer-mint-authority generation. */ +export type GenerateTransferMintAuthorityParams = SolanaGenerateParams + +/** Unsigned Solana transfer-mint-authority result. */ +export type GenerateTransferMintAuthorityResult = UnsignedSolanaTx + +/** Parameters for executing Solana transfer-mint-authority. */ +export type ExecuteTransferMintAuthorityParams = SolanaExecuteParams + +/** Result of executing Solana transfer-mint-authority. */ +export type ExecuteTransferMintAuthorityResult = TransactionHash + +/** + * Reassigns an SPL mint's `MintTokens` authority via `setAuthority`. The executing + * wallet (or `payer` for unsigned generation) must be the current mint authority. + */ +export class TransferMintAuthority extends SolanaOperation { + readonly name = 'transferMintAuthority' + + /** Validates the mint, new authority, and payer public keys before any RPC. */ + protected validate(params: GenerateTransferMintAuthorityParams): void { + validatePublicKey(this.name, 'payer', params.payer) + validatePublicKey(this.name, 'mint', params.mint) + validatePublicKey(this.name, 'newMintAuthority', params.newMintAuthority) + } + + /** Builds the unsigned SPL `setAuthority(MintTokens)` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + params: GenerateTransferMintAuthorityParams, + ): Promise { + const currentAuthority = new PublicKey(params.payer) + const mint = new PublicKey(params.mint) + const newMintAuthority = new PublicKey(params.newMintAuthority) + + const tokenProgramId = await detectMintTokenProgram(chain, this.name, 'mint', mint) + + const instruction = createSetAuthorityInstruction( + mint, + currentAuthority, + AuthorityType.MintTokens, + newMintAuthority, + [], + tokenProgramId, + ) + + chain.logger.debug( + `${this.name}: mint = ${mint.toBase58()}, newMintAuthority = ${newMintAuthority.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/validate.test.ts b/ccip-sdk/src/cct/solana/validate.test.ts new file mode 100644 index 00000000..345c1e78 --- /dev/null +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { PublicKey } from '@solana/web3.js' + +import { validatePublicKey, validateWritableIndexes } from './validate.ts' +import { CCTParamsInvalidError } from '../errors.ts' + +describe('cct/solana validate', () => { + it('accepts valid public keys', () => { + assert.doesNotThrow(() => validatePublicKey('op', 'payer', PublicKey.default.toBase58())) + }) + + it('rejects non-string public keys', () => { + assert.throws( + () => validatePublicKey('op', 'payer', 123), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'op' && + err.context.param === 'payer', + ) + }) + + it('rejects invalid public key strings', () => { + assert.throws( + () => validatePublicKey('op', 'payer', 'nope'), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'op' && + err.context.param === 'payer', + ) + }) + + it('accepts omitted and valid writable indexes', () => { + assert.doesNotThrow(() => validateWritableIndexes('op', 'writableIndexes', undefined)) + assert.doesNotThrow(() => validateWritableIndexes('op', 'writableIndexes', [0, 3, 255])) + }) + + it('rejects empty writable indexes', () => { + assert.throws( + () => validateWritableIndexes('op', 'writableIndexes', []), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'op' && + err.context.param === 'writableIndexes', + ) + }) + + it('rejects writable indexes outside byte range', () => { + assert.throws( + () => validateWritableIndexes('op', 'writableIndexes', [256]), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'op' && + err.context.param === 'writableIndexes[0]', + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts new file mode 100644 index 00000000..bb4fdae6 --- /dev/null +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -0,0 +1,51 @@ +import { PublicKey } from '@solana/web3.js' + +import { CCIPAddressInvalidError } from '../../errors/index.ts' +import { ChainFamily } from '../../networks.ts' +import { CCTParamsInvalidError } from '../errors.ts' + +/** Asserts `value` is a valid Solana public key string. */ +export function validatePublicKey(operation: string, param: string, value: unknown): void { + if (typeof value !== 'string') { + throw new CCTParamsInvalidError( + operation, + param, + `must be a valid Solana public key, got ${String(value)}`, + ) + } + + try { + new PublicKey(value) + } catch { + throw new CCTParamsInvalidError( + operation, + param, + `must be a valid Solana public key, got ${String(value)}`, + { + cause: new CCIPAddressInvalidError(value, ChainFamily.Solana), + }, + ) + } +} + +/** Asserts ALT writable indexes are a non-empty list of byte values when provided. */ +export function validateWritableIndexes( + operation: string, + param: string, + writableIndexes: unknown, +): void { + if (writableIndexes === undefined) return + if (!Array.isArray(writableIndexes) || writableIndexes.length === 0) { + throw new CCTParamsInvalidError(operation, param, 'must be a non-empty array') + } + + for (const [i, index] of writableIndexes.entries()) { + if (!Number.isInteger(index) || index < 0 || index > 255) { + throw new CCTParamsInvalidError( + operation, + `${param}[${i}]`, + 'must be an integer between 0 and 255', + ) + } + } +} diff --git a/ccip-sdk/src/cct/token-manager.ts b/ccip-sdk/src/cct/token-manager.ts new file mode 100644 index 00000000..9efe7b42 --- /dev/null +++ b/ccip-sdk/src/cct/token-manager.ts @@ -0,0 +1,18 @@ +/** + * Cross-family CCT manager base, the CCT analogue of core's {@link Chain}. + * Family-specific subclasses hold the chain and expose admin operations. + * + * @packageDocumentation + */ + +import type { Chain } from '../chain.ts' +import type { ChainFamily } from '../networks.ts' + +/** + * Abstract entry point for CCT admin writes on a chain family. Subclasses hold + * the concrete {@link Chain} and delegate to {@link Operation} instances. + */ +export abstract class TokenManager { + /** Chain this manager builds and submits through. */ + abstract readonly chain: Chain +} diff --git a/ccip-sdk/src/cct/treeshake.test.ts b/ccip-sdk/src/cct/treeshake.test.ts new file mode 100644 index 00000000..31abc7a5 --- /dev/null +++ b/ccip-sdk/src/cct/treeshake.test.ts @@ -0,0 +1,163 @@ +/** + * Tree-shaking verification tests for the CCT (`cct/`) facades. + * + * Uses esbuild JS API to bundle specific entry points and verifies that: + * 1. Each bundle contains its expected primary export (positive assertion) + * 2. Unwanted code (bytecodes, cross-chain deps) is excluded (negative assertion) + * 3. Heavy bytecode/Move data stays code-split out of the entry chunk + */ + +import assert from 'node:assert/strict' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { describe, it } from 'node:test' + +import * as esbuild from 'esbuild' + +/** Derive external packages from package.json dependencies + peerDependencies. */ +function getExternalPackages(): string[] { + const pkgPath = path.resolve(import.meta.dirname, '../../package.json') + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { + dependencies?: Record + peerDependencies?: Record + } + return [ + ...Object.keys(pkg.dependencies ?? {}), + ...Object.keys(pkg.peerDependencies ?? {}), + 'node:*', + ] +} + +const EXTERNAL = getExternalPackages() + +/** SDK source root for import paths. */ +const sdkSrc = path.resolve(import.meta.dirname, '..') + +/** + * Bundle entry code with esbuild and return the output string. + * Uses the JS API for speed and determinism (no npx cold-start). + */ +async function bundle(entryCode: string, opts?: { splitting?: boolean }): Promise { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'treeshake-')) + const entryFile = path.join(tmpDir, 'entry.ts') + + try { + fs.writeFileSync(entryFile, entryCode) + + const splitting = opts?.splitting ?? false + const outdir = path.join(tmpDir, 'out') + await esbuild.build({ + entryPoints: [entryFile], + bundle: true, + format: 'esm', + treeShaking: true, + platform: 'node', + write: true, + outdir, + splitting, + external: EXTERNAL, + }) + + return fs.readFileSync(path.join(outdir, 'entry.js'), 'utf8') + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } +} + +// All EVM bytecode constant names (cct/evm bytecodes) +const EVM_BYTECODES = [ + 'CROSS_CHAIN_TOKEN_BYTECODE', + 'CROSS_CHAIN_POOL_TOKEN_BYTECODE', + 'BURN_MINT_TOKEN_POOL_BYTECODE', + 'LOCK_RELEASE_TOKEN_POOL_BYTECODE', +] + +// Representative Aptos Move module markers (one per bytecode file) +const APTOS_MOVE_MARKERS = [ + 'module managed_token::managed_token', + 'module managed_token_pool::managed_token_pool', + 'module burn_mint_token_pool::burn_mint_token_pool', + 'module lock_release_token_pool::lock_release_token_pool', + 'module regulated_token_pool::regulated_token_pool', + 'module ccip::token_admin_registry', +] + +describe('cct facades — tree-shaking verification', () => { + // ------------------------------------------------------------------------- + // Main SDK entry — must exclude all CCT bytecodes and Move sources + // ------------------------------------------------------------------------- + it('main entry excludes all EVM bytecodes and Aptos Move sources', async () => { + const output = await bundle(`import '${sdkSrc}/index.ts'`) + + for (const name of EVM_BYTECODES) { + assert.ok(!output.includes(name), `main entry should not contain ${name}`) + } + for (const marker of APTOS_MOVE_MARKERS) { + assert.ok(!output.includes(marker), `main entry should not contain "${marker}"`) + } + }) + + // ------------------------------------------------------------------------- + // Cross-chain isolation: full 3×2 matrix over the cct/ facades + // ------------------------------------------------------------------------- + const chains = [ + { name: 'EVM', class: 'EVMTokenManager', path: 'cct/evm/index.ts' }, + { name: 'Solana', class: 'SolanaTokenManager', path: 'cct/solana/index.ts' }, + { name: 'Aptos', class: 'AptosTokenManager', path: 'cct/aptos/index.ts' }, + ] as const + + for (const importer of chains) { + for (const excluded of chains) { + if (importer.name === excluded.name) continue + + it(`${importer.name} manager does NOT include ${excluded.name} manager code`, async () => { + const output = await bundle( + `import { ${importer.class} } from '${sdkSrc}/${importer.path}'; console.log(${importer.class})`, + ) + + // Positive: the bundle contains the expected facade class + assert.ok(output.includes(importer.class), `bundle should contain ${importer.class}`) + + // Negative: the bundle excludes the other chain's facade class + assert.ok( + !output.includes(excluded.class), + `${importer.name} manager should not contain ${excluded.class}`, + ) + }) + } + } + + // ------------------------------------------------------------------------- + // Code-splitting: bytecodes and Move sources stay in separate chunks + // ------------------------------------------------------------------------- + it('EVM manager entry chunk does NOT eagerly include bytecode data (code-splitting)', async () => { + const output = await bundle( + `import { EVMTokenManager } from '${sdkSrc}/cct/evm/index.ts'; console.log(EVMTokenManager)`, + { splitting: true }, + ) + + assert.ok(output.includes('EVMTokenManager'), 'entry chunk should contain EVMTokenManager') + + // Distinctive substring from CrossChainToken bytecode hex + assert.ok( + !output.includes('module managed_token::managed_token'), + 'entry chunk should not contain Aptos Move source data', + ) + }) + + it('Aptos manager entry chunk does NOT eagerly include Move source data (code-splitting)', async () => { + const output = await bundle( + `import { AptosTokenManager } from '${sdkSrc}/cct/aptos/index.ts'; console.log(AptosTokenManager)`, + { splitting: true }, + ) + + assert.ok(output.includes('AptosTokenManager'), 'entry chunk should contain AptosTokenManager') + + // Distinctive substring from managed_token Move source + assert.ok( + !output.includes('module managed_token::managed_token'), + 'entry chunk should not contain managed_token Move source code', + ) + }) +}) diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index 250f7783..3b1d4b99 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -543,6 +543,10 @@ export type TokenPoolConfig = { token: string /** Address of the CCIP router this pool is registered with. */ router: string + /** Current owner of the token pool. */ + owner: string + /** Proposed new owner (if an ownership transfer is pending). */ + proposedOwner?: string /** * Version identifier string (e.g., "BurnMintTokenPool 1.5.1"). * @@ -550,6 +554,30 @@ export type TokenPoolConfig = { * May be undefined for older pool implementations that don't expose this method. */ typeAndVersion?: string + /** + * Address of the rate limit admin, if set. + * + * @remarks + * The rate limit admin can update rate limiter configs without being the pool owner. + * Not available on Aptos (setRateLimitAdmin is unsupported). + * A zero-address value indicates no rate limit admin is set. + */ + rateLimitAdmin?: string + /** + * Address of the fee admin (EVM v2.0+ only). + * + * @remarks + * The fee admin can configure token transfer fees. + * Only available on EVM pools v2.0+ (from `getDynamicConfig()`). + */ + feeAdmin?: string + /** + * Min custom block confirmations for Faster-Than-Finality (FTF), + * if TokenPool version \>= v2.0.0 and FTF is supported on this lane. + * `0` indicates FTF is supported but not enabled for this token; `>0` indicates FTF is enabled + * with this many minimum confirmations. + */ + minBlockConfirmations?: number /** * Token transfer fee configuration from the pool contract. * Only present when {@link TokenTransferFeeOpts} is provided to @@ -591,6 +619,10 @@ export type RegistryTokenConfig = { pendingAdministrator?: string /** Address of the token pool authorized to handle this token's transfers. */ tokenPool?: string + /** Address Lookup Table for pool accounts (Solana only). */ + poolLookupTable?: string + /** Addresses stored in the pool lookup table (Solana only). */ + poolLookupTableEntries?: string[] } /** diff --git a/ccip-sdk/src/commits.test.ts b/ccip-sdk/src/commits.test.ts index 758bc6ef..152bf09e 100644 --- a/ccip-sdk/src/commits.test.ts +++ b/ccip-sdk/src/commits.test.ts @@ -153,9 +153,15 @@ class MockChain extends Chain { async getTokenPoolConfig(_tokenPool: string): Promise<{ token: string router: string + owner: string typeAndVersion?: string }> { - return { token: '0xToken', router: '0xRouter', typeAndVersion: 'TokenPool 1.5.0' } + return { + token: '0xToken', + router: '0xRouter', + owner: '0xOwner', + typeAndVersion: 'TokenPool 1.5.0', + } } async getTokenPoolRemotes(_pool: string, _remoteChainSelector: bigint): Promise { diff --git a/ccip-sdk/src/errors/codes.ts b/ccip-sdk/src/errors/codes.ts index f50b6c73..7c7152d9 100644 --- a/ccip-sdk/src/errors/codes.ts +++ b/ccip-sdk/src/errors/codes.ts @@ -169,6 +169,112 @@ export const CCIPErrorCode = { INSUFFICIENT_BALANCE: 'INSUFFICIENT_BALANCE', INTERACTIVE_REQUIRED: 'INTERACTIVE_REQUIRED', + // Token Deployment + TOKEN_DEPLOY_PARAMS_INVALID: 'TOKEN_DEPLOY_PARAMS_INVALID', + TOKEN_DEPLOY_FAILED: 'TOKEN_DEPLOY_FAILED', + + // Pool Deployment + POOL_DEPLOY_PARAMS_INVALID: 'POOL_DEPLOY_PARAMS_INVALID', + POOL_DEPLOY_FAILED: 'POOL_DEPLOY_FAILED', + POOL_NOT_INITIALIZED: 'POOL_NOT_INITIALIZED', + + // Propose Admin Role + PROPOSE_ADMIN_ROLE_PARAMS_INVALID: 'PROPOSE_ADMIN_ROLE_PARAMS_INVALID', + PROPOSE_ADMIN_ROLE_FAILED: 'PROPOSE_ADMIN_ROLE_FAILED', + + // Accept Admin Role + ACCEPT_ADMIN_ROLE_PARAMS_INVALID: 'ACCEPT_ADMIN_ROLE_PARAMS_INVALID', + ACCEPT_ADMIN_ROLE_FAILED: 'ACCEPT_ADMIN_ROLE_FAILED', + + // Transfer Admin Role + TRANSFER_ADMIN_ROLE_PARAMS_INVALID: 'TRANSFER_ADMIN_ROLE_PARAMS_INVALID', + TRANSFER_ADMIN_ROLE_FAILED: 'TRANSFER_ADMIN_ROLE_FAILED', + + // Apply Chain Updates + APPLY_CHAIN_UPDATES_PARAMS_INVALID: 'APPLY_CHAIN_UPDATES_PARAMS_INVALID', + APPLY_CHAIN_UPDATES_FAILED: 'APPLY_CHAIN_UPDATES_FAILED', + + // Append Remote Pool Addresses + APPEND_REMOTE_POOL_ADDRESSES_PARAMS_INVALID: 'APPEND_REMOTE_POOL_ADDRESSES_PARAMS_INVALID', + APPEND_REMOTE_POOL_ADDRESSES_FAILED: 'APPEND_REMOTE_POOL_ADDRESSES_FAILED', + + // Delete Chain Config + DELETE_CHAIN_CONFIG_PARAMS_INVALID: 'DELETE_CHAIN_CONFIG_PARAMS_INVALID', + DELETE_CHAIN_CONFIG_FAILED: 'DELETE_CHAIN_CONFIG_FAILED', + + // Remove Remote Pool Addresses + REMOVE_REMOTE_POOL_ADDRESSES_PARAMS_INVALID: 'REMOVE_REMOTE_POOL_ADDRESSES_PARAMS_INVALID', + REMOVE_REMOTE_POOL_ADDRESSES_FAILED: 'REMOVE_REMOTE_POOL_ADDRESSES_FAILED', + + // Set Chain Rate Limiter Config + SET_RATE_LIMITER_CONFIG_PARAMS_INVALID: 'SET_RATE_LIMITER_CONFIG_PARAMS_INVALID', + SET_RATE_LIMITER_CONFIG_FAILED: 'SET_RATE_LIMITER_CONFIG_FAILED', + + // Set Rate Limit Admin + SET_RATE_LIMIT_ADMIN_PARAMS_INVALID: 'SET_RATE_LIMIT_ADMIN_PARAMS_INVALID', + SET_RATE_LIMIT_ADMIN_FAILED: 'SET_RATE_LIMIT_ADMIN_FAILED', + + // Provide Liquidity (lock-release pools) + PROVIDE_LIQUIDITY_PARAMS_INVALID: 'PROVIDE_LIQUIDITY_PARAMS_INVALID', + PROVIDE_LIQUIDITY_FAILED: 'PROVIDE_LIQUIDITY_FAILED', + + // Set Token Transfer Fee Config (EVM v2.0+ only) + SET_TOKEN_TRANSFER_FEE_CONFIG_PARAMS_INVALID: 'SET_TOKEN_TRANSFER_FEE_CONFIG_PARAMS_INVALID', + SET_TOKEN_TRANSFER_FEE_CONFIG_FAILED: 'SET_TOKEN_TRANSFER_FEE_CONFIG_FAILED', + + // Set Allowed Finality Config (EVM v2.0+ only) + SET_ALLOWED_FINALITY_CONFIG_PARAMS_INVALID: 'SET_ALLOWED_FINALITY_CONFIG_PARAMS_INVALID', + SET_ALLOWED_FINALITY_CONFIG_FAILED: 'SET_ALLOWED_FINALITY_CONFIG_FAILED', + + // Set Fee Admin (EVM v2.0+ only) + SET_FEE_ADMIN_PARAMS_INVALID: 'SET_FEE_ADMIN_PARAMS_INVALID', + SET_FEE_ADMIN_FAILED: 'SET_FEE_ADMIN_FAILED', + + // Create Pool Mint Authority Multisig (Solana-only) + CREATE_POOL_MULTISIG_PARAMS_INVALID: 'CREATE_POOL_MULTISIG_PARAMS_INVALID', + CREATE_POOL_MULTISIG_FAILED: 'CREATE_POOL_MULTISIG_FAILED', + + // Transfer Mint Authority (Solana-only) + TRANSFER_MINT_AUTHORITY_PARAMS_INVALID: 'TRANSFER_MINT_AUTHORITY_PARAMS_INVALID', + TRANSFER_MINT_AUTHORITY_FAILED: 'TRANSFER_MINT_AUTHORITY_FAILED', + + // Grant Mint/Burn Access + GRANT_MINT_BURN_ACCESS_PARAMS_INVALID: 'GRANT_MINT_BURN_ACCESS_PARAMS_INVALID', + GRANT_MINT_BURN_ACCESS_FAILED: 'GRANT_MINT_BURN_ACCESS_FAILED', + + // Revoke Mint/Burn Access + REVOKE_MINT_BURN_ACCESS_PARAMS_INVALID: 'REVOKE_MINT_BURN_ACCESS_PARAMS_INVALID', + REVOKE_MINT_BURN_ACCESS_FAILED: 'REVOKE_MINT_BURN_ACCESS_FAILED', + + // Create Pool Token Account (Solana-only) + CREATE_POOL_TOKEN_ACCOUNT_PARAMS_INVALID: 'CREATE_POOL_TOKEN_ACCOUNT_PARAMS_INVALID', + CREATE_POOL_TOKEN_ACCOUNT_FAILED: 'CREATE_POOL_TOKEN_ACCOUNT_FAILED', + + // Create Token Address Lookup Table (Solana-only) + CREATE_TOKEN_ALT_PARAMS_INVALID: 'CREATE_TOKEN_ALT_PARAMS_INVALID', + CREATE_TOKEN_ALT_FAILED: 'CREATE_TOKEN_ALT_FAILED', + + // Set Pool (register pool in TokenAdminRegistry) + SET_POOL_PARAMS_INVALID: 'SET_POOL_PARAMS_INVALID', + SET_POOL_FAILED: 'SET_POOL_FAILED', + + // Transfer Ownership (2-step pool ownership transfer) + TRANSFER_OWNERSHIP_PARAMS_INVALID: 'TRANSFER_OWNERSHIP_PARAMS_INVALID', + TRANSFER_OWNERSHIP_FAILED: 'TRANSFER_OWNERSHIP_FAILED', + + // Accept Ownership (2-step pool ownership acceptance) + ACCEPT_OWNERSHIP_PARAMS_INVALID: 'ACCEPT_OWNERSHIP_PARAMS_INVALID', + ACCEPT_OWNERSHIP_FAILED: 'ACCEPT_OWNERSHIP_FAILED', + + // Execute Ownership Transfer (Aptos 3rd step — owner finalizes transfer) + EXECUTE_OWNERSHIP_TRANSFER_PARAMS_INVALID: 'EXECUTE_OWNERSHIP_TRANSFER_PARAMS_INVALID', + EXECUTE_OWNERSHIP_TRANSFER_FAILED: 'EXECUTE_OWNERSHIP_TRANSFER_FAILED', + + // Contract Verification + CONTRACT_VERIFICATION_ERROR: 'CONTRACT_VERIFICATION_ERROR', + CONTRACT_VERIFICATION_FAILED: 'CONTRACT_VERIFICATION_FAILED', + VERIFICATION_CONTRACT_UNKNOWN: 'VERIFICATION_CONTRACT_UNKNOWN', + // Internal NOT_IMPLEMENTED: 'NOT_IMPLEMENTED', UNKNOWN: 'UNKNOWN', @@ -178,6 +284,11 @@ export const CCIPErrorCode = { // Canton CANTON_API_ERROR: 'CANTON_API_ERROR', + + // CCT (Cross-Chain Token) + CCT_PARAMS_INVALID: 'CCT_PARAMS_INVALID', + CCT_TX_FAILED: 'CCT_TX_FAILED', + CCT_TX_NOT_CONFIRMED: 'CCT_TX_NOT_CONFIRMED', } as const /** Union type of all error codes. */ diff --git a/ccip-sdk/src/errors/index.ts b/ccip-sdk/src/errors/index.ts index 1540a7bb..37268931 100644 --- a/ccip-sdk/src/errors/index.ts +++ b/ccip-sdk/src/errors/index.ts @@ -222,6 +222,153 @@ export { CCIPSolanaLaneVersionUnsupportedError } from './specialized.ts' /** @deprecated Deprecated in v1.7 (2026-05-25). Use {@link CCIPTokenNotRegisteredError}. */ export { CCIPAptosTokenNotRegisteredError } from './specialized.ts' +// Specialized errors - Token Deployment +export { CCIPTokenDeployFailedError, CCIPTokenDeployParamsInvalidError } from './specialized.ts' + +// Specialized errors - Pool Deployment +export { + CCIPPoolDeployFailedError, + CCIPPoolDeployParamsInvalidError, + CCIPPoolNotInitializedError, +} from './specialized.ts' + +// Specialized errors - Propose Admin Role +export { + CCIPProposeAdminRoleFailedError, + CCIPProposeAdminRoleParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Accept Admin Role +export { CCIPAcceptAdminRoleParamsInvalidError } from './specialized.ts' +export { CCIPAcceptAdminRoleFailedError } from './specialized.ts' + +// Specialized errors - Transfer Admin Role +export { + CCIPTransferAdminRoleFailedError, + CCIPTransferAdminRoleParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Apply Chain Updates +export { + CCIPApplyChainUpdatesFailedError, + CCIPApplyChainUpdatesParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Append Remote Pool Addresses +export { + CCIPAppendRemotePoolAddressesFailedError, + CCIPAppendRemotePoolAddressesParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Delete Chain Config +export { + CCIPDeleteChainConfigFailedError, + CCIPDeleteChainConfigParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Remove Remote Pool Addresses +export { + CCIPRemoveRemotePoolAddressesFailedError, + CCIPRemoveRemotePoolAddressesParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Set Chain Rate Limiter Config +export { + CCIPSetRateLimiterConfigFailedError, + CCIPSetRateLimiterConfigParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Set Rate Limit Admin +export { + CCIPSetRateLimitAdminFailedError, + CCIPSetRateLimitAdminParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Provide Liquidity (lock-release pools) +export { + CCIPProvideLiquidityFailedError, + CCIPProvideLiquidityParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Set Token Transfer Fee Config (EVM v2.0+ only) +export { + CCIPSetTokenTransferFeeConfigFailedError, + CCIPSetTokenTransferFeeConfigParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Set Allowed Finality Config (EVM v2.0+ only) +export { + CCIPSetAllowedFinalityConfigFailedError, + CCIPSetAllowedFinalityConfigParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Set Fee Admin (EVM v2.0+ only) +export { CCIPSetFeeAdminFailedError, CCIPSetFeeAdminParamsInvalidError } from './specialized.ts' + +// Specialized errors - Create Pool Mint Authority Multisig (Solana-only) +export { + CCIPCreatePoolMultisigFailedError, + CCIPCreatePoolMultisigParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Transfer Mint Authority (Solana-only) +export { + CCIPTransferMintAuthorityFailedError, + CCIPTransferMintAuthorityParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Grant Mint/Burn Access +export { + CCIPGrantMintBurnAccessFailedError, + CCIPGrantMintBurnAccessParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Revoke Mint/Burn Access +export { + CCIPRevokeMintBurnAccessFailedError, + CCIPRevokeMintBurnAccessParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Create Pool Token Account (Solana-only) +export { + CCIPCreatePoolTokenAccountFailedError, + CCIPCreatePoolTokenAccountParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Create Token Address Lookup Table (Solana-only) +export { + CCIPCreateTokenAltFailedError, + CCIPCreateTokenAltParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Set Pool +export { CCIPSetPoolFailedError, CCIPSetPoolParamsInvalidError } from './specialized.ts' + +// Specialized errors - Transfer Ownership +export { + CCIPTransferOwnershipFailedError, + CCIPTransferOwnershipParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Accept Ownership +export { + CCIPAcceptOwnershipFailedError, + CCIPAcceptOwnershipParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Execute Ownership Transfer (Aptos 3rd step) +export { + CCIPExecuteOwnershipTransferFailedError, + CCIPExecuteOwnershipTransferParamsInvalidError, +} from './specialized.ts' + +// Specialized errors - Contract Verification +export { + CCIPContractVerificationError, + CCIPContractVerificationFailedError, + CCIPUnknownVerificationContractError, +} from './specialized.ts' + // HTTP Status codes (re-exported from root) export { HttpStatus, isServerError, isTransientHttpStatus } from '../http-status.ts' diff --git a/ccip-sdk/src/errors/recovery.ts b/ccip-sdk/src/errors/recovery.ts index 00e9a7fb..a3546f54 100644 --- a/ccip-sdk/src/errors/recovery.ts +++ b/ccip-sdk/src/errors/recovery.ts @@ -49,7 +49,7 @@ export const DEFAULT_RECOVERY_HINTS: Partial> = { LANE_NOT_FOUND: 'This lane may not exist or is not yet supported by CCIP. Check the CCIP Directory for supported lanes: https://docs.chain.link/ccip/directory', - COMMIT_NOT_FOUND: 'Wait for the commit report. DON commit typically takes a few minutes.', + COMMIT_NOT_FOUND: 'Wait for the commit report. A DON commit typically takes a few minutes.', MERKLE_ROOT_MISMATCH: 'The computed merkle root does not match the committed root. Ensure all messages in the batch are included and ordered correctly.', MERKLE_TREE_EMPTY: 'Provide at least one leaf hash.', @@ -71,7 +71,8 @@ export const DEFAULT_RECOVERY_HINTS: Partial> = { VERSION_FEATURE_UNAVAILABLE: 'This feature requires CCIP v1.6 or later.', VERSION_REQUIRES_LANE: 'Decoding commits from CCIP <= v1.5 requires lane information.', - EXTRA_ARGS_PARSE_FAILED: 'Verify the format matches the source chain family.', + EXTRA_ARGS_PARSE_FAILED: + 'Verify the extraArgs bytes are properly encoded. Use EVMExtraArgsV1/V2 for EVM sources, SVMExtraArgsV1 for Solana sources. Check the source chain family.', EXTRA_ARGS_UNKNOWN: 'Use EVMExtraArgsV1/V2, SVMExtraArgsV1, or SuiExtraArgsV1.', EXTRA_ARGS_INVALID_EVM: 'ExtraArgs must be EVMExtraArgsV1 or EVMExtraArgsV2 format.', EXTRA_ARGS_INVALID_SVM: 'ExtraArgs must be SVMExtraArgsV1 format for Solana.', @@ -200,11 +201,154 @@ export const DEFAULT_RECOVERY_HINTS: Partial> = { INTERACTIVE_REQUIRED: 'Provide the required input via CLI flags or environment variables, or remove --no-interactive to allow prompts.', + TOKEN_DEPLOY_PARAMS_INVALID: + 'Verify the token deployment parameters: name and symbol must be non-empty, decimals must be within range for the chain family (0-18 EVM, 0-9 Solana).', + TOKEN_DEPLOY_FAILED: + 'The token deployment transaction failed. Check the transaction hash on a block explorer for revert reason. Ensure the wallet has sufficient funds for gas.', + + POOL_DEPLOY_PARAMS_INVALID: + 'Verify the pool deployment parameters: tokenAddress, poolType, localTokenDecimals, and chain-specific addresses (routerAddress, poolProgramId, mcmsAddress) must be valid.', + POOL_DEPLOY_FAILED: + 'The pool deployment transaction failed. Check the transaction hash on a block explorer for revert reason. Ensure the wallet has sufficient funds for gas.', + POOL_NOT_INITIALIZED: + 'This Aptos generic pool requires initialization by the token creator module. ' + + 'The token creator must call burn_mint_token_pool::initialize() or lock_release_token_pool::initialize() ' + + 'with stored capability refs (BurnRef/MintRef/TransferRef). This cannot be done via the SDK.', + + PROPOSE_ADMIN_ROLE_PARAMS_INVALID: + 'Verify the propose admin role parameters: tokenAddress, administrator, and routerAddress must be non-empty valid addresses.', + PROPOSE_ADMIN_ROLE_FAILED: + 'The propose admin role transaction failed. Ensure the caller is the TokenAdminRegistry owner or has permission to propose administrators.', + + ACCEPT_ADMIN_ROLE_PARAMS_INVALID: + 'Check that tokenAddress and routerAddress are valid, non-empty addresses.', + ACCEPT_ADMIN_ROLE_FAILED: + 'The accept admin role transaction failed. Ensure the caller is the pending administrator for this token.', + + TRANSFER_ADMIN_ROLE_PARAMS_INVALID: + 'Check that tokenAddress, newAdmin, and routerAddress are valid, non-empty addresses.', + TRANSFER_ADMIN_ROLE_FAILED: + 'The transfer admin role transaction failed. Ensure the caller is the current administrator for this token.', + + APPLY_CHAIN_UPDATES_PARAMS_INVALID: + 'Check that poolAddress is valid and chainsToAdd entries have valid remoteChainSelector, remotePoolAddresses, and remoteTokenAddress.', + APPLY_CHAIN_UPDATES_FAILED: + 'The apply chain updates transaction failed. Ensure the caller is the pool owner and the remote chain selectors are valid.', + + APPEND_REMOTE_POOL_ADDRESSES_PARAMS_INVALID: + 'Check that poolAddress, remoteChainSelector, and remotePoolAddresses are valid and non-empty.', + APPEND_REMOTE_POOL_ADDRESSES_FAILED: + 'The append remote pool addresses transaction failed. Ensure the caller is the pool owner and the remote chain config exists.', + + DELETE_CHAIN_CONFIG_PARAMS_INVALID: + 'Check that poolAddress and remoteChainSelector are valid and non-empty.', + DELETE_CHAIN_CONFIG_FAILED: + 'The delete chain config transaction failed. Ensure the caller is the pool owner and the remote chain config exists.', + + REMOVE_REMOTE_POOL_ADDRESSES_PARAMS_INVALID: + 'Check that poolAddress, remoteChainSelector, and remotePoolAddresses are valid and non-empty.', + REMOVE_REMOTE_POOL_ADDRESSES_FAILED: + 'The remove remote pool addresses transaction failed. Ensure the caller is the pool owner and the remote chain config exists with the specified pool addresses.', + + SET_RATE_LIMITER_CONFIG_PARAMS_INVALID: + 'Check that poolAddress is valid and each chain config has a valid remoteChainSelector with valid rate limiter configs (capacity and rate as non-negative string integers).', + SET_RATE_LIMITER_CONFIG_FAILED: + 'The set rate limiter config transaction failed. Ensure the caller is the pool owner or rate limit admin, and the remote chain selectors are configured.', + + SET_RATE_LIMIT_ADMIN_PARAMS_INVALID: + 'Check that poolAddress and rateLimitAdmin are valid non-empty addresses.', + SET_RATE_LIMIT_ADMIN_FAILED: + 'The set rate limit admin transaction failed. Ensure the caller is the pool owner.', + + PROVIDE_LIQUIDITY_PARAMS_INVALID: + 'Check that poolAddress is a valid lock-release pool and amount is a positive bigint (whole-unit amount times token decimals).', + PROVIDE_LIQUIDITY_FAILED: + 'The provide-liquidity transaction failed. Ensure the token approval succeeded, the caller is the pool rebalancer (v1.x), and the caller holds enough token balance.', + + SET_TOKEN_TRANSFER_FEE_CONFIG_PARAMS_INVALID: + 'Check that poolAddress is valid, at least one update or disable selector is provided, and each fee config field is a non-negative integer within range.', + SET_TOKEN_TRANSFER_FEE_CONFIG_FAILED: + 'The set token transfer fee config transaction failed. This operation requires an EVM v2.0+ pool. Ensure the caller is the pool owner or fee admin.', + + SET_ALLOWED_FINALITY_CONFIG_PARAMS_INVALID: + 'Check that poolAddress is valid and finality is "finalized", "safe", or a block depth between 0 and 65535.', + SET_ALLOWED_FINALITY_CONFIG_FAILED: + 'The set allowed finality config transaction failed. This operation requires an EVM v2.0+ pool. Ensure the caller is the pool owner.', + + SET_FEE_ADMIN_PARAMS_INVALID: + 'Check that poolAddress and feeAdmin are valid non-empty addresses.', + SET_FEE_ADMIN_FAILED: + 'The set fee admin transaction failed. This operation requires an EVM v2.0+ pool. Ensure the caller is the pool owner.', + + CREATE_POOL_MULTISIG_PARAMS_INVALID: + 'Check that mint and poolProgramId are valid public keys, additionalSigners is non-empty with valid public keys, and threshold is a positive integer not exceeding total signers (max 11).', + CREATE_POOL_MULTISIG_FAILED: + 'The create pool mint authority multisig transaction failed. Ensure the wallet has sufficient SOL for rent exemption and the mint account exists on-chain.', + + TRANSFER_MINT_AUTHORITY_PARAMS_INVALID: + 'Check that mint and newMintAuthority are valid public keys.', + TRANSFER_MINT_AUTHORITY_FAILED: + 'The transfer mint authority transaction failed. Ensure the caller is the current mint authority.', + + GRANT_MINT_BURN_ACCESS_PARAMS_INVALID: + 'Check that tokenAddress and authority are valid addresses for this chain.', + GRANT_MINT_BURN_ACCESS_FAILED: + 'The grant mint/burn access transaction failed. Ensure the caller is the token owner/admin and the authority address is valid.', + + REVOKE_MINT_BURN_ACCESS_PARAMS_INVALID: + 'Check that tokenAddress, authority, and role are valid. Role must be "mint" or "burn".', + REVOKE_MINT_BURN_ACCESS_FAILED: + 'The revoke mint/burn access transaction failed. Ensure the caller is the token owner/admin and the authority currently holds the role.', + + CREATE_POOL_TOKEN_ACCOUNT_PARAMS_INVALID: + 'Check that tokenAddress and poolAddress are valid Solana public keys and exist on-chain.', + CREATE_POOL_TOKEN_ACCOUNT_FAILED: + 'The create pool token account transaction failed. Ensure the wallet has sufficient SOL for rent exemption and the pool/mint accounts exist on-chain.', + + CREATE_TOKEN_ALT_PARAMS_INVALID: + 'Check that tokenAddress, poolAddress, and routerAddress are valid Solana public keys. If authority is provided, it must also be a valid public key.', + CREATE_TOKEN_ALT_FAILED: + 'The create token ALT transaction failed. Ensure the wallet has sufficient SOL for rent exemption and the pool/mint accounts exist on-chain.', + + SET_POOL_PARAMS_INVALID: + 'Check that tokenAddress, poolAddress, and routerAddress are valid. On Solana, poolLookupTable is also required.', + SET_POOL_FAILED: + 'The set pool transaction failed. Ensure the caller is the token administrator in the TokenAdminRegistry.', + + TRANSFER_OWNERSHIP_PARAMS_INVALID: + 'Check that poolAddress is valid and newOwner is a valid address for the target chain.', + TRANSFER_OWNERSHIP_FAILED: + 'The transfer ownership transaction failed. Ensure the caller is the current pool owner.', + + ACCEPT_OWNERSHIP_PARAMS_INVALID: 'Check that poolAddress is valid for the target chain.', + ACCEPT_OWNERSHIP_FAILED: + 'The accept ownership transaction failed. Ensure the caller is the pending (proposed) owner.', + + EXECUTE_OWNERSHIP_TRANSFER_PARAMS_INVALID: + 'Check that poolAddress is valid and newOwner matches the address that accepted ownership.', + EXECUTE_OWNERSHIP_TRANSFER_FAILED: + 'The execute ownership transfer failed. Ensure the caller is the current owner and the proposed owner has already called acceptOwnership. This step is Aptos-only.', + NOT_IMPLEMENTED: 'This feature is not yet implemented.', UNKNOWN: 'An unknown error occurred. Check the error details.', CANTON_API_ERROR: 'Canton Ledger API returned an error. Verify the party ID is correct, the contract is active, and the Canton node is reachable.', + + CONTRACT_VERIFICATION_ERROR: + 'The explorer rejected the verification request. Check the API key, chainId, and that the contract bytecode is indexed (a freshly-deployed contract may need a few seconds).', + CONTRACT_VERIFICATION_FAILED: + 'Verification failed. Ensure the contract, chainId, and constructor args match the deployed bytecode, or pass an explicit verifier.', + VERIFICATION_CONTRACT_UNKNOWN: + 'Unknown contract name. Use one of the bundled deployable contracts (see listDeployableContracts()).', + + // Cross-Chain Token + CCT_PARAMS_INVALID: + 'Verify the operation parameters. See error.context for the field name and reason.', + CCT_TX_FAILED: + 'The CCT transaction failed. Ensure the caller holds the required role for this operation.', + CCT_TX_NOT_CONFIRMED: + 'The transaction was submitted but not confirmed in time. Check the tx hash in error.context before resubmitting; it may still be mined.', } /** Returns default recovery hint for error code, or undefined if none. */ diff --git a/ccip-sdk/src/errors/specialized.ts b/ccip-sdk/src/errors/specialized.ts index 92279044..dea4268a 100644 --- a/ccip-sdk/src/errors/specialized.ts +++ b/ccip-sdk/src/errors/specialized.ts @@ -3721,3 +3721,1062 @@ export class CCIPFinalityNotAllowedError extends CCIPError { }) } } + +// ─── Token Deployment ───────────────────────────────────────────────────────── + +/** + * Thrown when token deployment parameters are invalid (e.g., empty name, decimals out of range). + * + * @example + * ```typescript + * try { + * await admin.deployToken({ name: '', symbol: 'MTK', decimals: 18 }) + * } catch (error) { + * if (error instanceof CCIPTokenDeployParamsInvalidError) { + * console.log(`Invalid param: ${error.context.param} — ${error.context.reason}`) + * } + * } + * ``` + */ +export class CCIPTokenDeployParamsInvalidError extends CCIPError { + override readonly name = 'CCIPTokenDeployParamsInvalidError' + /** Creates a token deploy params invalid error. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.TOKEN_DEPLOY_PARAMS_INVALID, + `Invalid token deployment parameter "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** + * Thrown when a token deployment transaction fails (reverts or is not confirmed). + * + * @example + * ```typescript + * try { + * await admin.deployToken({ name: 'My Token', symbol: 'MTK', decimals: 18 }) + * } catch (error) { + * if (error instanceof CCIPTokenDeployFailedError) { + * console.log(`Deploy failed: ${error.context.txHash}`) + * } + * } + * ``` + */ +export class CCIPTokenDeployFailedError extends CCIPError { + override readonly name = 'CCIPTokenDeployFailedError' + /** Creates a token deploy failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.TOKEN_DEPLOY_FAILED, `Token deployment failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +// ── Pool Deployment ────────────────────────────────────────────────────────── + +/** + * Thrown when pool deployment parameters fail validation. + * + * @example + * ```typescript + * try { + * await admin.deployPool(wallet, { poolType: 'burn-mint', tokenAddress: '', localTokenDecimals: 18, routerAddress: '0x...' }) + * } catch (error) { + * if (error instanceof CCIPPoolDeployParamsInvalidError) { + * console.log(`Invalid param: ${error.context.param} — ${error.context.reason}`) + * } + * } + * ``` + */ +export class CCIPPoolDeployParamsInvalidError extends CCIPError { + override readonly name = 'CCIPPoolDeployParamsInvalidError' + /** Creates a pool deploy params invalid error. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.POOL_DEPLOY_PARAMS_INVALID, + `Invalid pool deployment parameter "${param}": ${reason}`, + { ...options, isTransient: false, context: { ...options?.context, param, reason } }, + ) + } +} + +/** + * Thrown when pool deployment transaction fails on-chain. + * + * @example + * ```typescript + * try { + * await admin.deployPool(wallet, params) + * } catch (error) { + * if (error instanceof CCIPPoolDeployFailedError) { + * console.log(`Pool deploy failed: ${error.message}`) + * } + * } + * ``` + */ +export class CCIPPoolDeployFailedError extends CCIPError { + override readonly name = 'CCIPPoolDeployFailedError' + /** Creates a pool deploy failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.POOL_DEPLOY_FAILED, `Pool deployment failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +/** + * Thrown when an operation is attempted on an uninitialized Aptos generic pool. + * + * Generic pools (`burn_mint_token_pool`, `lock_release_token_pool`) require a + * separate `initialize()` call from the token creator module with capability refs + * (`BurnRef`/`MintRef`/`TransferRef`) that cannot be provided from TypeScript. + * + * @example + * ```typescript + * try { + * await admin.transferOwnership(wallet, { poolAddress, newOwner }) + * } catch (error) { + * if (error instanceof CCIPPoolNotInitializedError) { + * console.log(`Pool not initialized: ${error.message}`) + * } + * } + * ``` + */ +export class CCIPPoolNotInitializedError extends CCIPError { + override readonly name = 'CCIPPoolNotInitializedError' + /** Creates a pool not initialized error. */ + constructor(poolAddress: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.POOL_NOT_INITIALIZED, + `Pool at ${poolAddress} is not initialized. ` + + `The token creator module must call initialize() with capability refs ` + + `(BurnRef/MintRef/TransferRef) before this operation can be used.`, + { + ...options, + isTransient: false, + context: { ...options?.context, poolAddress }, + }, + ) + } +} + +// ── Propose Admin Role ────────────────────────────────────────────────────── + +/** + * Thrown when proposeAdminRole parameters are invalid. + * + * @example + * ```typescript + * try { + * await admin.proposeAdminRole(wallet, params) + * } catch (error) { + * if (error instanceof CCIPProposeAdminRoleParamsInvalidError) { + * console.log(`Invalid param: ${error.context.param} — ${error.context.reason}`) + * } + * } + * ``` + */ +export class CCIPProposeAdminRoleParamsInvalidError extends CCIPError { + override readonly name = 'CCIPProposeAdminRoleParamsInvalidError' + /** Creates a propose admin role params invalid error. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.PROPOSE_ADMIN_ROLE_PARAMS_INVALID, + `Invalid proposeAdminRole parameter "${param}": ${reason}`, + { ...options, isTransient: false, context: { ...options?.context, param, reason } }, + ) + } +} + +/** + * Thrown when proposeAdminRole transaction fails on-chain. + * + * @example + * ```typescript + * try { + * await admin.proposeAdminRole(wallet, params) + * } catch (error) { + * if (error instanceof CCIPProposeAdminRoleFailedError) { + * console.log(`Propose admin role failed: ${error.message}`) + * } + * } + * ``` + */ +export class CCIPProposeAdminRoleFailedError extends CCIPError { + override readonly name = 'CCIPProposeAdminRoleFailedError' + /** Creates a propose admin role failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.PROPOSE_ADMIN_ROLE_FAILED, `Propose admin role failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +/** + * Thrown when acceptAdminRole parameters are invalid. + * + * @example + * ```typescript + * try { + * await admin.acceptAdminRole(wallet, params) + * } catch (error) { + * if (error instanceof CCIPAcceptAdminRoleParamsInvalidError) { + * console.log(`Invalid param: ${error.context.param} — ${error.context.reason}`) + * } + * } + * ``` + */ +export class CCIPAcceptAdminRoleParamsInvalidError extends CCIPError { + override readonly name = 'CCIPAcceptAdminRoleParamsInvalidError' + /** Creates an accept admin role params invalid error. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.ACCEPT_ADMIN_ROLE_PARAMS_INVALID, + `Invalid acceptAdminRole parameter "${param}": ${reason}`, + { ...options, isTransient: false, context: { ...options?.context, param, reason } }, + ) + } +} + +/** + * Thrown when acceptAdminRole transaction fails on-chain. + * + * @example + * ```typescript + * try { + * await admin.acceptAdminRole(wallet, params) + * } catch (error) { + * if (error instanceof CCIPAcceptAdminRoleFailedError) { + * console.log(`Accept admin role failed: ${error.message}`) + * } + * } + * ``` + */ +export class CCIPAcceptAdminRoleFailedError extends CCIPError { + override readonly name = 'CCIPAcceptAdminRoleFailedError' + /** Creates an accept admin role failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.ACCEPT_ADMIN_ROLE_FAILED, `Accept admin role failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +// ── Transfer Admin Role ───────────────────────────────────────────────────── + +/** + * Thrown when transferAdminRole parameters are invalid. + * + * @example + * ```typescript + * try { + * await admin.transferAdminRole(wallet, params) + * } catch (error) { + * if (error instanceof CCIPTransferAdminRoleParamsInvalidError) { + * console.log(`Invalid param: ${error.context.param} — ${error.context.reason}`) + * } + * } + * ``` + */ +export class CCIPTransferAdminRoleParamsInvalidError extends CCIPError { + override readonly name = 'CCIPTransferAdminRoleParamsInvalidError' + /** Creates a transfer admin role params invalid error. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.TRANSFER_ADMIN_ROLE_PARAMS_INVALID, + `Invalid transferAdminRole parameter "${param}": ${reason}`, + { ...options, isTransient: false, context: { ...options?.context, param, reason } }, + ) + } +} + +/** + * Thrown when transferAdminRole transaction fails on-chain. + * + * @example + * ```typescript + * try { + * await admin.transferAdminRole(wallet, params) + * } catch (error) { + * if (error instanceof CCIPTransferAdminRoleFailedError) { + * console.log(`Transfer admin role failed: ${error.message}`) + * } + * } + * ``` + */ +export class CCIPTransferAdminRoleFailedError extends CCIPError { + override readonly name = 'CCIPTransferAdminRoleFailedError' + /** Creates a transfer admin role failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.TRANSFER_ADMIN_ROLE_FAILED, `Transfer admin role failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +// ── Apply Chain Updates ───────────────────────────────────────────────────── + +/** Thrown when applyChainUpdates parameters are invalid. */ +export class CCIPApplyChainUpdatesParamsInvalidError extends CCIPError { + override readonly name = 'CCIPApplyChainUpdatesParamsInvalidError' + /** Creates a params-invalid error for apply chain updates. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.APPLY_CHAIN_UPDATES_PARAMS_INVALID, + `Invalid applyChainUpdates param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the applyChainUpdates transaction fails. */ +export class CCIPApplyChainUpdatesFailedError extends CCIPError { + override readonly name = 'CCIPApplyChainUpdatesFailedError' + /** Creates an apply chain updates failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.APPLY_CHAIN_UPDATES_FAILED, `Apply chain updates failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +// ── Set Chain Rate Limiter Config ─────────────────────────────────────────── + +/** Thrown when setChainRateLimiterConfig parameters are invalid. */ +export class CCIPSetRateLimiterConfigParamsInvalidError extends CCIPError { + override readonly name = 'CCIPSetRateLimiterConfigParamsInvalidError' + /** Creates a params-invalid error for set rate limiter config. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.SET_RATE_LIMITER_CONFIG_PARAMS_INVALID, + `Invalid setChainRateLimiterConfig param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the setChainRateLimiterConfig transaction fails. */ +export class CCIPSetRateLimiterConfigFailedError extends CCIPError { + override readonly name = 'CCIPSetRateLimiterConfigFailedError' + /** Creates a set rate limiter config failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.SET_RATE_LIMITER_CONFIG_FAILED, + `Set rate limiter config failed: ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }, + ) + } +} + +// ── Set Rate Limit Admin ──────────────────────────────────────────────────── + +/** Thrown when setRateLimitAdmin parameters are invalid. */ +export class CCIPSetRateLimitAdminParamsInvalidError extends CCIPError { + override readonly name = 'CCIPSetRateLimitAdminParamsInvalidError' + /** Creates a params-invalid error for set rate limit admin. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.SET_RATE_LIMIT_ADMIN_PARAMS_INVALID, + `Invalid setRateLimitAdmin param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the setRateLimitAdmin transaction fails. */ +export class CCIPSetRateLimitAdminFailedError extends CCIPError { + override readonly name = 'CCIPSetRateLimitAdminFailedError' + /** Creates a set rate limit admin failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.SET_RATE_LIMIT_ADMIN_FAILED, `Set rate limit admin failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +// ── Provide Liquidity (lock-release pools) ────────────────────────────────── + +/** Thrown when provideLiquidity parameters are invalid. */ +export class CCIPProvideLiquidityParamsInvalidError extends CCIPError { + override readonly name = 'CCIPProvideLiquidityParamsInvalidError' + /** Creates a params-invalid error for provide liquidity. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.PROVIDE_LIQUIDITY_PARAMS_INVALID, + `Invalid provideLiquidity param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the provideLiquidity transaction fails. */ +export class CCIPProvideLiquidityFailedError extends CCIPError { + override readonly name = 'CCIPProvideLiquidityFailedError' + /** Creates a provide liquidity failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.PROVIDE_LIQUIDITY_FAILED, `Provide liquidity failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +// ── Set Token Transfer Fee Config (EVM v2.0+ only) ────────────────────────── + +/** Thrown when setTokenTransferFeeConfig parameters are invalid. */ +export class CCIPSetTokenTransferFeeConfigParamsInvalidError extends CCIPError { + override readonly name = 'CCIPSetTokenTransferFeeConfigParamsInvalidError' + /** Creates a params-invalid error for set token transfer fee config. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.SET_TOKEN_TRANSFER_FEE_CONFIG_PARAMS_INVALID, + `Invalid setTokenTransferFeeConfig param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the setTokenTransferFeeConfig transaction fails. */ +export class CCIPSetTokenTransferFeeConfigFailedError extends CCIPError { + override readonly name = 'CCIPSetTokenTransferFeeConfigFailedError' + /** Creates a set token transfer fee config failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.SET_TOKEN_TRANSFER_FEE_CONFIG_FAILED, + `Set token transfer fee config failed: ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }, + ) + } +} + +// ── Set Allowed Finality Config (EVM v2.0+ only) ──────────────────────────── + +/** Thrown when setAllowedFinalityConfig parameters are invalid. */ +export class CCIPSetAllowedFinalityConfigParamsInvalidError extends CCIPError { + override readonly name = 'CCIPSetAllowedFinalityConfigParamsInvalidError' + /** Creates a params-invalid error for set allowed finality config. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.SET_ALLOWED_FINALITY_CONFIG_PARAMS_INVALID, + `Invalid setAllowedFinalityConfig param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the setAllowedFinalityConfig transaction fails. */ +export class CCIPSetAllowedFinalityConfigFailedError extends CCIPError { + override readonly name = 'CCIPSetAllowedFinalityConfigFailedError' + /** Creates a set allowed finality config failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.SET_ALLOWED_FINALITY_CONFIG_FAILED, + `Set allowed finality config failed: ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }, + ) + } +} + +// ── Set Fee Admin (EVM v2.0+ only) ────────────────────────────────────────── + +/** Thrown when setFeeAdmin parameters are invalid. */ +export class CCIPSetFeeAdminParamsInvalidError extends CCIPError { + override readonly name = 'CCIPSetFeeAdminParamsInvalidError' + /** Creates a params-invalid error for set fee admin. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.SET_FEE_ADMIN_PARAMS_INVALID, + `Invalid setFeeAdmin param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the setFeeAdmin transaction fails. */ +export class CCIPSetFeeAdminFailedError extends CCIPError { + override readonly name = 'CCIPSetFeeAdminFailedError' + /** Creates a set fee admin failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.SET_FEE_ADMIN_FAILED, `Set fee admin failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +// ── Create Pool Mint Authority Multisig (Solana-only) ─────────────────────── + +/** Thrown when createPoolMintAuthorityMultisig parameters are invalid. */ +export class CCIPCreatePoolMultisigParamsInvalidError extends CCIPError { + override readonly name = 'CCIPCreatePoolMultisigParamsInvalidError' + /** Creates a params-invalid error for create pool multisig. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.CREATE_POOL_MULTISIG_PARAMS_INVALID, + `Invalid createPoolMintAuthorityMultisig param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the createPoolMintAuthorityMultisig transaction fails. */ +export class CCIPCreatePoolMultisigFailedError extends CCIPError { + override readonly name = 'CCIPCreatePoolMultisigFailedError' + /** Creates a create pool multisig failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.CREATE_POOL_MULTISIG_FAILED, + `Create pool mint authority multisig failed: ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }, + ) + } +} + +// Transfer Mint Authority (Solana-only) + +/** Thrown when transferMintAuthority params are invalid. */ +export class CCIPTransferMintAuthorityParamsInvalidError extends CCIPError { + override readonly name = 'CCIPTransferMintAuthorityParamsInvalidError' + /** Creates a params-invalid error for transfer mint authority. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.TRANSFER_MINT_AUTHORITY_PARAMS_INVALID, + `Invalid transferMintAuthority param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the transferMintAuthority transaction fails. */ +export class CCIPTransferMintAuthorityFailedError extends CCIPError { + override readonly name = 'CCIPTransferMintAuthorityFailedError' + /** Creates a transfer mint authority failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.TRANSFER_MINT_AUTHORITY_FAILED, + `Transfer mint authority failed: ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }, + ) + } +} + +// Grant Mint/Burn Access + +/** Thrown when grantMintBurnAccess params are invalid. */ +export class CCIPGrantMintBurnAccessParamsInvalidError extends CCIPError { + override readonly name = 'CCIPGrantMintBurnAccessParamsInvalidError' + /** Creates a params-invalid error for grant mint/burn access. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.GRANT_MINT_BURN_ACCESS_PARAMS_INVALID, + `Invalid grantMintBurnAccess param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the grantMintBurnAccess transaction fails. */ +export class CCIPGrantMintBurnAccessFailedError extends CCIPError { + override readonly name = 'CCIPGrantMintBurnAccessFailedError' + /** Creates a grant mint/burn access failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.GRANT_MINT_BURN_ACCESS_FAILED, `Grant mint/burn access failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +// Revoke Mint/Burn Access + +/** Thrown when revokeMintBurnAccess params are invalid. */ +export class CCIPRevokeMintBurnAccessParamsInvalidError extends CCIPError { + override readonly name = 'CCIPRevokeMintBurnAccessParamsInvalidError' + /** Creates a params-invalid error for revoke mint/burn access. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.REVOKE_MINT_BURN_ACCESS_PARAMS_INVALID, + `Invalid revokeMintBurnAccess param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the revokeMintBurnAccess transaction fails. */ +export class CCIPRevokeMintBurnAccessFailedError extends CCIPError { + override readonly name = 'CCIPRevokeMintBurnAccessFailedError' + /** Creates a revoke mint/burn access failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.REVOKE_MINT_BURN_ACCESS_FAILED, + `Revoke mint/burn access failed: ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }, + ) + } +} + +// ── Create Pool Token Account (Solana-only) ───────────────────────────────── + +/** Thrown when createPoolTokenAccount params are invalid. */ +export class CCIPCreatePoolTokenAccountParamsInvalidError extends CCIPError { + override readonly name = 'CCIPCreatePoolTokenAccountParamsInvalidError' + /** Creates a params-invalid error for create pool token account. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.CREATE_POOL_TOKEN_ACCOUNT_PARAMS_INVALID, + `Invalid createPoolTokenAccount param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the createPoolTokenAccount transaction fails. */ +export class CCIPCreatePoolTokenAccountFailedError extends CCIPError { + override readonly name = 'CCIPCreatePoolTokenAccountFailedError' + /** Creates a create pool token account failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.CREATE_POOL_TOKEN_ACCOUNT_FAILED, + `Create pool token account failed: ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }, + ) + } +} + +// ── Create Token Address Lookup Table (Solana-only) ───────────────────────── + +/** Thrown when createTokenAlt params are invalid. */ +export class CCIPCreateTokenAltParamsInvalidError extends CCIPError { + override readonly name = 'CCIPCreateTokenAltParamsInvalidError' + /** Creates a params-invalid error for create token ALT. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.CREATE_TOKEN_ALT_PARAMS_INVALID, + `Invalid createTokenAlt param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the createTokenAlt transaction fails. */ +export class CCIPCreateTokenAltFailedError extends CCIPError { + override readonly name = 'CCIPCreateTokenAltFailedError' + /** Creates a create token ALT failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.CREATE_TOKEN_ALT_FAILED, + `Create token address lookup table failed: ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }, + ) + } +} + +// ── Set Pool ──────────────────────────────────────────────────────────────── + +/** Thrown when setPool parameters are invalid. */ +export class CCIPSetPoolParamsInvalidError extends CCIPError { + override readonly name = 'CCIPSetPoolParamsInvalidError' + /** Creates a set pool params invalid error. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.SET_POOL_PARAMS_INVALID, + `Invalid setPool parameter "${param}": ${reason}`, + { ...options, isTransient: false, context: { ...options?.context, param, reason } }, + ) + } +} + +/** Thrown when the setPool transaction fails on-chain. */ +export class CCIPSetPoolFailedError extends CCIPError { + override readonly name = 'CCIPSetPoolFailedError' + /** Creates a set pool failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.SET_POOL_FAILED, `Set pool failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +// ── Transfer Ownership ────────────────────────────────────────────────────── + +/** Thrown when transferOwnership parameters are invalid. */ +export class CCIPTransferOwnershipParamsInvalidError extends CCIPError { + override readonly name = 'CCIPTransferOwnershipParamsInvalidError' + /** Creates a transfer ownership params invalid error. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.TRANSFER_OWNERSHIP_PARAMS_INVALID, + `Invalid transferOwnership parameter "${param}": ${reason}`, + { ...options, isTransient: false, context: { ...options?.context, param, reason } }, + ) + } +} + +/** Thrown when the transferOwnership transaction fails on-chain. */ +export class CCIPTransferOwnershipFailedError extends CCIPError { + override readonly name = 'CCIPTransferOwnershipFailedError' + /** Creates a transfer ownership failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.TRANSFER_OWNERSHIP_FAILED, `Transfer ownership failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +// ── Accept Ownership ──────────────────────────────────────────────────────── + +/** Thrown when acceptOwnership parameters are invalid. */ +export class CCIPAcceptOwnershipParamsInvalidError extends CCIPError { + override readonly name = 'CCIPAcceptOwnershipParamsInvalidError' + /** Creates an accept ownership params invalid error. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.ACCEPT_OWNERSHIP_PARAMS_INVALID, + `Invalid acceptOwnership parameter "${param}": ${reason}`, + { ...options, isTransient: false, context: { ...options?.context, param, reason } }, + ) + } +} + +/** Thrown when the acceptOwnership transaction fails on-chain. */ +export class CCIPAcceptOwnershipFailedError extends CCIPError { + override readonly name = 'CCIPAcceptOwnershipFailedError' + /** Creates an accept ownership failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.ACCEPT_OWNERSHIP_FAILED, `Accept ownership failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +// ── Execute Ownership Transfer (Aptos 3rd step) ───────────────────────────── + +/** Thrown when executeOwnershipTransfer parameters are invalid. */ +export class CCIPExecuteOwnershipTransferParamsInvalidError extends CCIPError { + override readonly name = 'CCIPExecuteOwnershipTransferParamsInvalidError' + /** Creates an execute ownership transfer params invalid error. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.EXECUTE_OWNERSHIP_TRANSFER_PARAMS_INVALID, + `Invalid executeOwnershipTransfer parameter "${param}": ${reason}`, + { ...options, isTransient: false, context: { ...options?.context, param, reason } }, + ) + } +} + +/** Thrown when the executeOwnershipTransfer transaction fails on-chain. */ +export class CCIPExecuteOwnershipTransferFailedError extends CCIPError { + override readonly name = 'CCIPExecuteOwnershipTransferFailedError' + /** Creates an execute ownership transfer failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.EXECUTE_OWNERSHIP_TRANSFER_FAILED, + `Execute ownership transfer failed: ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }, + ) + } +} + +// ── Append Remote Pool Addresses ───────────────────────────────────────────── + +/** Thrown when appendRemotePoolAddresses parameters are invalid. */ +export class CCIPAppendRemotePoolAddressesParamsInvalidError extends CCIPError { + override readonly name = 'CCIPAppendRemotePoolAddressesParamsInvalidError' + /** Creates a params-invalid error for append remote pool addresses. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.APPEND_REMOTE_POOL_ADDRESSES_PARAMS_INVALID, + `Invalid appendRemotePoolAddresses param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the appendRemotePoolAddresses transaction fails. */ +export class CCIPAppendRemotePoolAddressesFailedError extends CCIPError { + override readonly name = 'CCIPAppendRemotePoolAddressesFailedError' + /** Creates an append remote pool addresses failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.APPEND_REMOTE_POOL_ADDRESSES_FAILED, + `Append remote pool addresses failed: ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }, + ) + } +} + +// ── Delete Chain Config ───────────────────────────────────────────────────── + +/** Thrown when deleteChainConfig parameters are invalid. */ +export class CCIPDeleteChainConfigParamsInvalidError extends CCIPError { + override readonly name = 'CCIPDeleteChainConfigParamsInvalidError' + /** Creates a params-invalid error for delete chain config. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.DELETE_CHAIN_CONFIG_PARAMS_INVALID, + `Invalid deleteChainConfig param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the deleteChainConfig transaction fails. */ +export class CCIPDeleteChainConfigFailedError extends CCIPError { + override readonly name = 'CCIPDeleteChainConfigFailedError' + /** Creates a delete chain config failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.DELETE_CHAIN_CONFIG_FAILED, `Delete chain config failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +/** Thrown when removeRemotePoolAddresses params are invalid. */ +export class CCIPRemoveRemotePoolAddressesParamsInvalidError extends CCIPError { + override readonly name = 'CCIPRemoveRemotePoolAddressesParamsInvalidError' + /** Creates a remove remote pool addresses params invalid error. */ + constructor(param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.REMOVE_REMOTE_POOL_ADDRESSES_PARAMS_INVALID, + `Invalid removeRemotePoolAddresses param "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, param, reason }, + }, + ) + } +} + +/** Thrown when the removeRemotePoolAddresses transaction fails. */ +export class CCIPRemoveRemotePoolAddressesFailedError extends CCIPError { + override readonly name = 'CCIPRemoveRemotePoolAddressesFailedError' + /** Creates a remove remote pool addresses failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.REMOVE_REMOTE_POOL_ADDRESSES_FAILED, + `Remove remote pool addresses failed: ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }, + ) + } +} + +// ── Contract Verification ─────────────────────────────────────────────────── + +/** + * Thrown when a contract-verification request to an explorer errors (bad request, + * HTTP failure, unknown solc version, unreadable source, etc.). + * + * The explorer's raw `result` body, when present, is attached on `context.result`. + * The message is preserved verbatim so callers can match explorer markers + * (e.g. "Contract source code already verified"). + * + * @example + * ```typescript + * try { + * await verifyContract(input) + * } catch (error) { + * if (error instanceof CCIPContractVerificationError) { + * console.log(`Explorer error: ${error.message}`, error.context.result) + * } + * } + * ``` + */ +export class CCIPContractVerificationError extends CCIPError { + override readonly name = 'CCIPContractVerificationError' + /** Creates a contract verification error. */ + constructor(message: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.CONTRACT_VERIFICATION_ERROR, message, { + ...options, + isTransient: options?.isTransient ?? false, + }) + } +} + +/** + * Thrown when a contract cannot be verified (no known verifier for the chain, + * recompile mismatch, etc.). + * + * @example + * ```typescript + * try { + * await verifyDeployedContract(params) + * } catch (error) { + * if (error instanceof CCIPContractVerificationFailedError) { + * console.log(`Verification failed: ${error.context.reason}`) + * } + * } + * ``` + */ +export class CCIPContractVerificationFailedError extends CCIPError { + override readonly name = 'CCIPContractVerificationFailedError' + /** Creates a contract verification failed error. */ + constructor(reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.CONTRACT_VERIFICATION_FAILED, `Contract verification failed: ${reason}`, { + ...options, + isTransient: false, + context: { ...options?.context, reason }, + }) + } +} + +/** + * Thrown when an unknown contract name is requested from the bundled verification registry. + * + * @example + * ```typescript + * try { + * getVerificationArtifact('NotABundledContract') + * } catch (error) { + * if (error instanceof CCIPUnknownVerificationContractError) { + * console.log(`Available: ${error.context.available}`) + * } + * } + * ``` + */ +export class CCIPUnknownVerificationContractError extends CCIPError { + override readonly name = 'CCIPUnknownVerificationContractError' + /** Creates an unknown verification contract error. */ + constructor(contract: string, available: readonly string[], options?: CCIPErrorOptions) { + super( + CCIPErrorCode.VERIFICATION_CONTRACT_UNKNOWN, + `Unknown contract "${contract}". Bundled: ${available.join(', ')}`, + { ...options, isTransient: false, context: { ...options?.context, contract, available } }, + ) + } +} diff --git a/ccip-sdk/src/evm/abi/CrossChainToken.ts b/ccip-sdk/src/evm/abi/CrossChainToken.ts new file mode 100644 index 00000000..14b2b7ef --- /dev/null +++ b/ccip-sdk/src/evm/abi/CrossChainToken.ts @@ -0,0 +1,663 @@ +// TODO: track a v2 release tag and the v2.0.0 folder instead of refs/heads/main, once 2.0.0 is released in `chainlink-ccip` +export default [ + // generate: + // fetch('https://github.com/smartcontractkit/chainlink-ccip/raw/refs/heads/main/chains/evm/gobindings/generated/v2_0_0/cross_chain_token/cross_chain_token.go') + // .then((res) => res.text()) + // .then((body) => body.match(/^\s*ABI: "(.*?)",$/m)?.[1]) + // .then((abi) => JSON.parse(abi.replace(/\\"/g, '"'))) + // .then((obj) => require('util').inspect(obj, {depth:99}).split('\n').slice(1, -1)) + { + type: 'constructor', + inputs: [ + { + name: 'args', + type: 'tuple', + internalType: 'struct BaseERC20.ConstructorParams', + components: [ + { name: 'name', type: 'string', internalType: 'string' }, + { name: 'symbol', type: 'string', internalType: 'string' }, + { + name: 'maxSupply', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'preMint', type: 'uint256', internalType: 'uint256' }, + { + name: 'preMintRecipient', + type: 'address', + internalType: 'address', + }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { + name: 'ccipAdmin', + type: 'address', + internalType: 'address', + }, + ], + }, + { + name: 'burnMintRoleAdmin', + type: 'address', + internalType: 'address', + }, + { name: 'owner', type: 'address', internalType: 'address' }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'BURNER_ROLE', + inputs: [], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'BURN_MINT_ADMIN_ROLE', + inputs: [], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'DEFAULT_ADMIN_ROLE', + inputs: [], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'MINTER_ROLE', + inputs: [], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'acceptDefaultAdminTransfer', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'allowance', + inputs: [ + { name: 'owner', type: 'address', internalType: 'address' }, + { name: 'spender', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'approve', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { name: 'value', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'balanceOf', + inputs: [{ name: 'account', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'beginDefaultAdminTransfer', + inputs: [{ name: 'newAdmin', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'burn', + inputs: [{ name: 'amount', type: 'uint256', internalType: 'uint256' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'burn', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'burnFrom', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'cancelDefaultAdminTransfer', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'changeDefaultAdminDelay', + inputs: [{ name: 'newDelay', type: 'uint48', internalType: 'uint48' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'decimals', + inputs: [], + outputs: [{ name: '_decimals', type: 'uint8', internalType: 'uint8' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'defaultAdmin', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'defaultAdminDelay', + inputs: [], + outputs: [{ name: '', type: 'uint48', internalType: 'uint48' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'defaultAdminDelayIncreaseWait', + inputs: [], + outputs: [{ name: '', type: 'uint48', internalType: 'uint48' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getCCIPAdmin', + inputs: [], + outputs: [{ name: 'ccipAdmin', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getRoleAdmin', + inputs: [{ name: 'role', type: 'bytes32', internalType: 'bytes32' }], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'grantMintAndBurnRoles', + inputs: [ + { + name: 'burnAndMinter', + type: 'address', + internalType: 'address', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'grantRole', + inputs: [ + { name: 'role', type: 'bytes32', internalType: 'bytes32' }, + { name: 'account', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'hasRole', + inputs: [ + { name: 'role', type: 'bytes32', internalType: 'bytes32' }, + { name: 'account', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'maxSupply', + inputs: [], + outputs: [{ name: '_maxSupply', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'mint', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'name', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'owner', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'pendingDefaultAdmin', + inputs: [], + outputs: [ + { name: 'newAdmin', type: 'address', internalType: 'address' }, + { name: 'schedule', type: 'uint48', internalType: 'uint48' }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'pendingDefaultAdminDelay', + inputs: [], + outputs: [ + { name: 'newDelay', type: 'uint48', internalType: 'uint48' }, + { name: 'schedule', type: 'uint48', internalType: 'uint48' }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'renounceRole', + inputs: [ + { name: 'role', type: 'bytes32', internalType: 'bytes32' }, + { name: 'account', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'revokeRole', + inputs: [ + { name: 'role', type: 'bytes32', internalType: 'bytes32' }, + { name: 'account', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'rollbackDefaultAdminDelay', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setCCIPAdmin', + inputs: [{ name: 'newAdmin', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'supportsInterface', + inputs: [{ name: 'interfaceId', type: 'bytes4', internalType: 'bytes4' }], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'symbol', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'totalSupply', + inputs: [], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'transfer', + inputs: [ + { name: 'to', type: 'address', internalType: 'address' }, + { name: 'value', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferFrom', + inputs: [ + { name: 'from', type: 'address', internalType: 'address' }, + { name: 'to', type: 'address', internalType: 'address' }, + { name: 'value', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'pure', + }, + { + type: 'event', + name: 'Approval', + inputs: [ + { + name: 'owner', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'spender', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'value', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'CCIPAdminTransferred', + inputs: [ + { + name: 'previousAdmin', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'newAdmin', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'DefaultAdminDelayChangeCanceled', + inputs: [], + anonymous: false, + }, + { + type: 'event', + name: 'DefaultAdminDelayChangeScheduled', + inputs: [ + { + name: 'newDelay', + type: 'uint48', + indexed: false, + internalType: 'uint48', + }, + { + name: 'effectSchedule', + type: 'uint48', + indexed: false, + internalType: 'uint48', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'DefaultAdminTransferCanceled', + inputs: [], + anonymous: false, + }, + { + type: 'event', + name: 'DefaultAdminTransferScheduled', + inputs: [ + { + name: 'newAdmin', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'acceptSchedule', + type: 'uint48', + indexed: false, + internalType: 'uint48', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'RoleAdminChanged', + inputs: [ + { + name: 'role', + type: 'bytes32', + indexed: true, + internalType: 'bytes32', + }, + { + name: 'previousAdminRole', + type: 'bytes32', + indexed: true, + internalType: 'bytes32', + }, + { + name: 'newAdminRole', + type: 'bytes32', + indexed: true, + internalType: 'bytes32', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'RoleGranted', + inputs: [ + { + name: 'role', + type: 'bytes32', + indexed: true, + internalType: 'bytes32', + }, + { + name: 'account', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'sender', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'RoleRevoked', + inputs: [ + { + name: 'role', + type: 'bytes32', + indexed: true, + internalType: 'bytes32', + }, + { + name: 'account', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'sender', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Transfer', + inputs: [ + { + name: 'from', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'to', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'value', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { type: 'error', name: 'AccessControlBadConfirmation', inputs: [] }, + { + type: 'error', + name: 'AccessControlEnforcedDefaultAdminDelay', + inputs: [{ name: 'schedule', type: 'uint48', internalType: 'uint48' }], + }, + { + type: 'error', + name: 'AccessControlEnforcedDefaultAdminRules', + inputs: [], + }, + { + type: 'error', + name: 'AccessControlInvalidDefaultAdmin', + inputs: [ + { + name: 'defaultAdmin', + type: 'address', + internalType: 'address', + }, + ], + }, + { + type: 'error', + name: 'AccessControlUnauthorizedAccount', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'neededRole', type: 'bytes32', internalType: 'bytes32' }, + ], + }, + { type: 'error', name: 'CannotRenounceCCIPAdmin', inputs: [] }, + { + type: 'error', + name: 'ERC20InsufficientAllowance', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { name: 'allowance', type: 'uint256', internalType: 'uint256' }, + { name: 'needed', type: 'uint256', internalType: 'uint256' }, + ], + }, + { + type: 'error', + name: 'ERC20InsufficientBalance', + inputs: [ + { name: 'sender', type: 'address', internalType: 'address' }, + { name: 'balance', type: 'uint256', internalType: 'uint256' }, + { name: 'needed', type: 'uint256', internalType: 'uint256' }, + ], + }, + { + type: 'error', + name: 'ERC20InvalidApprover', + inputs: [{ name: 'approver', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'ERC20InvalidReceiver', + inputs: [{ name: 'receiver', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'ERC20InvalidSender', + inputs: [{ name: 'sender', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'ERC20InvalidSpender', + inputs: [{ name: 'spender', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'MaxSupplyExceeded', + inputs: [ + { + name: 'supplyAfterMint', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'maxSupply', type: 'uint256', internalType: 'uint256' }, + ], + }, + { type: 'error', name: 'OnlyCCIPAdmin', inputs: [] }, + { type: 'error', name: 'PreMintAddressNotSet', inputs: [] }, + { + type: 'error', + name: 'PreMintRecipientSetWithZeroPreMint', + inputs: [ + { + name: 'preMintRecipient', + type: 'address', + internalType: 'address', + }, + ], + }, + { + type: 'error', + name: 'SafeCastOverflowedUintDowncast', + inputs: [ + { name: 'bits', type: 'uint8', internalType: 'uint8' }, + { name: 'value', type: 'uint256', internalType: 'uint256' }, + ], + }, + // generate:end +] as const diff --git a/ccip-sdk/src/evm/abi/ERC20LockBox.ts b/ccip-sdk/src/evm/abi/ERC20LockBox.ts new file mode 100644 index 00000000..6c9caac0 --- /dev/null +++ b/ccip-sdk/src/evm/abi/ERC20LockBox.ts @@ -0,0 +1,256 @@ +// TODO: track a v2 release tag and the v2.0.0 folder instead of a commit + latest/ folder, once 2.0.0 is released in `chainlink-ccip` +export default [ + // generate: + // fetch('https://github.com/smartcontractkit/chainlink-ccip/raw/refs/heads/main/chains/evm/gobindings/generated/v2_0_0/erc20_lock_box/erc20_lock_box.go') + // .then((res) => res.text()) + // .then((body) => body.match(/^\s*ABI: "(.*?)",$/m)?.[1]) + // .then((abi) => JSON.parse(abi.replace(/\\"/g, '"'))) + // .then((obj) => require('util').inspect(obj, {depth:99}).split('\n').slice(1, -1)) + { + type: 'constructor', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'acceptOwnership', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'applyAuthorizedCallerUpdates', + inputs: [ + { + name: 'authorizedCallerArgs', + type: 'tuple', + internalType: 'struct AuthorizedCallers.AuthorizedCallerArgs', + components: [ + { + name: 'addedCallers', + type: 'address[]', + internalType: 'address[]', + }, + { + name: 'removedCallers', + type: 'address[]', + internalType: 'address[]', + }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'deposit', + inputs: [ + { name: 'token', type: 'address', internalType: 'address' }, + { name: '', type: 'uint64', internalType: 'uint64' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'getAllAuthorizedCallers', + inputs: [], + outputs: [{ name: '', type: 'address[]', internalType: 'address[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getToken', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'contract IERC20' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'isTokenSupported', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'owner', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'transferOwnership', + inputs: [{ name: 'to', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'pure', + }, + { + type: 'function', + name: 'withdraw', + inputs: [ + { name: 'token', type: 'address', internalType: 'address' }, + { name: '', type: 'uint64', internalType: 'uint64' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + { name: 'recipient', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'AuthorizedCallerAdded', + inputs: [ + { + name: 'caller', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'AuthorizedCallerRemoved', + inputs: [ + { + name: 'caller', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Deposit', + inputs: [ + { + name: 'token', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'depositor', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'OwnershipTransferRequested', + inputs: [ + { + name: 'from', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'to', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'OwnershipTransferred', + inputs: [ + { + name: 'from', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'to', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Withdrawal', + inputs: [ + { + name: 'token', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'recipient', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { type: 'error', name: 'CannotTransferToSelf', inputs: [] }, + { + type: 'error', + name: 'InsufficientBalance', + inputs: [ + { name: 'requested', type: 'uint256', internalType: 'uint256' }, + { name: 'available', type: 'uint256', internalType: 'uint256' }, + ], + }, + { type: 'error', name: 'MustBeProposedOwner', inputs: [] }, + { type: 'error', name: 'OnlyCallableByOwner', inputs: [] }, + { type: 'error', name: 'OwnerCannotBeZero', inputs: [] }, + { type: 'error', name: 'RecipientCannotBeZeroAddress', inputs: [] }, + { + type: 'error', + name: 'SafeERC20FailedOperation', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + }, + { type: 'error', name: 'TokenAmountCannotBeZero', inputs: [] }, + { + type: 'error', + name: 'UnauthorizedCaller', + inputs: [{ name: 'caller', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'UnsupportedToken', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + }, + { type: 'error', name: 'ZeroAddressNotAllowed', inputs: [] }, + // generate:end +] as const diff --git a/ccip-sdk/src/evm/abi/RegistryModuleOwnerCustom_1_6.ts b/ccip-sdk/src/evm/abi/RegistryModuleOwnerCustom_1_6.ts new file mode 100644 index 00000000..5fd9f05e --- /dev/null +++ b/ccip-sdk/src/evm/abi/RegistryModuleOwnerCustom_1_6.ts @@ -0,0 +1,85 @@ +export default [ + // generate: + // fetch('https://raw.githubusercontent.com/smartcontractkit/chainlink-ccip/release/contracts-ccip-1.6.2/chains/evm/gobindings/generated/v1_6_0/registry_module_owner_custom/registry_module_owner_custom.go') + // .then((res) => res.text()) + // .then((body) => body.match(/^\s*ABI: "(.*?)",$/m)?.[1]) + // .then((abi) => JSON.parse(abi.replace(/\\"/g, '"'))) + // .then((obj) => require('util').inspect(obj, {depth:99}).split('\n').slice(1, -1)) + { + type: 'constructor', + inputs: [ + { + name: 'tokenAdminRegistry', + type: 'address', + internalType: 'address', + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'registerAccessControlDefaultAdmin', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'registerAdminViaGetCCIPAdmin', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'registerAdminViaOwner', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'event', + name: 'AdministratorRegistered', + inputs: [ + { + name: 'token', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'administrator', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { type: 'error', name: 'AddressZero', inputs: [] }, + { + type: 'error', + name: 'CanOnlySelfRegister', + inputs: [ + { name: 'admin', type: 'address', internalType: 'address' }, + { name: 'token', type: 'address', internalType: 'address' }, + ], + }, + { + type: 'error', + name: 'RequiredRoleNotFound', + inputs: [ + { name: 'msgSender', type: 'address', internalType: 'address' }, + { name: 'role', type: 'bytes32', internalType: 'bytes32' }, + { name: 'token', type: 'address', internalType: 'address' }, + ], + }, + // generate:end +] as const diff --git a/ccip-sdk/src/evm/const.ts b/ccip-sdk/src/evm/const.ts index 6a6da28e..440a184f 100644 --- a/ccip-sdk/src/evm/const.ts +++ b/ccip-sdk/src/evm/const.ts @@ -6,6 +6,8 @@ import CCIPReceiver_2_0_ABI from './abi/CCIPReceiver_2_0.ts' import CCTPVerifier_2_0_ABI from './abi/CCTPVerifier_2_0.ts' import CommitStore_1_2_ABI from './abi/CommitStore_1_2.ts' import CommitStore_1_5_ABI from './abi/CommitStore_1_5.ts' +import CrossChainToken_ABI from './abi/CrossChainToken.ts' +import ERC20LockBox_2_0_ABI from './abi/ERC20LockBox.ts' import FeeQuoter_1_6_ABI from './abi/FeeQuoter_1_6.ts' import FeeQuoter_2_0_ABI from './abi/FeeQuoter_2_0.ts' import TokenPool_1_5_ABI from './abi/LockReleaseTokenPool_1_5.ts' @@ -20,6 +22,7 @@ import EVM2EVMOnRamp_1_5_ABI from './abi/OnRamp_1_5.ts' import OnRamp_1_6_ABI from './abi/OnRamp_1_6.ts' import OnRamp_2_0_ABI from './abi/OnRamp_2_0.ts' import PriceRegistry_1_2_ABI from './abi/PriceRegistry_1_2.ts' +import RegistryModuleOwnerCustom_1_6_ABI from './abi/RegistryModuleOwnerCustom_1_6.ts' import Router_ABI from './abi/Router.ts' import TokenAdminRegistry_ABI from './abi/TokenAdminRegistry_1_5.ts' import TokenPool_2_0_ABI from './abi/TokenPool_2_0.ts' @@ -49,6 +52,8 @@ export const interfaces = { Router: new Interface(Router_ABI), Token: new Interface(Token_ABI), TokenAdminRegistry: new Interface(TokenAdminRegistry_ABI), + RegistryModuleOwnerCustom: new Interface(RegistryModuleOwnerCustom_1_6_ABI), + CrossChainToken: new Interface(CrossChainToken_ABI), FeeQuoter_v1_6: new Interface(FeeQuoter_1_6_ABI), FeeQuoter_v2_0: new Interface(FeeQuoter_2_0_ABI), TokenPool_v2_0: new Interface(TokenPool_2_0_ABI), @@ -69,6 +74,7 @@ export const interfaces = { EVM2EVMOnRamp_v1_2: new Interface(EVM2EVMOnRamp_1_2_ABI), PriceRegistry_v1_2: new Interface(PriceRegistry_1_2_ABI), USDCTokenPoolProxy_v2_0: new Interface(USDCTokenPoolProxy_2_0_ABI), + ERC20LockBox_v2_0: new Interface(ERC20LockBox_2_0_ABI), CCTPVerifier_v2_0: new Interface(CCTPVerifier_2_0_ABI), VersionedVerifierResolver_v2_0: new Interface(VersionedVerifierResolver_2_0_ABI), Custom: new Interface(customErrors), diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index d3c78090..3dfffc41 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -163,8 +163,8 @@ function encodeAddressToEvm(address: BytesLike): string { return hexlify(encodeAddressToAny(address)) } -/** typeguard for ethers Signer interface (used for `wallet`s) */ -function isSigner(wallet: unknown): wallet is Signer { +/** Typeguard for ethers Signer interface (used for `wallet`s). */ +export function isSigner(wallet: unknown): wallet is Signer { return ( typeof wallet === 'object' && wallet !== null && @@ -178,7 +178,7 @@ function isSigner(wallet: unknown): wallet is Signer { * Try sendTransaction() first (works with browser wallets), * fallback to signTransaction() + broadcastTransaction() if unsupported. */ -async function submitTransaction( +export async function submitTransaction( wallet: Signer, tx: TransactionRequest, provider: JsonRpcApiProvider, @@ -406,6 +406,17 @@ export class EVMChain extends Chain { return this.nonces[address]!++ } + /** + * Undo the last {@link nextNonce} increment for a wallet address. + * {@link nextNonce} hands out a nonce optimistically; if the send then fails + * before broadcast, call this so the counter is reused rather than leaving a + * permanent gap that stalls every later transaction. No-op if uncached. + * @param address - Wallet address whose cached nonce to roll back + */ + rollbackNonce(address: string): void { + if (this.nonces[address] != null) this.nonces[address]-- + } + /** * Creates a JSON-RPC provider from a URL. * @param url - WebSocket (wss://) or HTTP (https://) endpoint URL. @@ -1895,14 +1906,14 @@ export class EVMChain extends Chain { const config = (await resultToObject(contract.getTokenConfig(token))) as CleanAddressable< Partial>> > - if (!config.administrator || config.administrator === ZeroAddress) + const hasPending = config.pendingAdministrator && config.pendingAdministrator !== ZeroAddress + if ((!config.administrator || config.administrator === ZeroAddress) && !hasPending) throw new CCIPTokenNotConfiguredError(token, registry) - if (!config.pendingAdministrator || config.pendingAdministrator === ZeroAddress) - delete config.pendingAdministrator + if (!hasPending) delete config.pendingAdministrator if (!config.tokenPool || config.tokenPool === ZeroAddress) delete config.tokenPool return { ...config, - administrator: config.administrator, + administrator: config.administrator ?? ZeroAddress, } } @@ -1931,6 +1942,9 @@ export class EVMChain extends Chain { let token, router, + owner, + rateLimitAdmin, + feeAdmin, allowedFinality, tokenTransferFeeConfig, previousPool: string | undefined, @@ -1942,7 +1956,9 @@ export class EVMChain extends Chain { this.provider, ) as unknown as TypedContract token = contract.getToken() + owner = contract.owner() router = contract.getRouter() + rateLimitAdmin = contract.getRateLimitAdmin() if (type.endsWith('AndProxy')) { const proxy = new Contract( tokenPool, @@ -1971,7 +1987,11 @@ export class EVMChain extends Chain { this.provider, ) as unknown as TypedContract token = contract.getToken() - router = contract.getDynamicConfig().then(([router]) => router) + owner = contract.owner() + const dynamicConfig = contract.getDynamicConfig() + router = dynamicConfig.then(([router]) => router) + rateLimitAdmin = dynamicConfig.then(([, rateLimitAdmin]) => rateLimitAdmin) + feeAdmin = dynamicConfig.then(([, , feeAdmin]) => feeAdmin) if (type.includes('LockRelease')) { const lockBox_ = await resultToObject(contract.getLockBox().catch(() => null)) if (lockBox_ && !lockBox_.match(/^(0x)?0*$/)) lockBox = lockBox_ @@ -2016,23 +2036,40 @@ export class EVMChain extends Chain { return Promise.all([ token, router, + owner, + rateLimitAdmin, + feeAdmin, allowedFinality, tokenTransferFeeConfig, previousTypeAndVersion, - ]).then(([token, router, allowedFinality, tokenTransferFeeConfig, previousTypeAndVersion]) => { - return { - token: token as CleanAddressable, - router: router as CleanAddressable, - typeAndVersion, - ...(allowedFinality != null && decodeFinalityAllowed(allowedFinality)), - ...(tokenTransferFeeConfig != null && { tokenTransferFeeConfig }), - ...(previousPool != null && { - previousPool, - previousTypeAndVersion: previousTypeAndVersion![2], - }), - ...(!!lockBox && { lockBox }), - } - }) + ]).then( + ([ + token, + router, + owner, + rateLimitAdmin, + feeAdmin, + allowedFinality, + tokenTransferFeeConfig, + previousTypeAndVersion, + ]) => { + return { + token: token as CleanAddressable, + router: router as CleanAddressable, + owner: owner as CleanAddressable, + typeAndVersion, + ...(rateLimitAdmin && { rateLimitAdmin: rateLimitAdmin as string }), + ...(feeAdmin && { feeAdmin: feeAdmin as string }), + ...(allowedFinality != null && decodeFinalityAllowed(allowedFinality)), + ...(tokenTransferFeeConfig != null && { tokenTransferFeeConfig }), + ...(previousPool != null && { + previousPool, + previousTypeAndVersion: previousTypeAndVersion![2], + }), + ...(!!lockBox && { lockBox }), + } + }, + ) } /** diff --git a/ccip-sdk/src/execution.test.ts b/ccip-sdk/src/execution.test.ts index f0496a6d..778ecdb3 100644 --- a/ccip-sdk/src/execution.test.ts +++ b/ccip-sdk/src/execution.test.ts @@ -165,9 +165,15 @@ class MockChain extends Chain { async getTokenPoolConfig(_tokenPool: string): Promise<{ token: string router: string + owner: string typeAndVersion?: string }> { - return { token: '0xToken', router: '0xRouter', typeAndVersion: 'TokenPool 1.5.0' } + return { + token: '0xToken', + router: '0xRouter', + owner: '0xOwner', + typeAndVersion: 'TokenPool 1.5.0', + } } async getTokenPoolRemotes(_pool: string, _remoteChainSelector: bigint): Promise { diff --git a/ccip-sdk/src/index.ts b/ccip-sdk/src/index.ts index 02dd0890..4991ed8d 100644 --- a/ccip-sdk/src/index.ts +++ b/ccip-sdk/src/index.ts @@ -109,6 +109,44 @@ export { // errors export * from './errors/index.ts' +// token-admin shared types +export type { + AcceptOwnershipParams, + AppendRemotePoolAddressesParams, + AppendRemotePoolAddressesResult, + ApplyChainUpdatesParams, + ChainRateLimiterConfig, + DeleteChainConfigParams, + DeleteChainConfigResult, + DeployVerificationTarget, + EVMFactoryDeployPoolParams, + EVMFactoryDeployTokenAndPoolParams, + ExecuteOwnershipTransferParams, + FactoryDeployPoolResult, + FactoryDeployTokenAndPoolResult, + GrantMintBurnAccessParams, + MintBurnRole, + OwnershipResult, + ProvideLiquidityParams, + ProvideLiquidityResult, + RateLimiterConfig, + RemoteChainConfig, + RemoveRemotePoolAddressesParams, + RemoveRemotePoolAddressesResult, + RevokeMintBurnAccessParams, + RevokeMintBurnAccessResult, + SetAllowedFinalityConfigParams, + SetAllowedFinalityConfigResult, + SetChainRateLimiterConfigParams, + SetFeeAdminParams, + SetFeeAdminResult, + SetRateLimitAdminParams, + SetTokenTransferFeeConfigParams, + SetTokenTransferFeeConfigResult, + TokenTransferFeeConfigUpdate, + TransferOwnershipParams, +} from './token-admin/types.ts' + // chains import { AptosChain } from './aptos/index.ts' export type { UnsignedAptosTx } from './aptos/index.ts' diff --git a/ccip-sdk/src/selectors.ts b/ccip-sdk/src/selectors.ts index 812f25e9..277f3c87 100644 --- a/ccip-sdk/src/selectors.ts +++ b/ccip-sdk/src/selectors.ts @@ -1420,6 +1420,12 @@ const SELECTORS: Selectors = { network_type: 'TESTNET', family: 'EVM', }, + '364301': { + selector: 17611928792452358269n, + name: 't-rex-testnet', + network_type: 'TESTNET', + family: 'EVM', + }, '421613': { selector: 6101244977088475029n, name: 'ethereum-testnet-goerli-arbitrum-1', diff --git a/ccip-sdk/src/solana/idl/1.6.0/LOCK_RELEASE_TOKEN_POOL.ts b/ccip-sdk/src/solana/idl/1.6.0/LOCK_RELEASE_TOKEN_POOL.ts new file mode 100644 index 00000000..2891e877 --- /dev/null +++ b/ccip-sdk/src/solana/idl/1.6.0/LOCK_RELEASE_TOKEN_POOL.ts @@ -0,0 +1,1972 @@ +// generate: +// fetch('https://github.com/smartcontractkit/chainlink-ccip/raw/refs/heads/main/chains/solana/contracts/target/types/lockrelease_token_pool.ts') +// .then((res) => res.text()) +// .then((text) => text.trim()) +export type LockreleaseTokenPool = { + version: '1.6.3' + name: 'lockrelease_token_pool' + instructions: [ + { + name: 'initGlobalConfig' + accounts: [ + { + name: 'config' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'routerAddress' + type: 'publicKey' + }, + { + name: 'rmnAddress' + type: 'publicKey' + }, + ] + }, + { + name: 'updateSelfServedAllowed' + accounts: [ + { + name: 'config' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'selfServedAllowed' + type: 'bool' + }, + ] + }, + { + name: 'updateDefaultRouter' + accounts: [ + { + name: 'config' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'routerAddress' + type: 'publicKey' + }, + ] + }, + { + name: 'updateDefaultRmn' + accounts: [ + { + name: 'config' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'rmnAddress' + type: 'publicKey' + }, + ] + }, + { + name: 'initialize' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + { + name: 'config' + isMut: false + isSigner: false + }, + ] + args: [] + }, + { + name: 'typeVersion' + docs: [ + 'Returns the program type (name) and version.', + 'Used by offchain code to easily determine which program & version is being interacted with.', + '', + '# Arguments', + '* `ctx` - The context', + ] + accounts: [ + { + name: 'clock' + isMut: false + isSigner: false + }, + ] + args: [] + returns: 'string' + }, + { + name: 'transferOwnership' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + ] + args: [ + { + name: 'proposedOwner' + type: 'publicKey' + }, + ] + }, + { + name: 'acceptOwnership' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + ] + args: [] + }, + { + name: 'setRouter' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'newRouter' + type: 'publicKey' + }, + ] + }, + { + name: 'setRmn' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'rmnAddress' + type: 'publicKey' + }, + ] + }, + { + name: 'initializeStateVersion' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + ] + args: [ + { + name: 'mint' + type: 'publicKey' + }, + ] + }, + { + name: 'initChainRemoteConfig' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'remoteChainSelector' + type: 'u64' + }, + { + name: 'mint' + type: 'publicKey' + }, + { + name: 'cfg' + type: { + defined: 'RemoteConfig' + } + }, + ] + }, + { + name: 'editChainRemoteConfig' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'remoteChainSelector' + type: 'u64' + }, + { + name: 'mint' + type: 'publicKey' + }, + { + name: 'cfg' + type: { + defined: 'RemoteConfig' + } + }, + ] + }, + { + name: 'appendRemotePoolAddresses' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'remoteChainSelector' + type: 'u64' + }, + { + name: 'mint' + type: 'publicKey' + }, + { + name: 'addresses' + type: { + vec: { + defined: 'RemoteAddress' + } + } + }, + ] + }, + { + name: 'setChainRateLimit' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + ] + args: [ + { + name: 'remoteChainSelector' + type: 'u64' + }, + { + name: 'mint' + type: 'publicKey' + }, + { + name: 'inbound' + type: { + defined: 'RateLimitConfig' + } + }, + { + name: 'outbound' + type: { + defined: 'RateLimitConfig' + } + }, + ] + }, + { + name: 'setRateLimitAdmin' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + ] + args: [ + { + name: 'mint' + type: 'publicKey' + }, + { + name: 'newRateLimitAdmin' + type: 'publicKey' + }, + ] + }, + { + name: 'deleteChainConfig' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + ] + args: [ + { + name: 'remoteChainSelector' + type: 'u64' + }, + { + name: 'mint' + type: 'publicKey' + }, + ] + }, + { + name: 'configureAllowList' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'add' + type: { + vec: 'publicKey' + } + }, + { + name: 'enabled' + type: 'bool' + }, + ] + }, + { + name: 'removeFromAllowList' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'remove' + type: { + vec: 'publicKey' + } + }, + ] + }, + { + name: 'releaseOrMintTokens' + accounts: [ + { + name: 'authority' + isMut: false + isSigner: true + }, + { + name: 'offrampProgram' + isMut: false + isSigner: false + docs: [ + 'CHECK offramp program: exists only to derive the allowed offramp PDA', + 'and the authority PDA.', + ] + }, + { + name: 'allowedOfframp' + isMut: false + isSigner: false + docs: [ + 'CHECK PDA of the router program verifying the signer is an allowed offramp.', + "If PDA does not exist, the router doesn't allow this offramp", + ] + }, + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'tokenProgram' + isMut: false + isSigner: false + }, + { + name: 'mint' + isMut: true + isSigner: false + }, + { + name: 'poolSigner' + isMut: false + isSigner: false + }, + { + name: 'poolTokenAccount' + isMut: true + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + { + name: 'rmnRemote' + isMut: false + isSigner: false + }, + { + name: 'rmnRemoteCurses' + isMut: false + isSigner: false + }, + { + name: 'rmnRemoteConfig' + isMut: false + isSigner: false + }, + { + name: 'receiverTokenAccount' + isMut: true + isSigner: false + }, + ] + args: [ + { + name: 'releaseOrMint' + type: { + defined: 'ReleaseOrMintInV1' + } + }, + ] + returns: { + defined: 'ReleaseOrMintOutV1' + } + }, + { + name: 'lockOrBurnTokens' + accounts: [ + { + name: 'authority' + isMut: false + isSigner: true + }, + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'tokenProgram' + isMut: false + isSigner: false + }, + { + name: 'mint' + isMut: true + isSigner: false + }, + { + name: 'poolSigner' + isMut: false + isSigner: false + }, + { + name: 'poolTokenAccount' + isMut: true + isSigner: false + }, + { + name: 'rmnRemote' + isMut: false + isSigner: false + }, + { + name: 'rmnRemoteCurses' + isMut: false + isSigner: false + }, + { + name: 'rmnRemoteConfig' + isMut: false + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + ] + args: [ + { + name: 'lockOrBurn' + type: { + defined: 'LockOrBurnInV1' + } + }, + ] + returns: { + defined: 'LockOrBurnOutV1' + } + }, + { + name: 'setRebalancer' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + ] + args: [ + { + name: 'rebalancer' + type: 'publicKey' + }, + ] + }, + { + name: 'setCanAcceptLiquidity' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + ] + args: [ + { + name: 'allow' + type: 'bool' + }, + ] + }, + { + name: 'provideLiquidity' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'tokenProgram' + isMut: false + isSigner: false + }, + { + name: 'mint' + isMut: true + isSigner: false + }, + { + name: 'poolSigner' + isMut: false + isSigner: false + }, + { + name: 'poolTokenAccount' + isMut: true + isSigner: false + }, + { + name: 'remoteTokenAccount' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + ] + args: [ + { + name: 'amount' + type: 'u64' + }, + ] + }, + { + name: 'withdrawLiquidity' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'tokenProgram' + isMut: false + isSigner: false + }, + { + name: 'mint' + isMut: true + isSigner: false + }, + { + name: 'poolSigner' + isMut: false + isSigner: false + }, + { + name: 'poolTokenAccount' + isMut: true + isSigner: false + }, + { + name: 'remoteTokenAccount' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + ] + args: [ + { + name: 'amount' + type: 'u64' + }, + ] + }, + ] + accounts: [ + { + name: 'poolConfig' + type: { + kind: 'struct' + fields: [ + { + name: 'version' + type: 'u8' + }, + { + name: 'selfServedAllowed' + type: 'bool' + }, + { + name: 'router' + type: 'publicKey' + }, + { + name: 'rmnRemote' + type: 'publicKey' + }, + ] + } + }, + { + name: 'state' + type: { + kind: 'struct' + fields: [ + { + name: 'version' + type: 'u8' + }, + { + name: 'config' + type: { + defined: 'BaseConfig' + } + }, + ] + } + }, + { + name: 'chainConfig' + type: { + kind: 'struct' + fields: [ + { + name: 'base' + type: { + defined: 'BaseChain' + } + }, + ] + } + }, + ] +} + +export const IDL: LockreleaseTokenPool = { + version: '1.6.3', + name: 'lockrelease_token_pool', + instructions: [ + { + name: 'initGlobalConfig', + accounts: [ + { + name: 'config', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'routerAddress', + type: 'publicKey', + }, + { + name: 'rmnAddress', + type: 'publicKey', + }, + ], + }, + { + name: 'updateSelfServedAllowed', + accounts: [ + { + name: 'config', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'selfServedAllowed', + type: 'bool', + }, + ], + }, + { + name: 'updateDefaultRouter', + accounts: [ + { + name: 'config', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'routerAddress', + type: 'publicKey', + }, + ], + }, + { + name: 'updateDefaultRmn', + accounts: [ + { + name: 'config', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'rmnAddress', + type: 'publicKey', + }, + ], + }, + { + name: 'initialize', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + { + name: 'config', + isMut: false, + isSigner: false, + }, + ], + args: [], + }, + { + name: 'typeVersion', + docs: [ + 'Returns the program type (name) and version.', + 'Used by offchain code to easily determine which program & version is being interacted with.', + '', + '# Arguments', + '* `ctx` - The context', + ], + accounts: [ + { + name: 'clock', + isMut: false, + isSigner: false, + }, + ], + args: [], + returns: 'string', + }, + { + name: 'transferOwnership', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + ], + args: [ + { + name: 'proposedOwner', + type: 'publicKey', + }, + ], + }, + { + name: 'acceptOwnership', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + ], + args: [], + }, + { + name: 'setRouter', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'newRouter', + type: 'publicKey', + }, + ], + }, + { + name: 'setRmn', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'rmnAddress', + type: 'publicKey', + }, + ], + }, + { + name: 'initializeStateVersion', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + ], + args: [ + { + name: 'mint', + type: 'publicKey', + }, + ], + }, + { + name: 'initChainRemoteConfig', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'remoteChainSelector', + type: 'u64', + }, + { + name: 'mint', + type: 'publicKey', + }, + { + name: 'cfg', + type: { + defined: 'RemoteConfig', + }, + }, + ], + }, + { + name: 'editChainRemoteConfig', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'remoteChainSelector', + type: 'u64', + }, + { + name: 'mint', + type: 'publicKey', + }, + { + name: 'cfg', + type: { + defined: 'RemoteConfig', + }, + }, + ], + }, + { + name: 'appendRemotePoolAddresses', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'remoteChainSelector', + type: 'u64', + }, + { + name: 'mint', + type: 'publicKey', + }, + { + name: 'addresses', + type: { + vec: { + defined: 'RemoteAddress', + }, + }, + }, + ], + }, + { + name: 'setChainRateLimit', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + ], + args: [ + { + name: 'remoteChainSelector', + type: 'u64', + }, + { + name: 'mint', + type: 'publicKey', + }, + { + name: 'inbound', + type: { + defined: 'RateLimitConfig', + }, + }, + { + name: 'outbound', + type: { + defined: 'RateLimitConfig', + }, + }, + ], + }, + { + name: 'setRateLimitAdmin', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + ], + args: [ + { + name: 'mint', + type: 'publicKey', + }, + { + name: 'newRateLimitAdmin', + type: 'publicKey', + }, + ], + }, + { + name: 'deleteChainConfig', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + ], + args: [ + { + name: 'remoteChainSelector', + type: 'u64', + }, + { + name: 'mint', + type: 'publicKey', + }, + ], + }, + { + name: 'configureAllowList', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'add', + type: { + vec: 'publicKey', + }, + }, + { + name: 'enabled', + type: 'bool', + }, + ], + }, + { + name: 'removeFromAllowList', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'remove', + type: { + vec: 'publicKey', + }, + }, + ], + }, + { + name: 'releaseOrMintTokens', + accounts: [ + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'offrampProgram', + isMut: false, + isSigner: false, + docs: [ + 'CHECK offramp program: exists only to derive the allowed offramp PDA', + 'and the authority PDA.', + ], + }, + { + name: 'allowedOfframp', + isMut: false, + isSigner: false, + docs: [ + 'CHECK PDA of the router program verifying the signer is an allowed offramp.', + "If PDA does not exist, the router doesn't allow this offramp", + ], + }, + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'tokenProgram', + isMut: false, + isSigner: false, + }, + { + name: 'mint', + isMut: true, + isSigner: false, + }, + { + name: 'poolSigner', + isMut: false, + isSigner: false, + }, + { + name: 'poolTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + { + name: 'rmnRemote', + isMut: false, + isSigner: false, + }, + { + name: 'rmnRemoteCurses', + isMut: false, + isSigner: false, + }, + { + name: 'rmnRemoteConfig', + isMut: false, + isSigner: false, + }, + { + name: 'receiverTokenAccount', + isMut: true, + isSigner: false, + }, + ], + args: [ + { + name: 'releaseOrMint', + type: { + defined: 'ReleaseOrMintInV1', + }, + }, + ], + returns: { + defined: 'ReleaseOrMintOutV1', + }, + }, + { + name: 'lockOrBurnTokens', + accounts: [ + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'tokenProgram', + isMut: false, + isSigner: false, + }, + { + name: 'mint', + isMut: true, + isSigner: false, + }, + { + name: 'poolSigner', + isMut: false, + isSigner: false, + }, + { + name: 'poolTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'rmnRemote', + isMut: false, + isSigner: false, + }, + { + name: 'rmnRemoteCurses', + isMut: false, + isSigner: false, + }, + { + name: 'rmnRemoteConfig', + isMut: false, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + ], + args: [ + { + name: 'lockOrBurn', + type: { + defined: 'LockOrBurnInV1', + }, + }, + ], + returns: { + defined: 'LockOrBurnOutV1', + }, + }, + { + name: 'setRebalancer', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + ], + args: [ + { + name: 'rebalancer', + type: 'publicKey', + }, + ], + }, + { + name: 'setCanAcceptLiquidity', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + ], + args: [ + { + name: 'allow', + type: 'bool', + }, + ], + }, + { + name: 'provideLiquidity', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'tokenProgram', + isMut: false, + isSigner: false, + }, + { + name: 'mint', + isMut: true, + isSigner: false, + }, + { + name: 'poolSigner', + isMut: false, + isSigner: false, + }, + { + name: 'poolTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'remoteTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + ], + args: [ + { + name: 'amount', + type: 'u64', + }, + ], + }, + { + name: 'withdrawLiquidity', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'tokenProgram', + isMut: false, + isSigner: false, + }, + { + name: 'mint', + isMut: true, + isSigner: false, + }, + { + name: 'poolSigner', + isMut: false, + isSigner: false, + }, + { + name: 'poolTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'remoteTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + ], + args: [ + { + name: 'amount', + type: 'u64', + }, + ], + }, + ], + accounts: [ + { + name: 'poolConfig', + type: { + kind: 'struct', + fields: [ + { + name: 'version', + type: 'u8', + }, + { + name: 'selfServedAllowed', + type: 'bool', + }, + { + name: 'router', + type: 'publicKey', + }, + { + name: 'rmnRemote', + type: 'publicKey', + }, + ], + }, + }, + { + name: 'state', + type: { + kind: 'struct', + fields: [ + { + name: 'version', + type: 'u8', + }, + { + name: 'config', + type: { + defined: 'BaseConfig', + }, + }, + ], + }, + }, + { + name: 'chainConfig', + type: { + kind: 'struct', + fields: [ + { + name: 'base', + type: { + defined: 'BaseChain', + }, + }, + ], + }, + }, + ], +} +// generate:end diff --git a/ccip-sdk/src/solana/index.ts b/ccip-sdk/src/solana/index.ts index 9fb04f40..cbaa0945 100644 --- a/ccip-sdk/src/solana/index.ts +++ b/ccip-sdk/src/solana/index.ts @@ -1520,6 +1520,8 @@ export class SolanaChain extends Chain { administrator: string pendingAdministrator?: string tokenPool?: string + poolLookupTable?: string + poolLookupTableEntries?: string[] }> { const registry_ = new PublicKey(registry) const tokenMint = new PublicKey(token) @@ -1536,6 +1538,8 @@ export class SolanaChain extends Chain { administrator: string pendingAdministrator?: string tokenPool?: string + poolLookupTable?: string + poolLookupTableEntries?: string[] } = { administrator: encodeBase58(tokenAdminRegistry.data.subarray(9, 9 + 32)), } @@ -1549,15 +1553,20 @@ export class SolanaChain extends Chain { config.pendingAdministrator = pendingAdministrator.toBase58() } - // Get token pool from lookup table if available + // Get token pool and lookup table from TAR data if available try { const lookupTableAddr = new PublicKey(tokenAdminRegistry.data.subarray(73, 73 + 32)) - const lookupTable = await this.connection.getAddressLookupTable(lookupTableAddr) - if (lookupTable.value) { - // tokenPool state PDA is at index [3] - const tokenPoolAddress = lookupTable.value.state.addresses[3] - if (tokenPoolAddress && !tokenPoolAddress.equals(PublicKey.default)) { - config.tokenPool = tokenPoolAddress.toBase58() + if (!lookupTableAddr.equals(PublicKey.default)) { + config.poolLookupTable = lookupTableAddr.toBase58() + const lookupTable = await this.connection.getAddressLookupTable(lookupTableAddr) + if (lookupTable.value) { + // Return all ALT entries + config.poolLookupTableEntries = lookupTable.value.state.addresses.map((a) => a.toBase58()) + // tokenPool state PDA is at index [3] + const tokenPoolAddress = lookupTable.value.state.addresses[3] + if (tokenPoolAddress && !tokenPoolAddress.equals(PublicKey.default)) { + config.tokenPool = tokenPoolAddress.toBase58() + } } } } catch (_err) { @@ -1576,6 +1585,9 @@ export class SolanaChain extends Chain { ): Promise<{ token: string router: string + owner: string + proposedOwner?: string + rateLimitAdmin?: string tokenPoolProgram: string typeAndVersion?: string }> { @@ -1592,14 +1604,27 @@ export class SolanaChain extends Chain { // TokenPool may not have a typeAndVersion } - // const { config }: { config: IdlTypes['BaseConfig'] } = - // tokenPoolCoder.accounts.decode('state', tokenPoolState.data) - const mint = new PublicKey(tokenPoolState.data.subarray(41, 41 + 32)) - const router = new PublicKey(tokenPoolState.data.subarray(266, 266 + 32)) + const { + config, + }: { + config: { + mint: PublicKey + router: PublicKey + owner: PublicKey + proposedOwner: PublicKey + rateLimitAdmin: PublicKey + } + } = tokenPoolCoder.accounts.decode('state', tokenPoolState.data) + + const isProposedOwnerZero = config.proposedOwner.equals(PublicKey.default) + const isRateLimitAdminZero = config.rateLimitAdmin.equals(PublicKey.default) return { - token: mint.toBase58(), - router: router.toBase58(), + token: config.mint.toBase58(), + router: config.router.toBase58(), + owner: config.owner.toBase58(), + ...(isProposedOwnerZero ? {} : { proposedOwner: config.proposedOwner.toBase58() }), + ...(isRateLimitAdminZero ? {} : { rateLimitAdmin: config.rateLimitAdmin.toBase58() }), tokenPoolProgram, typeAndVersion, } diff --git a/ccip-sdk/src/solana/utils.ts b/ccip-sdk/src/solana/utils.ts index 33fb7fba..6d9c4b83 100644 --- a/ccip-sdk/src/solana/utils.ts +++ b/ccip-sdk/src/solana/utils.ts @@ -90,6 +90,35 @@ export async function resolveATA( } } +/** CCIP token pool signer PDA seed. */ +const CCIP_TOKENPOOL_SIGNER_SEED = 'ccip_tokenpool_signer' + +/** + * Derives the Pool Signer PDA for a given mint and pool program. + * Seeds: `["ccip_tokenpool_signer", mint]` + * + * The Pool Signer PDA is the authority that signs mint/burn transactions + * autonomously for CCIP cross-chain operations. + * + * @param mint - Token mint public key + * @param poolProgramId - Pool program public key + * @returns `[poolSignerPda, bump]` + * + * @example + * ```typescript + * const [poolSignerPda] = derivePoolSignerPDA(mintPubkey, poolProgramPubkey) + * ``` + */ +export function derivePoolSignerPDA( + mint: PublicKey, + poolProgramId: PublicKey, +): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [Buffer.from(CCIP_TOKENPOOL_SIGNER_SEED), mint.toBuffer()], + poolProgramId, + ) +} + /** * Generates a hex-encoded discriminator for a Solana event. * @param eventName - Name of the event. diff --git a/ccip-sdk/src/token-admin/apply-chain-updates-utils.ts b/ccip-sdk/src/token-admin/apply-chain-updates-utils.ts new file mode 100644 index 00000000..d2ee1510 --- /dev/null +++ b/ccip-sdk/src/token-admin/apply-chain-updates-utils.ts @@ -0,0 +1,206 @@ +/** + * Shared utilities for applyChainUpdates across all chain families. + * + * Contains validation and address encoding logic used by EVM, Solana, and Aptos + * implementations to avoid code duplication. + * + * @packageDocumentation + */ + +import { hexlify, zeroPadValue } from 'ethers' + +import { + CCIPAppendRemotePoolAddressesParamsInvalidError, + CCIPApplyChainUpdatesParamsInvalidError, + CCIPDeleteChainConfigParamsInvalidError, + CCIPRemoveRemotePoolAddressesParamsInvalidError, +} from '../errors/index.ts' +import { getAddressBytes } from '../utils.ts' +import type { + AppendRemotePoolAddressesParams, + ApplyChainUpdatesParams, + DeleteChainConfigParams, + RemoveRemotePoolAddressesParams, +} from './types.ts' + +/** + * Validates applyChainUpdates parameters. + * + * Checks that poolAddress is non-empty and each chain config has valid fields: + * - `remoteChainSelector` must be non-empty + * - `remotePoolAddresses` must have at least one address + * - `remoteTokenAddress` must be non-empty + * + * @param params - Apply chain updates parameters to validate + * @throws {@link CCIPApplyChainUpdatesParamsInvalidError} on invalid params + */ +export function validateApplyChainUpdatesParams(params: ApplyChainUpdatesParams): void { + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCIPApplyChainUpdatesParamsInvalidError('poolAddress', 'must be non-empty') + } + for (let i = 0; i < params.chainsToAdd.length; i++) { + const chain = params.chainsToAdd[i]! + if (chain.remoteChainSelector == null || chain.remoteChainSelector === 0n) { + throw new CCIPApplyChainUpdatesParamsInvalidError( + `chainsToAdd[${i}].remoteChainSelector`, + 'must be non-zero', + ) + } + if (chain.remotePoolAddresses.length === 0) { + throw new CCIPApplyChainUpdatesParamsInvalidError( + `chainsToAdd[${i}].remotePoolAddresses`, + 'must have at least one address', + ) + } + if (!chain.remoteTokenAddress || chain.remoteTokenAddress.trim().length === 0) { + throw new CCIPApplyChainUpdatesParamsInvalidError( + `chainsToAdd[${i}].remoteTokenAddress`, + 'must be non-empty', + ) + } + } +} + +/** + * Encodes a remote address to 32-byte left-padded hex string. + * + * Handles all chain families: hex (EVM/Aptos), base58 (Solana), base64 (Sui/TON). + * Uses `getAddressBytes()` for universal address decoding + `zeroPadValue()` for 32-byte padding. + * Matches chainlink-deployments' `common.LeftPadBytes(addr.Bytes(), 32)`. + * + * @param address - Address in native format (hex, base58, base64) + * @returns 32-byte left-padded hex string (0x-prefixed) + */ +export function encodeRemoteAddress(address: string): string { + const bytes = getAddressBytes(address) + return zeroPadValue(hexlify(bytes), 32) +} + +/** + * Encodes a remote address to 32-byte left-padded Uint8Array. + * + * Same as {@link encodeRemoteAddress} but returns raw bytes instead of hex string. + * Used by Solana for Borsh encoding. + * + * @param address - Address in native format (hex, base58, base64) + * @returns 32-byte left-padded Uint8Array + */ +export function encodeRemoteAddressBytes(address: string): Uint8Array { + const bytes = getAddressBytes(address) + const hex = zeroPadValue(hexlify(bytes), 32) + return Uint8Array.from(Buffer.from(hex.slice(2), 'hex')) +} + +/** + * Encodes a remote pool address to raw bytes (no padding). + * + * Unlike token addresses which are always left-padded to 32 bytes, pool addresses + * preserve their original byte length (e.g. 20 bytes for EVM, 32 bytes for Solana). + * This matches the on-chain Solana program's expectation for pool address comparison + * during ReleaseOrMintTokens. + * + * @param address - Address in native format (hex, base58, base64) + * @returns Raw bytes Uint8Array (original length, no padding) + */ +export function encodeRemotePoolAddressBytes(address: string): Uint8Array { + return getAddressBytes(address) +} + +/** + * Validates appendRemotePoolAddresses parameters. + * + * Checks that: + * - `poolAddress` is non-empty + * - `remoteChainSelector` is non-empty + * - `remotePoolAddresses` has at least one entry, each non-empty + * + * @param params - Append remote pool addresses parameters to validate + * @throws {@link CCIPAppendRemotePoolAddressesParamsInvalidError} on invalid params + */ +export function validateAppendRemotePoolAddressesParams( + params: AppendRemotePoolAddressesParams, +): void { + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCIPAppendRemotePoolAddressesParamsInvalidError('poolAddress', 'must be non-empty') + } + if (params.remoteChainSelector == null || params.remoteChainSelector === 0n) { + throw new CCIPAppendRemotePoolAddressesParamsInvalidError( + 'remoteChainSelector', + 'must be non-zero', + ) + } + if (params.remotePoolAddresses.length === 0) { + throw new CCIPAppendRemotePoolAddressesParamsInvalidError( + 'remotePoolAddresses', + 'must have at least one address', + ) + } + for (let i = 0; i < params.remotePoolAddresses.length; i++) { + const addr = params.remotePoolAddresses[i]! + if (!addr || addr.trim().length === 0) { + throw new CCIPAppendRemotePoolAddressesParamsInvalidError( + `remotePoolAddresses[${i}]`, + 'must be non-empty', + ) + } + } +} + +/** + * Validates deleteChainConfig parameters. + * + * Checks that: + * - `poolAddress` is non-empty + * - `remoteChainSelector` is non-empty + * + * @param params - Delete chain config parameters to validate + * @throws {@link CCIPDeleteChainConfigParamsInvalidError} on invalid params + */ +export function validateDeleteChainConfigParams(params: DeleteChainConfigParams): void { + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCIPDeleteChainConfigParamsInvalidError('poolAddress', 'must be non-empty') + } + if (params.remoteChainSelector == null || params.remoteChainSelector === 0n) { + throw new CCIPDeleteChainConfigParamsInvalidError('remoteChainSelector', 'must be non-zero') + } +} + +/** + * Validates removeRemotePoolAddresses parameters. + * + * Checks that: + * - `poolAddress` is non-empty + * - `remoteChainSelector` is non-empty + * - `remotePoolAddresses` has at least one entry, each non-empty + * + * @param params - Remove remote pool addresses parameters to validate + * @throws {@link CCIPRemoveRemotePoolAddressesParamsInvalidError} on invalid params + */ +export function validateRemoveRemotePoolAddressesParams( + params: RemoveRemotePoolAddressesParams, +): void { + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCIPRemoveRemotePoolAddressesParamsInvalidError('poolAddress', 'must be non-empty') + } + if (params.remoteChainSelector == null || params.remoteChainSelector === 0n) { + throw new CCIPRemoveRemotePoolAddressesParamsInvalidError( + 'remoteChainSelector', + 'must be non-zero', + ) + } + if (params.remotePoolAddresses.length === 0) { + throw new CCIPRemoveRemotePoolAddressesParamsInvalidError( + 'remotePoolAddresses', + 'must have at least one address', + ) + } + for (let i = 0; i < params.remotePoolAddresses.length; i++) { + const addr = params.remotePoolAddresses[i]! + if (!addr || addr.trim().length === 0) { + throw new CCIPRemoveRemotePoolAddressesParamsInvalidError( + `remotePoolAddresses[${i}]`, + 'must be non-empty', + ) + } + } +} diff --git a/ccip-sdk/src/token-admin/set-rate-limiter-config-utils.ts b/ccip-sdk/src/token-admin/set-rate-limiter-config-utils.ts new file mode 100644 index 00000000..339de8eb --- /dev/null +++ b/ccip-sdk/src/token-admin/set-rate-limiter-config-utils.ts @@ -0,0 +1,94 @@ +/** + * Shared utilities for setChainRateLimiterConfig across all chain families. + * + * Contains validation logic used by EVM, Solana, and Aptos implementations. + * + * @packageDocumentation + */ + +import type { RateLimiterConfig, SetChainRateLimiterConfigParams } from './types.ts' +import { CCIPSetRateLimiterConfigParamsInvalidError } from '../errors/index.ts' + +/** + * Validates a single rate limiter config object. + * + * @param config - Rate limiter config to validate + * @param prefix - Parameter path prefix for error messages (e.g., "chainConfigs[0].outboundRateLimiterConfig") + * @throws {@link CCIPSetRateLimiterConfigParamsInvalidError} on invalid config + */ +function validateRateLimiterConfig(config: RateLimiterConfig, prefix: string): void { + if (config.capacity.trim().length === 0) { + throw new CCIPSetRateLimiterConfigParamsInvalidError(`${prefix}.capacity`, 'must be non-empty') + } + if (config.rate.trim().length === 0) { + throw new CCIPSetRateLimiterConfigParamsInvalidError(`${prefix}.rate`, 'must be non-empty') + } + // Validate they parse as non-negative bigints + try { + const cap = BigInt(config.capacity) + if (cap < 0n) { + throw new CCIPSetRateLimiterConfigParamsInvalidError( + `${prefix}.capacity`, + 'must be non-negative', + ) + } + } catch (e) { + if (e instanceof CCIPSetRateLimiterConfigParamsInvalidError) throw e + throw new CCIPSetRateLimiterConfigParamsInvalidError( + `${prefix}.capacity`, + 'must be a valid integer string', + ) + } + try { + const r = BigInt(config.rate) + if (r < 0n) { + throw new CCIPSetRateLimiterConfigParamsInvalidError(`${prefix}.rate`, 'must be non-negative') + } + } catch (e) { + if (e instanceof CCIPSetRateLimiterConfigParamsInvalidError) throw e + throw new CCIPSetRateLimiterConfigParamsInvalidError( + `${prefix}.rate`, + 'must be a valid integer string', + ) + } +} + +/** + * Validates setChainRateLimiterConfig parameters. + * + * Checks that poolAddress is non-empty, chainConfigs is non-empty, and each + * config entry has a valid remoteChainSelector and rate limiter configs. + * + * @param params - Set chain rate limiter config parameters to validate + * @throws {@link CCIPSetRateLimiterConfigParamsInvalidError} on invalid params + */ +export function validateSetChainRateLimiterConfigParams( + params: SetChainRateLimiterConfigParams, +): void { + if (!params.poolAddress || params.poolAddress.trim().length === 0) { + throw new CCIPSetRateLimiterConfigParamsInvalidError('poolAddress', 'must be non-empty') + } + if (params.chainConfigs.length === 0) { + throw new CCIPSetRateLimiterConfigParamsInvalidError( + 'chainConfigs', + 'must have at least one entry', + ) + } + for (let i = 0; i < params.chainConfigs.length; i++) { + const config = params.chainConfigs[i]! + if (config.remoteChainSelector == null || config.remoteChainSelector === 0n) { + throw new CCIPSetRateLimiterConfigParamsInvalidError( + `chainConfigs[${i}].remoteChainSelector`, + 'must be non-zero', + ) + } + validateRateLimiterConfig( + config.outboundRateLimiterConfig, + `chainConfigs[${i}].outboundRateLimiterConfig`, + ) + validateRateLimiterConfig( + config.inboundRateLimiterConfig, + `chainConfigs[${i}].inboundRateLimiterConfig`, + ) + } +} diff --git a/ccip-sdk/src/token-admin/types.ts b/ccip-sdk/src/token-admin/types.ts new file mode 100644 index 00000000..614bcdd8 --- /dev/null +++ b/ccip-sdk/src/token-admin/types.ts @@ -0,0 +1,1805 @@ +/** + * Shared types for token-admin entry points. + * + * These types define the unified interface for deploying CCIP-compatible tokens + * across all supported chain families (EVM, Solana, Aptos). + * + * @packageDocumentation + */ + +import type { TokenTransferFeeConfig } from '../chain.ts' +import type { FinalityAllowed } from '../extra-args.ts' + +/** + * Base parameters for deploying a new CCIP-compatible token. + * Extended by chain-specific param types. + * + * @example + * ```typescript + * const params: DeployTokenParams = { + * name: 'My Token', + * symbol: 'MTK', + * decimals: 18, + * maxSupply: 1_000_000n * 10n ** 18n, + * initialSupply: 10_000n * 10n ** 18n, + * } + * ``` + */ +export interface DeployTokenParams { + /** Token name (e.g., "My Token"). Must be non-empty. */ + name: string + /** Token symbol (e.g., "MTK"). Must be non-empty. */ + symbol: string + /** Token decimals (0-18 for EVM, 0-9 for Solana, typically 8 for Aptos). */ + decimals: number + /** Maximum supply cap. `undefined` or `0n` means unlimited. */ + maxSupply?: bigint + /** Amount to pre-mint to the deployer or recipient. `undefined` or `0n` means none. */ + initialSupply?: bigint +} + +/** + * Source-explorer verification handle for a freshly-deployed EVM contract. + * + * Carries everything a contract-verification call needs that isn't already known from the + * deploy result: which bundled contract was deployed and its ABI-encoded constructor args + * (the exact bytes appended to the init code). Feed these to the SDK `verifyDeployedContract` + * helper (in the `verify` subpath) as the encoded-args input. Present on EVM deploys only. + */ +export interface DeployVerification { + /** Bundled verification-registry key, e.g. `'CrossChainToken'` (see `listDeployableContracts()`). */ + contract: string + /** ABI-encoded constructor arguments, `0x`-prefixed (the bytes appended to the init code). */ + encodedConstructorArgs: string +} + +/** + * Unified result from {@link deployToken} on any chain family. + * + * Identical for EVM, Solana, and Aptos — matches the SDK pattern where + * signed methods return unified types (e.g., `sendMessage() -> CCIPRequest`). + * + * Chain-specific details (e.g., Aptos multi-tx hashes, Solana metadata PDA) + * are only exposed via the unsigned path ({@link generateUnsignedDeployToken}). + * + * @example + * ```typescript + * const { tokenAddress, txHash } = await admin.deployToken({ + * name: 'My Token', symbol: 'MTK', decimals: 18, + * }) + * console.log(`Deployed at ${tokenAddress}, tx: ${txHash}`) + * ``` + */ +export interface DeployTokenResult { + /** + * Deployed token address. + * - EVM: contract address (from `receipt.contractAddress`) + * - Solana: mint pubkey (base58) + * - Aptos: fungible asset metadata address (grandchild of the code object) + */ + tokenAddress: string + /** + * Primary deploy transaction hash or signature. + * - EVM: deploy tx hash + * - Solana: transaction signature (base58) + * - Aptos: publish tx hash (first of the sequential txs) + */ + txHash: string + + // ── Chain-specific optional fields ────────────────────────────────────────── + // These are populated when relevant for downstream operations (e.g., deployPool). + + /** + * Aptos code object address (parent of the FA metadata object). + * Needed as `managed_token` named address when deploying a token pool. + * Only set on Aptos deploys. + */ + codeObjectAddress?: string + /** + * Solana Metaplex metadata PDA for the mint. + * Only set on Solana deploys. + */ + metadataAddress?: string + /** EVM only: handle to verify the deployed token on a source-chain explorer. */ + verification?: DeployVerification +} + +// ─── EVM ────────────────────────────────────────────────────────────────────── + +/** + * EVM-specific parameters for deploying a canonical CCT v2.0 `CrossChainToken`. + * + * Constructor: `(ConstructorParams args, address burnMintRoleAdmin, address owner)` where + * `ConstructorParams = (name, symbol, maxSupply, preMint, preMintRecipient, decimals, ccipAdmin)`. + * `initialSupply` maps to `preMint`. When `preMint` is `0n` the `preMintRecipient` must be the + * zero address (enforced automatically). All address fields default to `ownerAddress`, and + * `ownerAddress` is required on the unsigned path / auto-filled from the signer on the signed path. + * + * @example + * ```typescript + * const params: EVMDeployTokenParams = { name: 'My Token', symbol: 'MTK', decimals: 18 } + * ``` + */ +export interface EVMDeployTokenParams extends DeployTokenParams { + /** + * Owner address (2-step `AccessControlDefaultAdminRules` admin). Required on the unsigned + * path; auto-filled from the signer on the signed path. Defaults the other address fields. + */ + ownerAddress?: string + /** CCIP admin (`getCCIPAdmin()`), used by `registration-method get-ccip-admin`. Defaults to `ownerAddress`. */ + ccipAdmin?: string + /** Admin that may grant/revoke MINTER/BURNER roles. Defaults to `ownerAddress`. */ + burnMintRoleAdmin?: string + /** Recipient of the `preMint` (`initialSupply`). Defaults to `ownerAddress`; ignored when `initialSupply` is `0n`. */ + preMintRecipient?: string +} + +/** + * EVM-specific parameters for deploying a `CrossChainPoolToken` — the CCT v2.0 contract that is + * simultaneously an ERC20 token and its own CCIP token pool (single deploy). + * + * Constructor: `(ConstructorParams tokenParams, address advancedPoolHooks, address rmnProxy, address router)`. + */ +export interface EVMDeployCrossChainPoolTokenParams extends DeployTokenParams { + /** CCIP Router address (used to derive `rmnProxy` via `getArmProxy()`). */ + routerAddress: string + /** Advanced pool hooks contract. Default: zero address (no hooks). */ + advancedPoolHooks?: string + /** CCIP admin (`getCCIPAdmin()`). Required on the unsigned path; auto-filled from the signer on the signed path. */ + ccipAdmin?: string + /** Recipient of the `preMint` (`initialSupply`). Defaults to `ccipAdmin`; ignored when `initialSupply` is `0n`. */ + preMintRecipient?: string +} + +/** + * Result of {@link deployCrossChainPoolToken}. The single deployed `address` is simultaneously the + * token and its pool, so `tokenAddress` and `poolAddress` are equal to it. + */ +export interface DeployCrossChainPoolTokenResult { + /** Deployed contract address (token == pool). */ + address: string + /** Same as `address` (the contract is its own token). */ + tokenAddress: string + /** Same as `address` (the contract is its own pool). */ + poolAddress: string + /** Deploy transaction hash. */ + txHash: string + /** Handle to verify the deployed combined token+pool on a source-chain explorer. */ + verification?: DeployVerification +} + +// ─── EVM TokenPoolFactory v2 ──────────────────────────────────────────────────── + +/** A {@link DeployVerification} handle together with the deployed address it refers to. */ +export interface DeployVerificationTarget extends DeployVerification { + /** The deployed contract address this verification handle is for. */ + address: string +} + +/** + * Params for {@link EVMTokenAdmin.deployTokenAndPoolViaFactory} — deploy a new CrossChainToken + * **and** its token pool in one transaction through a `TokenPoolFactory 2.0.0` (CREATE2). + */ +export interface EVMFactoryDeployTokenAndPoolParams { + /** The `TokenPoolFactory 2.0.0` address on this chain. */ + factoryAddress: string + name: string + symbol: string + decimals: number + maxSupply: bigint + /** Initial supply minted at deploy (default 0). */ + preMint?: bigint + /** Recipient of the pre-mint (default: the future owner). */ + preMintRecipient?: string + poolType: 'burn-mint' | 'lock-release' + /** Existing `ERC20LockBox` for lock-release; the factory auto-deploys one when omitted. */ + lockBoxAddress?: string + /** CREATE2 salt (a random 32-byte value is used when omitted). */ + salt?: string + /** Final owner of the token + pool (defaults to the signer). */ + futureOwner?: string +} + +/** + * Params for {@link EVMTokenAdmin.deployPoolViaFactory} — deploy a token pool for an **existing** + * token through a `TokenPoolFactory 2.0.0` (CREATE2). + */ +export interface EVMFactoryDeployPoolParams { + /** The `TokenPoolFactory 2.0.0` address on this chain. */ + factoryAddress: string + /** The existing token the pool will serve. */ + tokenAddress: string + decimals: number + poolType: 'burn-mint' | 'lock-release' + /** Existing `ERC20LockBox` for lock-release; the factory auto-deploys one when omitted. */ + lockBoxAddress?: string + /** CREATE2 salt (a random 32-byte value is used when omitted). */ + salt?: string + /** Final owner of the pool (defaults to the signer). */ + futureOwner?: string +} + +/** Result of {@link EVMTokenAdmin.deployTokenAndPoolViaFactory}. */ +export interface FactoryDeployTokenAndPoolResult { + /** The deployed CrossChainToken address. */ + tokenAddress: string + /** The deployed token-pool address. */ + poolAddress: string + /** Deploy transaction hash. */ + txHash: string + /** Lock-release only: the `ERC20LockBox` bound to the pool. */ + lockBoxAddress?: string + /** Verification handles for every contract the factory deployed (token, pool, [lockbox]). */ + verifications: DeployVerificationTarget[] +} + +/** Result of {@link EVMTokenAdmin.deployPoolViaFactory}. */ +export interface FactoryDeployPoolResult { + /** The deployed token-pool address. */ + poolAddress: string + /** Deploy transaction hash. */ + txHash: string + /** Lock-release only: the `ERC20LockBox` bound to the pool. */ + lockBoxAddress?: string + /** Verification handles for every contract the factory deployed (pool, [lockbox]). */ + verifications: DeployVerificationTarget[] +} + +// ─── Solana ─────────────────────────────────────────────────────────────────── + +/** + * Solana-specific parameters for deploying an SPL Token mint. + * + * Supports both SPL Token and Token-2022 programs. Metaplex metadata is + * **strongly recommended** — without it, wallets and explorers will show + * "Unknown Token". + * + * @example + * ```typescript + * const params: SolanaDeployTokenParams = { + * name: 'My Token', + * symbol: 'MTK', + * decimals: 9, + * tokenProgram: 'spl-token', + * metadataUri: 'https://arweave.net/abc123', + * initialSupply: 1_000_000n * 10n ** 9n, + * } + * ``` + */ +export interface SolanaDeployTokenParams extends DeployTokenParams { + /** Token program to use. Default: `'spl-token'`. */ + tokenProgram?: 'spl-token' | 'token-2022' + /** + * Metaplex metadata JSON URI. + * **Strongly recommended** — without it, wallets and explorers display "Unknown Token". + */ + metadataUri?: string + /** Mint authority pubkey. Default: sender/wallet pubkey. */ + mintAuthority?: string + /** Freeze authority. `null` disables freeze. Default: sender/wallet pubkey. */ + freezeAuthority?: string | null + /** Recipient for `initialSupply`. Default: sender/wallet pubkey. */ + recipient?: string +} + +// ─── Aptos ──────────────────────────────────────────────────────────────────── + +/** + * Aptos-specific parameters for deploying a managed_token Move module. + * + * Publishes the `managed_token` module bytecode, then calls `initialize()`. + * If `initialSupply > 0`, also calls `mint()`. + * + * @example + * ```typescript + * const params: AptosDeployTokenParams = { + * name: 'My Token', + * symbol: 'MTK', + * decimals: 8, + * initialSupply: 100_000_000_000n, + * icon: 'https://example.com/icon.png', + * } + * ``` + */ +export interface AptosDeployTokenParams extends DeployTokenParams { + /** Token icon URI. Passed to `initialize()` as empty string if omitted. */ + icon?: string + /** Project URL. Passed to `initialize()` as empty string if omitted. */ + project?: string + /** Recipient for `initialSupply`. Default: sender/deployer address. */ + recipient?: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Pool Deployment Types +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Supported CCIP token pool types. + * + * - `'burn-mint'` — Pool burns tokens on source and mints on destination. + * - `'lock-release'` — Pool locks tokens on source and releases on destination. + */ +export type PoolType = 'burn-mint' | 'lock-release' + +/** + * Base parameters for deploying a CCIP token pool. + * Extended by chain-specific param types. + * + * @example + * ```typescript + * const params: DeployPoolParams = { + * poolType: 'burn-mint', + * tokenAddress: '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888', + * localTokenDecimals: 18, + * } + * ``` + */ +export interface DeployPoolParams { + /** Pool type to deploy. */ + poolType: PoolType + /** + * Token address the pool manages. + * - EVM: ERC20 contract address + * - Solana: SPL mint pubkey (base58) + * - Aptos: fungible asset metadata address + */ + tokenAddress: string + /** Token decimals on this chain (must match the deployed token). */ + localTokenDecimals: number +} + +/** + * Unified result from {@link deployPool} on any chain family. + * + * @example + * ```typescript + * const { poolAddress, txHash } = await admin.deployPool(wallet, { + * poolType: 'burn-mint', + * tokenAddress: '0xa42BA...', + * localTokenDecimals: 18, + * routerAddress: '0x0BF3...', + * }) + * console.log(`Pool at ${poolAddress}, tx: ${txHash}`) + * ``` + */ +export interface DeployPoolResult { + /** + * Deployed pool address. + * - EVM: contract address + * - Solana: pool config PDA (base58) + * - Aptos: pool object address + */ + poolAddress: string + /** Primary deploy transaction hash/signature. */ + txHash: string + /** + * Whether the pool is fully initialized and ready to use. + * + * `false` for Aptos generic pools (`burn_mint_token_pool`, `lock_release_token_pool`) + * — the token creator module must call `initialize()` with stored capability refs + * (`BurnRef`/`MintRef`/`TransferRef`) before the pool can be used for CCIP operations. + * + * `true` (or `undefined` for backward compatibility) for managed/regulated pools and + * all EVM/Solana pools, which are fully initialized at deploy time. + */ + initialized?: boolean + /** + * EVM lock-release only: address of the `ERC20LockBox` bound to the token. The signed + * `deployPool` auto-deploys one and returns it here; for burn-mint pools this is omitted. + */ + lockBoxAddress?: string + /** EVM only: handle to verify the deployed pool on a source-chain explorer. */ + verification?: DeployVerification + /** EVM lock-release only: handle to verify the auto-deployed `ERC20LockBox`. */ + lockBoxVerification?: DeployVerification +} + +// ─── EVM Pool ──────────────────────────────────────────────────────────────── + +/** + * EVM-specific parameters for deploying a canonical CCT v2.0 CCIP token pool. + * + * v2.0 constructors: + * - `BurnMintTokenPool(token, localTokenDecimals, advancedPoolHooks, rmnProxy, router)` + * - `LockReleaseTokenPool(token, localTokenDecimals, advancedPoolHooks, rmnProxy, router, lockBox)` + * + * `rmnProxy` is derived automatically via `Router.getArmProxy()`; `advancedPoolHooks` defaults to + * the zero address. For `lock-release`, the signed `deployPool` auto-deploys an `ERC20LockBox`; + * the unsigned `generateUnsignedDeployPool` requires `lockBoxAddress`. + * + * @example + * ```typescript + * const params: EVMDeployPoolParams = { + * poolType: 'burn-mint', + * tokenAddress: '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888', + * localTokenDecimals: 18, + * routerAddress: '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59', + * } + * ``` + */ +export interface EVMDeployPoolParams extends DeployPoolParams { + /** CCIP Router address. Used to derive rmnProxy via `Router.getArmProxy()`. */ + routerAddress: string + /** Advanced pool hooks contract. Default: zero address (no hooks). */ + advancedPoolHooks?: string + /** + * `lock-release` only: address of an existing `ERC20LockBox`. Required by the unsigned path; + * the signed `deployPool` auto-deploys one when omitted. + */ + lockBoxAddress?: string +} + +// ─── Solana Pool ───────────────────────────────────────────────────────────── + +/** + * Solana-specific parameters for deploying (initializing) a CCIP token pool. + * + * Solana pools are pre-deployed programs. Users call `initialize` on an + * existing program — no binary deployment is needed. + * + * @example + * ```typescript + * const params: SolanaDeployPoolParams = { + * poolType: 'burn-mint', + * tokenAddress: 'J6fECVXwSX5UAeJuC2oCKrsJRjTizWa9uF1FjqzYLa9M', + * localTokenDecimals: 9, + * poolProgramId: '', + * } + * ``` + */ +export interface SolanaDeployPoolParams extends DeployPoolParams { + /** + * Program ID of the pre-deployed pool program. + * - burn-mint: burnmint_token_pool program + * - lock-release: lockrelease_token_pool program + */ + poolProgramId: string +} + +// ─── Aptos Pool ────────────────────────────────────────────────────────────── + +/** + * Aptos token module variant. Determines which Move pool module is compiled and deployed. + * + * Aptos has multiple pool implementations, each designed for a specific token standard. + * The `poolType` (`'burn-mint'` | `'lock-release'`) specifies the pool **behaviour**, + * while `tokenModule` specifies the token **standard** the pool targets. + * + * | tokenModule | poolType | Move module deployed | Use case | + * |---------------|-----------------|--------------------------------|----------| + * | `'managed'` | `'burn-mint'` | `managed_token_pool` | Tokens deployed with SDK's `deployToken()` | + * | `'generic'` | `'burn-mint'` | `burn_mint_token_pool` | Standard Fungible Asset tokens with BurnRef/MintRef | + * | `'generic'` | `'lock-release'`| `lock_release_token_pool` | Standard FA tokens (custody-based) | + * | `'regulated'` | `'burn-mint'` | `regulated_token_pool` | Tokens with pause/freeze/role-based access | + * + * Only `'generic'` supports `poolType: 'lock-release'`. Both `'managed'` and `'regulated'` + * are inherently burn-mint — they will reject `'lock-release'`. + * + * Default: `'managed'` + */ +export type AptosTokenModule = 'managed' | 'generic' | 'regulated' + +/** + * Aptos-specific parameters for deploying a CCIP token pool Move module. + * + * Publishes the appropriate pool bytecode — `init_module` runs automatically + * and creates the pool state, registers callbacks with the CCIP router. + * + * The `tokenModule` field (default: `'managed'`) selects which Move pool module + * to compile. If you deployed your token with `admin.deployToken()`, use the + * default. See {@link AptosTokenModule} for all options. + * + * For managed and regulated tokens, the SDK automatically resolves the code object + * address from the `tokenAddress` (FA metadata) by querying the on-chain object + * ownership chain. No separate code object address parameter is needed. + * + * @example Deploy pool for a managed token (default — matches `deployToken()` output) + * ```typescript + * const params: AptosDeployPoolParams = { + * poolType: 'burn-mint', + * tokenAddress: '0x89fd6b...', // FA metadata address from deployToken() + * localTokenDecimals: 8, + * routerAddress: '0xabc...', + * mcmsAddress: '0x123...', + * } + * ``` + * + * @example Deploy pool for a generic Fungible Asset (lock-release) + * ```typescript + * const params: AptosDeployPoolParams = { + * poolType: 'lock-release', + * tokenModule: 'generic', + * tokenAddress: '0x89fd6b...', + * localTokenDecimals: 8, + * routerAddress: '0xabc...', + * mcmsAddress: '0x123...', + * } + * ``` + * + * @example Deploy pool for a regulated token + * ```typescript + * const params: AptosDeployPoolParams = { + * poolType: 'burn-mint', + * tokenModule: 'regulated', + * tokenAddress: '0x89fd6b...', // FA metadata address + * localTokenDecimals: 8, + * routerAddress: '0xabc...', + * adminAddress: '0x456...', + * mcmsAddress: '0x123...', + * } + * ``` + */ +export interface AptosDeployPoolParams extends DeployPoolParams { + /** + * Aptos token module variant. Determines which Move pool is compiled. + * + * - `'managed'` (default) — For tokens deployed with the SDK's `deployToken()`. + * Only supports `poolType: 'burn-mint'`. + * - `'generic'` — For standard Aptos Fungible Asset tokens. + * Supports both `'burn-mint'` and `'lock-release'`. + * - `'regulated'` — For tokens deployed with the `regulated_token` package (pause/freeze/roles). + * Only supports `poolType: 'burn-mint'`. + * + * Default: `'managed'` + */ + tokenModule?: AptosTokenModule + /** CCIP router module address (`ccip` named address). */ + routerAddress: string + /** Address of the deployed `mcms` package. */ + mcmsAddress: string + /** + * Admin address for the regulated token's access control. + * **Required when `tokenModule` is `'regulated'`.** + * + * This is the `admin` named address in the regulated_token Move.toml — + * typically the account that manages roles (minter, burner, pauser, etc.). + */ + adminAddress?: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Propose Admin Role Types +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Base parameters for proposing an administrator in the TokenAdminRegistry. + * Extended by chain-specific param types. + * + * @example + * ```typescript + * const params: ProposeAdminRoleParams = { + * tokenAddress: '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888', + * administrator: '0x1234567890abcdef1234567890abcdef12345678', + * } + * ``` + */ +export interface ProposeAdminRoleParams { + /** Token address to propose an administrator for. */ + tokenAddress: string + /** Address of the proposed administrator. */ + administrator: string +} + +/** + * Unified result from {@link proposeAdminRole} on any chain family. + * + * @example + * ```typescript + * const { txHash } = await admin.proposeAdminRole(wallet, params) + * console.log(`Proposed admin, tx: ${txHash}`) + * ``` + */ +export interface ProposeAdminRoleResult { + /** Transaction hash/signature of the propose admin role transaction. */ + txHash: string +} + +// ─── EVM Propose Admin Role ────────────────────────────────────────────────── + +/** + * Registration method for the RegistryModuleOwnerCustom contract. + * + * - `owner` — token implements `owner()` (Ownable pattern, most common) + * - `getCCIPAdmin` — token implements `getCCIPAdmin()` (dedicated CCIP admin) + * - `accessControlDefaultAdmin` — token uses OZ AccessControl `DEFAULT_ADMIN_ROLE` + */ +export type EVMRegistrationMethod = 'owner' | 'getCCIPAdmin' | 'accessControlDefaultAdmin' + +/** + * EVM-specific parameters for proposing an administrator. + * + * On EVM, registration goes through the RegistryModuleOwnerCustom contract, + * which verifies the caller's authority over the token and then internally + * calls `proposeAdministrator(token, caller)` on the TokenAdminRegistry. + * + * The `registryModuleAddress` can be found via the CCIP API: + * `https://docs.chain.link/api/ccip/v1/chains?environment=testnet` → `registryModule` + * + * @example + * ```typescript + * // Most common: token uses Ownable (owner() method) + * const params: EVMProposeAdminRoleParams = { + * tokenAddress: '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888', + * registryModuleAddress: '0xa3c796d480638d7476792230da1E2ADa86e031b0', + * registrationMethod: 'owner', + * } + * ``` + */ +export interface EVMProposeAdminRoleParams { + /** Token address to propose admin for. */ + tokenAddress: string + /** RegistryModuleOwnerCustom contract address. */ + registryModuleAddress: string + /** Registration method — determines how the contract verifies caller authority. Defaults to `'owner'`. */ + registrationMethod?: EVMRegistrationMethod +} + +// ─── Solana Propose Admin Role ─────────────────────────────────────────────── + +/** + * Solana-specific parameters for proposing an administrator. + * + * On Solana, the TokenAdminRegistry is built into the Router program. + * + * @example + * ```typescript + * const params: SolanaProposeAdminRoleParams = { + * tokenAddress: 'J6fECVXwSX5UAeJuC2oCKrsJRjTizWa9uF1FjqzYLa9M', + * administrator: '5YNmS1R9nNSCDzb5a7mMJ1dwK9uHeAAF4CmPEwKgVWr8', + * routerAddress: '', + * } + * ``` + */ +export interface SolanaProposeAdminRoleParams extends ProposeAdminRoleParams { + /** Router address (bundles the TokenAdminRegistry on Solana). */ + routerAddress: string +} + +// ─── Aptos Propose Admin Role ──────────────────────────────────────────────── + +/** + * Aptos-specific parameters for proposing an administrator. + * + * On Aptos, the TokenAdminRegistry is a module within the CCIP router package + * (`routerAddress::token_admin_registry`). + * + * @example + * ```typescript + * const params: AptosProposeAdminRoleParams = { + * tokenAddress: '0x89fd6b...', + * administrator: '0x1234...', + * routerAddress: '0xabc...', + * } + * ``` + */ +export interface AptosProposeAdminRoleParams extends ProposeAdminRoleParams { + /** CCIP router module address. */ + routerAddress: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Accept Admin Role Types +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Base parameters for accepting an administrator role in the TokenAdminRegistry. + * Extended by chain-specific param types. + * + * @example + * ```typescript + * const params: AcceptAdminRoleParams = { + * tokenAddress: '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888', + * routerAddress: '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59', + * } + * ``` + */ +export interface AcceptAdminRoleParams { + /** Token address to accept admin role for. */ + tokenAddress: string + /** Router address (used to discover the TokenAdminRegistry). */ + routerAddress: string +} + +/** + * Unified result from {@link acceptAdminRole} on any chain family. + * + * @example + * ```typescript + * const { txHash } = await admin.acceptAdminRole(wallet, params) + * console.log(`Accepted admin, tx: ${txHash}`) + * ``` + */ +export interface AcceptAdminRoleResult { + /** Transaction hash/signature of the accept admin role transaction. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Transfer Admin Role Types +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Parameters for transferring a token administrator role. + * + * Called by the **current** administrator to hand off the admin role to a new + * address. The new admin must call {@link acceptAdminRole} to complete the transfer. + * + * Consistent across all chain families (EVM, Solana, Aptos). + * + * @example + * ```typescript + * const params: TransferAdminRoleParams = { + * tokenAddress: '0xa42BA...', + * newAdmin: '0x1234...', + * routerAddress: '0x0BF3...', + * } + * ``` + */ +export interface TransferAdminRoleParams { + /** Token address to transfer admin role for. */ + tokenAddress: string + /** Address of the new administrator. */ + newAdmin: string + /** Router address (used to discover the TokenAdminRegistry). */ + routerAddress: string +} + +/** + * Unified result from {@link transferAdminRole} on any chain family. + * + * @example + * ```typescript + * const { txHash } = await admin.transferAdminRole(wallet, params) + * console.log(`Transferred admin, tx: ${txHash}`) + * ``` + */ +export interface TransferAdminRoleResult { + /** Transaction hash/signature of the transfer admin role transaction. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Apply Chain Updates Types +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Rate limiter configuration for a remote chain. + * + * Controls the inbound/outbound token flow rate for a specific remote chain. + * Set `isEnabled: false` with `capacity: '0'` and `rate: '0'` to disable. + * + * @example + * ```typescript + * // Disabled rate limiter + * const disabled: RateLimiterConfig = { isEnabled: false, capacity: '0', rate: '0' } + * + * // Enabled: 100k tokens capacity, refilling at 167 tokens/sec (~10k/min) + * const enabled: RateLimiterConfig = { isEnabled: true, capacity: '100000000000000000000000', rate: '167000000000000000000' } + * ``` + */ +export interface RateLimiterConfig { + /** Whether the rate limiter is enabled. */ + isEnabled: boolean + /** Maximum token capacity (bigint as string to avoid JS precision loss). */ + capacity: string + /** Token refill rate per second (bigint as string). */ + rate: string +} + +/** + * Configuration for a single remote chain in a token pool. + * + * Defines how a local pool connects to its counterpart on a remote chain: + * the remote pool address(es), remote token address, and rate limits. + * + * Addresses are in their **native format** — hex for EVM/Aptos, base58 for Solana. + * The SDK handles encoding to 32-byte padded bytes internally. + * + * @example + * ```typescript + * const remoteChain: RemoteChainConfig = { + * remoteChainSelector: 16015286601757825753n, // Ethereum Sepolia + * remotePoolAddresses: ['0xd7BF0d8E6C242b6Dde4490Ab3aFc8C1e811ec9aD'], + * remoteTokenAddress: '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888', + * outboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + * inboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + * } + * ``` + */ +export interface RemoteChainConfig { + /** Remote chain selector. */ + remoteChainSelector: bigint + /** Remote pool address(es) in native format. At least one required. */ + remotePoolAddresses: string[] + /** Remote token address in native format. */ + remoteTokenAddress: string + /** Remote token decimals. Required for Solana pools (used in init_chain_remote_config). Ignored on EVM/Aptos. */ + remoteTokenDecimals?: number + /** Outbound rate limiter (local → remote). */ + outboundRateLimiterConfig: RateLimiterConfig + /** Inbound rate limiter (remote → local). */ + inboundRateLimiterConfig: RateLimiterConfig +} + +/** + * Parameters for configuring remote chains on a token pool. + * + * Uniform across all chain families — only `poolAddress` is needed. + * The SDK auto-discovers chain-specific details (program ID, mint, module name) + * from the pool account on-chain. + * + * @example + * ```typescript + * const params: ApplyChainUpdatesParams = { + * poolAddress: '0x1234...', + * remoteChainSelectorsToRemove: [], + * chainsToAdd: [{ + * remoteChainSelector: 16015286601757825753n, + * remotePoolAddresses: ['0xd7BF...'], + * remoteTokenAddress: '0xa42B...', + * outboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + * inboundRateLimiterConfig: { isEnabled: false, capacity: '0', rate: '0' }, + * }], + * } + * ``` + */ +export interface ApplyChainUpdatesParams { + /** Local pool address. */ + poolAddress: string + /** Remote chain selectors to remove (can be empty). */ + remoteChainSelectorsToRemove: bigint[] + /** Remote chain configurations to add (can be empty). */ + chainsToAdd: RemoteChainConfig[] +} + +/** + * Unified result from {@link applyChainUpdates} on any chain family. + * + * @example + * ```typescript + * const { txHash } = await admin.applyChainUpdates(wallet, params) + * console.log(`Chain updates applied, tx: ${txHash}`) + * ``` + */ +export interface ApplyChainUpdatesResult { + /** Transaction hash/signature. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Append Remote Pool Addresses Types +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Parameters for appending remote pool addresses to an existing chain config. + * + * Unlike {@link ApplyChainUpdatesParams}, this only adds pool addresses to a + * chain config that was already initialized via `applyChainUpdates`. No rate + * limiter configuration or chain initialization is performed. + * + * @example + * ```typescript + * const params: AppendRemotePoolAddressesParams = { + * poolAddress: '0x1234...', + * remoteChainSelector: 16015286601757825753n, + * remotePoolAddresses: ['0xd7BF...', '0xaabb...'], + * } + * ``` + */ +export interface AppendRemotePoolAddressesParams { + /** Local pool address. */ + poolAddress: string + /** Remote chain selector (uint64 as string). Must already be configured via applyChainUpdates. */ + remoteChainSelector: bigint + /** Remote pool addresses in native format. At least one required. */ + remotePoolAddresses: string[] +} + +/** + * Unified result from {@link appendRemotePoolAddresses} on any chain family. + * + * @example + * ```typescript + * const { txHash } = await admin.appendRemotePoolAddresses(wallet, params) + * console.log(`Remote pool addresses appended, tx: ${txHash}`) + * ``` + */ +export interface AppendRemotePoolAddressesResult { + /** Transaction hash/signature. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Remove Remote Pool Addresses Types +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Parameters for removing specific remote pool addresses from an existing chain config. + * + * Unlike {@link DeleteChainConfigParams}, this preserves the chain config and only + * removes specific pool addresses. The chain config must have been initialized via + * `applyChainUpdates` and must contain the specified pool addresses. + * + * @example + * ```typescript + * const params: RemoveRemotePoolAddressesParams = { + * poolAddress: '0x1234...', + * remoteChainSelector: 16015286601757825753n, + * remotePoolAddresses: ['0xd7BF...'], + * } + * ``` + */ +export interface RemoveRemotePoolAddressesParams { + /** Local pool address. */ + poolAddress: string + /** Remote chain selector (uint64 as string). Must already be configured via applyChainUpdates. */ + remoteChainSelector: bigint + /** Remote pool addresses to remove, in native format. At least one required. */ + remotePoolAddresses: string[] +} + +/** + * Unified result from removeRemotePoolAddresses on any chain family. + * + * @example + * ```typescript + * const { txHash } = await admin.removeRemotePoolAddresses(wallet, params) + * console.log(`Remote pool addresses removed, tx: ${txHash}`) + * ``` + */ +export interface RemoveRemotePoolAddressesResult { + /** Transaction hash/signature. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Delete Chain Config Types +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Parameters for removing an entire remote chain configuration from a token pool. + * + * This is a convenience wrapper around applyChainUpdates with only removals. + * The remote chain config must already exist (created via applyChainUpdates). + * + * @example + * ```typescript + * const params: DeleteChainConfigParams = { + * poolAddress: '0x1234...', + * remoteChainSelector: 16015286601757825753n, + * } + * ``` + */ +export interface DeleteChainConfigParams { + /** Local pool address. */ + poolAddress: string + /** Remote chain selector (uint64 as string) to remove. Must be currently configured. */ + remoteChainSelector: bigint +} + +/** + * Unified result from deleteChainConfig on any chain family. + * + * @example + * ```typescript + * const { txHash } = await admin.deleteChainConfig(wallet, params) + * console.log(`Chain config deleted, tx: ${txHash}`) + * ``` + */ +export interface DeleteChainConfigResult { + /** Transaction hash/signature. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Set Chain Rate Limiter Config Types +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Rate limiter configuration for a specific remote chain. + * + * Used by {@link SetChainRateLimiterConfigParams} to update rate limits + * on an already-configured remote chain. Unlike {@link RemoteChainConfig}, + * this does not include pool/token address fields — only rate limits. + * + * @example + * ```typescript + * const config: ChainRateLimiterConfig = { + * remoteChainSelector: 16015286601757825753n, + * outboundRateLimiterConfig: { isEnabled: true, capacity: '100000000000000000000000', rate: '167000000000000000000' }, + * inboundRateLimiterConfig: { isEnabled: true, capacity: '100000000000000000000000', rate: '167000000000000000000' }, + * } + * ``` + */ +export interface ChainRateLimiterConfig { + /** Remote chain selector (uint64 as string). */ + remoteChainSelector: bigint + /** Outbound rate limiter (local → remote). */ + outboundRateLimiterConfig: RateLimiterConfig + /** Inbound rate limiter (remote → local). */ + inboundRateLimiterConfig: RateLimiterConfig + /** + * Whether to set the custom block confirmations (FTF) rate limits. + * + * - `false` (default): sets the default rate limits (normal finality transfers) + * - `true`: sets the FTF (Faster-Than-Finality) rate limits bucket + * + * Only applies to EVM v2.0+ pools. Ignored on v1.5/v1.6 pools and non-EVM chains. + */ + customBlockConfirmations?: boolean +} + +/** + * Parameters for updating rate limiter configurations on a token pool. + * + * Updates rate limits for one or more already-configured remote chains. + * The remote chains must have been previously added via {@link applyChainUpdates}. + * + * @example + * ```typescript + * const params: SetChainRateLimiterConfigParams = { + * poolAddress: '0x1234...', + * chainConfigs: [{ + * remoteChainSelector: 16015286601757825753n, + * outboundRateLimiterConfig: { isEnabled: true, capacity: '100000000000000000000000', rate: '167000000000000000000' }, + * inboundRateLimiterConfig: { isEnabled: true, capacity: '100000000000000000000000', rate: '167000000000000000000' }, + * }], + * } + * ``` + */ +export interface SetChainRateLimiterConfigParams { + /** Local pool address. */ + poolAddress: string + /** Rate limiter configurations per remote chain. */ + chainConfigs: ChainRateLimiterConfig[] +} + +/** + * Unified result from {@link setChainRateLimiterConfig} on any chain family. + * + * @example + * ```typescript + * const { txHash } = await admin.setChainRateLimiterConfig(wallet, params) + * console.log(`Rate limits updated, tx: ${txHash}`) + * ``` + */ +export interface SetChainRateLimiterConfigResult { + /** Transaction hash/signature. */ + txHash: string +} + +// ── Set Rate Limit Admin ────────────────────────────────────────────────────── + +/** + * Parameters for {@link setRateLimitAdmin} — delegates rate-limit management + * to a separate admin address (EVM and Solana only; not available on Aptos). + * + * @example + * ```typescript + * const params: SetRateLimitAdminParams = { + * poolAddress: '0x1234...', + * rateLimitAdmin: '0xabcd...', + * } + * ``` + */ +export interface SetRateLimitAdminParams { + /** Local pool address. */ + poolAddress: string + /** New rate limit admin address. */ + rateLimitAdmin: string +} + +/** + * Unified result from {@link setRateLimitAdmin} on any chain family. + * + * @example + * ```typescript + * const { txHash } = await admin.setRateLimitAdmin(wallet, params) + * console.log(`Rate limit admin updated, tx: ${txHash}`) + * ``` + */ +export interface SetRateLimitAdminResult { + /** Transaction hash/signature. */ + txHash: string +} + +// ── Provide Liquidity (lock-release pools) ────────────────────────────────── + +/** + * Parameters for {@link provideLiquidity} — funds a lock-release token pool with + * liquidity so it can release tokens on inbound CCIP transfers. **EVM lock-release + * pools only** (burn-mint pools mint on demand and hold no liquidity). + * + * The operation is version-aware: + * - **v1.5 / v1.6**: liquidity is held by the pool. `approve(pool, amount)` then + * `pool.provideLiquidity(amount)` (caller must be the pool's rebalancer). + * - **v2.0**: liquidity lives in a separate `ERC20LockBox`. `approve(lockBox, amount)` + * then `lockBox.deposit(token, 0, amount)` (deposit is permissionless). + * + * @example + * ```typescript + * const params: ProvideLiquidityParams = { + * poolAddress: '0x1234...', + * amount: 1_000n * 10n ** 18n, + * } + * ``` + */ +export interface ProvideLiquidityParams { + /** Local lock-release pool address. */ + poolAddress: string + /** Amount of token (in smallest units) to provide as liquidity. Must be greater than 0. */ + amount: bigint +} + +/** + * Unified result from {@link provideLiquidity}. + * + * @example + * ```typescript + * const { txHash } = await admin.provideLiquidity(wallet, params) + * console.log(`Liquidity provided, tx: ${txHash}`) + * ``` + */ +export interface ProvideLiquidityResult { + /** Transaction hash of the provide/deposit transaction (the second of the two txs). */ + txHash: string +} + +// ── Set Token Transfer Fee Config (EVM v2.0+ only) ────────────────────────── + +/** + * A single per-destination token-transfer fee config update. + * + * Pairs a destination chain selector with the full {@link TokenTransferFeeConfig} + * (the 7 fields the v2.0 pool stores, including `isEnabled`). + * + * @example + * ```typescript + * const update: TokenTransferFeeConfigUpdate = { + * remoteChainSelector: 14767482510784806043n, + * config: { + * destGasOverhead: 90000, + * destBytesOverhead: 32, + * finalityFeeUSDCents: 10, + * fastFinalityFeeUSDCents: 50, + * finalityTransferFeeBps: 5, + * fastFinalityTransferFeeBps: 25, + * isEnabled: true, + * }, + * } + * ``` + */ +export interface TokenTransferFeeConfigUpdate { + /** Destination chain selector (uint64). */ + remoteChainSelector: bigint + /** Full token transfer fee config for this destination. */ + config: TokenTransferFeeConfig +} + +/** + * Parameters for {@link setTokenTransferFeeConfig} — sets per-destination token + * transfer fee configs on a CCIP token pool. **EVM v2.0+ pools only.** + * + * Encodes `applyTokenTransferFeeConfigUpdates(args[], disable[])` in a single tx: + * enabled/updated configs go in `updates`, while `disable` lists destination + * selectors whose fee config should be cleared. Access: pool owner or fee admin. + * + * @example + * ```typescript + * const params: SetTokenTransferFeeConfigParams = { + * poolAddress: '0x1234...', + * updates: [{ + * remoteChainSelector: 14767482510784806043n, + * config: { + * destGasOverhead: 90000, destBytesOverhead: 32, + * finalityFeeUSDCents: 10, fastFinalityFeeUSDCents: 50, + * finalityTransferFeeBps: 5, fastFinalityTransferFeeBps: 25, + * isEnabled: true, + * }, + * }], + * disable: [], + * } + * ``` + */ +export interface SetTokenTransferFeeConfigParams { + /** Local pool address. */ + poolAddress: string + /** Per-destination fee config updates (can be empty if only disabling). */ + updates: TokenTransferFeeConfigUpdate[] + /** Destination chain selectors whose fee config should be disabled/cleared. */ + disable?: bigint[] +} + +/** + * Unified result from {@link setTokenTransferFeeConfig}. + * + * @example + * ```typescript + * const { txHash } = await admin.setTokenTransferFeeConfig(wallet, params) + * console.log(`Token transfer fee config updated, tx: ${txHash}`) + * ``` + */ +export interface SetTokenTransferFeeConfigResult { + /** Transaction hash/signature. */ + txHash: string +} + +// ── Set Allowed Finality Config (EVM v2.0+ only) ──────────────────────────── + +/** + * Parameters for {@link setAllowedFinalityConfig} — sets the bytes4 allowed-finality + * config on a CCIP token pool. **EVM v2.0+ pools only.** Access: pool owner. + * + * `finality` is encoded to a bytes4 via the SDK finality codec: + * - `'finalized'` → `0x00000000` (full finality) + * - `'safe'` → safe-head flag set + * - a number → that many block confirmations (Faster-Than-Finality), [0..65535] + * - a {@link FinalityAllowed} object → combine the safe flag with a block depth + * + * @example + * ```typescript + * const params: SetAllowedFinalityConfigParams = { + * poolAddress: '0x1234...', + * finality: 5, // allow FTF down to 5 block confirmations + * } + * ``` + */ +export interface SetAllowedFinalityConfigParams { + /** Local pool address. */ + poolAddress: string + /** Allowed finality: `'finalized'`, `'safe'`, a block depth, or a {@link FinalityAllowed}. */ + finality: FinalityAllowed | 'finalized' | 'safe' | number +} + +/** + * Unified result from {@link setAllowedFinalityConfig}. + * + * @example + * ```typescript + * const { txHash } = await admin.setAllowedFinalityConfig(wallet, params) + * console.log(`Allowed finality config updated, tx: ${txHash}`) + * ``` + */ +export interface SetAllowedFinalityConfigResult { + /** Transaction hash/signature. */ + txHash: string +} + +// ── Set Fee Admin (EVM v2.0+ only) ────────────────────────────────────────── + +/** + * Parameters for {@link setFeeAdmin} — delegates token-transfer fee management to a + * separate admin address on a CCIP token pool. **EVM v2.0+ pools only.** + * + * Reads the current dynamic config `(router, rateLimitAdmin, feeAdmin)` and rewrites + * only the `feeAdmin`, preserving `router` and `rateLimitAdmin`. Access: pool owner. + * + * @example + * ```typescript + * const params: SetFeeAdminParams = { + * poolAddress: '0x1234...', + * feeAdmin: '0xabcd...', + * } + * ``` + */ +export interface SetFeeAdminParams { + /** Local pool address. */ + poolAddress: string + /** New fee admin address. */ + feeAdmin: string +} + +/** + * Unified result from {@link setFeeAdmin}. + * + * @example + * ```typescript + * const { txHash } = await admin.setFeeAdmin(wallet, params) + * console.log(`Fee admin updated, tx: ${txHash}`) + * ``` + */ +export interface SetFeeAdminResult { + /** Transaction hash/signature. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Create Pool Mint Authority Multisig Types (Solana-only) +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Parameters for creating an SPL Token multisig with the pool signer PDA + * as one of the signers. **Solana burn-mint pools only.** + * + * The Pool Signer PDA is automatically derived from `mint` and `poolProgramId` + * and included as the first signer. This allows the pool to autonomously + * mint/burn tokens for CCIP operations, while additional signers (e.g., a + * Squads vault) can also mint independently. + * + * @example + * ```typescript + * const params: CreatePoolMintAuthorityMultisigParams = { + * mint: 'J6fECVXwSX5UAeJuC2oCKrsJRjTizWa9uF1FjqzYLa9M', + * poolProgramId: '41FGToCmdaWa1dgZLKFAjvmx6e6AjVTX7SVRibvsMGVB', + * additionalSigners: ['59eNrRrxrZMdqJxS7J3WGaV4MLLog2er14kePiWVjXtY'], + * threshold: 1, + * } + * ``` + */ +export interface CreatePoolMintAuthorityMultisigParams { + /** SPL token mint pubkey (base58). */ + mint: string + /** Pool program ID (burn-mint pool program). */ + poolProgramId: string + /** Additional signers (e.g., Squads vault). Pool Signer PDA is auto-included as first signer. */ + additionalSigners: string[] + /** Required number of signers (m-of-n). Must be explicitly set — no default. */ + threshold: number + /** Optional seed for deterministic address derivation via createAccountWithSeed. If omitted, a random keypair is used (standard SPL pattern). */ + seed?: string +} + +/** + * Result from {@link createPoolMintAuthorityMultisig}. + * + * @example + * ```typescript + * const { multisigAddress, poolSignerPda, allSigners } = + * await admin.createPoolMintAuthorityMultisig(wallet, params) + * console.log(`Multisig: ${multisigAddress}, Pool Signer PDA: ${poolSignerPda}`) + * ``` + */ +// ═══════════════════════════════════════════════════════════════════════════════ +// Transfer Mint Authority Types (Solana-only) +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Parameters for transferring SPL token mint authority to a new address. + * **Solana only.** + * + * @example + * ```typescript + * const params: TransferMintAuthorityParams = { + * mint: 'J6fECVXwSX5UAeJuC2oCKrsJRjTizWa9uF1FjqzYLa9M', + * newMintAuthority: '2e8X9v1s9nro5ezG3osRm7bpusdYknNrQYzQMxsA4Gwh', + * } + * ``` + */ +export interface TransferMintAuthorityParams { + /** SPL token mint pubkey (base58). */ + mint: string + /** New mint authority address (base58) — typically a multisig. */ + newMintAuthority: string +} + +/** + * Result from {@link transferMintAuthority}. + * + * @example + * ```typescript + * const { txHash } = await admin.transferMintAuthority(wallet, params) + * console.log(`Mint authority transferred, tx: ${txHash}`) + * ``` + */ +export interface TransferMintAuthorityResult { + /** Transaction hash/signature. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Grant Mint/Burn Access Types +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Which role(s) to grant on a token. + * + * - `'mintAndBurn'` — grant both mint and burn (default, backwards compatible) + * - `'mint'` — grant mint only + * - `'burn'` — grant burn only + * + * **Chain-specific notes:** + * - **Solana:** Only `'mint'` and `'mintAndBurn'` are valid (SPL tokens have a + * single mint authority; burn is implicit for token holders). `'burn'` will + * throw an error. + */ +export type MintBurnRole = 'mint' | 'burn' | 'mintAndBurn' + +/** + * Parameters for granting mint and burn permissions on a token. + * + * This is a **token** operation — it modifies permissions on the token, + * not the pool. The `authority` receives permission to mint/burn. + * + * | Chain | `tokenAddress` | `authority` | What happens | + * |---------|--------------------|----------------------------------|-------------| + * | EVM | ERC20 address | Pool address | `grantMintAndBurnRoles(authority)` / `grantMintRole` / `grantBurnRole` | + * | Solana | SPL mint (base58) | New mint authority (multisig/PDA) | `setAuthority(MintTokens)` | + * | Aptos | FA metadata addr | Pool object address | Auto-detects pool type, grants access | + * + * @example + * ```typescript + * // Grant both roles (default) + * const params: GrantMintBurnAccessParams = { + * tokenAddress: '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888', + * authority: '0x1234567890abcdef1234567890abcdef12345678', + * } + * + * // Grant mint only + * const mintOnly: GrantMintBurnAccessParams = { + * tokenAddress: '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888', + * authority: '0x1234567890abcdef1234567890abcdef12345678', + * role: 'mint', + * } + * ``` + */ +export interface GrantMintBurnAccessParams { + /** Token address (EVM contract, Solana mint, Aptos FA metadata). */ + tokenAddress: string + /** Address to grant mint/burn access to (pool, multisig, etc.). */ + authority: string + /** Which role(s) to grant. Defaults to `'mintAndBurn'`. */ + role?: MintBurnRole +} + +/** + * Unified result from {@link grantMintBurnAccess} on any chain family. + * + * @example + * ```typescript + * const { txHash } = await admin.grantMintBurnAccess(wallet, params) + * console.log(`Granted mint/burn access, tx: ${txHash}`) + * ``` + */ +export interface GrantMintBurnAccessResult { + /** Transaction hash/signature. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Revoke Mint/Burn Access Types +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Parameters for revoking mint or burn permissions on a token. + * + * This is a **token** operation — it modifies permissions on the token, + * not the pool. The `authority` loses the specified role. + * + * | Chain | `role: 'mint'` | `role: 'burn'` | + * |---------|---------------------------------------|---------------------------------------| + * | EVM | `revokeMintRole(authority)` | `revokeBurnRole(authority)` | + * | Aptos | Remove from minter allowlist / revoke MINTER_ROLE | Remove from burner allowlist / revoke BURNER_ROLE | + * | Solana | Not supported (use `transferMintAuthority`) | Not supported | + * + * @example + * ```typescript + * const params: RevokeMintBurnAccessParams = { + * tokenAddress: '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888', + * authority: '0x1234567890abcdef1234567890abcdef12345678', + * role: 'mint', + * } + * ``` + */ +export interface RevokeMintBurnAccessParams { + /** Token address (EVM contract, Aptos FA metadata). */ + tokenAddress: string + /** Address to revoke mint/burn access from. */ + authority: string + /** Which role to revoke — must be specified explicitly. */ + role: 'mint' | 'burn' +} + +/** + * Unified result from {@link revokeMintBurnAccess} on any chain family. + */ +export interface RevokeMintBurnAccessResult { + /** Transaction hash/signature. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Get Mint/Burn Roles Types (read-only) +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * EVM result from querying mint/burn roles on a CrossChainToken token. + * + * Uses OpenZeppelin AccessControl `getRoleMember` / `getRoleMemberCount` + * to enumerate all addresses with `MINTER_ROLE` and `BURNER_ROLE`. + */ +export interface EVMMintBurnRolesResult { + /** Addresses with the MINTER_ROLE. */ + minters: string[] + /** Addresses with the BURNER_ROLE. */ + burners: string[] +} + +/** + * Solana result from querying mint/burn authority on an SPL token. + * + * SPL tokens have a single `mintAuthority`. If the authority is an + * SPL Token multisig account, the members and threshold are returned. + */ +export interface SolanaMintBurnRolesResult { + /** Current mint authority (base58), or `null` if disabled. */ + mintAuthority: string | null + /** Whether the mint authority is an SPL Token multisig. */ + isMultisig: boolean + /** Multisig threshold (m-of-n). Only set when `isMultisig` is true. */ + multisigThreshold?: number + /** Multisig members. Only set when `isMultisig` is true. */ + multisigMembers?: Array<{ address: string }> +} + +/** + * Aptos result from querying mint/burn roles on a managed or regulated token. + * + * - **managed**: `get_allowed_minters()` / `get_allowed_burners()` + * - **regulated**: `get_minters()` / `get_burners()` / `get_bridge_minters_or_burners()` + */ +export interface AptosMintBurnRolesResult { + /** Detected token module type. */ + tokenModule: 'managed' | 'regulated' | 'unknown' + /** Owner of the code object — can always mint/burn as owner, independent of the allowed lists. */ + owner?: string + /** Addresses allowed to mint. */ + allowedMinters?: string[] + /** Addresses allowed to burn. */ + allowedBurners?: string[] + /** Addresses with BRIDGE_MINTER_OR_BURNER role (regulated only). */ + bridgeMintersOrBurners?: string[] +} + +/** + * Result from {@link createPoolMintAuthorityMultisig} on Solana. + */ +export interface CreatePoolMintAuthorityMultisigResult { + /** The created SPL Token multisig account address (base58). */ + multisigAddress: string + /** The auto-derived Pool Signer PDA (base58). */ + poolSignerPda: string + /** All signers in order: [poolSignerPda, ...additionalSigners]. */ + allSigners: string[] + /** Transaction hash/signature. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Create Pool Token Account Types (Solana-only) +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Parameters for creating the Pool Signer's Associated Token Account (ATA). + * + * The Pool Token ATA is owned by the Pool Signer PDA and acts as the token + * "vault" the pool uses to hold/transfer tokens during cross-chain operations. + * This account **must** exist before any CCIP transfer involving this pool. + * + * @example + * ```typescript + * const params: CreatePoolTokenAccountParams = { + * tokenAddress: '4w7NYkV9pLjPMeCyg8L2TPEQRJh7xpqpKPokQSfjUfLv', + * poolAddress: '7SWikMcRz3Ffdkm3fYCqfN7DNqhRa7y3GzcGFnLNqLbz', + * } + * ``` + */ +export interface CreatePoolTokenAccountParams { + /** SPL token mint pubkey (base58). */ + tokenAddress: string + /** Pool state PDA (base58). The SDK derives poolProgramId from its on-chain owner. */ + poolAddress: string +} + +/** + * Result from creating the Pool Token Account. + * + * @example + * ```typescript + * const { poolTokenAccount, poolSignerPda, txHash } = await admin.createPoolTokenAccount(wallet, params) + * console.log(`Pool ATA created at ${poolTokenAccount}, tx: ${txHash}`) + * ``` + */ +export interface CreatePoolTokenAccountResult { + /** Address of the created ATA (base58). */ + poolTokenAccount: string + /** Pool Signer PDA that owns this ATA (base58). */ + poolSignerPda: string + /** Transaction signature. Empty string if account already existed. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Create Token Address Lookup Table Types (Solana-only) +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Parameters for creating an Address Lookup Table (ALT) for a token's CCIP pool. + * + * The ALT contains 10 base CCIP addresses auto-derived from the token, pool, and router. + * These addresses are used by the CCIP router during cross-chain pool operations. + * + * @example + * ```typescript + * const params: CreateTokenAltParams = { + * tokenAddress: 'J6fECVXwSX5UAeJuC2oCKrsJRjTizWa9uF1FjqzYLa9M', + * poolAddress: '2pGY9WAjanpR3RnY5hQ1a23uDNomzFCAD5HMBgo8nH6M', + * routerAddress: 'Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C', + * } + * ``` + */ +export interface CreateTokenAltParams { + /** SPL token mint pubkey (base58). */ + tokenAddress: string + /** Pool state PDA (base58). The SDK derives poolProgramId from its on-chain owner. */ + poolAddress: string + /** CCIP Router program ID (base58). The SDK discovers feeQuoter from its config. */ + routerAddress: string + /** + * ALT authority (base58). Defaults to sender (wallet) if omitted. + * Can differ from the payer — useful for multisig setups where the authority + * is a Squads vault that can later extend/close the ALT. + */ + authority?: string + /** + * Extra addresses to append after the 10 base CCIP addresses (max 246). + * + * When to use: + * - **Burn-mint with SPL Token Multisig**: pass the multisig address here. + * The pool's on-chain mint instruction needs the multisig account in the + * transaction to mint through it (appended at index 10). + * - **Lock-release**: not needed (10 base addresses are sufficient). + * - **Burn-mint with direct mint authority**: not needed. + */ + additionalAddresses?: string[] +} + +/** + * Result from creating a token Address Lookup Table. + * + * @example + * ```typescript + * const { lookupTableAddress, txHash } = await admin.createTokenAlt(wallet, params) + * console.log(`ALT created at ${lookupTableAddress}, tx: ${txHash}`) + * ``` + */ +export interface CreateTokenAltResult { + /** Address of the created ALT (base58). */ + lookupTableAddress: string + /** Transaction signature. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Set Pool Types +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Parameters for setPool — register a pool in the TokenAdminRegistry. + * + * Links a token to its pool so the CCIP router can route cross-chain + * messages through it. + * + * @example + * ```typescript + * const params: SetPoolParams = { + * tokenAddress: '0xa42BA090720aEE0602aD4381FAdcC9380aD3d888', + * poolAddress: '0xd7BF0d8E6C242b6Dde4490Ab3aFc8C1e811ec9aD', + * routerAddress: '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59', + * } + * ``` + */ +export interface SetPoolParams { + /** Token address (EVM hex / Solana base58 / Aptos hex). */ + tokenAddress: string + /** Pool address to link (EVM: pool contract / Solana: pool state PDA / Aptos: pool resource address). */ + poolAddress: string + /** Router address (used to discover TokenAdminRegistry on EVM, program ID on Solana/Aptos). */ + routerAddress: string +} + +/** + * Solana-specific setPool params — extends base with ALT requirement. + * + * @example + * ```typescript + * const params: SolanaSetPoolParams = { + * tokenAddress: 'J6fECVXwSX5UAeJuC2oCKrsJRjTizWa9uF1FjqzYLa9M', + * poolAddress: '99UxveAueaH64QFiTMKdo9NYD99dMVnMmiqUKv9JQ7xr', + * routerAddress: 'Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C', + * poolLookupTable: 'C6jBE3MDmnqTzo5Dc3BopMyP8vc8jsEDwuHi5rwQgLxC', + * } + * ``` + */ +export interface SolanaSetPoolParams extends SetPoolParams { + /** Address Lookup Table (base58) created via `createTokenAlt`. Required on Solana. */ + poolLookupTable: string +} + +/** + * Result of setPool operation. + * + * @example + * ```typescript + * const { txHash } = await admin.setPool(wallet, params) + * console.log(`Pool registered, tx: ${txHash}`) + * ``` + */ +export interface SetPoolResult { + /** Transaction hash/signature. */ + txHash: string +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Transfer Ownership Types (2-step pool ownership transfer) +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Parameters for transferOwnership — propose new pool owner. + * + * @example + * ```typescript + * const params: TransferOwnershipParams = { + * poolAddress: '0x1234...', + * newOwner: '0xabcd...', + * } + * ``` + */ +export interface TransferOwnershipParams { + /** Pool address (EVM hex / Solana base58 / Aptos hex). */ + poolAddress: string + /** New owner address to propose. */ + newOwner: string +} + +/** + * Parameters for acceptOwnership — accept proposed pool ownership. + * + * @example + * ```typescript + * const params: AcceptOwnershipParams = { + * poolAddress: '0x1234...', + * } + * ``` + */ +export interface AcceptOwnershipParams { + /** Pool address (EVM hex / Solana base58 / Aptos hex). */ + poolAddress: string +} + +/** + * Parameters for executeOwnershipTransfer — Aptos-only 3rd step. + * + * Aptos uses a 3-step ownership transfer: + * 1. `transferOwnership(newOwner)` — current owner proposes + * 2. `acceptOwnership()` — proposed owner signals acceptance + * 3. `executeOwnershipTransfer(newOwner)` — current owner finalizes the AptosFramework object transfer + * + * @example + * ```typescript + * const params: ExecuteOwnershipTransferParams = { + * poolAddress: '0x1234...', + * newOwner: '0xabcd...', + * } + * ``` + */ +export interface ExecuteOwnershipTransferParams { + /** Pool address (Aptos hex). */ + poolAddress: string + /** New owner address — must match the address that called acceptOwnership. */ + newOwner: string +} + +/** + * Result of transferOwnership, acceptOwnership, or executeOwnershipTransfer. + * + * @example + * ```typescript + * const { txHash } = await admin.transferOwnership(wallet, params) + * console.log(`Ownership proposed, tx: ${txHash}`) + * ``` + */ +export interface OwnershipResult { + /** Transaction hash/signature. */ + txHash: string +} diff --git a/ccip-sdk/src/verify/constructor-args.ts b/ccip-sdk/src/verify/constructor-args.ts new file mode 100644 index 00000000..f5f098c6 --- /dev/null +++ b/ccip-sdk/src/verify/constructor-args.ts @@ -0,0 +1,57 @@ +import { type InterfaceAbi, AbiCoder, Interface } from 'ethers' + +import type { ConstructorArgs } from './types.ts' + +/* + * Produce the constructor-arguments string Etherscan expects: ABI-encoded calldata, + * hex, WITHOUT a 0x prefix and WITHOUT any function selector. + * + * Foundry (foundry-src crates/verify/src/etherscan/mod.rs constructor_args): for value + * inputs it ABI-encodes against the constructor and strips the leading 4-byte selector + * (encoded[8..]); for --constructor-args hex it passes the hex through verbatim. + * + * Hardhat (hardhat3-src packages/hardhat-verify/src/internal/constructor-args.ts): + * Interface.encodeDeploy(args) then strips the 0x. encodeDeploy already omits the + * selector, which is why no [8..] slice is needed there. + * + * We support both shapes the SDK might receive: + * - values: decoded args + the constructor ABI, encoded here (the clean path for a + * "deploy then verify" SDK, since we already hold the user's params). + * - encoded: raw hex (e.g. extracted from init code: creationBytecode || encodedArgs). + */ +/** ABI-encode constructor args into the hex string Etherscan expects (no `0x`, no selector). */ +export function encodeConstructorArgs(args: ConstructorArgs): string { + switch (args.kind) { + case 'none': + return '' + + case 'encoded': + return normalizeEncodedArgs(args.hex) + + case 'values': { + // `encodeDeploy` encodes the tuple of constructor inputs with NO selector — exactly + // what Etherscan wants. Using the full Interface gives ethers the constructor's + // input types (including nested structs/tuples like CrossChainToken's ConstructorParams). + const iface = new Interface(args.abi as InterfaceAbi) + const encoded = iface.encodeDeploy(args.values) + return stripHexPrefix(encoded) + } + } +} + +/** Encode constructor args from bare parameter types (no ABI) via `AbiCoder`. */ +export function encodeConstructorArgsFromTypes(types: string[], values: unknown[]): string { + return stripHexPrefix(AbiCoder.defaultAbiCoder().encode(types, values)) +} + +function normalizeEncodedArgs(hex: string): string { + const h = stripHexPrefix(hex.trim()) + // Defensive: if a full creation calldata or a selector-prefixed blob was passed, the + // caller is responsible for slicing. We only strip 0x here, matching foundry's + // `.constructor_arguments()` which trims and strips 0x but does not guess. + return h +} + +function stripHexPrefix(hex: string): string { + return hex.startsWith('0x') || hex.startsWith('0X') ? hex.slice(2) : hex +} diff --git a/ccip-sdk/src/verify/etherscan.ts b/ccip-sdk/src/verify/etherscan.ts new file mode 100644 index 00000000..1139ada4 --- /dev/null +++ b/ccip-sdk/src/verify/etherscan.ts @@ -0,0 +1,191 @@ +/* + * Low-level Etherscan V2 API client for contract verification. + * + * V2 model (confirmed in BOTH references): + * - Single endpoint for every chain: https://api.etherscan.io/v2/api + * - The target chain is selected by the `chainid` query parameter. + * - One API key works across all 60+ supported chains. + * foundry-src: foundry-block-explorers test asserts + * https://api.etherscan.io/v2/api?chainid=11155111 + * hardhat3-src: packages/hardhat-verify/src/internal/etherscan.ts + * ETHERSCAN_API_URL = "https://api.etherscan.io/v2/api", chainid passed per call. + * + * Transport: + * - verifysourcecode -> POST, body application/x-www-form-urlencoded + * - checkverifystatus -> GET (foundry POSTs it; both are accepted) + * - module/action/chainid/apikey go in the query string; the large fields + * (sourceCode, contractaddress, ...) go in the POST body. + * + * We use the built-in fetch (Node >= 18). The real ccip-sdk uses axios; swapping the + * transport is mechanical — only request() below would change. + */ + +import { CCIPContractVerificationError } from '../errors/index.ts' + +/** The single Etherscan V2 API endpoint; the target chain is selected via `chainid`. */ +export const ETHERSCAN_V2_API_URL = 'https://api.etherscan.io/v2/api' + +/** Default `fetch` wrapper that always invokes the global `fetch` with the correct receiver. */ +export const defaultFetch: typeof fetch = (...args) => fetch(...args) + +/** Envelope returned by every Etherscan action; `status` is the string "0" or "1". */ +export interface EtherscanResponse { + /** "0" (failure) or "1" (success). */ + status: string + /** Short status message, e.g. "OK" or "NOTOK". */ + message: string + /** Action payload: a GUID, a status string, or a JSON array (depending on the action). */ + result: string +} + +/** Body of a `verifysourcecode` submission. */ +export interface VerifySourceCodeBody { + /** Source encoding; "solidity-standard-json-input" for the standard-json flow. */ + codeformat: 'solidity-standard-json-input' | 'solidity-single-file' | 'vyper-json' + /** The stringified standard JSON input (or flattened source for single-file). */ + sourceCode: string + /** The deployed contract address. */ + contractaddress: string + /** Fully-qualified name `path/File.sol:Name`. */ + contractname: string + /** Long form, e.g. "v0.8.26+commit.8a97fa7a". */ + compilerversion: string + /** ABI-encoded constructor args, hex, no `0x`, no selector. */ + constructorArguments?: string + /** Optional SPDX license code (1..14). */ + licenseType?: number + /** Single-file path only: whether the optimizer was enabled ("0" or "1"). */ + optimizationUsed?: '0' | '1' + /** Single-file path only: optimizer runs. */ + runs?: number + /** Single-file path only: target EVM version. */ + evmversion?: string +} + +/* + * Which explorer family we're talking to. Both speak the same Etherscan-style + * verifysourcecode/checkverifystatus actions, but differ in URL/auth: + * - 'etherscan' : V2 single endpoint, chainid + apikey required. + * - 'blockscout' : per-chain {base}/api endpoint, NO chainid, apikey optional/unused. + */ +/** Which Etherscan-compatible explorer family a client talks to. */ +export type ExplorerProvider = 'etherscan' | 'blockscout' + +/** Low-level Etherscan V2 (and Blockscout) client for the verify/status actions. */ +export class EtherscanV2Client { + private readonly chainId: number + private readonly apiKey: string + private readonly apiUrl: string + private readonly fetchImpl: typeof fetch + private readonly provider: ExplorerProvider + + /** Builds a client for one chain/explorer, with optional URL, fetch impl, and provider. */ + constructor( + chainId: number, + apiKey: string, + apiUrl: string = ETHERSCAN_V2_API_URL, + fetchImpl: typeof fetch = defaultFetch, + provider: ExplorerProvider = 'etherscan', + ) { + this.chainId = chainId + this.apiKey = apiKey + this.apiUrl = apiUrl + this.fetchImpl = fetchImpl + this.provider = provider + } + + /** Submit a verification request; returns the GUID to poll. Throws on hard errors. */ + async verifySourceCode(body: VerifySourceCodeBody): Promise { + // Etherscan historically misspells the field as `constructorArguements`. Foundry sends + // BOTH spellings (the misspelled one for Etherscan, the correct one for Blockscout). + // We do the same for maximum compatibility. + const form: Record = { + codeformat: body.codeformat, + sourceCode: body.sourceCode, + contractaddress: body.contractaddress, + contractname: body.contractname, + compilerversion: body.compilerversion, + } + if (body.constructorArguments) { + form.constructorArguements = body.constructorArguments // Etherscan (sic) + form.constructorArguments = body.constructorArguments // Blockscout + } + if (body.licenseType != null) form.licenseType = String(body.licenseType) + if (body.optimizationUsed != null) form.optimizationUsed = body.optimizationUsed + if (body.runs != null) form.runs = String(body.runs) + if (body.evmversion) form.evmversion = body.evmversion + + const res = await this.post('verifysourcecode', form) + if (res.status !== '1') { + // On failure Etherscan puts a generic "NOTOK" in `message` and the ACTUAL reason in + // `result` (e.g. "Unable to locate ContractCode at 0x…", "Invalid API Key", + // "Missing/unsupported chainid"). Surface the detailed one. + throw new CCIPContractVerificationError(res.result || res.message, { + context: { result: res.result }, + }) + } + return res.result // GUID + } + + /** Poll a verification GUID. Returns the raw envelope; caller interprets `result`. */ + async checkVerifyStatus(guid: string): Promise { + return this.get('checkverifystatus', { guid }) + } + + /** Whether the address already has verified source (skip work if so). */ + async isVerified(address: string): Promise { + const res = await this.get('getsourcecode', { address }) + if (res.status !== '1') return false + // result is a JSON array string; SourceCode non-empty => verified. + try { + const parsed = JSON.parse(res.result) as Array<{ SourceCode?: string }> + return Boolean(parsed[0]?.SourceCode) + } catch { + return false + } + } + + // --- transport --------------------------------------------------------------- + + /** Build the action URL with the right query params for the configured provider. */ + private query(action: string): string { + const u = new URL(this.apiUrl) + u.searchParams.set('module', 'contract') + u.searchParams.set('action', action) + if (this.provider === 'etherscan') { + // V2 needs chainid + apikey on every call. + u.searchParams.set('chainid', String(this.chainId)) + u.searchParams.set('apikey', this.apiKey) + } else { + // Blockscout: instance is single-chain (no chainid). apikey only if the instance wants one. + if (this.apiKey) u.searchParams.set('apikey', this.apiKey) + } + return u.toString() + } + + /** POST a form-encoded body to the given action and parse the envelope. */ + private async post(action: string, form: Record): Promise { + const res = await this.fetchImpl(this.query(action), { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams(form).toString(), + }) + return this.parse(res) + } + + /** GET the given action with query params appended and parse the envelope. */ + private async get(action: string, params: Record): Promise { + const u = new URL(this.query(action)) + for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v) + const res = await this.fetchImpl(u.toString(), { method: 'GET' }) + return this.parse(res) + } + + /** Parse an HTTP response into an Etherscan envelope, throwing on a non-OK status. */ + private async parse(res: Response): Promise { + if (!res.ok) + throw new CCIPContractVerificationError(`Etherscan HTTP ${res.status} ${res.statusText}`) + const json = (await res.json()) as EtherscanResponse + return json + } +} diff --git a/ccip-sdk/src/verify/fixtures/AdvancedPoolHooks.abi.json b/ccip-sdk/src/verify/fixtures/AdvancedPoolHooks.abi.json new file mode 100644 index 00000000..427f1d69 --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/AdvancedPoolHooks.abi.json @@ -0,0 +1,775 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "allowlist", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "thresholdAmountForAdditionalCCVs", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "policyEngine", + "type": "address", + "internalType": "address" + }, + { + "name": "authorizedCallers", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "applyAllowListUpdates", + "inputs": [ + { + "name": "removes", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "adds", + "type": "address[]", + "internalType": "address[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "applyAuthorizedCallerUpdates", + "inputs": [ + { + "name": "authorizedCallerArgs", + "type": "tuple", + "internalType": "struct AuthorizedCallers.AuthorizedCallerArgs", + "components": [ + { + "name": "addedCallers", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "removedCallers", + "type": "address[]", + "internalType": "address[]" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "applyCCVConfigUpdates", + "inputs": [ + { + "name": "ccvConfigArgs", + "type": "tuple[]", + "internalType": "struct AdvancedPoolHooks.CCVConfigArg[]", + "components": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "outboundCCVs", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "thresholdOutboundCCVs", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "inboundCCVs", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "thresholdInboundCCVs", + "type": "address[]", + "internalType": "address[]" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "checkAllowList", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAllAuthorizedCallers", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAllCCVConfigs", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "tuple[]", + "internalType": "struct AdvancedPoolHooks.CCVConfigArg[]", + "components": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "outboundCCVs", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "thresholdOutboundCCVs", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "inboundCCVs", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "thresholdInboundCCVs", + "type": "address[]", + "internalType": "address[]" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAllowList", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAllowListEnabled", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getCCVConfig", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct AdvancedPoolHooks.CCVConfig", + "components": [ + { + "name": "outboundCCVs", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "thresholdOutboundCCVs", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "inboundCCVs", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "thresholdInboundCCVs", + "type": "address[]", + "internalType": "address[]" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getPolicyEngine", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRequiredCCVs", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "direction", + "type": "uint8", + "internalType": "enum IPoolV2.MessageDirection" + } + ], + "outputs": [ + { + "name": "requiredCCVs", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getThresholdAmount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "postflightCheck", + "inputs": [ + { + "name": "releaseOrMintIn", + "type": "tuple", + "internalType": "struct Pool.ReleaseOrMintInV1", + "components": [ + { + "name": "originalSender", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "sourceDenominatedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + }, + { + "name": "sourcePoolAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "sourcePoolData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "offchainTokenData", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "preflightCheck", + "inputs": [ + { + "name": "lockOrBurnIn", + "type": "tuple", + "internalType": "struct Pool.LockOrBurnInV1", + "components": [ + { + "name": "receiver", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "originalSender", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + } + ] + }, + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "tokenArgs", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setPolicyEngine", + "inputs": [ + { + "name": "newPolicyEngine", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setPolicyEngineAllowFailedDetach", + "inputs": [ + { + "name": "newPolicyEngine", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setThresholdAmount", + "inputs": [ + { + "name": "thresholdAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "typeAndVersion", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "pure" + }, + { + "type": "event", + "name": "AllowListAdd", + "inputs": [ + { + "name": "sender", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AllowListRemove", + "inputs": [ + { + "name": "sender", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AuthorizedCallerAdded", + "inputs": [ + { + "name": "caller", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AuthorizedCallerRemoved", + "inputs": [ + { + "name": "caller", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "CCVConfigUpdated", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "outboundCCVs", + "type": "address[]", + "indexed": false, + "internalType": "address[]" + }, + { + "name": "thresholdOutboundCCVs", + "type": "address[]", + "indexed": false, + "internalType": "address[]" + }, + { + "name": "inboundCCVs", + "type": "address[]", + "indexed": false, + "internalType": "address[]" + }, + { + "name": "thresholdInboundCCVs", + "type": "address[]", + "indexed": false, + "internalType": "address[]" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferRequested", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PolicyEngineAttached", + "inputs": [ + { + "name": "policyEngine", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PolicyEngineDetachFailed", + "inputs": [ + { + "name": "policyEngine", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "reason", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ThresholdAmountSet", + "inputs": [ + { + "name": "thresholdAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AllowListNotEnabled", + "inputs": [] + }, + { + "type": "error", + "name": "CannotTransferToSelf", + "inputs": [] + }, + { + "type": "error", + "name": "DuplicateCCVNotAllowed", + "inputs": [ + { + "name": "ccvAddress", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "MustBeProposedOwner", + "inputs": [] + }, + { + "type": "error", + "name": "MustSpecifyUnderThresholdCCVsForThresholdCCVs", + "inputs": [] + }, + { + "type": "error", + "name": "OnlyCallableByOwner", + "inputs": [] + }, + { + "type": "error", + "name": "OwnerCannotBeZero", + "inputs": [] + }, + { + "type": "error", + "name": "PolicyEngineDetachReverted", + "inputs": [ + { + "name": "oldPolicyEngine", + "type": "address", + "internalType": "address" + }, + { + "name": "err", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "SenderNotAllowed", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "UnauthorizedCaller", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ZeroAddressNotAllowed", + "inputs": [] + } +] diff --git a/ccip-sdk/src/verify/fixtures/AdvancedPoolHooks.standard-input.json b/ccip-sdk/src/verify/fixtures/AdvancedPoolHooks.standard-input.json new file mode 100644 index 00000000..2a173c1d --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/AdvancedPoolHooks.standard-input.json @@ -0,0 +1,399 @@ +{ + "language": "Solidity", + "sources": { + "contracts/interfaces/IAdvancedPoolHooks.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.24;\n\nimport {Pool} from \"../libraries/Pool.sol\";\nimport {IPoolV2} from \"./IPoolV2.sol\";\n\n/// @notice Interface for AdvancedPoolHooks contract. Implementations may contain no-op logic.\ninterface IAdvancedPoolHooks {\n /// @notice Preflight check before lock or burn operation.\n /// @param lockOrBurnIn The lock or burn input parameters.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Additional token arguments.\n /// @param amountPostFee The amount after token pool bps-based fees have been deducted.\n /// @dev This function may revert if the preflight check fails. This means the transaction is rolled back on source.\n function preflightCheck(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs,\n uint256 amountPostFee\n ) external;\n\n /// @notice Postflight check before releasing or minting tokens.\n /// @param releaseOrMintIn The release or mint output parameters.\n /// @param localAmount The local amount to be released or minted.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @dev This function may revert if the postflight check fails. This means the transaction is unexecutable until\n /// the issue is resolved.\n function postflightCheck(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n uint256 localAmount,\n bytes4 requestedFinalityConfig\n ) external;\n\n /// @notice Returns the set of required CCVs for transfers in a specific direction.\n /// @param localToken The address of the local token.\n /// @param remoteChainSelector The remote chain selector for this transfer.\n /// @param amount The amount being transferred.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param extraData Direction-specific payload forwarded by the caller (e.g. token args or source pool data).\n /// @param direction The direction of the transfer (Inbound or Outbound).\n /// @return requiredCCVs Set of required CCV addresses.\n function getRequiredCCVs(\n address localToken,\n uint64 remoteChainSelector,\n uint256 amount,\n bytes4 requestedFinalityConfig,\n bytes calldata extraData,\n IPoolV2.MessageDirection direction\n ) external view returns (address[] memory requiredCCVs);\n}\n" + }, + "contracts/interfaces/IPoolV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport {Pool} from \"../libraries/Pool.sol\";\n\nimport {IERC165} from \"@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol\";\n\n/// @notice Shared public interface for multiple V2 pool types.\n/// Each pool type handles a different child token model e.g. lock/release, mint/burn.\ninterface IPoolV2 is IERC165 {\n struct TokenTransferFeeConfig {\n uint32 destGasOverhead; // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e Gas charged to execute the token transfer on the destination chain.\n uint32 destBytesOverhead; // \u2502 Data availability bytes.\n uint32 finalityFeeUSDCents; // \u2502 Fee to charge for token transfer with default (wait-for-finality) finality, multiples of 0.01 USD.\n uint32 fastFinalityFeeUSDCents; // \u2502 Fee to charge for token transfer with fast finality (FTF), multiples of 0.01 USD.\n // \u2502 The following two fee is deducted from the transferred asset, not added on top.\n uint16 finalityTransferFeeBps; // \u2502 Fee in basis points for default finality transfers [0-10_000].\n uint16 fastFinalityTransferFeeBps; //\u2502 Fee in basis points for custom finality transfers [0-10_000].\n bool isEnabled; // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f Whether this config is enabled.\n }\n\n enum MessageDirection {\n Outbound,\n Inbound\n }\n\n /// @notice Lock tokens into the pool or burn the tokens.\n /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Additional token arguments.\n /// @return lockOrBurnOut Encoded data fields for the processing of tokens on the destination chain.\n /// @return destTokenAmount The amount of tokens that will be set in TokenTransferV1.amount to be released/mint on destination.\n function lockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n ) external returns (Pool.LockOrBurnOutV1 memory lockOrBurnOut, uint256 destTokenAmount);\n\n /// @notice Releases or mints tokens on the destination chain.\n /// @param releaseOrMintIn Encoded data fields for the processing of tokens on the destination chain.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @return releaseOrMintOut Encoded data fields describing the result of the release or mint.\n function releaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n bytes4 requestedFinalityConfig\n ) external returns (Pool.ReleaseOrMintOutV1 memory releaseOrMintOut);\n\n /// @notice Returns the set of required CCVs for transfers in a given direction.\n /// @param localToken The address of the local token.\n /// @param remoteChainSelector The chain selector of the remote chain.\n /// @param sourceAmount The source-denominated amount of tokens to be transferred.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param extraData Direction-specific payload forwarded by the caller (e.g. token args or source pool data).\n /// @param direction Whether CCVs are required for outbound (source -> remote) or inbound (remote -> destination) transfers.\n /// @return requiredCCVs A set of addresses representing the required CCVs.\n function getRequiredCCVs(\n address localToken,\n uint64 remoteChainSelector,\n uint256 sourceAmount,\n bytes4 requestedFinalityConfig,\n bytes calldata extraData,\n MessageDirection direction\n ) external view returns (address[] memory requiredCCVs);\n\n /// @notice Returns the fee overrides for transferring the pool's token to a destination chain.\n /// @param localToken The address of the local token.\n /// @param destChainSelector The chain selector of the destination chain.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Additional token argument from the CCIP message.\n /// @return feeConfig the fee configuration for transferring the token to the destination chain.\n function getTokenTransferFeeConfig(\n address localToken,\n uint64 destChainSelector,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n ) external view returns (TokenTransferFeeConfig memory feeConfig);\n\n /// @notice Returns the pool fee parameters that will apply to a transfer.\n /// @param localToken The local asset being transferred.\n /// @param destChainSelector The destination lane selector.\n /// @param amount The amount of tokens being bridged on this lane.\n /// @param feeToken The token used to pay feeUSDCents.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Opaque token arguments supplied by the caller.\n /// @return feeUSDCents Flat fee charged in USD cents (crumbs) for this transfer.\n /// @return destGasOverhead Destination gas charged for accounting in the cost model.\n /// @return destBytesOverhead Destination calldata size attributed to the transfer.\n /// @return tokenFeeBps Bps charged in token units. Value of zero implies no in-token fee.\n /// @return isEnabled Whether the pool's fee config is enabled. If false, OnRamp should use FeeQuoter defaults.\n function getFee(\n address localToken,\n uint64 destChainSelector,\n uint256 amount,\n address feeToken,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n )\n external\n view\n returns (uint256 feeUSDCents, uint32 destGasOverhead, uint32 destBytesOverhead, uint16 tokenFeeBps, bool isEnabled);\n\n /// @notice Gets the token address on the remote chain.\n /// @param remoteChainSelector Remote chain selector.\n /// @dev To support non-evm chains, this value is encoded into bytes.\n function getRemoteToken(\n uint64 remoteChainSelector\n ) external view returns (bytes memory);\n}\n" + }, + "contracts/libraries/CCVConfigValidation.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\n/// @notice CCV config validation helpers.\nlibrary CCVConfigValidation {\n error MustSpecifyDefaultOrRequiredCCVs();\n error DuplicateCCVNotAllowed(address ccvAddress);\n error ZeroAddressNotAllowed();\n\n /// @notice Ensures at least one CCV combined, no zero addresses, no duplicates within or across both sets.\n /// @param defaultCCV The default CCVs.\n /// @param laneMandatedCCVs The mandated CCVs.\n function _validateDefaultAndMandatedCCVs(\n address[] memory defaultCCV,\n address[] memory laneMandatedCCVs\n ) internal pure {\n uint256 defaultLength = defaultCCV.length;\n uint256 mandatedLength = laneMandatedCCVs.length;\n uint256 totalLength = defaultLength + mandatedLength;\n\n // There must always be at least one default or mandated CCV. This ensures that any receiver who does not specify\n // CCVs will always have at least one CCV to validate the message.\n if (totalLength == 0) revert MustSpecifyDefaultOrRequiredCCVs();\n\n // We check for duplicates and zero addresses in the default and mandated CCVs. We need to check for duplicates\n // between the two sets of CCVs as well as within each set. Doing these checks here means we can assume there are\n // no duplicates or zero addresses in the rest of the code.\n for (uint256 combinedIndex = 0; combinedIndex < totalLength; ++combinedIndex) {\n address currentCCVAddress =\n combinedIndex < defaultLength ? defaultCCV[combinedIndex] : laneMandatedCCVs[combinedIndex - defaultLength];\n if (currentCCVAddress == address(0)) revert ZeroAddressNotAllowed();\n\n for (uint256 nextIndex = combinedIndex + 1; nextIndex < totalLength; ++nextIndex) {\n address compareCCVAddress =\n nextIndex < defaultLength ? defaultCCV[nextIndex] : laneMandatedCCVs[nextIndex - defaultLength];\n if (currentCCVAddress == compareCCVAddress) revert DuplicateCCVNotAllowed(currentCCVAddress);\n }\n }\n }\n\n function _assertNoDuplicates(\n address[] memory addresses\n ) internal pure {\n uint256 length = addresses.length;\n for (uint256 i = 0; i < length; ++i) {\n for (uint256 j = i + 1; j < length; ++j) {\n if (addresses[i] == addresses[j]) revert DuplicateCCVNotAllowed(addresses[i]);\n }\n }\n }\n\n function _assertNoDuplicatedBetweenLists(\n address[] memory listA,\n address[] memory listB\n ) internal pure {\n uint256 lengthA = listA.length;\n uint256 lengthB = listB.length;\n for (uint256 i = 0; i < lengthA; ++i) {\n for (uint256 j = 0; j < lengthB; ++j) {\n if (listA[i] == listB[j]) revert DuplicateCCVNotAllowed(listA[i]);\n }\n }\n }\n}\n" + }, + "contracts/libraries/Pool.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n/// @notice This library contains various token pool functions to aid constructing the return data.\nlibrary Pool {\n // The tag used to signal support for the pool v1 standard.\n // bytes4(keccak256(\"CCIP_POOL_V1\"))\n bytes4 public constant CCIP_POOL_V1 = 0xaff2afbf;\n\n // The number of bytes in the return data for a pool v1 releaseOrMint call.\n // This should match the size of the ReleaseOrMintOutV1 struct.\n uint16 public constant CCIP_POOL_V1_RET_BYTES = 32;\n\n // The default max number of bytes in the return data for a pool v1 lockOrBurn call.\n // This data can be used to send information to the destination chain token pool. Can be overwritten\n // in the TokenTransferFeeConfig.destBytesOverhead if more data is required.\n uint32 public constant CCIP_LOCK_OR_BURN_V1_RET_BYTES = 32;\n\n struct LockOrBurnInV1 {\n bytes receiver; // The recipient of the tokens on the destination chain. For EVM source chains, this is abi-encoded (32 bytes).\n uint64 remoteChainSelector; // \u2500\u256e The chain ID of the destination chain.\n address originalSender; // \u2500\u2500\u2500\u2500\u2500\u256f The original sender of the tx on the source chain.\n uint256 amount; // The amount of tokens to lock or burn, denominated in the source token's decimals.\n address localToken; // The address on this chain of the token to lock or burn.\n }\n\n struct LockOrBurnOutV1 {\n // The address of the destination token, abi encoded in the case of EVM chains.\n // This value is UNTRUSTED as any pool owner can return whatever value they want.\n bytes destTokenAddress;\n // Optional pool data to be transferred to the destination chain. Be default this is capped at\n // CCIP_LOCK_OR_BURN_V1_RET_BYTES bytes. If more data is required, the TokenTransferFeeConfig.destBytesOverhead\n // has to be set for the specific token.\n bytes destPoolData;\n }\n\n struct ReleaseOrMintInV1 {\n bytes originalSender; // The original sender of the tx on the source chain.\n uint64 remoteChainSelector; // \u2500\u2500\u2500\u256e The chain ID of the source chain.\n address receiver; // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f The recipient of the tokens on the destination chain.\n uint256 sourceDenominatedAmount; // The amount of tokens to release or mint, denominated in the source token's decimals.\n address localToken; // The address on this chain of the token to release or mint.\n /// @dev WARNING: sourcePoolAddress should be checked prior to any processing of funds. Make sure it matches the\n /// expected pool address for the given remoteChainSelector.\n bytes sourcePoolAddress; // The address of the source pool, abi encoded in the case of EVM chains.\n bytes sourcePoolData; // The data received from the source pool to process the release or mint.\n /// @dev WARNING: offchainTokenData is untrusted data.\n bytes offchainTokenData; // The offchain data to process the release or mint.\n }\n\n struct ReleaseOrMintOutV1 {\n // The number of tokens released or minted on the destination chain, denominated in the local token's decimals.\n // This value is expected to be equal to the ReleaseOrMintInV1.amount in the case where the source and destination\n // chain have the same number of decimals.\n uint256 destinationAmount;\n }\n}\n" + }, + "contracts/pools/AdvancedPoolHooks.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.24;\n\nimport {IAdvancedPoolHooks} from \"../interfaces/IAdvancedPoolHooks.sol\";\nimport {IPoolV2} from \"../interfaces/IPoolV2.sol\";\nimport {ITypeAndVersion} from \"@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol\";\nimport {IPolicyEngine} from \"@chainlink/policy-management/interfaces/IPolicyEngine.sol\";\n\nimport {CCVConfigValidation} from \"../libraries/CCVConfigValidation.sol\";\nimport {Pool} from \"../libraries/Pool.sol\";\nimport {AuthorizedCallers} from \"@chainlink/contracts/src/v0.8/shared/access/AuthorizedCallers.sol\";\n\nimport {EnumerableSet} from \"@openzeppelin/contracts@5.3.0/utils/structs/EnumerableSet.sol\";\n\n/// @notice Advanced pool hooks for additional security features like allowlists, CCV management, and policy engine runs.\n/// @dev This is a standalone contract that can optionally be used by TokenPools.\ncontract AdvancedPoolHooks is IAdvancedPoolHooks, ITypeAndVersion, AuthorizedCallers {\n using EnumerableSet for EnumerableSet.AddressSet;\n using EnumerableSet for EnumerableSet.UintSet;\n\n function typeAndVersion() external pure virtual override returns (string memory) {\n return \"AdvancedPoolHooks 2.0.0\";\n }\n\n error AllowListNotEnabled();\n error SenderNotAllowed(address sender);\n error MustSpecifyUnderThresholdCCVsForThresholdCCVs();\n error PolicyEngineDetachReverted(address oldPolicyEngine, bytes err);\n\n event AllowListAdd(address sender);\n event AllowListRemove(address sender);\n event CCVConfigUpdated(\n uint64 indexed remoteChainSelector,\n address[] outboundCCVs,\n address[] thresholdOutboundCCVs,\n address[] inboundCCVs,\n address[] thresholdInboundCCVs\n );\n event ThresholdAmountSet(uint256 thresholdAmount);\n event PolicyEngineAttached(address indexed policyEngine);\n event PolicyEngineDetachFailed(address indexed policyEngine, bytes reason);\n\n struct CCVConfig {\n address[] outboundCCVs; // CCVs required for outgoing messages to the remote chain.\n address[] thresholdOutboundCCVs; // Additional CCVs that are required for outgoing messages after reaching the threshold amount.\n address[] inboundCCVs; // CCVs required for incoming messages from the remote chain.\n address[] thresholdInboundCCVs; // Additional CCVs that are required for incoming messages after reaching the threshold amount.\n }\n\n struct CCVConfigArg {\n uint64 remoteChainSelector;\n address[] outboundCCVs;\n address[] thresholdOutboundCCVs;\n address[] inboundCCVs;\n address[] thresholdInboundCCVs;\n }\n\n /// @dev The immutable flag that indicates if the allowlist is access-controlled.\n bool internal immutable i_allowlistEnabled;\n\n /// @dev A set of addresses allowed to trigger lockOrBurn as original senders.\n /// Only takes effect if i_allowlistEnabled is true.\n /// This can be used to ensure only token-issuer specified addresses can move tokens.\n EnumerableSet.AddressSet internal s_allowlist;\n\n /// @dev Threshold token transfer amount at which additional CCVs are required.\n /// Value of 0 means that there is no threshold and additional CCVs are not required for any transfer amount.\n uint256 internal s_thresholdAmountForAdditionalCCVs;\n\n /// @dev The policy engine to use. Value of 0 disables policy engine checks.\n IPolicyEngine internal s_policyEngine;\n\n /// @dev Stores verifier (CCV) requirements keyed by remote chain selector.\n mapping(uint64 remoteChainSelector => CCVConfig ccvConfig) internal s_verifierConfig;\n\n /// @dev Tracks all remote chain selectors that have CCV configurations.\n EnumerableSet.UintSet internal s_configuredChainSelectors;\n\n constructor(\n address[] memory allowlist,\n uint256 thresholdAmountForAdditionalCCVs,\n address policyEngine,\n address[] memory authorizedCallers\n ) AuthorizedCallers(authorizedCallers) {\n // Allowlist can be set as enabled or disabled at deployment time only to save hot-path gas.\n i_allowlistEnabled = allowlist.length > 0;\n if (i_allowlistEnabled) {\n _applyAllowListUpdates(new address[](0), allowlist);\n }\n s_thresholdAmountForAdditionalCCVs = thresholdAmountForAdditionalCCVs;\n _setPolicyEngine(policyEngine, false);\n }\n\n /// @inheritdoc IAdvancedPoolHooks\n /// @dev Performs allowlist check and policy engine validation for outbound transfers.\n function preflightCheck(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4,\n bytes calldata tokenArgs,\n uint256\n ) public virtual {\n _validateCaller();\n checkAllowList(lockOrBurnIn.originalSender);\n\n IPolicyEngine policyEngine = s_policyEngine;\n if (address(policyEngine) == address(0)) {\n return;\n }\n\n policyEngine.run(\n IPolicyEngine.Payload({selector: msg.sig, sender: msg.sender, data: msg.data[4:], context: tokenArgs})\n );\n }\n\n /// @inheritdoc IAdvancedPoolHooks\n /// @dev Performs policy engine validation for inbound transfers.\n function postflightCheck(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n uint256,\n bytes4\n ) public virtual {\n _validateCaller();\n\n IPolicyEngine policyEngine = s_policyEngine;\n if (address(policyEngine) == address(0)) {\n return;\n }\n\n policyEngine.run(\n IPolicyEngine.Payload({\n selector: msg.sig, sender: msg.sender, data: msg.data[4:], context: releaseOrMintIn.offchainTokenData\n })\n );\n }\n\n // ================================================================\n // \u2502 Allowlist \u2502\n // ================================================================\n\n /// @notice Checks if the sender is allowed to perform an operation.\n /// @param sender The address to check.\n function checkAllowList(\n address sender\n ) public view virtual {\n if (i_allowlistEnabled) {\n if (!s_allowlist.contains(sender)) {\n revert SenderNotAllowed(sender);\n }\n }\n }\n\n /// @notice Gets whether the allowlist functionality is enabled.\n /// @return true is enabled, false if not.\n function getAllowListEnabled() public view virtual returns (bool) {\n return i_allowlistEnabled;\n }\n\n /// @notice Gets the allowed addresses.\n /// @return The allowed addresses.\n function getAllowList() public view virtual returns (address[] memory) {\n return s_allowlist.values();\n }\n\n /// @notice Apply updates to the allow list.\n /// @param removes The addresses to be removed.\n /// @param adds The addresses to be added.\n function applyAllowListUpdates(\n address[] calldata removes,\n address[] calldata adds\n ) public virtual onlyOwner {\n _applyAllowListUpdates(removes, adds);\n }\n\n /// @notice Internal version of applyAllowListUpdates to allow for reuse in the constructor.\n /// @param removes The addresses to be removed.\n /// @param adds The addresses to be added.\n function _applyAllowListUpdates(\n address[] memory removes,\n address[] memory adds\n ) internal virtual {\n if (!i_allowlistEnabled) revert AllowListNotEnabled();\n\n for (uint256 i = 0; i < removes.length; ++i) {\n address toRemove = removes[i];\n if (s_allowlist.remove(toRemove)) {\n emit AllowListRemove(toRemove);\n }\n }\n for (uint256 i = 0; i < adds.length; ++i) {\n address toAdd = adds[i];\n if (toAdd == address(0)) {\n continue;\n }\n if (s_allowlist.add(toAdd)) {\n emit AllowListAdd(toAdd);\n }\n }\n }\n\n // ================================================================\n // \u2502 CCV \u2502\n // ================================================================\n\n /// @notice Returns the full CCV configuration for a given remote chain selector.\n /// @param remoteChainSelector The remote chain selector.\n /// @return The CCV configuration containing outbound, threshold outbound, inbound, and threshold inbound CCVs.\n function getCCVConfig(\n uint64 remoteChainSelector\n ) public view virtual returns (CCVConfig memory) {\n return s_verifierConfig[remoteChainSelector];\n }\n\n /// @notice Returns all CCV configurations across every configured remote chain selector.\n /// @return The array of CCVConfigArg structs, one per configured chain selector.\n function getAllCCVConfigs() public view virtual returns (CCVConfigArg[] memory) {\n uint256 length = s_configuredChainSelectors.length();\n CCVConfigArg[] memory configs = new CCVConfigArg[](length);\n\n for (uint256 i = 0; i < length; ++i) {\n uint64 selector = uint64(s_configuredChainSelectors.at(i));\n CCVConfig storage cfg = s_verifierConfig[selector];\n configs[i] = CCVConfigArg({\n remoteChainSelector: selector,\n outboundCCVs: cfg.outboundCCVs,\n thresholdOutboundCCVs: cfg.thresholdOutboundCCVs,\n inboundCCVs: cfg.inboundCCVs,\n thresholdInboundCCVs: cfg.thresholdInboundCCVs\n });\n }\n\n return configs;\n }\n\n /// @notice Updates the CCV configuration for specified remote chains.\n /// If the array includes address(0), it indicates that the default CCV should be used alongside any other specified CCVs.\n /// @dev Additional CCVs should only be configured for transfers at or above the threshold amount and should not duplicate base CCVs.\n /// Base CCVs are always required, while add-above-threshold CCVs are only required when the transfer amount exceeds the threshold.\n /// @param ccvConfigArgs The CCV configuration updates to apply.\n function applyCCVConfigUpdates(\n CCVConfigArg[] calldata ccvConfigArgs\n ) public virtual onlyOwner {\n for (uint256 i = 0; i < ccvConfigArgs.length; ++i) {\n uint64 remoteChainSelector = ccvConfigArgs[i].remoteChainSelector;\n address[] calldata outboundCCVs = ccvConfigArgs[i].outboundCCVs;\n address[] calldata thresholdOutboundCCVs = ccvConfigArgs[i].thresholdOutboundCCVs;\n address[] calldata inboundCCVs = ccvConfigArgs[i].inboundCCVs;\n address[] calldata thresholdInboundCCVs = ccvConfigArgs[i].thresholdInboundCCVs;\n\n // Check for duplicates in outbound CCVs.\n CCVConfigValidation._assertNoDuplicates(outboundCCVs);\n\n // Check for duplicates in inbound CCVs.\n CCVConfigValidation._assertNoDuplicates(inboundCCVs);\n\n if (thresholdOutboundCCVs.length > 0) {\n // Must have base CCVs if specifying above-threshold CCVs. If the defaults are used below the threshold,\n // specify address(0) in the outboundCCVs array.\n if (outboundCCVs.length == 0) {\n revert MustSpecifyUnderThresholdCCVsForThresholdCCVs();\n }\n\n CCVConfigValidation._assertNoDuplicates(thresholdOutboundCCVs);\n CCVConfigValidation._assertNoDuplicatedBetweenLists(outboundCCVs, thresholdOutboundCCVs);\n }\n\n if (thresholdInboundCCVs.length > 0) {\n // Must have base CCVs if specifying above-threshold CCVs. If the defaults are used below the threshold,\n // specify address(0) in the inboundCCVs array.\n if (inboundCCVs.length == 0) {\n revert MustSpecifyUnderThresholdCCVsForThresholdCCVs();\n }\n\n CCVConfigValidation._assertNoDuplicates(thresholdInboundCCVs);\n CCVConfigValidation._assertNoDuplicatedBetweenLists(inboundCCVs, thresholdInboundCCVs);\n }\n\n s_verifierConfig[remoteChainSelector] = CCVConfig({\n outboundCCVs: outboundCCVs,\n thresholdOutboundCCVs: thresholdOutboundCCVs,\n inboundCCVs: inboundCCVs,\n thresholdInboundCCVs: thresholdInboundCCVs\n });\n\n // If the config has no CCVs, remove it from the configured selectors. Otherwise, add it.\n if (outboundCCVs.length > 0 || inboundCCVs.length > 0) {\n s_configuredChainSelectors.add(remoteChainSelector);\n } else {\n s_configuredChainSelectors.remove(remoteChainSelector);\n }\n\n emit CCVConfigUpdated({\n remoteChainSelector: remoteChainSelector,\n outboundCCVs: outboundCCVs,\n thresholdOutboundCCVs: thresholdOutboundCCVs,\n inboundCCVs: inboundCCVs,\n thresholdInboundCCVs: thresholdInboundCCVs\n });\n }\n }\n\n /// @notice Returns the set of required CCVs for transfers in a specific direction.\n /// @param remoteChainSelector The remote chain selector for this transfer.\n /// @param amount The amount being transferred.\n /// @param direction The direction of the transfer (Inbound or Outbound).\n /// This implementation returns base CCVs for all transfers, and includes additional CCVs when the transfer amount\n /// is above the configured threshold.\n /// @return requiredCCVs Set of required CCV addresses.\n function getRequiredCCVs(\n address,\n uint64 remoteChainSelector,\n uint256 amount,\n bytes4,\n bytes calldata,\n IPoolV2.MessageDirection direction\n ) public view virtual returns (address[] memory requiredCCVs) {\n CCVConfig storage config = s_verifierConfig[remoteChainSelector];\n if (direction == IPoolV2.MessageDirection.Inbound) {\n return _resolveRequiredCCVs(config.inboundCCVs, config.thresholdInboundCCVs, amount);\n }\n return _resolveRequiredCCVs(config.outboundCCVs, config.thresholdOutboundCCVs, amount);\n }\n\n /// @notice Gets the threshold amount above which additional CCVs are required.\n /// @return The threshold amount.\n function getThresholdAmount() public view virtual returns (uint256) {\n return s_thresholdAmountForAdditionalCCVs;\n }\n\n /// @notice Sets the threshold amount above which additional CCVs are required.\n /// @param thresholdAmount The new threshold amount.\n function setThresholdAmount(\n uint256 thresholdAmount\n ) public virtual onlyOwner {\n s_thresholdAmountForAdditionalCCVs = thresholdAmount;\n\n emit ThresholdAmountSet(thresholdAmount);\n }\n\n function _resolveRequiredCCVs(\n address[] memory baseCCVs,\n address[] storage requiredCCVsAboveThresholdStorage,\n uint256 amount\n ) internal view virtual returns (address[] memory requiredCCVs) {\n // If amount is above threshold, combine base and additional CCVs.\n uint256 thresholdAmount = s_thresholdAmountForAdditionalCCVs;\n if (thresholdAmount != 0 && amount >= thresholdAmount) {\n address[] memory thresholdCCVs = requiredCCVsAboveThresholdStorage;\n if (thresholdCCVs.length > 0) {\n requiredCCVs = new address[](baseCCVs.length + thresholdCCVs.length);\n // Copy base CCVs.\n for (uint256 i = 0; i < baseCCVs.length; ++i) {\n requiredCCVs[i] = baseCCVs[i];\n }\n // Copy additional CCVs.\n for (uint256 i = 0; i < thresholdCCVs.length; ++i) {\n requiredCCVs[baseCCVs.length + i] = thresholdCCVs[i];\n }\n return requiredCCVs;\n }\n }\n return baseCCVs;\n }\n\n // ================================================================\n // \u2502 Policy Engine \u2502\n // ================================================================\n\n /// @notice Sets a new policy engine.\n /// @param newPolicyEngine The address of the new policy engine.\n function setPolicyEngine(\n address newPolicyEngine\n ) public virtual onlyOwner {\n _setPolicyEngine(newPolicyEngine, false);\n }\n\n /// @notice Sets a new policy engine while tolerating a pre-existing policy engine's detach reverting.\n /// @dev Use this to force update an old policy engine whose detach() reverts.\n /// @param newPolicyEngine The address of the new policy engine.\n function setPolicyEngineAllowFailedDetach(\n address newPolicyEngine\n ) public virtual onlyOwner {\n _setPolicyEngine(newPolicyEngine, true);\n }\n\n /// @notice Internal function to set and attach to a policy engine.\n /// @param newPolicyEngine The address of the new policy engine, or address(0) to disable.\n /// @param allowFailedDetach Whether to revert if old policy engine's detach reverts.\n function _setPolicyEngine(\n address newPolicyEngine,\n bool allowFailedDetach\n ) internal virtual {\n address oldPolicyEngine = address(s_policyEngine);\n\n if (newPolicyEngine == oldPolicyEngine) {\n return;\n }\n\n if (oldPolicyEngine != address(0)) {\n // Guarding detach reverts to offer escape hatch from adversarial policy engine instances.\n try IPolicyEngine(oldPolicyEngine).detach() {}\n catch (bytes memory err) {\n if (!allowFailedDetach) {\n revert PolicyEngineDetachReverted(oldPolicyEngine, err);\n }\n emit PolicyEngineDetachFailed(oldPolicyEngine, err);\n }\n }\n\n s_policyEngine = IPolicyEngine(newPolicyEngine);\n if (newPolicyEngine != address(0)) {\n IPolicyEngine(newPolicyEngine).attach();\n }\n\n emit PolicyEngineAttached(newPolicyEngine);\n }\n\n /// @notice Gets the current policy engine address.\n /// @return The address of the policy engine.\n function getPolicyEngine() public view virtual returns (address) {\n return address(s_policyEngine);\n }\n}\n" + }, + "node_modules/@chainlink/ace/packages/policy-management/src/interfaces/IPolicyEngine.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.20;\n\n/**\n * @title IPolicyEngine\n * @dev Interface for the policy engine.\n */\ninterface IPolicyEngine {\n /// @notice Error emitted when the target is not attached to the policy engine.\n error TargetNotAttached(address target);\n /// @notice Error emitted when the target is already attached to the policy engine.\n error TargetAlreadyAttached(address target);\n /// @notice Error emitted when the policy engine is missing or not present.\n error PolicyEngineUndefined();\n /// @notice Error emitted when the PolicyEngine run has been rejected by one of the polices.\n error PolicyRunRejected(address policy, string rejectReason, Payload payload);\n /// @notice Error emitted when a policy mapper results in an error.\n error PolicyMapperError(address policy, bytes errorReason, Payload payload);\n /// @notice Error emitted when an individual policy is rejecting a transaction.\n error PolicyRejected(string rejectReason);\n /// @notice Error emitted when the PolicyEngine run encounters an error while executing one of the policies.\n error PolicyRunError(address policy, bytes errorReason, Payload payload);\n /// @notice Error emitted when a policy run is unauthorized.\n error PolicyRunUnauthorizedError(address account);\n /// @notice Error emitted when a policy postRun results in an error.\n error PolicyPostRunError(address policy, bytes errorReason, Payload payload);\n /// @notice Error emitted when a policy extractor is run with an unsupported selector.\n error UnsupportedSelector(bytes4 selector);\n /// @notice Error emitted when a policy action results in an error.\n error PolicyActionError(address policy, bytes errorReason);\n /// @notice Error emitted when a policy configuration change results in an error.\n error PolicyConfigurationError(address policy, bytes errorReason);\n /// @notice Error emitted when a policy configuration version does not match the expected version.\n error PolicyConfigurationVersionError(address policy, uint256 expectedVersion, uint256 actualVersion);\n /// @notice Error emitted when an extraction of parameters results in an error.\n error ExtractorError(address extractor, bytes errorReason, Payload payload);\n\n /**\n * @notice Emitted when a target contract has attached to the policy engine.\n * @param target The target contract.\n */\n event TargetAttached(address indexed target);\n\n /**\n * @notice Emitted when a target contract has detached from the policy engine.\n * @param target The target contract.\n */\n event TargetDetached(address indexed target);\n\n /**\n * @notice Emitted when a policy configuration is performed.\n * @param policy The address of the policy.\n * @param configSelector The selector of the configuration function.\n * @param configVersion The version of the configuration.\n * @param configData The data of the configuration.\n */\n event PolicyConfigured(\n address indexed policy, uint256 indexed configVersion, bytes4 indexed configSelector, bytes configData\n );\n\n /**\n * @notice Emitted when a policy engine run has completed successfully.\n * @param sender The sender of the transaction.\n * @param target The target contract that invoked the method.\n * @param selector The selector of the method invoked on the target.\n * @param extractedParameters The parameters extracted from the payload for policy evaluation.\n * @param context Additional context data from the payload.\n */\n event PolicyRunComplete(\n address indexed sender,\n address indexed target,\n bytes4 indexed selector,\n Parameter[] extractedParameters,\n bytes context\n );\n\n /**\n * @notice Emitted when a policy is added to the policy engine.\n * @param target The address of the target contract for which the policy was configured.\n * @param selector The selector of the policy.\n * @param policy The policy address.\n * @param position The position of the policy in the policy chain.\n * @param policyParameterNames The parameter names for the policy.\n */\n event PolicyAdded(\n address indexed target, bytes4 indexed selector, address policy, uint256 position, bytes32[] policyParameterNames\n );\n\n /**\n * @notice Emitted when a policy is added to the policy engine at a specific position.\n * @param target The address of the target contract for which the policy was configured.\n * @param selector The selector of the policy.\n * @param policy The policy address.\n * @param position The position of the policy in the policy chain.\n * @param policyParameterNames The parameter names for the policy.\n * @param policies The complete ordered array of all policy addresses after the insertion.\n */\n event PolicyAddedAt(\n address indexed target,\n bytes4 indexed selector,\n address policy,\n uint256 position,\n bytes32[] policyParameterNames,\n address[] policies\n );\n\n /**\n * @notice Emitted when a policy is removed from the policy engine.\n * @param target The address of the target contract for which the policy was configured.\n * @param selector The selector of the policy.\n * @param policy The policy address.\n */\n event PolicyRemoved(address indexed target, bytes4 indexed selector, address policy);\n\n /**\n * @notice Emitted when an extractor is set for a selector.\n * @param selector The selector.\n * @param extractor The extractor address.\n */\n event ExtractorSet(bytes4 indexed selector, address indexed extractor);\n\n /**\n * @notice Emitted when a policy mapper is set for a policy.\n * @param policy The policy address.\n * @param mapper The mapper address.\n */\n event PolicyMapperSet(address indexed policy, address indexed mapper);\n\n /**\n * @notice Emitted when policy parameters are set for a policy.\n * @param policy The policy address.\n * @param parameters The parameters for the policy.\n */\n event PolicyParametersSet(address indexed policy, bytes[] parameters);\n\n /**\n * @notice Emitted when the default policy action rule is set for the policy engine.\n * @param defaultAllow Indicates whether to allow or reject a transaction if no policy explicitly returns an Allow\n * or a Reject. True to allow, false to reject.\n */\n event DefaultPolicyAllowSet(bool defaultAllow);\n\n /**\n * @notice Emitted when the default policy allow rule for a target is set.\n * @param target The target contract.\n * @param defaultAllow Indicates whether to allow or reject a transaction if no policy explicitly returns an Allow\n * or a Reject. True to allow, false to reject.\n */\n event TargetDefaultPolicyAllowSet(address indexed target, bool defaultAllow);\n\n /**\n * @notice The PolicyResult enum represents the possible types of success results of a policy run. When a policy\n * should reject a transaction, it MUST revert using the `PolicyReject` error with a descriptive reject message.\n * @param None No specific policy result, typically used as a default or uninitialized state.\n * @param Allowed The policy allowed the run.\n * @param Continue The policy did not reject the run and processing should continue to the next policy.\n */\n enum PolicyResult {\n None,\n Allowed,\n Continue\n }\n\n /**\n * @notice The Payload struct combines the components on which policies operate.\n * @param selector The selector of the method being invoked on the target.\n * @param sender The sender of the transaction.\n * @param data The original calldata of the invoked transaction.\n * @param context Additional information or authorization to perform the operation.\n */\n struct Payload {\n bytes4 selector;\n address sender;\n bytes data;\n bytes context;\n }\n\n /**\n * @notice The Parameter struct contains the data of the parameters sent to policies.\n * @param name The name of the parameter.\n * @param value The value of the parameter.\n */\n struct Parameter {\n bytes32 name;\n bytes value;\n }\n\n /**\n * @notice Returns the type and version of the policy engine.\n * @return A string representing the type and version of the policy engine.\n */\n function typeAndVersion() external pure returns (string memory);\n\n /**\n * @notice Attaches the calling contract to the policy engine.\n */\n function attach() external;\n\n /**\n * @notice Detaches the calling contract from the policy engine.\n */\n function detach() external;\n\n /**\n * @notice Assigns an extractor to the specified selector, enabling policies to utilize it for parameter extraction.\n * @param selector The selector of the policy.\n * @param extractor The extractor address.\n */\n function setExtractor(bytes4 selector, address extractor) external;\n\n /**\n * @notice Assigns an extractor to the specified selectors, enabling policies to utilize it for parameter extraction.\n * @param selectors The selectors of the policies.\n * @param extractor The extractor address.\n */\n function setExtractors(bytes4[] calldata selectors, address extractor) external;\n\n /**\n * @notice Gets the extractor for a given selector.\n * @param selector The selector.\n * @return The extractor for the selector.\n */\n function getExtractor(bytes4 selector) external view returns (address);\n\n /**\n * @notice Sets the custom policy parameter mapper for a policy.\n * @param policy The policy address.\n * @param mapper The mapper address, address(0) to use the default mapper.\n */\n function setPolicyMapper(address policy, address mapper) external;\n\n /**\n * @notice Gets the policy parameter mapper for a given policy.\n * @param policy The policy address.\n * @return The custom policy parameter mapper for the policy, address(0) if the policy uses the default mapper.\n */\n function getPolicyMapper(address policy) external view returns (address);\n\n /**\n * @notice Adds a policy to the policy engine.\n *\n * - Policy MUST be added to the end of the current policy list.\n *\n * @param target The address of the target contract for which the policy apply.\n * @param selector The selector of the policy.\n * @param policy The policy address.\n * @param policyParameterNames The parameter names for the policy.\n */\n function addPolicy(address target, bytes4 selector, address policy, bytes32[] calldata policyParameterNames) external;\n\n /**\n * @notice Adds a policy to the policy engine at a specific position.\n *\n * @param target The address of the target contract for which the policy apply.\n * @param selector The selector of the policy.\n * @param policy The policy address.\n * @param policyParameterNames The parameter names for the policy.\n * @param position The position to add the policy at.\n */\n function addPolicyAt(\n address target,\n bytes4 selector,\n address policy,\n bytes32[] calldata policyParameterNames,\n uint256 position\n )\n external;\n\n /**\n * @notice Removes a policy from the policy engine.\n * @param target The address of the target contract for which the policy was configured.\n * @param selector The selector of the policy.\n * @param policy The policy address.\n */\n function removePolicy(address target, bytes4 selector, address policy) external;\n\n /**\n * @notice Gets the policies for a given selector and target.\n *\n * - MUST return the policies in the order they will execute.\n * - MUST return an empty array if no policies are found.\n *\n * @param selector The selector of the policy.\n * @param target The address of the target contract for which the policies are configured.\n * @return The policies for the selector and target.\n */\n function getPolicies(address target, bytes4 selector) external view returns (address[] memory);\n\n /**\n * @notice Sets the configuration for a policy.\n * @param policy The address of the policy to configure.\n * @param configVersion The version of the configuration.\n * @param configSelector The selector of the configuration function.\n * @param configData The calldata for the configuration function.\n */\n function setPolicyConfiguration(\n address policy,\n uint256 configVersion,\n bytes4 configSelector,\n bytes calldata configData\n )\n external;\n\n /**\n * @notice Gets the current configuration version for a policy.\n * @param policy The address of the policy.\n * @return The current configuration version for the policy.\n */\n function getPolicyConfigVersion(address policy) external view returns (uint256);\n\n /**\n * @notice Sets whether to allow or reject the transaction if no policy explicitly returns an Allow or a Reject.\n * @param defaultAllow Indicates whether to allow or reject a transaction if no policy explicitly returns an Allow\n * or a Reject. True to allow, false to reject.\n */\n function setDefaultPolicyAllow(bool defaultAllow) external;\n\n /**\n * @notice Sets whether to allow or reject the transaction if no policy explicitly returns an Allow or a Reject\n * for a specific target.\n * @param target The address of the target contract.\n * @param defaultAllow Indicates whether to allow or reject a transaction if no policy explicitly returns an Allow\n * or a Reject. True to allow, false to reject.\n */\n function setTargetDefaultPolicyAllow(address target, bool defaultAllow) external;\n\n /**\n * @notice Runs the policies for a given payload for offchain pre-validation. MUST revert on policy rejection/failure.\n * @param payload The payload to run the policies on.\n */\n function check(Payload calldata payload) external view;\n\n /**\n * @notice Runs the policies for a given operation payload.\n *\n * - MUST revert on policy rejection/failure.\n * - MUST revert if the target contract that invoked the method is not allowed. Target contract address is\n * obtained from the msg.sender global variable.\n * - MUST execute policies in the order they were added or that were specified using `addPolicyAt`.\n *\n * @param payload The payload to run the policies on.\n */\n function run(Payload calldata payload) external;\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/AuthorizedCallers.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.4;\n\nimport {Ownable2StepMsgSender} from \"./Ownable2StepMsgSender.sol\";\nimport {EnumerableSet} from \"@openzeppelin/contracts@4.8.3/utils/structs/EnumerableSet.sol\";\n\n/// @title The AuthorizedCallers contract\n/// @notice A contract that manages multiple authorized callers. Enables restricting access to certain functions to a\n/// set of addresses.\ncontract AuthorizedCallers is Ownable2StepMsgSender {\n using EnumerableSet for EnumerableSet.AddressSet;\n\n event AuthorizedCallerAdded(address caller);\n event AuthorizedCallerRemoved(address caller);\n\n error UnauthorizedCaller(address caller);\n error ZeroAddressNotAllowed();\n\n /// @notice Update args for changing the authorized callers\n struct AuthorizedCallerArgs {\n address[] addedCallers;\n address[] removedCallers;\n }\n\n /// @dev Set of authorized callers\n EnumerableSet.AddressSet internal s_authorizedCallers;\n\n /// @param authorizedCallers the authorized callers to set\n constructor(\n address[] memory authorizedCallers\n ) {\n _applyAuthorizedCallerUpdates(\n AuthorizedCallerArgs({addedCallers: authorizedCallers, removedCallers: new address[](0)})\n );\n }\n\n /// @return authorizedCallers Returns all authorized callers\n function getAllAuthorizedCallers() external view returns (address[] memory) {\n return s_authorizedCallers.values();\n }\n\n /// @notice Updates the list of authorized callers\n /// @param authorizedCallerArgs Callers to add and remove. Removals are performed first.\n function applyAuthorizedCallerUpdates(\n AuthorizedCallerArgs memory authorizedCallerArgs\n ) external onlyOwner {\n _applyAuthorizedCallerUpdates(authorizedCallerArgs);\n }\n\n /// @notice Updates the list of authorized callers\n /// @param authorizedCallerArgs Callers to add and remove. Removals are performed first.\n function _applyAuthorizedCallerUpdates(\n AuthorizedCallerArgs memory authorizedCallerArgs\n ) internal {\n address[] memory removedCallers = authorizedCallerArgs.removedCallers;\n for (uint256 i = 0; i < removedCallers.length; ++i) {\n address caller = removedCallers[i];\n\n if (s_authorizedCallers.remove(caller)) {\n emit AuthorizedCallerRemoved(caller);\n }\n }\n\n address[] memory addedCallers = authorizedCallerArgs.addedCallers;\n for (uint256 i = 0; i < addedCallers.length; ++i) {\n address caller = addedCallers[i];\n\n if (caller == address(0)) {\n revert ZeroAddressNotAllowed();\n }\n\n s_authorizedCallers.add(caller);\n emit AuthorizedCallerAdded(caller);\n }\n }\n\n /// @notice Checks the sender and reverts if it is anyone other than a listed authorized caller.\n function _validateCaller() internal view {\n if (!s_authorizedCallers.contains(msg.sender)) {\n revert UnauthorizedCaller(msg.sender);\n }\n }\n\n /// @notice Checks the sender and reverts if it is anyone other than a listed authorized caller.\n modifier onlyAuthorizedCallers() {\n _validateCaller();\n _;\n }\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2Step.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {IOwnable} from \"../interfaces/IOwnable.sol\";\n\n/// @notice A minimal contract that implements 2-step ownership transfer and nothing more. It's made to be minimal\n/// to reduce the impact of the bytecode size on any contract that inherits from it.\ncontract Ownable2Step is IOwnable {\n /// @notice The pending owner is the address to which ownership may be transferred.\n address private s_pendingOwner;\n /// @notice The owner is the current owner of the contract.\n /// @dev The owner is the second storage variable so any implementing contract could pack other state with it\n /// instead of the much less used s_pendingOwner.\n address private s_owner;\n\n error OwnerCannotBeZero();\n error MustBeProposedOwner();\n error CannotTransferToSelf();\n error OnlyCallableByOwner();\n\n event OwnershipTransferRequested(address indexed from, address indexed to);\n event OwnershipTransferred(address indexed from, address indexed to);\n\n constructor(address newOwner, address pendingOwner) {\n if (newOwner == address(0)) {\n revert OwnerCannotBeZero();\n }\n\n s_owner = newOwner;\n if (pendingOwner != address(0)) {\n _transferOwnership(pendingOwner);\n }\n }\n\n /// @notice Get the current owner\n function owner() public view override returns (address) {\n return s_owner;\n }\n\n /// @notice Allows an owner to begin transferring ownership to a new address. The new owner needs to call\n /// `acceptOwnership` to accept the transfer before any permissions are changed.\n /// @param to The address to which ownership will be transferred.\n function transferOwnership(\n address to\n ) public override onlyOwner {\n _transferOwnership(to);\n }\n\n /// @notice validate, transfer ownership, and emit relevant events\n /// @param to The address to which ownership will be transferred.\n function _transferOwnership(\n address to\n ) private {\n if (to == msg.sender) {\n revert CannotTransferToSelf();\n }\n\n s_pendingOwner = to;\n\n emit OwnershipTransferRequested(s_owner, to);\n }\n\n /// @notice Allows an ownership transfer to be completed by the recipient.\n function acceptOwnership() external override {\n if (msg.sender != s_pendingOwner) {\n revert MustBeProposedOwner();\n }\n\n address oldOwner = s_owner;\n s_owner = msg.sender;\n s_pendingOwner = address(0);\n\n emit OwnershipTransferred(oldOwner, msg.sender);\n }\n\n /// @notice validate access\n function _validateOwnership() internal view {\n if (msg.sender != s_owner) {\n revert OnlyCallableByOwner();\n }\n }\n\n /// @notice Reverts if called by anyone other than the contract owner.\n modifier onlyOwner() {\n _validateOwnership();\n _;\n }\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {Ownable2Step} from \"./Ownable2Step.sol\";\n\n/// @notice Sets the msg.sender to be the owner of the contract and does not set a pending owner.\ncontract Ownable2StepMsgSender is Ownable2Step {\n constructor() Ownable2Step(msg.sender, address(0)) {}\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/IOwnable.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface IOwnable {\n function owner() external returns (address);\n\n function transferOwnership(\n address recipient\n ) external;\n\n function acceptOwnership() external;\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface ITypeAndVersion {\n function typeAndVersion() external pure returns (string memory);\n}\n" + }, + "node_modules/@openzeppelin/contracts-4.8.3/utils/structs/EnumerableSet.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v4.8.0) (utils/structs/EnumerableSet.sol)\n// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.\n\npragma solidity ^0.8.0;\n\n/**\n * @dev Library for managing\n * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive\n * types.\n *\n * Sets have the following properties:\n *\n * - Elements are added, removed, and checked for existence in constant time\n * (O(1)).\n * - Elements are enumerated in O(n). No guarantees are made on the ordering.\n *\n * ```\n * contract Example {\n * // Add the library methods\n * using EnumerableSet for EnumerableSet.AddressSet;\n *\n * // Declare a set state variable\n * EnumerableSet.AddressSet private mySet;\n * }\n * ```\n *\n * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)\n * and `uint256` (`UintSet`) are supported.\n *\n * [WARNING]\n * ====\n * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure\n * unusable.\n * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.\n *\n * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an\n * array of EnumerableSet.\n * ====\n */\nlibrary EnumerableSet {\n // To implement this library for multiple types with as little code\n // repetition as possible, we write it in terms of a generic Set type with\n // bytes32 values.\n // The Set implementation uses private functions, and user-facing\n // implementations (such as AddressSet) are just wrappers around the\n // underlying Set.\n // This means that we can only create new EnumerableSets for types that fit\n // in bytes32.\n\n struct Set {\n // Storage of set values\n bytes32[] _values;\n // Position of the value in the `values` array, plus 1 because index 0\n // means a value is not in the set.\n mapping(bytes32 => uint256) _indexes;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function _add(Set storage set, bytes32 value) private returns (bool) {\n if (!_contains(set, value)) {\n set._values.push(value);\n // The value is stored at length-1, but we add 1 to all indexes\n // and use 0 as a sentinel value\n set._indexes[value] = set._values.length;\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function _remove(Set storage set, bytes32 value) private returns (bool) {\n // We read and store the value's index to prevent multiple reads from the same storage slot\n uint256 valueIndex = set._indexes[value];\n\n if (valueIndex != 0) {\n // Equivalent to contains(set, value)\n // To delete an element from the _values array in O(1), we swap the element to delete with the last one in\n // the array, and then remove the last element (sometimes called as 'swap and pop').\n // This modifies the order of the array, as noted in {at}.\n\n uint256 toDeleteIndex = valueIndex - 1;\n uint256 lastIndex = set._values.length - 1;\n\n if (lastIndex != toDeleteIndex) {\n bytes32 lastValue = set._values[lastIndex];\n\n // Move the last value to the index where the value to delete is\n set._values[toDeleteIndex] = lastValue;\n // Update the index for the moved value\n set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex\n }\n\n // Delete the slot where the moved value was stored\n set._values.pop();\n\n // Delete the index for the deleted slot\n delete set._indexes[value];\n\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function _contains(Set storage set, bytes32 value) private view returns (bool) {\n return set._indexes[value] != 0;\n }\n\n /**\n * @dev Returns the number of values on the set. O(1).\n */\n function _length(Set storage set) private view returns (uint256) {\n return set._values.length;\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function _at(Set storage set, uint256 index) private view returns (bytes32) {\n return set._values[index];\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function _values(Set storage set) private view returns (bytes32[] memory) {\n return set._values;\n }\n\n // Bytes32Set\n\n struct Bytes32Set {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _add(set._inner, value);\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _remove(set._inner, value);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {\n return _contains(set._inner, value);\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(Bytes32Set storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {\n return _at(set._inner, index);\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {\n bytes32[] memory store = _values(set._inner);\n bytes32[] memory result;\n\n /// @solidity memory-safe-assembly\n assembly {\n result := store\n }\n\n return result;\n }\n\n // AddressSet\n\n struct AddressSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(AddressSet storage set, address value) internal returns (bool) {\n return _add(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(AddressSet storage set, address value) internal returns (bool) {\n return _remove(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(AddressSet storage set, address value) internal view returns (bool) {\n return _contains(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(AddressSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(AddressSet storage set, uint256 index) internal view returns (address) {\n return address(uint160(uint256(_at(set._inner, index))));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(AddressSet storage set) internal view returns (address[] memory) {\n bytes32[] memory store = _values(set._inner);\n address[] memory result;\n\n /// @solidity memory-safe-assembly\n assembly {\n result := store\n }\n\n return result;\n }\n\n // UintSet\n\n struct UintSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(UintSet storage set, uint256 value) internal returns (bool) {\n return _add(set._inner, bytes32(value));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(UintSet storage set, uint256 value) internal returns (bool) {\n return _remove(set._inner, bytes32(value));\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(UintSet storage set, uint256 value) internal view returns (bool) {\n return _contains(set._inner, bytes32(value));\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(UintSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(UintSet storage set, uint256 index) internal view returns (uint256) {\n return uint256(_at(set._inner, index));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(UintSet storage set) internal view returns (uint256[] memory) {\n bytes32[] memory store = _values(set._inner);\n uint256[] memory result;\n\n /// @solidity memory-safe-assembly\n assembly {\n result := store\n }\n\n return result;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Arrays.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/Arrays.sol)\n// This file was procedurally generated from scripts/generate/templates/Arrays.js.\n\npragma solidity ^0.8.20;\n\nimport {Comparators} from \"./Comparators.sol\";\nimport {SlotDerivation} from \"./SlotDerivation.sol\";\nimport {StorageSlot} from \"./StorageSlot.sol\";\nimport {Math} from \"./math/Math.sol\";\n\n/**\n * @dev Collection of functions related to array types.\n */\nlibrary Arrays {\n using SlotDerivation for bytes32;\n using StorageSlot for bytes32;\n\n /**\n * @dev Sort an array of uint256 (in memory) following the provided comparator function.\n *\n * This function does the sorting \"in place\", meaning that it overrides the input. The object is returned for\n * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.\n *\n * NOTE: this function's cost is `O(n \u00b7 log(n))` in average and `O(n\u00b2)` in the worst case, with n the length of the\n * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful\n * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may\n * consume more gas than is available in a block, leading to potential DoS.\n *\n * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.\n */\n function sort(\n uint256[] memory array,\n function(uint256, uint256) pure returns (bool) comp\n ) internal pure returns (uint256[] memory) {\n _quickSort(_begin(array), _end(array), comp);\n return array;\n }\n\n /**\n * @dev Variant of {sort} that sorts an array of uint256 in increasing order.\n */\n function sort(uint256[] memory array) internal pure returns (uint256[] memory) {\n sort(array, Comparators.lt);\n return array;\n }\n\n /**\n * @dev Sort an array of address (in memory) following the provided comparator function.\n *\n * This function does the sorting \"in place\", meaning that it overrides the input. The object is returned for\n * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.\n *\n * NOTE: this function's cost is `O(n \u00b7 log(n))` in average and `O(n\u00b2)` in the worst case, with n the length of the\n * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful\n * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may\n * consume more gas than is available in a block, leading to potential DoS.\n *\n * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.\n */\n function sort(\n address[] memory array,\n function(address, address) pure returns (bool) comp\n ) internal pure returns (address[] memory) {\n sort(_castToUint256Array(array), _castToUint256Comp(comp));\n return array;\n }\n\n /**\n * @dev Variant of {sort} that sorts an array of address in increasing order.\n */\n function sort(address[] memory array) internal pure returns (address[] memory) {\n sort(_castToUint256Array(array), Comparators.lt);\n return array;\n }\n\n /**\n * @dev Sort an array of bytes32 (in memory) following the provided comparator function.\n *\n * This function does the sorting \"in place\", meaning that it overrides the input. The object is returned for\n * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.\n *\n * NOTE: this function's cost is `O(n \u00b7 log(n))` in average and `O(n\u00b2)` in the worst case, with n the length of the\n * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful\n * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may\n * consume more gas than is available in a block, leading to potential DoS.\n *\n * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.\n */\n function sort(\n bytes32[] memory array,\n function(bytes32, bytes32) pure returns (bool) comp\n ) internal pure returns (bytes32[] memory) {\n sort(_castToUint256Array(array), _castToUint256Comp(comp));\n return array;\n }\n\n /**\n * @dev Variant of {sort} that sorts an array of bytes32 in increasing order.\n */\n function sort(bytes32[] memory array) internal pure returns (bytes32[] memory) {\n sort(_castToUint256Array(array), Comparators.lt);\n return array;\n }\n\n /**\n * @dev Performs a quick sort of a segment of memory. The segment sorted starts at `begin` (inclusive), and stops\n * at end (exclusive). Sorting follows the `comp` comparator.\n *\n * Invariant: `begin <= end`. This is the case when initially called by {sort} and is preserved in subcalls.\n *\n * IMPORTANT: Memory locations between `begin` and `end` are not validated/zeroed. This function should\n * be used only if the limits are within a memory array.\n */\n function _quickSort(uint256 begin, uint256 end, function(uint256, uint256) pure returns (bool) comp) private pure {\n unchecked {\n if (end - begin < 0x40) return;\n\n // Use first element as pivot\n uint256 pivot = _mload(begin);\n // Position where the pivot should be at the end of the loop\n uint256 pos = begin;\n\n for (uint256 it = begin + 0x20; it < end; it += 0x20) {\n if (comp(_mload(it), pivot)) {\n // If the value stored at the iterator's position comes before the pivot, we increment the\n // position of the pivot and move the value there.\n pos += 0x20;\n _swap(pos, it);\n }\n }\n\n _swap(begin, pos); // Swap pivot into place\n _quickSort(begin, pos, comp); // Sort the left side of the pivot\n _quickSort(pos + 0x20, end, comp); // Sort the right side of the pivot\n }\n }\n\n /**\n * @dev Pointer to the memory location of the first element of `array`.\n */\n function _begin(uint256[] memory array) private pure returns (uint256 ptr) {\n assembly (\"memory-safe\") {\n ptr := add(array, 0x20)\n }\n }\n\n /**\n * @dev Pointer to the memory location of the first memory word (32bytes) after `array`. This is the memory word\n * that comes just after the last element of the array.\n */\n function _end(uint256[] memory array) private pure returns (uint256 ptr) {\n unchecked {\n return _begin(array) + array.length * 0x20;\n }\n }\n\n /**\n * @dev Load memory word (as a uint256) at location `ptr`.\n */\n function _mload(uint256 ptr) private pure returns (uint256 value) {\n assembly {\n value := mload(ptr)\n }\n }\n\n /**\n * @dev Swaps the elements memory location `ptr1` and `ptr2`.\n */\n function _swap(uint256 ptr1, uint256 ptr2) private pure {\n assembly {\n let value1 := mload(ptr1)\n let value2 := mload(ptr2)\n mstore(ptr1, value2)\n mstore(ptr2, value1)\n }\n }\n\n /// @dev Helper: low level cast address memory array to uint256 memory array\n function _castToUint256Array(address[] memory input) private pure returns (uint256[] memory output) {\n assembly {\n output := input\n }\n }\n\n /// @dev Helper: low level cast bytes32 memory array to uint256 memory array\n function _castToUint256Array(bytes32[] memory input) private pure returns (uint256[] memory output) {\n assembly {\n output := input\n }\n }\n\n /// @dev Helper: low level cast address comp function to uint256 comp function\n function _castToUint256Comp(\n function(address, address) pure returns (bool) input\n ) private pure returns (function(uint256, uint256) pure returns (bool) output) {\n assembly {\n output := input\n }\n }\n\n /// @dev Helper: low level cast bytes32 comp function to uint256 comp function\n function _castToUint256Comp(\n function(bytes32, bytes32) pure returns (bool) input\n ) private pure returns (function(uint256, uint256) pure returns (bool) output) {\n assembly {\n output := input\n }\n }\n\n /**\n * @dev Searches a sorted `array` and returns the first index that contains\n * a value greater or equal to `element`. If no such index exists (i.e. all\n * values in the array are strictly less than `element`), the array length is\n * returned. Time complexity O(log n).\n *\n * NOTE: The `array` is expected to be sorted in ascending order, and to\n * contain no repeated elements.\n *\n * IMPORTANT: Deprecated. This implementation behaves as {lowerBound} but lacks\n * support for repeated elements in the array. The {lowerBound} function should\n * be used instead.\n */\n function findUpperBound(uint256[] storage array, uint256 element) internal view returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeAccess(array, mid).value > element) {\n high = mid;\n } else {\n low = mid + 1;\n }\n }\n\n // At this point `low` is the exclusive upper bound. We will return the inclusive upper bound.\n if (low > 0 && unsafeAccess(array, low - 1).value == element) {\n return low - 1;\n } else {\n return low;\n }\n }\n\n /**\n * @dev Searches an `array` sorted in ascending order and returns the first\n * index that contains a value greater or equal than `element`. If no such index\n * exists (i.e. all values in the array are strictly less than `element`), the array\n * length is returned. Time complexity O(log n).\n *\n * See C++'s https://en.cppreference.com/w/cpp/algorithm/lower_bound[lower_bound].\n */\n function lowerBound(uint256[] storage array, uint256 element) internal view returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeAccess(array, mid).value < element) {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n } else {\n high = mid;\n }\n }\n\n return low;\n }\n\n /**\n * @dev Searches an `array` sorted in ascending order and returns the first\n * index that contains a value strictly greater than `element`. If no such index\n * exists (i.e. all values in the array are strictly less than `element`), the array\n * length is returned. Time complexity O(log n).\n *\n * See C++'s https://en.cppreference.com/w/cpp/algorithm/upper_bound[upper_bound].\n */\n function upperBound(uint256[] storage array, uint256 element) internal view returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeAccess(array, mid).value > element) {\n high = mid;\n } else {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n }\n }\n\n return low;\n }\n\n /**\n * @dev Same as {lowerBound}, but with an array in memory.\n */\n function lowerBoundMemory(uint256[] memory array, uint256 element) internal pure returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeMemoryAccess(array, mid) < element) {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n } else {\n high = mid;\n }\n }\n\n return low;\n }\n\n /**\n * @dev Same as {upperBound}, but with an array in memory.\n */\n function upperBoundMemory(uint256[] memory array, uint256 element) internal pure returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeMemoryAccess(array, mid) > element) {\n high = mid;\n } else {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n }\n }\n\n return low;\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeAccess(address[] storage arr, uint256 pos) internal pure returns (StorageSlot.AddressSlot storage) {\n bytes32 slot;\n assembly (\"memory-safe\") {\n slot := arr.slot\n }\n return slot.deriveArray().offset(pos).getAddressSlot();\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeAccess(bytes32[] storage arr, uint256 pos) internal pure returns (StorageSlot.Bytes32Slot storage) {\n bytes32 slot;\n assembly (\"memory-safe\") {\n slot := arr.slot\n }\n return slot.deriveArray().offset(pos).getBytes32Slot();\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeAccess(uint256[] storage arr, uint256 pos) internal pure returns (StorageSlot.Uint256Slot storage) {\n bytes32 slot;\n assembly (\"memory-safe\") {\n slot := arr.slot\n }\n return slot.deriveArray().offset(pos).getUint256Slot();\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeMemoryAccess(address[] memory arr, uint256 pos) internal pure returns (address res) {\n assembly {\n res := mload(add(add(arr, 0x20), mul(pos, 0x20)))\n }\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeMemoryAccess(bytes32[] memory arr, uint256 pos) internal pure returns (bytes32 res) {\n assembly {\n res := mload(add(add(arr, 0x20), mul(pos, 0x20)))\n }\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeMemoryAccess(uint256[] memory arr, uint256 pos) internal pure returns (uint256 res) {\n assembly {\n res := mload(add(add(arr, 0x20), mul(pos, 0x20)))\n }\n }\n\n /**\n * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden.\n *\n * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.\n */\n function unsafeSetLength(address[] storage array, uint256 len) internal {\n assembly (\"memory-safe\") {\n sstore(array.slot, len)\n }\n }\n\n /**\n * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden.\n *\n * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.\n */\n function unsafeSetLength(bytes32[] storage array, uint256 len) internal {\n assembly (\"memory-safe\") {\n sstore(array.slot, len)\n }\n }\n\n /**\n * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden.\n *\n * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.\n */\n function unsafeSetLength(uint256[] storage array, uint256 len) internal {\n assembly (\"memory-safe\") {\n sstore(array.slot, len)\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Comparators.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/Comparators.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Provides a set of functions to compare values.\n *\n * _Available since v5.1._\n */\nlibrary Comparators {\n function lt(uint256 a, uint256 b) internal pure returns (bool) {\n return a < b;\n }\n\n function gt(uint256 a, uint256 b) internal pure returns (bool) {\n return a > b;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Panic.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/Panic.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Helper library for emitting standardized panic codes.\n *\n * ```solidity\n * contract Example {\n * using Panic for uint256;\n *\n * // Use any of the declared internal constants\n * function foo() { Panic.GENERIC.panic(); }\n *\n * // Alternatively\n * function foo() { Panic.panic(Panic.GENERIC); }\n * }\n * ```\n *\n * Follows the list from https://github.com/ethereum/solidity/blob/v0.8.24/libsolutil/ErrorCodes.h[libsolutil].\n *\n * _Available since v5.1._\n */\n// slither-disable-next-line unused-state\nlibrary Panic {\n /// @dev generic / unspecified error\n uint256 internal constant GENERIC = 0x00;\n /// @dev used by the assert() builtin\n uint256 internal constant ASSERT = 0x01;\n /// @dev arithmetic underflow or overflow\n uint256 internal constant UNDER_OVERFLOW = 0x11;\n /// @dev division or modulo by zero\n uint256 internal constant DIVISION_BY_ZERO = 0x12;\n /// @dev enum conversion error\n uint256 internal constant ENUM_CONVERSION_ERROR = 0x21;\n /// @dev invalid encoding in storage\n uint256 internal constant STORAGE_ENCODING_ERROR = 0x22;\n /// @dev empty array pop\n uint256 internal constant EMPTY_ARRAY_POP = 0x31;\n /// @dev array out of bounds access\n uint256 internal constant ARRAY_OUT_OF_BOUNDS = 0x32;\n /// @dev resource error (too large allocation or too large array)\n uint256 internal constant RESOURCE_ERROR = 0x41;\n /// @dev calling invalid internal function\n uint256 internal constant INVALID_INTERNAL_FUNCTION = 0x51;\n\n /// @dev Reverts with a panic code. Recommended to use with\n /// the internal constants with predefined codes.\n function panic(uint256 code) internal pure {\n assembly (\"memory-safe\") {\n mstore(0x00, 0x4e487b71)\n mstore(0x20, code)\n revert(0x1c, 0x24)\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/SlotDerivation.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/SlotDerivation.sol)\n// This file was procedurally generated from scripts/generate/templates/SlotDerivation.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Library for computing storage (and transient storage) locations from namespaces and deriving slots\n * corresponding to standard patterns. The derivation method for array and mapping matches the storage layout used by\n * the solidity language / compiler.\n *\n * See https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays[Solidity docs for mappings and dynamic arrays.].\n *\n * Example usage:\n * ```solidity\n * contract Example {\n * // Add the library methods\n * using StorageSlot for bytes32;\n * using SlotDerivation for bytes32;\n *\n * // Declare a namespace\n * string private constant _NAMESPACE = \"\"; // eg. OpenZeppelin.Slot\n *\n * function setValueInNamespace(uint256 key, address newValue) internal {\n * _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value = newValue;\n * }\n *\n * function getValueInNamespace(uint256 key) internal view returns (address) {\n * return _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value;\n * }\n * }\n * ```\n *\n * TIP: Consider using this library along with {StorageSlot}.\n *\n * NOTE: This library provides a way to manipulate storage locations in a non-standard way. Tooling for checking\n * upgrade safety will ignore the slots accessed through this library.\n *\n * _Available since v5.1._\n */\nlibrary SlotDerivation {\n /**\n * @dev Derive an ERC-7201 slot from a string (namespace).\n */\n function erc7201Slot(string memory namespace) internal pure returns (bytes32 slot) {\n assembly (\"memory-safe\") {\n mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1))\n slot := and(keccak256(0x00, 0x20), not(0xff))\n }\n }\n\n /**\n * @dev Add an offset to a slot to get the n-th element of a structure or an array.\n */\n function offset(bytes32 slot, uint256 pos) internal pure returns (bytes32 result) {\n unchecked {\n return bytes32(uint256(slot) + pos);\n }\n }\n\n /**\n * @dev Derive the location of the first element in an array from the slot where the length is stored.\n */\n function deriveArray(bytes32 slot) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, slot)\n result := keccak256(0x00, 0x20)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, address key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, and(key, shr(96, not(0))))\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, bool key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, iszero(iszero(key)))\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, bytes32 key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, key)\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, uint256 key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, key)\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, int256 key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, key)\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, string memory key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n let length := mload(key)\n let begin := add(key, 0x20)\n let end := add(begin, length)\n let cache := mload(end)\n mstore(end, slot)\n result := keccak256(begin, add(length, 0x20))\n mstore(end, cache)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, bytes memory key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n let length := mload(key)\n let begin := add(key, 0x20)\n let end := add(begin, length)\n let cache := mload(end)\n mstore(end, slot)\n result := keccak256(begin, add(length, 0x20))\n mstore(end, cache)\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/StorageSlot.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/StorageSlot.sol)\n// This file was procedurally generated from scripts/generate/templates/StorageSlot.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Library for reading and writing primitive types to specific storage slots.\n *\n * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts.\n * This library helps with reading and writing to such slots without the need for inline assembly.\n *\n * The functions in this library return Slot structs that contain a `value` member that can be used to read or write.\n *\n * Example usage to set ERC-1967 implementation slot:\n * ```solidity\n * contract ERC1967 {\n * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot.\n * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;\n *\n * function _getImplementation() internal view returns (address) {\n * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;\n * }\n *\n * function _setImplementation(address newImplementation) internal {\n * require(newImplementation.code.length > 0);\n * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;\n * }\n * }\n * ```\n *\n * TIP: Consider using this library along with {SlotDerivation}.\n */\nlibrary StorageSlot {\n struct AddressSlot {\n address value;\n }\n\n struct BooleanSlot {\n bool value;\n }\n\n struct Bytes32Slot {\n bytes32 value;\n }\n\n struct Uint256Slot {\n uint256 value;\n }\n\n struct Int256Slot {\n int256 value;\n }\n\n struct StringSlot {\n string value;\n }\n\n struct BytesSlot {\n bytes value;\n }\n\n /**\n * @dev Returns an `AddressSlot` with member `value` located at `slot`.\n */\n function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `BooleanSlot` with member `value` located at `slot`.\n */\n function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Bytes32Slot` with member `value` located at `slot`.\n */\n function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Uint256Slot` with member `value` located at `slot`.\n */\n function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Int256Slot` with member `value` located at `slot`.\n */\n function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `StringSlot` with member `value` located at `slot`.\n */\n function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns an `StringSlot` representation of the string storage pointer `store`.\n */\n function getStringSlot(string storage store) internal pure returns (StringSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := store.slot\n }\n }\n\n /**\n * @dev Returns a `BytesSlot` with member `value` located at `slot`.\n */\n function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`.\n */\n function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := store.slot\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/introspection/IERC165.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/IERC165.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-165 standard, as defined in the\n * https://eips.ethereum.org/EIPS/eip-165[ERC].\n *\n * Implementers can declare support of contract interfaces, which can then be\n * queried by others ({ERC165Checker}).\n *\n * For an implementation, see {ERC165}.\n */\ninterface IERC165 {\n /**\n * @dev Returns true if this contract implements the interface defined by\n * `interfaceId`. See the corresponding\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]\n * to learn more about how these ids are created.\n *\n * This function call must use less than 30 000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/Math.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/math/Math.sol)\n\npragma solidity ^0.8.20;\n\nimport {Panic} from \"../Panic.sol\";\nimport {SafeCast} from \"./SafeCast.sol\";\n\n/**\n * @dev Standard math utilities missing in the Solidity language.\n */\nlibrary Math {\n enum Rounding {\n Floor, // Toward negative infinity\n Ceil, // Toward positive infinity\n Trunc, // Toward zero\n Expand // Away from zero\n }\n\n /**\n * @dev Return the 512-bit addition of two uint256.\n *\n * The result is stored in two 256 variables such that sum = high * 2\u00b2\u2075\u2076 + low.\n */\n function add512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {\n assembly (\"memory-safe\") {\n low := add(a, b)\n high := lt(low, a)\n }\n }\n\n /**\n * @dev Return the 512-bit multiplication of two uint256.\n *\n * The result is stored in two 256 variables such that product = high * 2\u00b2\u2075\u2076 + low.\n */\n function mul512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {\n // 512-bit multiply [high low] = x * y. Compute the product mod 2\u00b2\u2075\u2076 and mod 2\u00b2\u2075\u2076 - 1, then use\n // the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256\n // variables such that product = high * 2\u00b2\u2075\u2076 + low.\n assembly (\"memory-safe\") {\n let mm := mulmod(a, b, not(0))\n low := mul(a, b)\n high := sub(sub(mm, low), lt(mm, low))\n }\n }\n\n /**\n * @dev Returns the addition of two unsigned integers, with a success flag (no overflow).\n */\n function tryAdd(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a + b;\n success = c >= a;\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the subtraction of two unsigned integers, with a success flag (no overflow).\n */\n function trySub(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a - b;\n success = c <= a;\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the multiplication of two unsigned integers, with a success flag (no overflow).\n */\n function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a * b;\n assembly (\"memory-safe\") {\n // Only true when the multiplication doesn't overflow\n // (c / a == b) || (a == 0)\n success := or(eq(div(c, a), b), iszero(a))\n }\n // equivalent to: success ? c : 0\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the division of two unsigned integers, with a success flag (no division by zero).\n */\n function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n success = b > 0;\n assembly (\"memory-safe\") {\n // The `DIV` opcode returns zero when the denominator is 0.\n result := div(a, b)\n }\n }\n }\n\n /**\n * @dev Returns the remainder of dividing two unsigned integers, with a success flag (no division by zero).\n */\n function tryMod(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n success = b > 0;\n assembly (\"memory-safe\") {\n // The `MOD` opcode returns zero when the denominator is 0.\n result := mod(a, b)\n }\n }\n }\n\n /**\n * @dev Unsigned saturating addition, bounds to `2\u00b2\u2075\u2076 - 1` instead of overflowing.\n */\n function saturatingAdd(uint256 a, uint256 b) internal pure returns (uint256) {\n (bool success, uint256 result) = tryAdd(a, b);\n return ternary(success, result, type(uint256).max);\n }\n\n /**\n * @dev Unsigned saturating subtraction, bounds to zero instead of overflowing.\n */\n function saturatingSub(uint256 a, uint256 b) internal pure returns (uint256) {\n (, uint256 result) = trySub(a, b);\n return result;\n }\n\n /**\n * @dev Unsigned saturating multiplication, bounds to `2\u00b2\u2075\u2076 - 1` instead of overflowing.\n */\n function saturatingMul(uint256 a, uint256 b) internal pure returns (uint256) {\n (bool success, uint256 result) = tryMul(a, b);\n return ternary(success, result, type(uint256).max);\n }\n\n /**\n * @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant.\n *\n * IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone.\n * However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute\n * one branch when needed, making this function more expensive.\n */\n function ternary(bool condition, uint256 a, uint256 b) internal pure returns (uint256) {\n unchecked {\n // branchless ternary works because:\n // b ^ (a ^ b) == a\n // b ^ 0 == b\n return b ^ ((a ^ b) * SafeCast.toUint(condition));\n }\n }\n\n /**\n * @dev Returns the largest of two numbers.\n */\n function max(uint256 a, uint256 b) internal pure returns (uint256) {\n return ternary(a > b, a, b);\n }\n\n /**\n * @dev Returns the smallest of two numbers.\n */\n function min(uint256 a, uint256 b) internal pure returns (uint256) {\n return ternary(a < b, a, b);\n }\n\n /**\n * @dev Returns the average of two numbers. The result is rounded towards\n * zero.\n */\n function average(uint256 a, uint256 b) internal pure returns (uint256) {\n // (a + b) / 2 can overflow.\n return (a & b) + (a ^ b) / 2;\n }\n\n /**\n * @dev Returns the ceiling of the division of two numbers.\n *\n * This differs from standard division with `/` in that it rounds towards infinity instead\n * of rounding towards zero.\n */\n function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {\n if (b == 0) {\n // Guarantee the same behavior as in a regular Solidity division.\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n\n // The following calculation ensures accurate ceiling division without overflow.\n // Since a is non-zero, (a - 1) / b will not overflow.\n // The largest possible result occurs when (a - 1) / b is type(uint256).max,\n // but the largest value we can obtain is type(uint256).max - 1, which happens\n // when a = type(uint256).max and b = 1.\n unchecked {\n return SafeCast.toUint(a > 0) * ((a - 1) / b + 1);\n }\n }\n\n /**\n * @dev Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or\n * denominator == 0.\n *\n * Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by\n * Uniswap Labs also under MIT license.\n */\n function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {\n unchecked {\n (uint256 high, uint256 low) = mul512(x, y);\n\n // Handle non-overflow cases, 256 by 256 division.\n if (high == 0) {\n // Solidity will revert if denominator == 0, unlike the div opcode on its own.\n // The surrounding unchecked block does not change this fact.\n // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.\n return low / denominator;\n }\n\n // Make sure the result is less than 2\u00b2\u2075\u2076. Also prevents denominator == 0.\n if (denominator <= high) {\n Panic.panic(ternary(denominator == 0, Panic.DIVISION_BY_ZERO, Panic.UNDER_OVERFLOW));\n }\n\n ///////////////////////////////////////////////\n // 512 by 256 division.\n ///////////////////////////////////////////////\n\n // Make division exact by subtracting the remainder from [high low].\n uint256 remainder;\n assembly (\"memory-safe\") {\n // Compute remainder using mulmod.\n remainder := mulmod(x, y, denominator)\n\n // Subtract 256 bit number from 512 bit number.\n high := sub(high, gt(remainder, low))\n low := sub(low, remainder)\n }\n\n // Factor powers of two out of denominator and compute largest power of two divisor of denominator.\n // Always >= 1. See https://cs.stackexchange.com/q/138556/92363.\n\n uint256 twos = denominator & (0 - denominator);\n assembly (\"memory-safe\") {\n // Divide denominator by twos.\n denominator := div(denominator, twos)\n\n // Divide [high low] by twos.\n low := div(low, twos)\n\n // Flip twos such that it is 2\u00b2\u2075\u2076 / twos. If twos is zero, then it becomes one.\n twos := add(div(sub(0, twos), twos), 1)\n }\n\n // Shift in bits from high into low.\n low |= high * twos;\n\n // Invert denominator mod 2\u00b2\u2075\u2076. Now that denominator is an odd number, it has an inverse modulo 2\u00b2\u2075\u2076 such\n // that denominator * inv \u2261 1 mod 2\u00b2\u2075\u2076. Compute the inverse by starting with a seed that is correct for\n // four bits. That is, denominator * inv \u2261 1 mod 2\u2074.\n uint256 inverse = (3 * denominator) ^ 2;\n\n // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also\n // works in modular arithmetic, doubling the correct bits in each step.\n inverse *= 2 - denominator * inverse; // inverse mod 2\u2078\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b9\u2076\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b3\u00b2\n inverse *= 2 - denominator * inverse; // inverse mod 2\u2076\u2074\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b9\u00b2\u2078\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b2\u2075\u2076\n\n // Because the division is now exact we can divide by multiplying with the modular inverse of denominator.\n // This will give us the correct result modulo 2\u00b2\u2075\u2076. Since the preconditions guarantee that the outcome is\n // less than 2\u00b2\u2075\u2076, this is the final result. We don't need to compute the high bits of the result and high\n // is no longer required.\n result = low * inverse;\n return result;\n }\n }\n\n /**\n * @dev Calculates x * y / denominator with full precision, following the selected rounding direction.\n */\n function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {\n return mulDiv(x, y, denominator) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0);\n }\n\n /**\n * @dev Calculates floor(x * y >> n) with full precision. Throws if result overflows a uint256.\n */\n function mulShr(uint256 x, uint256 y, uint8 n) internal pure returns (uint256 result) {\n unchecked {\n (uint256 high, uint256 low) = mul512(x, y);\n if (high >= 1 << n) {\n Panic.panic(Panic.UNDER_OVERFLOW);\n }\n return (high << (256 - n)) | (low >> n);\n }\n }\n\n /**\n * @dev Calculates x * y >> n with full precision, following the selected rounding direction.\n */\n function mulShr(uint256 x, uint256 y, uint8 n, Rounding rounding) internal pure returns (uint256) {\n return mulShr(x, y, n) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, 1 << n) > 0);\n }\n\n /**\n * @dev Calculate the modular multiplicative inverse of a number in Z/nZ.\n *\n * If n is a prime, then Z/nZ is a field. In that case all elements are inversible, except 0.\n * If n is not a prime, then Z/nZ is not a field, and some elements might not be inversible.\n *\n * If the input value is not inversible, 0 is returned.\n *\n * NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Fermat's little theorem and get the\n * inverse using `Math.modExp(a, n - 2, n)`. See {invModPrime}.\n */\n function invMod(uint256 a, uint256 n) internal pure returns (uint256) {\n unchecked {\n if (n == 0) return 0;\n\n // The inverse modulo is calculated using the Extended Euclidean Algorithm (iterative version)\n // Used to compute integers x and y such that: ax + ny = gcd(a, n).\n // When the gcd is 1, then the inverse of a modulo n exists and it's x.\n // ax + ny = 1\n // ax = 1 + (-y)n\n // ax \u2261 1 (mod n) # x is the inverse of a modulo n\n\n // If the remainder is 0 the gcd is n right away.\n uint256 remainder = a % n;\n uint256 gcd = n;\n\n // Therefore the initial coefficients are:\n // ax + ny = gcd(a, n) = n\n // 0a + 1n = n\n int256 x = 0;\n int256 y = 1;\n\n while (remainder != 0) {\n uint256 quotient = gcd / remainder;\n\n (gcd, remainder) = (\n // The old remainder is the next gcd to try.\n remainder,\n // Compute the next remainder.\n // Can't overflow given that (a % gcd) * (gcd // (a % gcd)) <= gcd\n // where gcd is at most n (capped to type(uint256).max)\n gcd - remainder * quotient\n );\n\n (x, y) = (\n // Increment the coefficient of a.\n y,\n // Decrement the coefficient of n.\n // Can overflow, but the result is casted to uint256 so that the\n // next value of y is \"wrapped around\" to a value between 0 and n - 1.\n x - y * int256(quotient)\n );\n }\n\n if (gcd != 1) return 0; // No inverse exists.\n return ternary(x < 0, n - uint256(-x), uint256(x)); // Wrap the result if it's negative.\n }\n }\n\n /**\n * @dev Variant of {invMod}. More efficient, but only works if `p` is known to be a prime greater than `2`.\n *\n * From https://en.wikipedia.org/wiki/Fermat%27s_little_theorem[Fermat's little theorem], we know that if p is\n * prime, then `a**(p-1) \u2261 1 mod p`. As a consequence, we have `a * a**(p-2) \u2261 1 mod p`, which means that\n * `a**(p-2)` is the modular multiplicative inverse of a in Fp.\n *\n * NOTE: this function does NOT check that `p` is a prime greater than `2`.\n */\n function invModPrime(uint256 a, uint256 p) internal view returns (uint256) {\n unchecked {\n return Math.modExp(a, p - 2, p);\n }\n }\n\n /**\n * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m)\n *\n * Requirements:\n * - modulus can't be zero\n * - underlying staticcall to precompile must succeed\n *\n * IMPORTANT: The result is only valid if the underlying call succeeds. When using this function, make\n * sure the chain you're using it on supports the precompiled contract for modular exponentiation\n * at address 0x05 as specified in https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise,\n * the underlying function will succeed given the lack of a revert, but the result may be incorrectly\n * interpreted as 0.\n */\n function modExp(uint256 b, uint256 e, uint256 m) internal view returns (uint256) {\n (bool success, uint256 result) = tryModExp(b, e, m);\n if (!success) {\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n return result;\n }\n\n /**\n * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m).\n * It includes a success flag indicating if the operation succeeded. Operation will be marked as failed if trying\n * to operate modulo 0 or if the underlying precompile reverted.\n *\n * IMPORTANT: The result is only valid if the success flag is true. When using this function, make sure the chain\n * you're using it on supports the precompiled contract for modular exponentiation at address 0x05 as specified in\n * https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise, the underlying function will succeed given the lack\n * of a revert, but the result may be incorrectly interpreted as 0.\n */\n function tryModExp(uint256 b, uint256 e, uint256 m) internal view returns (bool success, uint256 result) {\n if (m == 0) return (false, 0);\n assembly (\"memory-safe\") {\n let ptr := mload(0x40)\n // | Offset | Content | Content (Hex) |\n // |-----------|------------|--------------------------------------------------------------------|\n // | 0x00:0x1f | size of b | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x20:0x3f | size of e | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x40:0x5f | size of m | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x60:0x7f | value of b | 0x<.............................................................b> |\n // | 0x80:0x9f | value of e | 0x<.............................................................e> |\n // | 0xa0:0xbf | value of m | 0x<.............................................................m> |\n mstore(ptr, 0x20)\n mstore(add(ptr, 0x20), 0x20)\n mstore(add(ptr, 0x40), 0x20)\n mstore(add(ptr, 0x60), b)\n mstore(add(ptr, 0x80), e)\n mstore(add(ptr, 0xa0), m)\n\n // Given the result < m, it's guaranteed to fit in 32 bytes,\n // so we can use the memory scratch space located at offset 0.\n success := staticcall(gas(), 0x05, ptr, 0xc0, 0x00, 0x20)\n result := mload(0x00)\n }\n }\n\n /**\n * @dev Variant of {modExp} that supports inputs of arbitrary length.\n */\n function modExp(bytes memory b, bytes memory e, bytes memory m) internal view returns (bytes memory) {\n (bool success, bytes memory result) = tryModExp(b, e, m);\n if (!success) {\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n return result;\n }\n\n /**\n * @dev Variant of {tryModExp} that supports inputs of arbitrary length.\n */\n function tryModExp(\n bytes memory b,\n bytes memory e,\n bytes memory m\n ) internal view returns (bool success, bytes memory result) {\n if (_zeroBytes(m)) return (false, new bytes(0));\n\n uint256 mLen = m.length;\n\n // Encode call args in result and move the free memory pointer\n result = abi.encodePacked(b.length, e.length, mLen, b, e, m);\n\n assembly (\"memory-safe\") {\n let dataPtr := add(result, 0x20)\n // Write result on top of args to avoid allocating extra memory.\n success := staticcall(gas(), 0x05, dataPtr, mload(result), dataPtr, mLen)\n // Overwrite the length.\n // result.length > returndatasize() is guaranteed because returndatasize() == m.length\n mstore(result, mLen)\n // Set the memory pointer after the returned data.\n mstore(0x40, add(dataPtr, mLen))\n }\n }\n\n /**\n * @dev Returns whether the provided byte array is zero.\n */\n function _zeroBytes(bytes memory byteArray) private pure returns (bool) {\n for (uint256 i = 0; i < byteArray.length; ++i) {\n if (byteArray[i] != 0) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded\n * towards zero.\n *\n * This method is based on Newton's method for computing square roots; the algorithm is restricted to only\n * using integer operations.\n */\n function sqrt(uint256 a) internal pure returns (uint256) {\n unchecked {\n // Take care of easy edge cases when a == 0 or a == 1\n if (a <= 1) {\n return a;\n }\n\n // In this function, we use Newton's method to get a root of `f(x) := x\u00b2 - a`. It involves building a\n // sequence x_n that converges toward sqrt(a). For each iteration x_n, we also define the error between\n // the current value as `\u03b5_n = | x_n - sqrt(a) |`.\n //\n // For our first estimation, we consider `e` the smallest power of 2 which is bigger than the square root\n // of the target. (i.e. `2**(e-1) \u2264 sqrt(a) < 2**e`). We know that `e \u2264 128` because `(2\u00b9\u00b2\u2078)\u00b2 = 2\u00b2\u2075\u2076` is\n // bigger than any uint256.\n //\n // By noticing that\n // `2**(e-1) \u2264 sqrt(a) < 2**e \u2192 (2**(e-1))\u00b2 \u2264 a < (2**e)\u00b2 \u2192 2**(2*e-2) \u2264 a < 2**(2*e)`\n // we can deduce that `e - 1` is `log2(a) / 2`. We can thus compute `x_n = 2**(e-1)` using a method similar\n // to the msb function.\n uint256 aa = a;\n uint256 xn = 1;\n\n if (aa >= (1 << 128)) {\n aa >>= 128;\n xn <<= 64;\n }\n if (aa >= (1 << 64)) {\n aa >>= 64;\n xn <<= 32;\n }\n if (aa >= (1 << 32)) {\n aa >>= 32;\n xn <<= 16;\n }\n if (aa >= (1 << 16)) {\n aa >>= 16;\n xn <<= 8;\n }\n if (aa >= (1 << 8)) {\n aa >>= 8;\n xn <<= 4;\n }\n if (aa >= (1 << 4)) {\n aa >>= 4;\n xn <<= 2;\n }\n if (aa >= (1 << 2)) {\n xn <<= 1;\n }\n\n // We now have x_n such that `x_n = 2**(e-1) \u2264 sqrt(a) < 2**e = 2 * x_n`. This implies \u03b5_n \u2264 2**(e-1).\n //\n // We can refine our estimation by noticing that the middle of that interval minimizes the error.\n // If we move x_n to equal 2**(e-1) + 2**(e-2), then we reduce the error to \u03b5_n \u2264 2**(e-2).\n // This is going to be our x_0 (and \u03b5_0)\n xn = (3 * xn) >> 1; // \u03b5_0 := | x_0 - sqrt(a) | \u2264 2**(e-2)\n\n // From here, Newton's method give us:\n // x_{n+1} = (x_n + a / x_n) / 2\n //\n // One should note that:\n // x_{n+1}\u00b2 - a = ((x_n + a / x_n) / 2)\u00b2 - a\n // = ((x_n\u00b2 + a) / (2 * x_n))\u00b2 - a\n // = (x_n\u2074 + 2 * a * x_n\u00b2 + a\u00b2) / (4 * x_n\u00b2) - a\n // = (x_n\u2074 + 2 * a * x_n\u00b2 + a\u00b2 - 4 * a * x_n\u00b2) / (4 * x_n\u00b2)\n // = (x_n\u2074 - 2 * a * x_n\u00b2 + a\u00b2) / (4 * x_n\u00b2)\n // = (x_n\u00b2 - a)\u00b2 / (2 * x_n)\u00b2\n // = ((x_n\u00b2 - a) / (2 * x_n))\u00b2\n // \u2265 0\n // Which proves that for all n \u2265 1, sqrt(a) \u2264 x_n\n //\n // This gives us the proof of quadratic convergence of the sequence:\n // \u03b5_{n+1} = | x_{n+1} - sqrt(a) |\n // = | (x_n + a / x_n) / 2 - sqrt(a) |\n // = | (x_n\u00b2 + a - 2*x_n*sqrt(a)) / (2 * x_n) |\n // = | (x_n - sqrt(a))\u00b2 / (2 * x_n) |\n // = | \u03b5_n\u00b2 / (2 * x_n) |\n // = \u03b5_n\u00b2 / | (2 * x_n) |\n //\n // For the first iteration, we have a special case where x_0 is known:\n // \u03b5_1 = \u03b5_0\u00b2 / | (2 * x_0) |\n // \u2264 (2**(e-2))\u00b2 / (2 * (2**(e-1) + 2**(e-2)))\n // \u2264 2**(2*e-4) / (3 * 2**(e-1))\n // \u2264 2**(e-3) / 3\n // \u2264 2**(e-3-log2(3))\n // \u2264 2**(e-4.5)\n //\n // For the following iterations, we use the fact that, 2**(e-1) \u2264 sqrt(a) \u2264 x_n:\n // \u03b5_{n+1} = \u03b5_n\u00b2 / | (2 * x_n) |\n // \u2264 (2**(e-k))\u00b2 / (2 * 2**(e-1))\n // \u2264 2**(2*e-2*k) / 2**e\n // \u2264 2**(e-2*k)\n xn = (xn + a / xn) >> 1; // \u03b5_1 := | x_1 - sqrt(a) | \u2264 2**(e-4.5) -- special case, see above\n xn = (xn + a / xn) >> 1; // \u03b5_2 := | x_2 - sqrt(a) | \u2264 2**(e-9) -- general case with k = 4.5\n xn = (xn + a / xn) >> 1; // \u03b5_3 := | x_3 - sqrt(a) | \u2264 2**(e-18) -- general case with k = 9\n xn = (xn + a / xn) >> 1; // \u03b5_4 := | x_4 - sqrt(a) | \u2264 2**(e-36) -- general case with k = 18\n xn = (xn + a / xn) >> 1; // \u03b5_5 := | x_5 - sqrt(a) | \u2264 2**(e-72) -- general case with k = 36\n xn = (xn + a / xn) >> 1; // \u03b5_6 := | x_6 - sqrt(a) | \u2264 2**(e-144) -- general case with k = 72\n\n // Because e \u2264 128 (as discussed during the first estimation phase), we know have reached a precision\n // \u03b5_6 \u2264 2**(e-144) < 1. Given we're operating on integers, then we can ensure that xn is now either\n // sqrt(a) or sqrt(a) + 1.\n return xn - SafeCast.toUint(xn > a / xn);\n }\n }\n\n /**\n * @dev Calculates sqrt(a), following the selected rounding direction.\n */\n function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = sqrt(a);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && result * result < a);\n }\n }\n\n /**\n * @dev Return the log in base 2 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n */\n function log2(uint256 x) internal pure returns (uint256 r) {\n // If value has upper 128 bits set, log2 result is at least 128\n r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7;\n // If upper 64 bits of 128-bit half set, add 64 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6;\n // If upper 32 bits of 64-bit half set, add 32 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5;\n // If upper 16 bits of 32-bit half set, add 16 to result\n r |= SafeCast.toUint((x >> r) > 0xffff) << 4;\n // If upper 8 bits of 16-bit half set, add 8 to result\n r |= SafeCast.toUint((x >> r) > 0xff) << 3;\n // If upper 4 bits of 8-bit half set, add 4 to result\n r |= SafeCast.toUint((x >> r) > 0xf) << 2;\n\n // Shifts value right by the current result and use it as an index into this lookup table:\n //\n // | x (4 bits) | index | table[index] = MSB position |\n // |------------|---------|-----------------------------|\n // | 0000 | 0 | table[0] = 0 |\n // | 0001 | 1 | table[1] = 0 |\n // | 0010 | 2 | table[2] = 1 |\n // | 0011 | 3 | table[3] = 1 |\n // | 0100 | 4 | table[4] = 2 |\n // | 0101 | 5 | table[5] = 2 |\n // | 0110 | 6 | table[6] = 2 |\n // | 0111 | 7 | table[7] = 2 |\n // | 1000 | 8 | table[8] = 3 |\n // | 1001 | 9 | table[9] = 3 |\n // | 1010 | 10 | table[10] = 3 |\n // | 1011 | 11 | table[11] = 3 |\n // | 1100 | 12 | table[12] = 3 |\n // | 1101 | 13 | table[13] = 3 |\n // | 1110 | 14 | table[14] = 3 |\n // | 1111 | 15 | table[15] = 3 |\n //\n // The lookup table is represented as a 32-byte value with the MSB positions for 0-15 in the last 16 bytes.\n assembly (\"memory-safe\") {\n r := or(r, byte(shr(r, x), 0x0000010102020202030303030303030300000000000000000000000000000000))\n }\n }\n\n /**\n * @dev Return the log in base 2, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log2(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << result < value);\n }\n }\n\n /**\n * @dev Return the log in base 10 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n */\n function log10(uint256 value) internal pure returns (uint256) {\n uint256 result = 0;\n unchecked {\n if (value >= 10 ** 64) {\n value /= 10 ** 64;\n result += 64;\n }\n if (value >= 10 ** 32) {\n value /= 10 ** 32;\n result += 32;\n }\n if (value >= 10 ** 16) {\n value /= 10 ** 16;\n result += 16;\n }\n if (value >= 10 ** 8) {\n value /= 10 ** 8;\n result += 8;\n }\n if (value >= 10 ** 4) {\n value /= 10 ** 4;\n result += 4;\n }\n if (value >= 10 ** 2) {\n value /= 10 ** 2;\n result += 2;\n }\n if (value >= 10 ** 1) {\n result += 1;\n }\n }\n return result;\n }\n\n /**\n * @dev Return the log in base 10, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log10(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 10 ** result < value);\n }\n }\n\n /**\n * @dev Return the log in base 256 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n *\n * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.\n */\n function log256(uint256 x) internal pure returns (uint256 r) {\n // If value has upper 128 bits set, log2 result is at least 128\n r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7;\n // If upper 64 bits of 128-bit half set, add 64 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6;\n // If upper 32 bits of 64-bit half set, add 32 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5;\n // If upper 16 bits of 32-bit half set, add 16 to result\n r |= SafeCast.toUint((x >> r) > 0xffff) << 4;\n // Add 1 if upper 8 bits of 16-bit half set, and divide accumulated result by 8\n return (r >> 3) | SafeCast.toUint((x >> r) > 0xff);\n }\n\n /**\n * @dev Return the log in base 256, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log256(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << (result << 3) < value);\n }\n }\n\n /**\n * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.\n */\n function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {\n return uint8(rounding) % 2 == 1;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/SafeCast.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SafeCast.sol)\n// This file was procedurally generated from scripts/generate/templates/SafeCast.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Wrappers over Solidity's uintXX/intXX/bool casting operators with added overflow\n * checks.\n *\n * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can\n * easily result in undesired exploitation or bugs, since developers usually\n * assume that overflows raise errors. `SafeCast` restores this intuition by\n * reverting the transaction when such an operation overflows.\n *\n * Using this library instead of the unchecked operations eliminates an entire\n * class of bugs, so it's recommended to use it always.\n */\nlibrary SafeCast {\n /**\n * @dev Value doesn't fit in an uint of `bits` size.\n */\n error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);\n\n /**\n * @dev An int value doesn't fit in an uint of `bits` size.\n */\n error SafeCastOverflowedIntToUint(int256 value);\n\n /**\n * @dev Value doesn't fit in an int of `bits` size.\n */\n error SafeCastOverflowedIntDowncast(uint8 bits, int256 value);\n\n /**\n * @dev An uint value doesn't fit in an int of `bits` size.\n */\n error SafeCastOverflowedUintToInt(uint256 value);\n\n /**\n * @dev Returns the downcasted uint248 from uint256, reverting on\n * overflow (when the input is greater than largest uint248).\n *\n * Counterpart to Solidity's `uint248` operator.\n *\n * Requirements:\n *\n * - input must fit into 248 bits\n */\n function toUint248(uint256 value) internal pure returns (uint248) {\n if (value > type(uint248).max) {\n revert SafeCastOverflowedUintDowncast(248, value);\n }\n return uint248(value);\n }\n\n /**\n * @dev Returns the downcasted uint240 from uint256, reverting on\n * overflow (when the input is greater than largest uint240).\n *\n * Counterpart to Solidity's `uint240` operator.\n *\n * Requirements:\n *\n * - input must fit into 240 bits\n */\n function toUint240(uint256 value) internal pure returns (uint240) {\n if (value > type(uint240).max) {\n revert SafeCastOverflowedUintDowncast(240, value);\n }\n return uint240(value);\n }\n\n /**\n * @dev Returns the downcasted uint232 from uint256, reverting on\n * overflow (when the input is greater than largest uint232).\n *\n * Counterpart to Solidity's `uint232` operator.\n *\n * Requirements:\n *\n * - input must fit into 232 bits\n */\n function toUint232(uint256 value) internal pure returns (uint232) {\n if (value > type(uint232).max) {\n revert SafeCastOverflowedUintDowncast(232, value);\n }\n return uint232(value);\n }\n\n /**\n * @dev Returns the downcasted uint224 from uint256, reverting on\n * overflow (when the input is greater than largest uint224).\n *\n * Counterpart to Solidity's `uint224` operator.\n *\n * Requirements:\n *\n * - input must fit into 224 bits\n */\n function toUint224(uint256 value) internal pure returns (uint224) {\n if (value > type(uint224).max) {\n revert SafeCastOverflowedUintDowncast(224, value);\n }\n return uint224(value);\n }\n\n /**\n * @dev Returns the downcasted uint216 from uint256, reverting on\n * overflow (when the input is greater than largest uint216).\n *\n * Counterpart to Solidity's `uint216` operator.\n *\n * Requirements:\n *\n * - input must fit into 216 bits\n */\n function toUint216(uint256 value) internal pure returns (uint216) {\n if (value > type(uint216).max) {\n revert SafeCastOverflowedUintDowncast(216, value);\n }\n return uint216(value);\n }\n\n /**\n * @dev Returns the downcasted uint208 from uint256, reverting on\n * overflow (when the input is greater than largest uint208).\n *\n * Counterpart to Solidity's `uint208` operator.\n *\n * Requirements:\n *\n * - input must fit into 208 bits\n */\n function toUint208(uint256 value) internal pure returns (uint208) {\n if (value > type(uint208).max) {\n revert SafeCastOverflowedUintDowncast(208, value);\n }\n return uint208(value);\n }\n\n /**\n * @dev Returns the downcasted uint200 from uint256, reverting on\n * overflow (when the input is greater than largest uint200).\n *\n * Counterpart to Solidity's `uint200` operator.\n *\n * Requirements:\n *\n * - input must fit into 200 bits\n */\n function toUint200(uint256 value) internal pure returns (uint200) {\n if (value > type(uint200).max) {\n revert SafeCastOverflowedUintDowncast(200, value);\n }\n return uint200(value);\n }\n\n /**\n * @dev Returns the downcasted uint192 from uint256, reverting on\n * overflow (when the input is greater than largest uint192).\n *\n * Counterpart to Solidity's `uint192` operator.\n *\n * Requirements:\n *\n * - input must fit into 192 bits\n */\n function toUint192(uint256 value) internal pure returns (uint192) {\n if (value > type(uint192).max) {\n revert SafeCastOverflowedUintDowncast(192, value);\n }\n return uint192(value);\n }\n\n /**\n * @dev Returns the downcasted uint184 from uint256, reverting on\n * overflow (when the input is greater than largest uint184).\n *\n * Counterpart to Solidity's `uint184` operator.\n *\n * Requirements:\n *\n * - input must fit into 184 bits\n */\n function toUint184(uint256 value) internal pure returns (uint184) {\n if (value > type(uint184).max) {\n revert SafeCastOverflowedUintDowncast(184, value);\n }\n return uint184(value);\n }\n\n /**\n * @dev Returns the downcasted uint176 from uint256, reverting on\n * overflow (when the input is greater than largest uint176).\n *\n * Counterpart to Solidity's `uint176` operator.\n *\n * Requirements:\n *\n * - input must fit into 176 bits\n */\n function toUint176(uint256 value) internal pure returns (uint176) {\n if (value > type(uint176).max) {\n revert SafeCastOverflowedUintDowncast(176, value);\n }\n return uint176(value);\n }\n\n /**\n * @dev Returns the downcasted uint168 from uint256, reverting on\n * overflow (when the input is greater than largest uint168).\n *\n * Counterpart to Solidity's `uint168` operator.\n *\n * Requirements:\n *\n * - input must fit into 168 bits\n */\n function toUint168(uint256 value) internal pure returns (uint168) {\n if (value > type(uint168).max) {\n revert SafeCastOverflowedUintDowncast(168, value);\n }\n return uint168(value);\n }\n\n /**\n * @dev Returns the downcasted uint160 from uint256, reverting on\n * overflow (when the input is greater than largest uint160).\n *\n * Counterpart to Solidity's `uint160` operator.\n *\n * Requirements:\n *\n * - input must fit into 160 bits\n */\n function toUint160(uint256 value) internal pure returns (uint160) {\n if (value > type(uint160).max) {\n revert SafeCastOverflowedUintDowncast(160, value);\n }\n return uint160(value);\n }\n\n /**\n * @dev Returns the downcasted uint152 from uint256, reverting on\n * overflow (when the input is greater than largest uint152).\n *\n * Counterpart to Solidity's `uint152` operator.\n *\n * Requirements:\n *\n * - input must fit into 152 bits\n */\n function toUint152(uint256 value) internal pure returns (uint152) {\n if (value > type(uint152).max) {\n revert SafeCastOverflowedUintDowncast(152, value);\n }\n return uint152(value);\n }\n\n /**\n * @dev Returns the downcasted uint144 from uint256, reverting on\n * overflow (when the input is greater than largest uint144).\n *\n * Counterpart to Solidity's `uint144` operator.\n *\n * Requirements:\n *\n * - input must fit into 144 bits\n */\n function toUint144(uint256 value) internal pure returns (uint144) {\n if (value > type(uint144).max) {\n revert SafeCastOverflowedUintDowncast(144, value);\n }\n return uint144(value);\n }\n\n /**\n * @dev Returns the downcasted uint136 from uint256, reverting on\n * overflow (when the input is greater than largest uint136).\n *\n * Counterpart to Solidity's `uint136` operator.\n *\n * Requirements:\n *\n * - input must fit into 136 bits\n */\n function toUint136(uint256 value) internal pure returns (uint136) {\n if (value > type(uint136).max) {\n revert SafeCastOverflowedUintDowncast(136, value);\n }\n return uint136(value);\n }\n\n /**\n * @dev Returns the downcasted uint128 from uint256, reverting on\n * overflow (when the input is greater than largest uint128).\n *\n * Counterpart to Solidity's `uint128` operator.\n *\n * Requirements:\n *\n * - input must fit into 128 bits\n */\n function toUint128(uint256 value) internal pure returns (uint128) {\n if (value > type(uint128).max) {\n revert SafeCastOverflowedUintDowncast(128, value);\n }\n return uint128(value);\n }\n\n /**\n * @dev Returns the downcasted uint120 from uint256, reverting on\n * overflow (when the input is greater than largest uint120).\n *\n * Counterpart to Solidity's `uint120` operator.\n *\n * Requirements:\n *\n * - input must fit into 120 bits\n */\n function toUint120(uint256 value) internal pure returns (uint120) {\n if (value > type(uint120).max) {\n revert SafeCastOverflowedUintDowncast(120, value);\n }\n return uint120(value);\n }\n\n /**\n * @dev Returns the downcasted uint112 from uint256, reverting on\n * overflow (when the input is greater than largest uint112).\n *\n * Counterpart to Solidity's `uint112` operator.\n *\n * Requirements:\n *\n * - input must fit into 112 bits\n */\n function toUint112(uint256 value) internal pure returns (uint112) {\n if (value > type(uint112).max) {\n revert SafeCastOverflowedUintDowncast(112, value);\n }\n return uint112(value);\n }\n\n /**\n * @dev Returns the downcasted uint104 from uint256, reverting on\n * overflow (when the input is greater than largest uint104).\n *\n * Counterpart to Solidity's `uint104` operator.\n *\n * Requirements:\n *\n * - input must fit into 104 bits\n */\n function toUint104(uint256 value) internal pure returns (uint104) {\n if (value > type(uint104).max) {\n revert SafeCastOverflowedUintDowncast(104, value);\n }\n return uint104(value);\n }\n\n /**\n * @dev Returns the downcasted uint96 from uint256, reverting on\n * overflow (when the input is greater than largest uint96).\n *\n * Counterpart to Solidity's `uint96` operator.\n *\n * Requirements:\n *\n * - input must fit into 96 bits\n */\n function toUint96(uint256 value) internal pure returns (uint96) {\n if (value > type(uint96).max) {\n revert SafeCastOverflowedUintDowncast(96, value);\n }\n return uint96(value);\n }\n\n /**\n * @dev Returns the downcasted uint88 from uint256, reverting on\n * overflow (when the input is greater than largest uint88).\n *\n * Counterpart to Solidity's `uint88` operator.\n *\n * Requirements:\n *\n * - input must fit into 88 bits\n */\n function toUint88(uint256 value) internal pure returns (uint88) {\n if (value > type(uint88).max) {\n revert SafeCastOverflowedUintDowncast(88, value);\n }\n return uint88(value);\n }\n\n /**\n * @dev Returns the downcasted uint80 from uint256, reverting on\n * overflow (when the input is greater than largest uint80).\n *\n * Counterpart to Solidity's `uint80` operator.\n *\n * Requirements:\n *\n * - input must fit into 80 bits\n */\n function toUint80(uint256 value) internal pure returns (uint80) {\n if (value > type(uint80).max) {\n revert SafeCastOverflowedUintDowncast(80, value);\n }\n return uint80(value);\n }\n\n /**\n * @dev Returns the downcasted uint72 from uint256, reverting on\n * overflow (when the input is greater than largest uint72).\n *\n * Counterpart to Solidity's `uint72` operator.\n *\n * Requirements:\n *\n * - input must fit into 72 bits\n */\n function toUint72(uint256 value) internal pure returns (uint72) {\n if (value > type(uint72).max) {\n revert SafeCastOverflowedUintDowncast(72, value);\n }\n return uint72(value);\n }\n\n /**\n * @dev Returns the downcasted uint64 from uint256, reverting on\n * overflow (when the input is greater than largest uint64).\n *\n * Counterpart to Solidity's `uint64` operator.\n *\n * Requirements:\n *\n * - input must fit into 64 bits\n */\n function toUint64(uint256 value) internal pure returns (uint64) {\n if (value > type(uint64).max) {\n revert SafeCastOverflowedUintDowncast(64, value);\n }\n return uint64(value);\n }\n\n /**\n * @dev Returns the downcasted uint56 from uint256, reverting on\n * overflow (when the input is greater than largest uint56).\n *\n * Counterpart to Solidity's `uint56` operator.\n *\n * Requirements:\n *\n * - input must fit into 56 bits\n */\n function toUint56(uint256 value) internal pure returns (uint56) {\n if (value > type(uint56).max) {\n revert SafeCastOverflowedUintDowncast(56, value);\n }\n return uint56(value);\n }\n\n /**\n * @dev Returns the downcasted uint48 from uint256, reverting on\n * overflow (when the input is greater than largest uint48).\n *\n * Counterpart to Solidity's `uint48` operator.\n *\n * Requirements:\n *\n * - input must fit into 48 bits\n */\n function toUint48(uint256 value) internal pure returns (uint48) {\n if (value > type(uint48).max) {\n revert SafeCastOverflowedUintDowncast(48, value);\n }\n return uint48(value);\n }\n\n /**\n * @dev Returns the downcasted uint40 from uint256, reverting on\n * overflow (when the input is greater than largest uint40).\n *\n * Counterpart to Solidity's `uint40` operator.\n *\n * Requirements:\n *\n * - input must fit into 40 bits\n */\n function toUint40(uint256 value) internal pure returns (uint40) {\n if (value > type(uint40).max) {\n revert SafeCastOverflowedUintDowncast(40, value);\n }\n return uint40(value);\n }\n\n /**\n * @dev Returns the downcasted uint32 from uint256, reverting on\n * overflow (when the input is greater than largest uint32).\n *\n * Counterpart to Solidity's `uint32` operator.\n *\n * Requirements:\n *\n * - input must fit into 32 bits\n */\n function toUint32(uint256 value) internal pure returns (uint32) {\n if (value > type(uint32).max) {\n revert SafeCastOverflowedUintDowncast(32, value);\n }\n return uint32(value);\n }\n\n /**\n * @dev Returns the downcasted uint24 from uint256, reverting on\n * overflow (when the input is greater than largest uint24).\n *\n * Counterpart to Solidity's `uint24` operator.\n *\n * Requirements:\n *\n * - input must fit into 24 bits\n */\n function toUint24(uint256 value) internal pure returns (uint24) {\n if (value > type(uint24).max) {\n revert SafeCastOverflowedUintDowncast(24, value);\n }\n return uint24(value);\n }\n\n /**\n * @dev Returns the downcasted uint16 from uint256, reverting on\n * overflow (when the input is greater than largest uint16).\n *\n * Counterpart to Solidity's `uint16` operator.\n *\n * Requirements:\n *\n * - input must fit into 16 bits\n */\n function toUint16(uint256 value) internal pure returns (uint16) {\n if (value > type(uint16).max) {\n revert SafeCastOverflowedUintDowncast(16, value);\n }\n return uint16(value);\n }\n\n /**\n * @dev Returns the downcasted uint8 from uint256, reverting on\n * overflow (when the input is greater than largest uint8).\n *\n * Counterpart to Solidity's `uint8` operator.\n *\n * Requirements:\n *\n * - input must fit into 8 bits\n */\n function toUint8(uint256 value) internal pure returns (uint8) {\n if (value > type(uint8).max) {\n revert SafeCastOverflowedUintDowncast(8, value);\n }\n return uint8(value);\n }\n\n /**\n * @dev Converts a signed int256 into an unsigned uint256.\n *\n * Requirements:\n *\n * - input must be greater than or equal to 0.\n */\n function toUint256(int256 value) internal pure returns (uint256) {\n if (value < 0) {\n revert SafeCastOverflowedIntToUint(value);\n }\n return uint256(value);\n }\n\n /**\n * @dev Returns the downcasted int248 from int256, reverting on\n * overflow (when the input is less than smallest int248 or\n * greater than largest int248).\n *\n * Counterpart to Solidity's `int248` operator.\n *\n * Requirements:\n *\n * - input must fit into 248 bits\n */\n function toInt248(int256 value) internal pure returns (int248 downcasted) {\n downcasted = int248(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(248, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int240 from int256, reverting on\n * overflow (when the input is less than smallest int240 or\n * greater than largest int240).\n *\n * Counterpart to Solidity's `int240` operator.\n *\n * Requirements:\n *\n * - input must fit into 240 bits\n */\n function toInt240(int256 value) internal pure returns (int240 downcasted) {\n downcasted = int240(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(240, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int232 from int256, reverting on\n * overflow (when the input is less than smallest int232 or\n * greater than largest int232).\n *\n * Counterpart to Solidity's `int232` operator.\n *\n * Requirements:\n *\n * - input must fit into 232 bits\n */\n function toInt232(int256 value) internal pure returns (int232 downcasted) {\n downcasted = int232(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(232, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int224 from int256, reverting on\n * overflow (when the input is less than smallest int224 or\n * greater than largest int224).\n *\n * Counterpart to Solidity's `int224` operator.\n *\n * Requirements:\n *\n * - input must fit into 224 bits\n */\n function toInt224(int256 value) internal pure returns (int224 downcasted) {\n downcasted = int224(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(224, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int216 from int256, reverting on\n * overflow (when the input is less than smallest int216 or\n * greater than largest int216).\n *\n * Counterpart to Solidity's `int216` operator.\n *\n * Requirements:\n *\n * - input must fit into 216 bits\n */\n function toInt216(int256 value) internal pure returns (int216 downcasted) {\n downcasted = int216(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(216, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int208 from int256, reverting on\n * overflow (when the input is less than smallest int208 or\n * greater than largest int208).\n *\n * Counterpart to Solidity's `int208` operator.\n *\n * Requirements:\n *\n * - input must fit into 208 bits\n */\n function toInt208(int256 value) internal pure returns (int208 downcasted) {\n downcasted = int208(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(208, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int200 from int256, reverting on\n * overflow (when the input is less than smallest int200 or\n * greater than largest int200).\n *\n * Counterpart to Solidity's `int200` operator.\n *\n * Requirements:\n *\n * - input must fit into 200 bits\n */\n function toInt200(int256 value) internal pure returns (int200 downcasted) {\n downcasted = int200(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(200, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int192 from int256, reverting on\n * overflow (when the input is less than smallest int192 or\n * greater than largest int192).\n *\n * Counterpart to Solidity's `int192` operator.\n *\n * Requirements:\n *\n * - input must fit into 192 bits\n */\n function toInt192(int256 value) internal pure returns (int192 downcasted) {\n downcasted = int192(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(192, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int184 from int256, reverting on\n * overflow (when the input is less than smallest int184 or\n * greater than largest int184).\n *\n * Counterpart to Solidity's `int184` operator.\n *\n * Requirements:\n *\n * - input must fit into 184 bits\n */\n function toInt184(int256 value) internal pure returns (int184 downcasted) {\n downcasted = int184(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(184, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int176 from int256, reverting on\n * overflow (when the input is less than smallest int176 or\n * greater than largest int176).\n *\n * Counterpart to Solidity's `int176` operator.\n *\n * Requirements:\n *\n * - input must fit into 176 bits\n */\n function toInt176(int256 value) internal pure returns (int176 downcasted) {\n downcasted = int176(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(176, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int168 from int256, reverting on\n * overflow (when the input is less than smallest int168 or\n * greater than largest int168).\n *\n * Counterpart to Solidity's `int168` operator.\n *\n * Requirements:\n *\n * - input must fit into 168 bits\n */\n function toInt168(int256 value) internal pure returns (int168 downcasted) {\n downcasted = int168(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(168, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int160 from int256, reverting on\n * overflow (when the input is less than smallest int160 or\n * greater than largest int160).\n *\n * Counterpart to Solidity's `int160` operator.\n *\n * Requirements:\n *\n * - input must fit into 160 bits\n */\n function toInt160(int256 value) internal pure returns (int160 downcasted) {\n downcasted = int160(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(160, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int152 from int256, reverting on\n * overflow (when the input is less than smallest int152 or\n * greater than largest int152).\n *\n * Counterpart to Solidity's `int152` operator.\n *\n * Requirements:\n *\n * - input must fit into 152 bits\n */\n function toInt152(int256 value) internal pure returns (int152 downcasted) {\n downcasted = int152(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(152, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int144 from int256, reverting on\n * overflow (when the input is less than smallest int144 or\n * greater than largest int144).\n *\n * Counterpart to Solidity's `int144` operator.\n *\n * Requirements:\n *\n * - input must fit into 144 bits\n */\n function toInt144(int256 value) internal pure returns (int144 downcasted) {\n downcasted = int144(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(144, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int136 from int256, reverting on\n * overflow (when the input is less than smallest int136 or\n * greater than largest int136).\n *\n * Counterpart to Solidity's `int136` operator.\n *\n * Requirements:\n *\n * - input must fit into 136 bits\n */\n function toInt136(int256 value) internal pure returns (int136 downcasted) {\n downcasted = int136(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(136, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int128 from int256, reverting on\n * overflow (when the input is less than smallest int128 or\n * greater than largest int128).\n *\n * Counterpart to Solidity's `int128` operator.\n *\n * Requirements:\n *\n * - input must fit into 128 bits\n */\n function toInt128(int256 value) internal pure returns (int128 downcasted) {\n downcasted = int128(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(128, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int120 from int256, reverting on\n * overflow (when the input is less than smallest int120 or\n * greater than largest int120).\n *\n * Counterpart to Solidity's `int120` operator.\n *\n * Requirements:\n *\n * - input must fit into 120 bits\n */\n function toInt120(int256 value) internal pure returns (int120 downcasted) {\n downcasted = int120(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(120, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int112 from int256, reverting on\n * overflow (when the input is less than smallest int112 or\n * greater than largest int112).\n *\n * Counterpart to Solidity's `int112` operator.\n *\n * Requirements:\n *\n * - input must fit into 112 bits\n */\n function toInt112(int256 value) internal pure returns (int112 downcasted) {\n downcasted = int112(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(112, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int104 from int256, reverting on\n * overflow (when the input is less than smallest int104 or\n * greater than largest int104).\n *\n * Counterpart to Solidity's `int104` operator.\n *\n * Requirements:\n *\n * - input must fit into 104 bits\n */\n function toInt104(int256 value) internal pure returns (int104 downcasted) {\n downcasted = int104(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(104, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int96 from int256, reverting on\n * overflow (when the input is less than smallest int96 or\n * greater than largest int96).\n *\n * Counterpart to Solidity's `int96` operator.\n *\n * Requirements:\n *\n * - input must fit into 96 bits\n */\n function toInt96(int256 value) internal pure returns (int96 downcasted) {\n downcasted = int96(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(96, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int88 from int256, reverting on\n * overflow (when the input is less than smallest int88 or\n * greater than largest int88).\n *\n * Counterpart to Solidity's `int88` operator.\n *\n * Requirements:\n *\n * - input must fit into 88 bits\n */\n function toInt88(int256 value) internal pure returns (int88 downcasted) {\n downcasted = int88(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(88, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int80 from int256, reverting on\n * overflow (when the input is less than smallest int80 or\n * greater than largest int80).\n *\n * Counterpart to Solidity's `int80` operator.\n *\n * Requirements:\n *\n * - input must fit into 80 bits\n */\n function toInt80(int256 value) internal pure returns (int80 downcasted) {\n downcasted = int80(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(80, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int72 from int256, reverting on\n * overflow (when the input is less than smallest int72 or\n * greater than largest int72).\n *\n * Counterpart to Solidity's `int72` operator.\n *\n * Requirements:\n *\n * - input must fit into 72 bits\n */\n function toInt72(int256 value) internal pure returns (int72 downcasted) {\n downcasted = int72(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(72, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int64 from int256, reverting on\n * overflow (when the input is less than smallest int64 or\n * greater than largest int64).\n *\n * Counterpart to Solidity's `int64` operator.\n *\n * Requirements:\n *\n * - input must fit into 64 bits\n */\n function toInt64(int256 value) internal pure returns (int64 downcasted) {\n downcasted = int64(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(64, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int56 from int256, reverting on\n * overflow (when the input is less than smallest int56 or\n * greater than largest int56).\n *\n * Counterpart to Solidity's `int56` operator.\n *\n * Requirements:\n *\n * - input must fit into 56 bits\n */\n function toInt56(int256 value) internal pure returns (int56 downcasted) {\n downcasted = int56(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(56, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int48 from int256, reverting on\n * overflow (when the input is less than smallest int48 or\n * greater than largest int48).\n *\n * Counterpart to Solidity's `int48` operator.\n *\n * Requirements:\n *\n * - input must fit into 48 bits\n */\n function toInt48(int256 value) internal pure returns (int48 downcasted) {\n downcasted = int48(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(48, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int40 from int256, reverting on\n * overflow (when the input is less than smallest int40 or\n * greater than largest int40).\n *\n * Counterpart to Solidity's `int40` operator.\n *\n * Requirements:\n *\n * - input must fit into 40 bits\n */\n function toInt40(int256 value) internal pure returns (int40 downcasted) {\n downcasted = int40(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(40, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int32 from int256, reverting on\n * overflow (when the input is less than smallest int32 or\n * greater than largest int32).\n *\n * Counterpart to Solidity's `int32` operator.\n *\n * Requirements:\n *\n * - input must fit into 32 bits\n */\n function toInt32(int256 value) internal pure returns (int32 downcasted) {\n downcasted = int32(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(32, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int24 from int256, reverting on\n * overflow (when the input is less than smallest int24 or\n * greater than largest int24).\n *\n * Counterpart to Solidity's `int24` operator.\n *\n * Requirements:\n *\n * - input must fit into 24 bits\n */\n function toInt24(int256 value) internal pure returns (int24 downcasted) {\n downcasted = int24(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(24, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int16 from int256, reverting on\n * overflow (when the input is less than smallest int16 or\n * greater than largest int16).\n *\n * Counterpart to Solidity's `int16` operator.\n *\n * Requirements:\n *\n * - input must fit into 16 bits\n */\n function toInt16(int256 value) internal pure returns (int16 downcasted) {\n downcasted = int16(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(16, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int8 from int256, reverting on\n * overflow (when the input is less than smallest int8 or\n * greater than largest int8).\n *\n * Counterpart to Solidity's `int8` operator.\n *\n * Requirements:\n *\n * - input must fit into 8 bits\n */\n function toInt8(int256 value) internal pure returns (int8 downcasted) {\n downcasted = int8(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(8, value);\n }\n }\n\n /**\n * @dev Converts an unsigned uint256 into a signed int256.\n *\n * Requirements:\n *\n * - input must be less than or equal to maxInt256.\n */\n function toInt256(uint256 value) internal pure returns (int256) {\n // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive\n if (value > uint256(type(int256).max)) {\n revert SafeCastOverflowedUintToInt(value);\n }\n return int256(value);\n }\n\n /**\n * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump.\n */\n function toUint(bool b) internal pure returns (uint256 u) {\n assembly (\"memory-safe\") {\n u := iszero(iszero(b))\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/structs/EnumerableSet.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/structs/EnumerableSet.sol)\n// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.\n\npragma solidity ^0.8.20;\n\nimport {Arrays} from \"../Arrays.sol\";\n\n/**\n * @dev Library for managing\n * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive\n * types.\n *\n * Sets have the following properties:\n *\n * - Elements are added, removed, and checked for existence in constant time\n * (O(1)).\n * - Elements are enumerated in O(n). No guarantees are made on the ordering.\n * - Set can be cleared (all elements removed) in O(n).\n *\n * ```solidity\n * contract Example {\n * // Add the library methods\n * using EnumerableSet for EnumerableSet.AddressSet;\n *\n * // Declare a set state variable\n * EnumerableSet.AddressSet private mySet;\n * }\n * ```\n *\n * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)\n * and `uint256` (`UintSet`) are supported.\n *\n * [WARNING]\n * ====\n * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure\n * unusable.\n * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.\n *\n * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an\n * array of EnumerableSet.\n * ====\n */\nlibrary EnumerableSet {\n // To implement this library for multiple types with as little code\n // repetition as possible, we write it in terms of a generic Set type with\n // bytes32 values.\n // The Set implementation uses private functions, and user-facing\n // implementations (such as AddressSet) are just wrappers around the\n // underlying Set.\n // This means that we can only create new EnumerableSets for types that fit\n // in bytes32.\n\n struct Set {\n // Storage of set values\n bytes32[] _values;\n // Position is the index of the value in the `values` array plus 1.\n // Position 0 is used to mean a value is not in the set.\n mapping(bytes32 value => uint256) _positions;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function _add(Set storage set, bytes32 value) private returns (bool) {\n if (!_contains(set, value)) {\n set._values.push(value);\n // The value is stored at length-1, but we add 1 to all indexes\n // and use 0 as a sentinel value\n set._positions[value] = set._values.length;\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function _remove(Set storage set, bytes32 value) private returns (bool) {\n // We cache the value's position to prevent multiple reads from the same storage slot\n uint256 position = set._positions[value];\n\n if (position != 0) {\n // Equivalent to contains(set, value)\n // To delete an element from the _values array in O(1), we swap the element to delete with the last one in\n // the array, and then remove the last element (sometimes called as 'swap and pop').\n // This modifies the order of the array, as noted in {at}.\n\n uint256 valueIndex = position - 1;\n uint256 lastIndex = set._values.length - 1;\n\n if (valueIndex != lastIndex) {\n bytes32 lastValue = set._values[lastIndex];\n\n // Move the lastValue to the index where the value to delete is\n set._values[valueIndex] = lastValue;\n // Update the tracked position of the lastValue (that was just moved)\n set._positions[lastValue] = position;\n }\n\n // Delete the slot where the moved value was stored\n set._values.pop();\n\n // Delete the tracked position for the deleted slot\n delete set._positions[value];\n\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function _clear(Set storage set) private {\n uint256 len = _length(set);\n for (uint256 i = 0; i < len; ++i) {\n delete set._positions[set._values[i]];\n }\n Arrays.unsafeSetLength(set._values, 0);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function _contains(Set storage set, bytes32 value) private view returns (bool) {\n return set._positions[value] != 0;\n }\n\n /**\n * @dev Returns the number of values on the set. O(1).\n */\n function _length(Set storage set) private view returns (uint256) {\n return set._values.length;\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function _at(Set storage set, uint256 index) private view returns (bytes32) {\n return set._values[index];\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function _values(Set storage set) private view returns (bytes32[] memory) {\n return set._values;\n }\n\n // Bytes32Set\n\n struct Bytes32Set {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _add(set._inner, value);\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _remove(set._inner, value);\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function clear(Bytes32Set storage set) internal {\n _clear(set._inner);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {\n return _contains(set._inner, value);\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(Bytes32Set storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {\n return _at(set._inner, index);\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {\n bytes32[] memory store = _values(set._inner);\n bytes32[] memory result;\n\n assembly (\"memory-safe\") {\n result := store\n }\n\n return result;\n }\n\n // AddressSet\n\n struct AddressSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(AddressSet storage set, address value) internal returns (bool) {\n return _add(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(AddressSet storage set, address value) internal returns (bool) {\n return _remove(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function clear(AddressSet storage set) internal {\n _clear(set._inner);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(AddressSet storage set, address value) internal view returns (bool) {\n return _contains(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(AddressSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(AddressSet storage set, uint256 index) internal view returns (address) {\n return address(uint160(uint256(_at(set._inner, index))));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(AddressSet storage set) internal view returns (address[] memory) {\n bytes32[] memory store = _values(set._inner);\n address[] memory result;\n\n assembly (\"memory-safe\") {\n result := store\n }\n\n return result;\n }\n\n // UintSet\n\n struct UintSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(UintSet storage set, uint256 value) internal returns (bool) {\n return _add(set._inner, bytes32(value));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(UintSet storage set, uint256 value) internal returns (bool) {\n return _remove(set._inner, bytes32(value));\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function clear(UintSet storage set) internal {\n _clear(set._inner);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(UintSet storage set, uint256 value) internal view returns (bool) {\n return _contains(set._inner, bytes32(value));\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(UintSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(UintSet storage set, uint256 index) internal view returns (uint256) {\n return uint256(_at(set._inner, index));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(UintSet storage set) internal view returns (uint256[] memory) {\n bytes32[] memory store = _values(set._inner);\n uint256[] memory result;\n\n assembly (\"memory-safe\") {\n result := store\n }\n\n return result;\n }\n}\n" + } + }, + "settings": { + "evmVersion": "paris", + "libraries": {}, + "metadata": { "appendCBOR": true, "bytecodeHash": "none", "useLiteralContent": false }, + "optimizer": { "enabled": true, "runs": 50000 }, + "outputSelection": { + "contracts/interfaces/IAdvancedPoolHooks.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/IPoolV2.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/libraries/CCVConfigValidation.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/libraries/Pool.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/pools/AdvancedPoolHooks.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/ace/packages/policy-management/src/interfaces/IPolicyEngine.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/AuthorizedCallers.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2Step.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/IOwnable.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-4.8.3/utils/structs/EnumerableSet.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Arrays.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Comparators.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Panic.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/SlotDerivation.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/StorageSlot.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/introspection/IERC165.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/Math.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/SafeCast.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/structs/EnumerableSet.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + } + }, + "remappings": [ + "forge-std/=node_modules/@chainlink/contracts/src/v0.8/vendor/forge-std/src/", + "@chainlink/policy-management/=node_modules/@chainlink/ace/packages/policy-management/src/", + "@chainlink/contracts/=node_modules/@chainlink/contracts/", + "@openzeppelin/contracts@4.8.3/=node_modules/@openzeppelin/contracts-4.8.3/", + "@openzeppelin/contracts@5.3.0/=node_modules/@openzeppelin/contracts-5.3.0/" + ], + "viaIR": true + } +} diff --git a/ccip-sdk/src/verify/fixtures/BurnMintTokenPool.abi.json b/ccip-sdk/src/verify/fixtures/BurnMintTokenPool.abi.json new file mode 100644 index 00000000..0cf4eb35 --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/BurnMintTokenPool.abi.json @@ -0,0 +1,2055 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "contract IBurnMintERC20" + }, + { + "name": "localTokenDecimals", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "advancedPoolHooks", + "type": "address", + "internalType": "address" + }, + { + "name": "rmnProxy", + "type": "address", + "internalType": "address" + }, + { + "name": "router", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addRemotePool", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "applyChainUpdates", + "inputs": [ + { + "name": "remoteChainSelectorsToRemove", + "type": "uint64[]", + "internalType": "uint64[]" + }, + { + "name": "chainsToAdd", + "type": "tuple[]", + "internalType": "struct TokenPool.ChainUpdate[]", + "components": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddresses", + "type": "bytes[]", + "internalType": "bytes[]" + }, + { + "name": "remoteTokenAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "outboundRateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "applyTokenTransferFeeConfigUpdates", + "inputs": [ + { + "name": "tokenTransferFeeConfigArgs", + "type": "tuple[]", + "internalType": "struct TokenPool.TokenTransferFeeConfigArgs[]", + "components": [ + { + "name": "destChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "tokenTransferFeeConfig", + "type": "tuple", + "internalType": "struct IPoolV2.TokenTransferFeeConfig", + "components": [ + { + "name": "destGasOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destBytesOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "fastFinalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "fastFinalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + } + ] + } + ] + }, + { + "name": "disableTokenTransferFeeConfigs", + "type": "uint64[]", + "internalType": "uint64[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getAdvancedPoolHooks", + "inputs": [], + "outputs": [ + { + "name": "advancedPoolHook", + "type": "address", + "internalType": "contract IAdvancedPoolHooks" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAllowedFinalityConfig", + "inputs": [], + "outputs": [ + { + "name": "allowedFinality", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getCurrentRateLimiterState", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "fastFinality", + "type": "bool", + "internalType": "bool" + } + ], + "outputs": [ + { + "name": "outboundRateLimiterState", + "type": "tuple", + "internalType": "struct RateLimiter.TokenBucket", + "components": [ + { + "name": "tokens", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "lastUpdated", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterState", + "type": "tuple", + "internalType": "struct RateLimiter.TokenBucket", + "components": [ + { + "name": "tokens", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "lastUpdated", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getDynamicConfig", + "inputs": [], + "outputs": [ + { + "name": "router", + "type": "address", + "internalType": "address" + }, + { + "name": "rateLimitAdmin", + "type": "address", + "internalType": "address" + }, + { + "name": "feeAdmin", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getFee", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "destChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "requestedFinalityConfig", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "feeUSDCents", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "destGasOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destBytesOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "tokenFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRemotePools", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes[]", + "internalType": "bytes[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRemoteToken", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRequiredCCVs", + "inputs": [ + { + "name": "localToken", + "type": "address", + "internalType": "address" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "sourceDenominatedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "requestedFinalityConfig", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "direction", + "type": "uint8", + "internalType": "enum IPoolV2.MessageDirection" + } + ], + "outputs": [ + { + "name": "requiredCCVs", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRmnProxy", + "inputs": [], + "outputs": [ + { + "name": "rmnProxy", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getSupportedChains", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint64[]", + "internalType": "uint64[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getToken", + "inputs": [], + "outputs": [ + { + "name": "token", + "type": "address", + "internalType": "contract IERC20" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTokenDecimals", + "inputs": [], + "outputs": [ + { + "name": "decimals", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTokenTransferFeeConfig", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "destChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "feeConfig", + "type": "tuple", + "internalType": "struct IPoolV2.TokenTransferFeeConfig", + "components": [ + { + "name": "destGasOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destBytesOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "fastFinalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "fastFinalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isRemotePool", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isSupportedChain", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isSupportedToken", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "lockOrBurn", + "inputs": [ + { + "name": "lockOrBurnIn", + "type": "tuple", + "internalType": "struct Pool.LockOrBurnInV1", + "components": [ + { + "name": "receiver", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "originalSender", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + } + ] + } + ], + "outputs": [ + { + "name": "lockOrBurnOutV1", + "type": "tuple", + "internalType": "struct Pool.LockOrBurnOutV1", + "components": [ + { + "name": "destTokenAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "destPoolData", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "lockOrBurn", + "inputs": [ + { + "name": "lockOrBurnIn", + "type": "tuple", + "internalType": "struct Pool.LockOrBurnInV1", + "components": [ + { + "name": "receiver", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "originalSender", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + } + ] + }, + { + "name": "requestedFinalityConfig", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "tokenArgs", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct Pool.LockOrBurnOutV1", + "components": [ + { + "name": "destTokenAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "destPoolData", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "destTokenAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "releaseOrMint", + "inputs": [ + { + "name": "releaseOrMintIn", + "type": "tuple", + "internalType": "struct Pool.ReleaseOrMintInV1", + "components": [ + { + "name": "originalSender", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "sourceDenominatedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + }, + { + "name": "sourcePoolAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "sourcePoolData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "offchainTokenData", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "requestedFinalityConfig", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct Pool.ReleaseOrMintOutV1", + "components": [ + { + "name": "destinationAmount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "releaseOrMint", + "inputs": [ + { + "name": "releaseOrMintIn", + "type": "tuple", + "internalType": "struct Pool.ReleaseOrMintInV1", + "components": [ + { + "name": "originalSender", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "sourceDenominatedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + }, + { + "name": "sourcePoolAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "sourcePoolData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "offchainTokenData", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct Pool.ReleaseOrMintOutV1", + "components": [ + { + "name": "destinationAmount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeRemotePool", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setAllowedFinalityConfig", + "inputs": [ + { + "name": "allowedFinality", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setDynamicConfig", + "inputs": [ + { + "name": "router", + "type": "address", + "internalType": "address" + }, + { + "name": "rateLimitAdmin", + "type": "address", + "internalType": "address" + }, + { + "name": "feeAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setRateLimitConfig", + "inputs": [ + { + "name": "rateLimitConfigArgs", + "type": "tuple[]", + "internalType": "struct TokenPool.RateLimitConfigArgs[]", + "components": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "fastFinality", + "type": "bool", + "internalType": "bool" + }, + { + "name": "outboundRateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "typeAndVersion", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "updateAdvancedPoolHooks", + "inputs": [ + { + "name": "newHook", + "type": "address", + "internalType": "contract IAdvancedPoolHooks" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "withdrawFeeTokens", + "inputs": [ + { + "name": "feeTokens", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "AdvancedPoolHooksUpdated", + "inputs": [ + { + "name": "oldHook", + "type": "address", + "indexed": false, + "internalType": "contract IAdvancedPoolHooks" + }, + { + "name": "newHook", + "type": "address", + "indexed": false, + "internalType": "contract IAdvancedPoolHooks" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ChainAdded", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + }, + { + "name": "remoteToken", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "outboundRateLimiterConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ChainRemoved", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DynamicConfigSet", + "inputs": [ + { + "name": "router", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "rateLimitAdmin", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "feeAdmin", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FastFinalityInboundRateLimitConsumed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FastFinalityOutboundRateLimitConsumed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FeeTokenWithdrawn", + "inputs": [ + { + "name": "receiver", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "feeToken", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FinalityConfigSet", + "inputs": [ + { + "name": "allowedFinality", + "type": "bytes4", + "indexed": false, + "internalType": "bytes4" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "InboundRateLimitConsumed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "LockedOrBurned", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OutboundRateLimitConsumed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferRequested", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RateLimitConfigured", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "fastFinality", + "type": "bool", + "indexed": false, + "internalType": "bool" + }, + { + "name": "outboundRateLimiterConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ReleasedOrMinted", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "recipient", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RemotePoolAdded", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RemotePoolRemoved", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "TokenTransferFeeConfigDeleted", + "inputs": [ + { + "name": "destChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "TokenTransferFeeConfigUpdated", + "inputs": [ + { + "name": "destChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "tokenTransferFeeConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct IPoolV2.TokenTransferFeeConfig", + "components": [ + { + "name": "destGasOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destBytesOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "fastFinalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "fastFinalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + } + ] + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "BucketOverfilled", + "inputs": [] + }, + { + "type": "error", + "name": "CallerIsNotARampOnRouter", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "CallerIsNotOwnerOrFeeAdmin", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "CannotTransferToSelf", + "inputs": [] + }, + { + "type": "error", + "name": "ChainAlreadyExists", + "inputs": [ + { + "name": "chainSelector", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "ChainNotAllowed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "CursedByRMN", + "inputs": [] + }, + { + "type": "error", + "name": "DisabledNonZeroRateLimit", + "inputs": [ + { + "name": "config", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ] + }, + { + "type": "error", + "name": "InvalidDecimalArgs", + "inputs": [ + { + "name": "expected", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "actual", + "type": "uint8", + "internalType": "uint8" + } + ] + }, + { + "type": "error", + "name": "InvalidRateLimitRate", + "inputs": [ + { + "name": "rateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ] + }, + { + "type": "error", + "name": "InvalidRemoteChainDecimals", + "inputs": [ + { + "name": "sourcePoolData", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "InvalidRemotePoolForChain", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "InvalidRequestedFinality", + "inputs": [ + { + "name": "requestedFinality", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "allowedFinality", + "type": "bytes4", + "internalType": "bytes4" + } + ] + }, + { + "type": "error", + "name": "InvalidSourcePoolAddress", + "inputs": [ + { + "name": "sourcePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "InvalidToken", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "InvalidTokenTransferFeeConfig", + "inputs": [ + { + "name": "destChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "InvalidTransferFeeBps", + "inputs": [ + { + "name": "bps", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "MustBeProposedOwner", + "inputs": [] + }, + { + "type": "error", + "name": "NonExistentChain", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "OnlyCallableByOwner", + "inputs": [] + }, + { + "type": "error", + "name": "OverflowDetected", + "inputs": [ + { + "name": "remoteDecimals", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "localDecimals", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "remoteAmount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "OwnerCannotBeZero", + "inputs": [] + }, + { + "type": "error", + "name": "PoolAlreadyAdded", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "RequestedFinalityCanOnlyHaveOneMode", + "inputs": [ + { + "name": "encodedFinality", + "type": "bytes4", + "internalType": "bytes4" + } + ] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "TokenMaxCapacityExceeded", + "inputs": [ + { + "name": "capacity", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "tokenAddress", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "TokenRateLimitReached", + "inputs": [ + { + "name": "minWaitInSeconds", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "tokenAddress", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "Unauthorized", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ZeroAddressInvalid", + "inputs": [] + }, + { + "type": "error", + "name": "ZeroAddressNotAllowed", + "inputs": [] + } +] diff --git a/ccip-sdk/src/verify/fixtures/BurnMintTokenPool.standard-input.json b/ccip-sdk/src/verify/fixtures/BurnMintTokenPool.standard-input.json new file mode 100644 index 00000000..a79afb8c --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/BurnMintTokenPool.standard-input.json @@ -0,0 +1,633 @@ +{ + "language": "Solidity", + "sources": { + "contracts/interfaces/IAdvancedPoolHooks.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.24;\n\nimport {Pool} from \"../libraries/Pool.sol\";\nimport {IPoolV2} from \"./IPoolV2.sol\";\n\n/// @notice Interface for AdvancedPoolHooks contract. Implementations may contain no-op logic.\ninterface IAdvancedPoolHooks {\n /// @notice Preflight check before lock or burn operation.\n /// @param lockOrBurnIn The lock or burn input parameters.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Additional token arguments.\n /// @param amountPostFee The amount after token pool bps-based fees have been deducted.\n /// @dev This function may revert if the preflight check fails. This means the transaction is rolled back on source.\n function preflightCheck(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs,\n uint256 amountPostFee\n ) external;\n\n /// @notice Postflight check before releasing or minting tokens.\n /// @param releaseOrMintIn The release or mint output parameters.\n /// @param localAmount The local amount to be released or minted.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @dev This function may revert if the postflight check fails. This means the transaction is unexecutable until\n /// the issue is resolved.\n function postflightCheck(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n uint256 localAmount,\n bytes4 requestedFinalityConfig\n ) external;\n\n /// @notice Returns the set of required CCVs for transfers in a specific direction.\n /// @param localToken The address of the local token.\n /// @param remoteChainSelector The remote chain selector for this transfer.\n /// @param amount The amount being transferred.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param extraData Direction-specific payload forwarded by the caller (e.g. token args or source pool data).\n /// @param direction The direction of the transfer (Inbound or Outbound).\n /// @return requiredCCVs Set of required CCV addresses.\n function getRequiredCCVs(\n address localToken,\n uint64 remoteChainSelector,\n uint256 amount,\n bytes4 requestedFinalityConfig,\n bytes calldata extraData,\n IPoolV2.MessageDirection direction\n ) external view returns (address[] memory requiredCCVs);\n}\n" + }, + "contracts/interfaces/IBurnMintERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol\";\n\n/// @notice Minimal ERC20 interface with mint/burn extensions used across CCIP.\n/// @dev Mirrors the Chainlink `IBurnMintERC20` interface but targets OpenZeppelin Contracts v5.3.0.\ninterface IBurnMintERC20 is IERC20 {\n /// @notice Mints new tokens for a given address.\n /// @param account The address to mint the new tokens to.\n /// @param amount The number of tokens to be minted.\n /// @dev This function increases the total supply.\n function mint(\n address account,\n uint256 amount\n ) external;\n\n /// @notice Burns tokens from the sender.\n /// @param amount The number of tokens to be burned.\n /// @dev This function decreases the total supply.\n function burn(\n uint256 amount\n ) external;\n\n /// @notice Burns tokens from a given address.\n /// @param account The address to burn tokens from.\n /// @param amount The number of tokens to be burned.\n /// @dev This function decreases the total supply.\n function burn(\n address account,\n uint256 amount\n ) external;\n\n /// @notice Burns tokens from a given address.\n /// @param account The address to burn tokens from.\n /// @param amount The number of tokens to be burned.\n /// @dev This function decreases the total supply.\n function burnFrom(\n address account,\n uint256 amount\n ) external;\n}\n\n" + }, + "contracts/interfaces/IPool.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport {Pool} from \"../libraries/Pool.sol\";\n\nimport {IERC165} from \"@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol\";\n\n/// @notice Shared public interface for multiple V1 pool types.\n/// Each pool type handles a different child token model e.g. lock/unlock, mint/burn.\ninterface IPoolV1 is IERC165 {\n /// @notice Lock tokens into the pool or burn the tokens.\n /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain.\n /// @return lockOrBurnOut Encoded data fields for the processing of tokens on the destination chain.\n function lockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn\n ) external returns (Pool.LockOrBurnOutV1 memory lockOrBurnOut);\n\n /// @notice Releases or mints tokens to the receiver address.\n /// @param releaseOrMintIn All data required to release or mint tokens.\n /// @return releaseOrMintOut The amount of tokens released or minted on the local chain, denominated\n /// in the local token's decimals.\n /// @dev The offRamp asserts that the balanceOf of the receiver has been incremented by exactly the number\n /// of tokens that is returned in ReleaseOrMintOutV1.destinationAmount. If the amounts do not match, the tx reverts.\n function releaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn\n ) external returns (Pool.ReleaseOrMintOutV1 memory);\n\n /// @notice Checks whether a remote chain is supported in the token pool.\n /// @param remoteChainSelector The selector of the remote chain.\n /// @return true if the given chain is a permissioned remote chain.\n function isSupportedChain(\n uint64 remoteChainSelector\n ) external view returns (bool);\n\n /// @notice Returns if the token pool supports the given token.\n /// @param token The address of the token.\n /// @return true if the token is supported by the pool.\n function isSupportedToken(\n address token\n ) external view returns (bool);\n}\n" + }, + "contracts/interfaces/IPoolV1V2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport {IPoolV1} from \"./IPool.sol\";\nimport {IPoolV2} from \"./IPoolV2.sol\";\n\ninterface IPoolV1V2 is IPoolV1, IPoolV2 {}\n" + }, + "contracts/interfaces/IPoolV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport {Pool} from \"../libraries/Pool.sol\";\n\nimport {IERC165} from \"@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol\";\n\n/// @notice Shared public interface for multiple V2 pool types.\n/// Each pool type handles a different child token model e.g. lock/release, mint/burn.\ninterface IPoolV2 is IERC165 {\n struct TokenTransferFeeConfig {\n uint32 destGasOverhead; // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e Gas charged to execute the token transfer on the destination chain.\n uint32 destBytesOverhead; // \u2502 Data availability bytes.\n uint32 finalityFeeUSDCents; // \u2502 Fee to charge for token transfer with default (wait-for-finality) finality, multiples of 0.01 USD.\n uint32 fastFinalityFeeUSDCents; // \u2502 Fee to charge for token transfer with fast finality (FTF), multiples of 0.01 USD.\n // \u2502 The following two fee is deducted from the transferred asset, not added on top.\n uint16 finalityTransferFeeBps; // \u2502 Fee in basis points for default finality transfers [0-10_000].\n uint16 fastFinalityTransferFeeBps; //\u2502 Fee in basis points for custom finality transfers [0-10_000].\n bool isEnabled; // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f Whether this config is enabled.\n }\n\n enum MessageDirection {\n Outbound,\n Inbound\n }\n\n /// @notice Lock tokens into the pool or burn the tokens.\n /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Additional token arguments.\n /// @return lockOrBurnOut Encoded data fields for the processing of tokens on the destination chain.\n /// @return destTokenAmount The amount of tokens that will be set in TokenTransferV1.amount to be released/mint on destination.\n function lockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n ) external returns (Pool.LockOrBurnOutV1 memory lockOrBurnOut, uint256 destTokenAmount);\n\n /// @notice Releases or mints tokens on the destination chain.\n /// @param releaseOrMintIn Encoded data fields for the processing of tokens on the destination chain.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @return releaseOrMintOut Encoded data fields describing the result of the release or mint.\n function releaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n bytes4 requestedFinalityConfig\n ) external returns (Pool.ReleaseOrMintOutV1 memory releaseOrMintOut);\n\n /// @notice Returns the set of required CCVs for transfers in a given direction.\n /// @param localToken The address of the local token.\n /// @param remoteChainSelector The chain selector of the remote chain.\n /// @param sourceAmount The source-denominated amount of tokens to be transferred.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param extraData Direction-specific payload forwarded by the caller (e.g. token args or source pool data).\n /// @param direction Whether CCVs are required for outbound (source -> remote) or inbound (remote -> destination) transfers.\n /// @return requiredCCVs A set of addresses representing the required CCVs.\n function getRequiredCCVs(\n address localToken,\n uint64 remoteChainSelector,\n uint256 sourceAmount,\n bytes4 requestedFinalityConfig,\n bytes calldata extraData,\n MessageDirection direction\n ) external view returns (address[] memory requiredCCVs);\n\n /// @notice Returns the fee overrides for transferring the pool's token to a destination chain.\n /// @param localToken The address of the local token.\n /// @param destChainSelector The chain selector of the destination chain.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Additional token argument from the CCIP message.\n /// @return feeConfig the fee configuration for transferring the token to the destination chain.\n function getTokenTransferFeeConfig(\n address localToken,\n uint64 destChainSelector,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n ) external view returns (TokenTransferFeeConfig memory feeConfig);\n\n /// @notice Returns the pool fee parameters that will apply to a transfer.\n /// @param localToken The local asset being transferred.\n /// @param destChainSelector The destination lane selector.\n /// @param amount The amount of tokens being bridged on this lane.\n /// @param feeToken The token used to pay feeUSDCents.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Opaque token arguments supplied by the caller.\n /// @return feeUSDCents Flat fee charged in USD cents (crumbs) for this transfer.\n /// @return destGasOverhead Destination gas charged for accounting in the cost model.\n /// @return destBytesOverhead Destination calldata size attributed to the transfer.\n /// @return tokenFeeBps Bps charged in token units. Value of zero implies no in-token fee.\n /// @return isEnabled Whether the pool's fee config is enabled. If false, OnRamp should use FeeQuoter defaults.\n function getFee(\n address localToken,\n uint64 destChainSelector,\n uint256 amount,\n address feeToken,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n )\n external\n view\n returns (uint256 feeUSDCents, uint32 destGasOverhead, uint32 destBytesOverhead, uint16 tokenFeeBps, bool isEnabled);\n\n /// @notice Gets the token address on the remote chain.\n /// @param remoteChainSelector Remote chain selector.\n /// @dev To support non-evm chains, this value is encoded into bytes.\n function getRemoteToken(\n uint64 remoteChainSelector\n ) external view returns (bytes memory);\n}\n" + }, + "contracts/interfaces/IRMN.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n/// @notice This interface contains the only RMN-related functions that might be used on-chain by other CCIP contracts.\ninterface IRMN {\n /// @notice A Merkle root tagged with the address of the commit store contract it is destined for.\n struct TaggedRoot {\n address commitStore;\n bytes32 root;\n }\n\n /// @notice Callers MUST NOT cache the return value as a blessed tagged root could become unblessed.\n function isBlessed(\n TaggedRoot calldata taggedRoot\n ) external view returns (bool);\n\n /// @notice Iff there is an active global or legacy curse, this function returns true.\n function isCursed() external view returns (bool);\n\n /// @notice Iff there is an active global curse, or an active curse for `subject`, this function returns true.\n /// @param subject To check whether a particular chain is cursed, set to bytes16(uint128(chainSelector)).\n function isCursed(\n bytes16 subject\n ) external view returns (bool);\n}\n" + }, + "contracts/interfaces/IRouter.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport {Client} from \"../libraries/Client.sol\";\n\ninterface IRouter {\n error OnlyOffRamp();\n\n /// @notice Route the message to its intended receiver contract.\n /// @param message Client.Any2EVMMessage struct.\n /// @param gasForCallExactCheck of params for exec.\n /// @param gasLimit set of params for exec.\n /// @param receiver set of params for exec.\n /// @dev if the receiver is a contracts that signals support for CCIP execution through EIP-165.\n /// the contract is called. If not, only tokens are transferred.\n /// @return success A boolean value indicating whether the ccip message was received without errors.\n /// @return retBytes A bytes array containing return data form CCIP receiver.\n /// @return gasUsed the gas used by the external customer call. Does not include any overhead.\n function routeMessage(\n Client.Any2EVMMessage calldata message,\n uint16 gasForCallExactCheck,\n uint256 gasLimit,\n address receiver\n ) external returns (bool success, bytes memory retBytes, uint256 gasUsed);\n\n /// @notice Returns the configured onRamp for a specific destination chain.\n /// @param destChainSelector The destination chain Id to get the onRamp for.\n /// @return onRampAddress The address of the onRamp.\n function getOnRamp(\n uint64 destChainSelector\n ) external view returns (address onRampAddress);\n\n /// @notice Return true if the given offRamp is a configured offRamp for the given source chain.\n /// @param sourceChainSelector The source chain selector to check.\n /// @param offRamp The address of the offRamp to check.\n function isOffRamp(\n uint64 sourceChainSelector,\n address offRamp\n ) external view returns (bool isOffRamp);\n}\n" + }, + "contracts/libraries/Client.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n// End consumer library.\nlibrary Client {\n struct EVMTokenAmount {\n address token; // token address on the local chain.\n uint256 amount; // Amount of tokens.\n }\n\n struct Any2EVMMessage {\n bytes32 messageId; // MessageId corresponding to ccipSend on source.\n uint64 sourceChainSelector; // Source chain selector.\n bytes sender; // abi.encode(address) on EVM source chains; abi.decode(sender, (address)) to recover.\n bytes data; // payload sent in original message.\n EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation.\n }\n\n // If extraArgs is empty bytes, the default is 200k gas limit.\n struct EVM2AnyMessage {\n bytes receiver; // abi.encode(receiver address) for dest EVM chains.\n bytes data; // Data payload.\n EVMTokenAmount[] tokenAmounts; // Token transfers.\n address feeToken; // Address of feeToken. address(0) means you will send msg.value.\n bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV3).\n }\n\n /// @notice Tag to indicate no execution on the destination chain. Execution will need to be done manually.\n /// @dev Preimage for this tag is: keccak256(\"NO_EXECUTION_TAG\")[:4]\n bytes4 public constant NO_EXECUTION_TAG = 0xeba517d2;\n address public constant NO_EXECUTION_ADDRESS = address(bytes20(NO_EXECUTION_TAG));\n\n // ================================================================\n // \u2502 Legacy \u2502\n // ================================================================\n\n // Tag to indicate only a gas limit. Only usable for EVM as destination chain.\n bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9;\n\n struct EVMExtraArgsV1 {\n uint256 gasLimit;\n }\n\n function _argsToBytes(\n EVMExtraArgsV1 memory extraArgs\n ) internal pure returns (bytes memory bts) {\n return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs);\n }\n\n // Tag to indicate a gas limit (or dest chain equivalent processing units) and Out Of Order Execution. This tag is\n // available for multiple chain families. If there is no chain family specific tag, this is the default available\n // for a chain.\n // Note: not available for Solana or Sui VM based chains.\n bytes4 public constant GENERIC_EXTRA_ARGS_V2_TAG = 0x181dcf10;\n\n /// @param gasLimit: gas limit for the callback on the destination chain.\n /// @param allowOutOfOrderExecution: if true, it indicates that the message can be executed in any order relative to\n /// other messages from the same sender. This value's default varies by chain. On some chains, a particular value is\n /// enforced, meaning if the expected value is not set, the message request will revert.\n /// @dev Fully compatible with the previously existing EVMExtraArgsV2.\n struct GenericExtraArgsV2 {\n uint256 gasLimit;\n bool allowOutOfOrderExecution;\n }\n\n // Extra args tag for chains that use the Sui VM.\n bytes4 public constant SUI_EXTRA_ARGS_V1_TAG = 0x21ea4ca9;\n\n // Extra args tag for chains that use the Solana VM.\n bytes4 public constant SVM_EXTRA_ARGS_V1_TAG = 0x1f3b3aba;\n\n struct SVMExtraArgsV1 {\n uint32 computeUnits;\n uint64 accountIsWritableBitmap;\n bool allowOutOfOrderExecution;\n bytes32 tokenReceiver;\n // Additional accounts needed for execution of CCIP receiver. Must be empty if message.receiver is zero.\n // Token transfer related accounts are specified in the token pool lookup table on SVM.\n bytes32[] accounts;\n }\n\n /// @dev The maximum number of accounts that can be passed in SVMExtraArgs.\n uint256 public constant SVM_EXTRA_ARGS_MAX_ACCOUNTS = 64;\n\n /// @dev The expected static payload size of a token transfer when Borsh encoded and submitted to SVM.\n /// TokenPool extra data and offchain data sizes are dynamic, and should be accounted for separately.\n uint256 public constant SVM_TOKEN_TRANSFER_DATA_OVERHEAD = (4 + 32) // source_pool\n + 32 // token_address\n + 4 // gas_amount\n + 4 // extra_data overhead\n + 32 // amount\n + 32 // size of the token lookup table account\n + 32 // token-related accounts in the lookup table, over-estimated to 32, typically between 11 - 13\n + 32 // token account belonging to the token receiver, e.g ATA, not included in the token lookup table\n + 32 // per-chain token pool config, not included in the token lookup table\n + 32 // per-chain token billing config, not always included in the token lookup table\n + 32; // OffRamp pool signer PDA, not included in the token lookup table\n\n /// @dev Number of overhead accounts needed for message execution on SVM.\n /// @dev These are message.receiver, and the OffRamp Signer PDA specific to the receiver.\n uint256 public constant SVM_MESSAGING_ACCOUNTS_OVERHEAD = 2;\n\n /// @dev The size of each SVM account address in bytes.\n uint256 public constant SVM_ACCOUNT_BYTE_SIZE = 32;\n\n struct SuiExtraArgsV1 {\n uint256 gasLimit;\n bool allowOutOfOrderExecution;\n bytes32 tokenReceiver;\n bytes32[] receiverObjectIds;\n }\n\n /// @dev The expected static payload size of a token transfer when BCS encoded and submitted to SUI.\n /// TokenPool extra data and offchain data sizes are dynamic, and should be accounted for separately.\n uint256 public constant SUI_TOKEN_TRANSFER_DATA_OVERHEAD = (4 + 32) // source_pool, 4 bytes for length, 32 bytes for address\n + 32 // dest_token_address\n + 4 // dest_gas_amount\n + 4 // extra_data length, the contents are calculated separately\n + 32; // amount\n\n /// @dev Number of overhead accounts needed for message execution on SUI.\n /// @dev This is the message.receiver.\n uint256 public constant SUI_MESSAGING_ACCOUNTS_OVERHEAD = 1;\n\n /// @dev The maximum number of receiver object ids that can be passed in SuiExtraArgs.\n uint256 public constant SUI_EXTRA_ARGS_MAX_RECEIVER_OBJECT_IDS = 64;\n\n /// @dev The size of each SUI account address in bytes.\n uint256 public constant SUI_ACCOUNT_BYTE_SIZE = 32;\n\n function _argsToBytes(\n GenericExtraArgsV2 memory extraArgs\n ) internal pure returns (bytes memory bts) {\n return abi.encodeWithSelector(GENERIC_EXTRA_ARGS_V2_TAG, extraArgs);\n }\n\n function _svmArgsToBytes(\n SVMExtraArgsV1 memory extraArgs\n ) internal pure returns (bytes memory bts) {\n return abi.encodeWithSelector(SVM_EXTRA_ARGS_V1_TAG, extraArgs);\n }\n\n function _suiArgsToBytes(\n SuiExtraArgsV1 memory extraArgs\n ) internal pure returns (bytes memory bts) {\n return abi.encodeWithSelector(SUI_EXTRA_ARGS_V1_TAG, extraArgs);\n }\n}\n" + }, + "contracts/libraries/FeeTokenHandler.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {IERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/utils/SafeERC20.sol\";\n\nlibrary FeeTokenHandler {\n using SafeERC20 for IERC20;\n\n error ZeroAddressNotAllowed();\n\n event FeeTokenWithdrawn(address indexed receiver, address indexed feeToken, uint256 amount);\n\n /// @notice Withdraws the outstanding fee token balances to the fee aggregator.\n /// @param feeTokens The fee tokens to withdraw.\n /// @param feeAggregator The address to withdraw the fee tokens to, cannot be the zero address.\n function _withdrawFeeTokens(\n address[] calldata feeTokens,\n address feeAggregator\n ) internal {\n if (feeAggregator == address(0)) revert ZeroAddressNotAllowed();\n\n for (uint256 i = 0; i < feeTokens.length; ++i) {\n IERC20 feeToken = IERC20(feeTokens[i]);\n uint256 feeTokenBalance = feeToken.balanceOf(address(this));\n\n if (feeTokenBalance > 0) {\n feeToken.safeTransfer(feeAggregator, feeTokenBalance);\n\n emit FeeTokenWithdrawn(feeAggregator, address(feeToken), feeTokenBalance);\n }\n }\n }\n}\n" + }, + "contracts/libraries/FinalityCodec.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\n/// @notice This library provides encoding and validation for finality parameters used in cross-chain transfers.\n/// @dev this codec supports all the bit flags, even though some might not be assigned any meaning yet. This is\n/// intentional to allow for future flexibility.\n///\n/// @dev Bit layout of the `bytes4` finality value (32 bits, MSB on the left):\n///\n/// Bit: 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 | 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0\n/// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+\n/// | R | R | R | R | R | R | R | R | R | R | R | R | R | R | R | S | block depth (16 bits) |\n/// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+\n/// \\_______________________________ _____________________________/ \\______________________________ _____________________________/\n/// \\/ \\/\n/// flags (16 bits) depth (16 bits)\n/// max = 65535 (0xFFFF)\n///\n/// S (bit 16) = WAIT_FOR_SAFE_FLAG \u2014 wait for the `safe` tag.\n/// R (bits 17-31) = Reserved for future flags (currently unassigned; accepted on the wire).\n/// Reserved bits may be assigned in the future, read the docs for the latest bit definitions.\n///\n/// Special values:\n/// 0x00000000 WAIT_FOR_FINALITY_FLAG \u2014 wait for full finality (safest, default).\n/// 0x00010000 WAIT_FOR_SAFE_FLAG \u2014 wait for the `safe` head (bit 16 set, no depth).\n/// 0x00000001..0x0000FFFF \u2014 wait for N blocks.\nlibrary FinalityCodec {\n error InvalidRequestedFinality(bytes4 requestedFinality, bytes4 allowedFinality);\n /// @notice Requested finality must be exactly one mode: any of the flag bits or a block depth with no upper flag bits.\n /// It cannot combine a flag with a block depth.\n error RequestedFinalityCanOnlyHaveOneMode(bytes4 encodedFinality);\n\n /// @notice The block depth is stored in the lower 16 bits, leaving the upper 16 bits for flags.\n /// For more security, users should wait for finality instead (bytes4(0)).\n uint256 public constant BLOCK_DEPTH_BITS = 16;\n /// @notice The maximum block depth that can be encoded in the finality params.\n uint16 public constant MAX_BLOCK_DEPTH = type(uint16).max;\n /// @notice The block depth mask to extract the block depth from the finality params.\n bytes4 public constant BLOCK_DEPTH_MASK = bytes4(uint32(MAX_BLOCK_DEPTH));\n\n /// @notice The finality flag for waiting for finality is 0, this is the safest option. Any block depth that's deeper\n /// than finality will fall back to finality, meaning a very deep block depth will not be more secure than finality.\n bytes4 public constant WAIT_FOR_FINALITY_FLAG = bytes4(0);\n /// @notice Signals to wait for the `safe` tag.\n bytes4 public constant WAIT_FOR_SAFE_FLAG = bytes4(uint32(1 << BLOCK_DEPTH_BITS));\n\n /// @notice Helper to encode block depth into the finality params. Returns WAIT_FOR_FINALITY_FLAG if the block depth\n /// is zero.\n /// @param blockDepth The block depth to encode into the finality params.\n /// @return The encoded finality params with the block depth.\n function _encodeBlockDepth(\n uint16 blockDepth\n ) internal pure returns (bytes4) {\n return bytes4(uint32(blockDepth));\n }\n\n /// @notice Helper to encode the `safe` tag plus a block depth into the finality params.\n /// NOTE: this format is only allowed for allowed finality, not requested finality, as requested finality can only\n /// contain a single flag or block depth, but allowed finality can contain multiple.\n /// @param blockDepth The block depth to encode into the finality params.\n /// @return The encoded finality params with the `safe` tag and block depth.\n function _encodeBlockDepthAndSafeFlag(\n uint16 blockDepth\n ) internal pure returns (bytes4) {\n return _encodeBlockDepth(blockDepth) | WAIT_FOR_SAFE_FLAG;\n }\n\n /// @notice Validates requested finality: either `bytes4(0)`, exactly one set bit among the upper flag bits, or a pure\n /// block depth (no flag bits, depth in `1..MAX_BLOCK_DEPTH`). Never a flag combined with a non-zero depth. Unknown\n /// flags are accepted here for wire compatibility; pools/CCVs reject modes they do not implement.\n /// @param encodedFinality The encoded finality params to validate.\n function _validateRequestedFinality(\n bytes4 encodedFinality\n ) internal pure {\n // Waiting for finality is always valid.\n if (encodedFinality == WAIT_FOR_FINALITY_FLAG) {\n return;\n }\n bool hasBlockDepth = encodedFinality & BLOCK_DEPTH_MASK != 0;\n uint256 activeModes = hasBlockDepth ? 1 : 0; // If it has depth, it counts as one active mode.\n\n uint32 flags = uint32(encodedFinality) >> BLOCK_DEPTH_BITS;\n if (flags != 0) {\n for (uint256 i = 0; i < 16; ++i) {\n if ((flags & (1 << i)) != 0) {\n activeModes += 1;\n }\n }\n }\n // There must be exactly one active mode: either a block depth or a single flag. Selecting multiple modes is only\n // allowed for `allowedFinality` set by Pools, CCVs, etc., but not for `requestedFinality` set by senders.\n if (activeModes != 1) {\n revert RequestedFinalityCanOnlyHaveOneMode(encodedFinality);\n }\n }\n\n /// @notice Validates that `requestedFinality` is well-formed and permitted by `allowedFinality`.\n /// @param requestedFinality The requested finality params to check.\n /// @param allowedFinality The allowed finality params to check against.\n function _ensureRequestedFinalityAllowed(\n bytes4 requestedFinality,\n bytes4 allowedFinality\n ) internal pure {\n // Finality is always allowed.\n if (requestedFinality == WAIT_FOR_FINALITY_FLAG) {\n return;\n }\n\n // Validate the structural shape of the requested finality, as it is only allowed to signal one mode.\n _validateRequestedFinality(requestedFinality);\n\n // If any of the flags match, the request is allowed only when it has no depth field (flag-only request).\n if (((requestedFinality >> BLOCK_DEPTH_BITS) & (allowedFinality >> BLOCK_DEPTH_BITS)) != 0) {\n return;\n }\n // Otherwise, it must be block-depth based.\n uint32 requestedBlockDepth = uint32(requestedFinality & BLOCK_DEPTH_MASK);\n uint32 allowedBlockDepth = uint32(allowedFinality & BLOCK_DEPTH_MASK);\n if (allowedBlockDepth == 0 || requestedBlockDepth < allowedBlockDepth) {\n revert InvalidRequestedFinality(requestedFinality, allowedFinality);\n }\n }\n}\n" + }, + "contracts/libraries/Pool.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n/// @notice This library contains various token pool functions to aid constructing the return data.\nlibrary Pool {\n // The tag used to signal support for the pool v1 standard.\n // bytes4(keccak256(\"CCIP_POOL_V1\"))\n bytes4 public constant CCIP_POOL_V1 = 0xaff2afbf;\n\n // The number of bytes in the return data for a pool v1 releaseOrMint call.\n // This should match the size of the ReleaseOrMintOutV1 struct.\n uint16 public constant CCIP_POOL_V1_RET_BYTES = 32;\n\n // The default max number of bytes in the return data for a pool v1 lockOrBurn call.\n // This data can be used to send information to the destination chain token pool. Can be overwritten\n // in the TokenTransferFeeConfig.destBytesOverhead if more data is required.\n uint32 public constant CCIP_LOCK_OR_BURN_V1_RET_BYTES = 32;\n\n struct LockOrBurnInV1 {\n bytes receiver; // The recipient of the tokens on the destination chain. For EVM source chains, this is abi-encoded (32 bytes).\n uint64 remoteChainSelector; // \u2500\u256e The chain ID of the destination chain.\n address originalSender; // \u2500\u2500\u2500\u2500\u2500\u256f The original sender of the tx on the source chain.\n uint256 amount; // The amount of tokens to lock or burn, denominated in the source token's decimals.\n address localToken; // The address on this chain of the token to lock or burn.\n }\n\n struct LockOrBurnOutV1 {\n // The address of the destination token, abi encoded in the case of EVM chains.\n // This value is UNTRUSTED as any pool owner can return whatever value they want.\n bytes destTokenAddress;\n // Optional pool data to be transferred to the destination chain. Be default this is capped at\n // CCIP_LOCK_OR_BURN_V1_RET_BYTES bytes. If more data is required, the TokenTransferFeeConfig.destBytesOverhead\n // has to be set for the specific token.\n bytes destPoolData;\n }\n\n struct ReleaseOrMintInV1 {\n bytes originalSender; // The original sender of the tx on the source chain.\n uint64 remoteChainSelector; // \u2500\u2500\u2500\u256e The chain ID of the source chain.\n address receiver; // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f The recipient of the tokens on the destination chain.\n uint256 sourceDenominatedAmount; // The amount of tokens to release or mint, denominated in the source token's decimals.\n address localToken; // The address on this chain of the token to release or mint.\n /// @dev WARNING: sourcePoolAddress should be checked prior to any processing of funds. Make sure it matches the\n /// expected pool address for the given remoteChainSelector.\n bytes sourcePoolAddress; // The address of the source pool, abi encoded in the case of EVM chains.\n bytes sourcePoolData; // The data received from the source pool to process the release or mint.\n /// @dev WARNING: offchainTokenData is untrusted data.\n bytes offchainTokenData; // The offchain data to process the release or mint.\n }\n\n struct ReleaseOrMintOutV1 {\n // The number of tokens released or minted on the destination chain, denominated in the local token's decimals.\n // This value is expected to be equal to the ReleaseOrMintInV1.amount in the case where the source and destination\n // chain have the same number of decimals.\n uint256 destinationAmount;\n }\n}\n" + }, + "contracts/libraries/RateLimiter.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.4;\n\n/// @notice Implements Token Bucket rate limiting.\n/// @dev uint128 is safe for rate limiter state.\n/// - For USD value rate limiting, it can adequately store USD value in 18 decimals.\n/// - For ERC20 token amount rate limiting, all tokens that will be listed will have at most a supply of uint128.max\n/// tokens, and it will therefore not overflow the bucket. In exceptional scenarios where tokens consumed may be larger\n/// than uint128, e.g. compromised issuer, an enabled RateLimiter will check and revert.\nlibrary RateLimiter {\n error BucketOverfilled();\n error TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress);\n error TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress);\n error InvalidRateLimitRate(Config rateLimiterConfig);\n error DisabledNonZeroRateLimit(Config config);\n\n struct TokenBucket {\n uint128 tokens; // \u2500\u2500\u2500\u2500\u256e Current number of tokens that are in the bucket.\n uint32 lastUpdated; // \u2502 Timestamp in seconds of the last token refill, good for 100+ years.\n bool isEnabled; // \u2500\u2500\u2500\u2500\u256f Indication whether the rate limiting is enabled or not.\n uint128 capacity; // \u2500\u2500\u256e Maximum number of tokens that can be in the bucket.\n uint128 rate; // \u2500\u2500\u2500\u2500\u2500\u2500\u256f Number of tokens per second that the bucket is refilled.\n }\n\n struct Config {\n bool isEnabled; // Indication whether the rate limiting should be enabled.\n uint128 capacity; // \u2500\u2500\u256e Specifies the capacity of the rate limiter.\n uint128 rate; // \u2500\u2500\u2500\u2500\u2500\u256f Specifies the rate of the rate limiter.\n }\n\n /// @notice _consume removes the given tokens from the pool, lowering the rate tokens allowed to be\n /// consumed for subsequent calls.\n /// @param requestTokens The total tokens to be consumed from the bucket.\n /// @param tokenAddress The token to consume capacity for, use 0x0 to indicate aggregate value capacity.\n /// @dev Reverts when requestTokens exceeds bucket capacity or available tokens in the bucket.\n /// @dev emits removal of requestTokens if requestTokens is > 0.\n function _consume(\n TokenBucket storage s_bucket,\n uint256 requestTokens,\n address tokenAddress\n ) internal {\n // If there is no value to remove or rate limiting is turned off, skip this step to reduce gas usage.\n if (!s_bucket.isEnabled || requestTokens == 0) {\n return;\n }\n\n uint256 tokens = s_bucket.tokens;\n uint256 capacity = s_bucket.capacity;\n uint256 timeDiff = block.timestamp - s_bucket.lastUpdated;\n\n if (timeDiff != 0) {\n if (tokens > capacity) revert BucketOverfilled();\n\n // Refill tokens when arriving at a new block time.\n tokens = _calculateRefill(capacity, tokens, timeDiff, s_bucket.rate);\n\n s_bucket.lastUpdated = uint32(block.timestamp);\n }\n\n if (capacity < requestTokens) {\n revert TokenMaxCapacityExceeded(capacity, requestTokens, tokenAddress);\n }\n if (tokens < requestTokens) {\n uint256 rate = s_bucket.rate;\n if (rate == 0) {\n // No tokens will ever be refilled. Check is required to avoid division by zero later.\n revert TokenRateLimitReached(type(uint256).max, tokens, tokenAddress);\n }\n // Wait required until the bucket is refilled enough to accept this value, round up to next higher second.\n // Consume is not guaranteed to succeed after wait time passes if there is competing traffic.\n // This acts as a lower bound of wait time.\n uint256 minWaitInSeconds = ((requestTokens - tokens) + (rate - 1)) / rate;\n\n revert TokenRateLimitReached(minWaitInSeconds, tokens, tokenAddress);\n }\n tokens -= requestTokens;\n\n // Downcast is safe here, as tokens is not larger than capacity.\n s_bucket.tokens = uint128(tokens);\n }\n\n /// @notice Gets the token bucket with its values for the block it was requested at.\n /// @return The token bucket.\n function _currentTokenBucketState(\n TokenBucket memory bucket\n ) internal view returns (TokenBucket memory) {\n // We update the bucket to reflect the status at the exact time of the call. This means we might need to refill a\n // part of the bucket based on the time that has passed since the last update.\n bucket.tokens =\n uint128(_calculateRefill(bucket.capacity, bucket.tokens, block.timestamp - bucket.lastUpdated, bucket.rate));\n bucket.lastUpdated = uint32(block.timestamp);\n return bucket;\n }\n\n /// @notice Sets the rate limited config.\n /// @param s_bucket The token bucket.\n /// @param config The new config.\n function _setTokenBucketConfig(\n TokenBucket storage s_bucket,\n Config memory config\n ) internal {\n if (config.isEnabled) {\n if (config.rate > config.capacity) {\n revert InvalidRateLimitRate(config);\n }\n } else {\n if (config.rate != 0 || config.capacity != 0) {\n revert DisabledNonZeroRateLimit(config);\n }\n }\n\n s_bucket.isEnabled = config.isEnabled;\n s_bucket.tokens = config.capacity;\n s_bucket.capacity = config.capacity;\n s_bucket.rate = config.rate;\n s_bucket.lastUpdated = uint32(block.timestamp);\n }\n\n /// @notice Calculate refilled tokens.\n /// @param capacity bucket capacity.\n /// @param tokens current bucket tokens.\n /// @param timeDiff block time difference since last refill.\n /// @param rate bucket refill rate.\n /// @return the value of tokens after refill.\n function _calculateRefill(\n uint256 capacity,\n uint256 tokens,\n uint256 timeDiff,\n uint256 rate\n ) private pure returns (uint256) {\n return _min(capacity, tokens + timeDiff * rate);\n }\n\n /// @notice Return the smallest of two integers.\n /// @param a first int.\n /// @param b second int.\n /// @return smallest.\n function _min(\n uint256 a,\n uint256 b\n ) internal pure returns (uint256) {\n return a < b ? a : b;\n }\n}\n" + }, + "contracts/pools/BurnMintTokenPool.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.24;\n\nimport {IBurnMintERC20} from \"../interfaces/IBurnMintERC20.sol\";\nimport {ITypeAndVersion} from \"@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol\";\n\nimport {BurnMintTokenPoolAbstract} from \"./BurnMintTokenPoolAbstract.sol\";\nimport {TokenPool} from \"./TokenPool.sol\";\n\n/// @notice This pool mints and burns a 3rd-party token.\n/// @dev Pool whitelisting mode is set in the constructor and cannot be modified later.\n/// It either accepts any address as originalSender, or only accepts whitelisted originalSender.\n/// The only way to change whitelisting mode is to deploy a new pool.\n/// If that is expected, please make sure the token's burner/minter roles are adjustable.\n/// @dev This contract is a variant of BurnMintTokenPool that uses `burn(amount)`.\ncontract BurnMintTokenPool is BurnMintTokenPoolAbstract, ITypeAndVersion {\n function typeAndVersion() external pure virtual override returns (string memory) {\n return \"BurnMintTokenPool 2.0.0\";\n }\n\n constructor(\n IBurnMintERC20 token,\n uint8 localTokenDecimals,\n address advancedPoolHooks,\n address rmnProxy,\n address router\n ) TokenPool(token, localTokenDecimals, advancedPoolHooks, rmnProxy, router) {}\n\n /// @notice Burns tokens held by the pool.\n function _lockOrBurn(\n uint64, // remoteChainSelector\n uint256 amount\n ) internal virtual override {\n IBurnMintERC20(address(i_token)).burn(amount);\n }\n}\n" + }, + "contracts/pools/BurnMintTokenPoolAbstract.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.24;\n\nimport {IBurnMintERC20} from \"../interfaces/IBurnMintERC20.sol\";\n\nimport {TokenPool} from \"./TokenPool.sol\";\n\nabstract contract BurnMintTokenPoolAbstract is TokenPool {\n /// @notice Contains the specific release or mint token logic for a pool.\n /// @dev overriding this method allows us to create pools with different release/mint signatures\n /// without duplicating the underlying logic.\n function _releaseOrMint(\n address receiver,\n uint256 amount,\n uint64 // remoteChainSelector\n ) internal virtual override {\n IBurnMintERC20(address(i_token)).mint(receiver, amount);\n }\n}\n" + }, + "contracts/pools/TokenPool.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.24;\n\nimport {IAdvancedPoolHooks} from \"../interfaces/IAdvancedPoolHooks.sol\";\nimport {IPoolV1} from \"../interfaces/IPool.sol\";\nimport {IPoolV1V2} from \"../interfaces/IPoolV1V2.sol\";\nimport {IPoolV2} from \"../interfaces/IPoolV2.sol\";\nimport {IRMN} from \"../interfaces/IRMN.sol\";\nimport {IRouter} from \"../interfaces/IRouter.sol\";\n\nimport {FeeTokenHandler} from \"../libraries/FeeTokenHandler.sol\";\nimport {FinalityCodec} from \"../libraries/FinalityCodec.sol\";\nimport {Pool} from \"../libraries/Pool.sol\";\nimport {RateLimiter} from \"../libraries/RateLimiter.sol\";\nimport {Ownable2StepMsgSender} from \"@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol\";\n\nimport {IERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol\";\nimport {IERC20Metadata} from \"@openzeppelin/contracts@5.3.0/token/ERC20/extensions/IERC20Metadata.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/utils/SafeERC20.sol\";\nimport {IERC165} from \"@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol\";\nimport {EnumerableSet} from \"@openzeppelin/contracts@5.3.0/utils/structs/EnumerableSet.sol\";\n\n/// @notice Base abstract class with common functions for all token pools.\n/// A token pool serves as isolated place for holding tokens and token specific logic\n/// that may execute as tokens move across the bridge.\n/// @dev This pool supports different decimals on different chains but using this feature could impact the total number\n/// of tokens in circulation. Since all of the tokens are locked/burned on the source, and a rounded amount is\n/// minted/released on the destination, the number of tokens minted/released could be less than the number of tokens\n/// burned/locked. This is because the source chain does not know about the destination token decimals. This is not a\n/// problem if the decimals are the same on both chains.\n///\n/// Example:\n/// Assume there is a token with 6 decimals on chain A and 3 decimals on chain B.\n/// - 1.234567 tokens are burned on chain A.\n/// - 1.234 tokens are minted on chain B.\n/// When sending the 1.234 tokens back to chain A, you will receive 1.234000 tokens on chain A, effectively losing\n/// 0.000567 tokens.\n/// In the case of a burnMint pool on chain A, these funds are burned in the pool on chain A.\n/// In the case of a lockRelease pool on chain A, these funds accumulate in the pool on chain A.\nabstract contract TokenPool is IPoolV1V2, Ownable2StepMsgSender {\n using EnumerableSet for EnumerableSet.Bytes32Set;\n using EnumerableSet for EnumerableSet.UintSet;\n using RateLimiter for RateLimiter.TokenBucket;\n using SafeERC20 for IERC20;\n\n error InvalidTransferFeeBps(uint256 bps);\n error InvalidTokenTransferFeeConfig(uint64 destChainSelector);\n error CallerIsNotARampOnRouter(address caller);\n error ZeroAddressInvalid();\n error NonExistentChain(uint64 remoteChainSelector);\n error ChainNotAllowed(uint64 remoteChainSelector);\n error CursedByRMN();\n error ChainAlreadyExists(uint64 chainSelector);\n error InvalidSourcePoolAddress(bytes sourcePoolAddress);\n error InvalidToken(address token);\n error Unauthorized(address caller);\n error PoolAlreadyAdded(uint64 remoteChainSelector, bytes remotePoolAddress);\n error InvalidRemotePoolForChain(uint64 remoteChainSelector, bytes remotePoolAddress);\n error InvalidRemoteChainDecimals(bytes sourcePoolData);\n error OverflowDetected(uint8 remoteDecimals, uint8 localDecimals, uint256 remoteAmount);\n error InvalidDecimalArgs(uint8 expected, uint8 actual);\n error CallerIsNotOwnerOrFeeAdmin(address caller);\n\n event LockedOrBurned(uint64 indexed remoteChainSelector, address token, address sender, uint256 amount);\n event ReleasedOrMinted(\n uint64 indexed remoteChainSelector, address token, address sender, address recipient, uint256 amount\n );\n event ChainAdded(\n uint64 remoteChainSelector,\n bytes remoteToken,\n RateLimiter.Config outboundRateLimiterConfig,\n RateLimiter.Config inboundRateLimiterConfig\n );\n event ChainRemoved(uint64 remoteChainSelector);\n event RemotePoolAdded(uint64 indexed remoteChainSelector, bytes remotePoolAddress);\n event RemotePoolRemoved(uint64 indexed remoteChainSelector, bytes remotePoolAddress);\n event DynamicConfigSet(address router, address rateLimitAdmin, address feeAdmin);\n event OutboundRateLimitConsumed(uint64 indexed remoteChainSelector, address token, uint256 amount);\n event InboundRateLimitConsumed(uint64 indexed remoteChainSelector, address token, uint256 amount);\n event TokenTransferFeeConfigUpdated(uint64 indexed destChainSelector, TokenTransferFeeConfig tokenTransferFeeConfig);\n event TokenTransferFeeConfigDeleted(uint64 indexed destChainSelector);\n event FastFinalityOutboundRateLimitConsumed(uint64 indexed remoteChainSelector, address token, uint256 amount);\n event FastFinalityInboundRateLimitConsumed(uint64 indexed remoteChainSelector, address token, uint256 amount);\n event RateLimitConfigured(\n uint64 indexed remoteChainSelector,\n bool fastFinality,\n RateLimiter.Config outboundRateLimiterConfig,\n RateLimiter.Config inboundRateLimiterConfig\n );\n event FinalityConfigSet(bytes4 allowedFinality);\n event AdvancedPoolHooksUpdated(IAdvancedPoolHooks oldHook, IAdvancedPoolHooks newHook);\n\n struct ChainUpdate {\n uint64 remoteChainSelector; // Remote chain selector.\n bytes[] remotePoolAddresses; // Address of the remote pool, ABI encoded in the case of a remote EVM chain.\n bytes remoteTokenAddress; // Address of the remote token, ABI encoded in the case of a remote EVM chain.\n RateLimiter.Config outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain.\n RateLimiter.Config inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain.\n }\n\n struct RemoteChainConfig {\n RateLimiter.TokenBucket outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain.\n RateLimiter.TokenBucket inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain.\n bytes remoteTokenAddress; // Address of the remote token, ABI encoded in the case of a remote EVM chain.\n EnumerableSet.Bytes32Set remotePools; // Set of remote pool hashes, ABI encoded in the case of a remote EVM chain.\n }\n\n struct RateLimitConfigArgs {\n uint64 remoteChainSelector; // Remote chain selector.\n bool fastFinality; // Whether the rate limit config is for fast finality transfers.\n RateLimiter.Config outboundRateLimiterConfig; // Outbound rate limiter configuration.\n RateLimiter.Config inboundRateLimiterConfig; // Inbound rate limiter configuration.\n }\n\n /// @dev Struct with args for setting the token transfer fee configurations for a destination chain and a set of tokens.\n struct TokenTransferFeeConfigArgs {\n uint64 destChainSelector; // Destination chain selector.\n TokenTransferFeeConfig tokenTransferFeeConfig; // Token transfer fee configuration.\n }\n\n /// @notice The division factor for bps. This also represents the maximum bps fee.\n uint256 internal constant BPS_DIVIDER = 10_000;\n /// @dev The bridgeable token that is managed by this pool. Pools could support multiple tokens at the same time if\n /// required, but this implementation only supports one token.\n IERC20 internal immutable i_token;\n /// @dev The number of decimals of the token managed by this pool.\n uint8 internal immutable i_tokenDecimals;\n /// @dev The address of the RMN proxy.\n address internal immutable i_rmnProxy;\n\n /// @dev The address of the router.\n IRouter internal s_router;\n /// @dev Allowed finality config for fast finality transfers (see `FinalityCodec`).\n /// FinalityCodec.WAIT_FOR_FINALITY_FLAG means wait for finality.\n bytes4 internal s_allowedFinalityConfig;\n /// @dev Optional advanced pool hooks contract for additional features like allowlists and CCV management.\n IAdvancedPoolHooks internal s_advancedPoolHooks;\n /// @dev Separate buckets provide isolated rate limits for fast finality transfers, as their risk\n /// profiles differ from default transfers. When these are not configured, the default buckets are used for all\n /// transfers regardless of the finality requirements.\n mapping(uint64 remoteChainSelector => RateLimiter.TokenBucket tokenBucketOutbound) internal\n s_fastFinalityOutboundRateLimiterConfig;\n mapping(uint64 remoteChainSelector => RateLimiter.TokenBucket tokenBucketInbound) internal\n s_fastFinalityInboundRateLimiterConfig;\n /// @dev A set of allowed chain selectors. We want the allowlist to be enumerable to\n /// be able to quickly determine (without parsing logs) who can access the pool.\n /// @dev The chain selectors are in uint256 format because of the EnumerableSet implementation.\n EnumerableSet.UintSet internal s_remoteChainSelectors;\n mapping(uint64 remoteChainSelector => RemoteChainConfig) internal s_remoteChainConfigs;\n /// @notice A mapping of hashed pool addresses to their unhashed form. This is used to be able to find the actually\n /// configured pools and not just their hashed versions.\n mapping(bytes32 poolAddressHash => bytes poolAddress) internal s_remotePoolAddresses;\n /// @notice The address of the rate limiter admin.\n /// @dev Can be address(0) if none is configured.\n address internal s_rateLimitAdmin;\n /// @dev Optional token-transfer fee overrides keyed by destination chain selector.\n mapping(uint64 destChainSelector => TokenTransferFeeConfig tokenTransferFeeConfig) internal s_tokenTransferFeeConfig;\n /// @notice The address of the fee admin.\n /// @dev Constructor does not set this value so it is opt in only.\n address internal s_feeAdmin;\n\n constructor(\n IERC20 token,\n uint8 localTokenDecimals,\n address advancedPoolHooks,\n address rmnProxy,\n address router\n ) {\n if (address(token) == address(0) || router == address(0) || rmnProxy == address(0)) {\n revert ZeroAddressInvalid();\n }\n i_token = token;\n i_rmnProxy = rmnProxy;\n\n // In the case the token is also the pool, it won't exist yet so we skip this check.\n if (address(token) != address(this)) {\n try IERC20Metadata(address(token)).decimals() returns (uint8 actualTokenDecimals) {\n if (localTokenDecimals != actualTokenDecimals) {\n revert InvalidDecimalArgs(localTokenDecimals, actualTokenDecimals);\n }\n } catch {\n // The decimals function doesn't exist, which is possible since it's optional in the ERC20 spec. We skip the\n // check and assume the supplied token decimals are correct.\n }\n }\n i_tokenDecimals = localTokenDecimals;\n s_advancedPoolHooks = IAdvancedPoolHooks(advancedPoolHooks);\n\n s_router = IRouter(router);\n }\n\n /// @inheritdoc IPoolV1\n /// @param token The token address to check.\n function isSupportedToken(\n address token\n ) public view virtual returns (bool) {\n return token == address(i_token);\n }\n\n /// @notice Gets the IERC20 token that this pool can lock or burn.\n /// @return token The IERC20 token representation.\n function getToken() public view virtual returns (IERC20 token) {\n return i_token;\n }\n\n /// @notice Get RMN proxy address.\n /// @return rmnProxy Address of RMN proxy.\n function getRmnProxy() public view virtual returns (address rmnProxy) {\n return i_rmnProxy;\n }\n\n /// @notice Gets the pools dynamic configuration.\n function getDynamicConfig() public view virtual returns (address router, address rateLimitAdmin, address feeAdmin) {\n return (address(s_router), s_rateLimitAdmin, s_feeAdmin);\n }\n\n /// @notice Gets the finality config as defined in the FinalityCodec library. This value does NOT 1:1 translate to\n /// a block depth. The finality config contains special flags and should only be encoded/decoded using the\n /// FinalityCodec library. Checks must happen by calling `FinalityCodec._ensureRequestedFinalityAllowed`.\n function getAllowedFinalityConfig() public view virtual returns (bytes4 allowedFinality) {\n return s_allowedFinalityConfig;\n }\n\n /// @notice Gets the advanced pool hook contract address used by this pool.\n function getAdvancedPoolHooks() public view virtual returns (IAdvancedPoolHooks advancedPoolHook) {\n return s_advancedPoolHooks;\n }\n\n /// @notice Sets the dynamic configuration for the pool.\n /// @param router The address of the router contract.\n /// @param rateLimitAdmin The address of the rate limiter admin.\n /// @param feeAdmin An additional address that can withdraw fees from this contract.\n /// @dev FeeTokenHandler will revert if feeAdmin is zero when withdrawing fees.\n /// @dev If only the owner can withdraw fees, set feeAdmin to address(0).\n function setDynamicConfig(\n address router,\n address rateLimitAdmin,\n address feeAdmin\n ) public virtual onlyOwner {\n if (router == address(0)) revert ZeroAddressInvalid();\n s_router = IRouter(router);\n s_rateLimitAdmin = rateLimitAdmin;\n s_feeAdmin = feeAdmin;\n\n emit DynamicConfigSet(router, rateLimitAdmin, feeAdmin);\n }\n\n /// @notice Sets the finality config according to the FinalityCodec library encoding.\n /// @param allowedFinality The finality settings allowed in this pool, according to the FinalityCodec encoding.\n function setAllowedFinalityConfig(\n bytes4 allowedFinality\n ) public virtual onlyOwner {\n // Any bytes4 value is accepted as allowedFinality; the FinalityCodec semantics are enforced when requests are\n // checked against this value via FinalityCodec._ensureRequestedFinalityAllowed.\n s_allowedFinalityConfig = allowedFinality;\n\n emit FinalityConfigSet(allowedFinality);\n }\n\n /// @notice Updates the advanced pool hook.\n /// @param newHook The new advanced pool hooks contract.\n function updateAdvancedPoolHooks(\n IAdvancedPoolHooks newHook\n ) public virtual onlyOwner {\n emit AdvancedPoolHooksUpdated(s_advancedPoolHooks, newHook);\n s_advancedPoolHooks = newHook;\n }\n\n /// @notice Signals which version of the pool interface is supported.\n /// @param interfaceId The interface identifier, as specified in ERC-165.\n function supportsInterface(\n bytes4 interfaceId\n ) public view virtual override returns (bool) {\n return interfaceId == Pool.CCIP_POOL_V1 || interfaceId == type(IPoolV2).interfaceId\n || interfaceId == type(IPoolV1).interfaceId || interfaceId == type(IERC165).interfaceId;\n }\n\n // ================================================================\n // \u2502 Lock or Burn \u2502\n // ================================================================\n\n /// @inheritdoc IPoolV2\n /// @dev The _validateLockOrBurn check is an essential security check.\n /// @dev The _getFee function deducts the fee from the amount and returns the amount after fee deduction.\n /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain.\n /// @param requestedFinalityConfig Requested finality config according to the FinalityCodec.\n /// @param tokenArgs Additional token arguments.\n function lockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n ) public virtual returns (Pool.LockOrBurnOutV1 memory, uint256 destTokenAmount) {\n uint256 feeAmount = _getFee(lockOrBurnIn, requestedFinalityConfig);\n _validateLockOrBurn(lockOrBurnIn, requestedFinalityConfig, tokenArgs, feeAmount);\n destTokenAmount = lockOrBurnIn.amount - feeAmount;\n _lockOrBurn(lockOrBurnIn.remoteChainSelector, destTokenAmount);\n\n emit LockedOrBurned({\n remoteChainSelector: lockOrBurnIn.remoteChainSelector,\n token: lockOrBurnIn.localToken,\n sender: msg.sender,\n amount: destTokenAmount\n });\n\n return (\n Pool.LockOrBurnOutV1({\n destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: _encodeLocalDecimals()\n }),\n destTokenAmount\n );\n }\n\n /// @inheritdoc IPoolV1\n /// @dev The _validateLockOrBurn check is an essential security check.\n /// @dev _getFee is not called in this legacy method, so the full amount is locked or burned.\n /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain.\n function lockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn\n ) public virtual returns (Pool.LockOrBurnOutV1 memory lockOrBurnOutV1) {\n _validateLockOrBurn(lockOrBurnIn, FinalityCodec.WAIT_FOR_FINALITY_FLAG, \"\", 0); // feeAmount is zero\n _lockOrBurn(lockOrBurnIn.remoteChainSelector, lockOrBurnIn.amount);\n\n emit LockedOrBurned({\n remoteChainSelector: lockOrBurnIn.remoteChainSelector,\n token: lockOrBurnIn.localToken,\n sender: msg.sender,\n amount: lockOrBurnIn.amount\n });\n\n return Pool.LockOrBurnOutV1({\n destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: _encodeLocalDecimals()\n });\n }\n\n /// @notice Contains the specific lock or burn token logic for a pool.\n /// @dev overriding this method allows us to create pools with different lock/burn signatures\n /// without duplicating the underlying logic.\n /// @param remoteChainSelector The selector of the remote chain.\n /// @param amount The amount of tokens to lock or burn.\n function _lockOrBurn(\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {}\n\n // ================================================================\n // \u2502 Release or Mint \u2502\n // ================================================================\n\n /// @inheritdoc IPoolV2\n /// @dev The _validateReleaseOrMint check is an essential security check.\n /// @param releaseOrMintIn Encoded data fields for the processing of tokens on the destination chain.\n /// @param requestedFinalityConfig Requested finality config according to the FinalityCodec.\n function releaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n bytes4 requestedFinalityConfig\n ) public virtual override(IPoolV2) returns (Pool.ReleaseOrMintOutV1 memory) {\n uint256 localAmount = _calculateLocalAmount(\n releaseOrMintIn.sourceDenominatedAmount, _parseRemoteDecimals(releaseOrMintIn.sourcePoolData)\n );\n\n _validateReleaseOrMint(releaseOrMintIn, localAmount, requestedFinalityConfig);\n\n _releaseOrMint(releaseOrMintIn.receiver, localAmount, releaseOrMintIn.remoteChainSelector);\n\n emit ReleasedOrMinted({\n remoteChainSelector: releaseOrMintIn.remoteChainSelector,\n token: releaseOrMintIn.localToken,\n sender: msg.sender,\n recipient: releaseOrMintIn.receiver,\n amount: localAmount\n });\n\n return Pool.ReleaseOrMintOutV1({destinationAmount: localAmount});\n }\n\n /// @inheritdoc IPoolV1\n /// @dev calls IPoolV2.releaseOrMint with default finality.\n /// @param releaseOrMintIn Encoded data fields for the processing of tokens on the destination chain.\n function releaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn\n ) public virtual override returns (Pool.ReleaseOrMintOutV1 memory) {\n return releaseOrMint(releaseOrMintIn, FinalityCodec.WAIT_FOR_FINALITY_FLAG);\n }\n\n /// @notice Contains the specific release or mint token logic for a pool.\n /// @dev overriding this method allows us to create pools with different release/mint signatures\n /// without duplicating the underlying logic.\n /// @param receiver The address to receive the tokens.\n /// @param amount The amount of tokens to release or mint.\n /// @param remoteChainSelector The selector of the remote chain.\n function _releaseOrMint(\n address receiver,\n uint256 amount,\n uint64 remoteChainSelector\n ) internal virtual {}\n\n // ================================================================\n // \u2502 Validation \u2502\n // ================================================================\n\n /// @notice Validates the lock or burn input for correctness on\n /// - token to be locked or burned\n /// - RMN curse status\n /// - if the sender is a valid onRamp\n /// - rate limiting for either default or FTF transfer messages.\n /// - preflight checks hooks (if enabled)\n /// @param lockOrBurnIn The input to validate.\n /// @param requestedFinality The requested finality speed according to the FinalityCodec encoding.\n /// @param tokenArgs Additional token arguments passed in by the sender of the message.\n /// @param feeAmount The fee amount deducted from the transfer amount.\n /// @dev This function should always be called before executing a lock or burn. Not doing so would allow\n /// for various exploits.\n function _validateLockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinality,\n bytes memory tokenArgs,\n uint256 feeAmount\n ) internal virtual {\n if (!isSupportedToken(lockOrBurnIn.localToken)) {\n revert InvalidToken(lockOrBurnIn.localToken);\n }\n if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(lockOrBurnIn.remoteChainSelector)))) revert CursedByRMN();\n\n _onlyOnRamp(lockOrBurnIn.remoteChainSelector);\n\n uint256 amount = lockOrBurnIn.amount - feeAmount;\n\n // If FTF is requested, validate against the allowed and apply the custom rate limit.\n if (requestedFinality != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n // Use the codec to validate that the requested finality is allowed by the pool's configuration. This will revert\n // if the requested finality is not allowed.\n FinalityCodec._ensureRequestedFinalityAllowed(requestedFinality, s_allowedFinalityConfig);\n _consumeFastFinalityOutboundRateLimit(lockOrBurnIn.localToken, lockOrBurnIn.remoteChainSelector, amount);\n } else {\n _consumeOutboundRateLimit(lockOrBurnIn.localToken, lockOrBurnIn.remoteChainSelector, amount);\n }\n\n _preflightCheck(lockOrBurnIn, requestedFinality, tokenArgs, amount);\n }\n\n /// @notice Hook for pre-flight checks on lock or burn.\n /// @dev These hooks are optional but take up a lot of space in the contracts bytecode. To avoid this overhead when\n /// not needed, you can override this function in the derived contract with an empty implementation. This will result\n /// in the compiler removing the function and all related code, saving close to 1KB.\n /// @param lockOrBurnIn The input to validate.\n /// @param requestedFinalityConfig The requested finality config according to the FinalityCodec encoding.\n /// @param tokenArgs Additional token arguments passed in by the sender of the message.\n /// @param amountPostFee The amount after token pool bps-based fees have been deducted.\n function _preflightCheck(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes memory tokenArgs,\n uint256 amountPostFee\n ) internal virtual {\n if (address(s_advancedPoolHooks) != address(0)) {\n s_advancedPoolHooks.preflightCheck(lockOrBurnIn, requestedFinalityConfig, tokenArgs, amountPostFee);\n }\n }\n\n /// @notice Validates the release or mint input for correctness on\n /// - token to be released or minted\n /// - RMN curse status\n /// - if the sender is a valid offRamp\n /// - if the source pool is configured for the remote chain\n /// - rate limiting for either default or FTF transfer messages.\n /// @param releaseOrMintIn The input to validate.\n /// @param localAmount The local amount to be released or minted.\n /// @param requestedFinalityConfig The requested finality encoding (see `FinalityCodec`).\n /// @dev This function should always be called before executing a release or mint. Not doing so would allow\n /// for various exploits.\n function _validateReleaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n uint256 localAmount,\n bytes4 requestedFinalityConfig\n ) internal virtual {\n if (!isSupportedToken(releaseOrMintIn.localToken)) {\n revert InvalidToken(releaseOrMintIn.localToken);\n }\n if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(releaseOrMintIn.remoteChainSelector)))) revert CursedByRMN();\n _onlyOffRamp(releaseOrMintIn.remoteChainSelector);\n\n // Validates that the source pool address is configured on this pool.\n if (!isRemotePool(releaseOrMintIn.remoteChainSelector, releaseOrMintIn.sourcePoolAddress)) {\n revert InvalidSourcePoolAddress(releaseOrMintIn.sourcePoolAddress);\n }\n if (requestedFinalityConfig != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n _consumeFastFinalityInboundRateLimit(releaseOrMintIn.localToken, releaseOrMintIn.remoteChainSelector, localAmount);\n } else {\n _consumeInboundRateLimit(releaseOrMintIn.localToken, releaseOrMintIn.remoteChainSelector, localAmount);\n }\n\n _postflightCheck(releaseOrMintIn, localAmount, requestedFinalityConfig);\n }\n\n /// @notice Hook for post-flight checks on release or mint.\n /// @dev These hooks are optional but take up a lot of space in the contracts bytecode. To avoid this overhead when\n /// not needed, you can override this function in the derived contract with an empty implementation. This will result\n /// in the compiler removing the function and all related code, saving close to 1KB.\n /// @param releaseOrMintIn The input to validate.\n /// @param localAmount The local amount to be released or minted.\n /// @param requestedFinalityConfig The requested finality encoding (see `FinalityCodec`).\n function _postflightCheck(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n uint256 localAmount,\n bytes4 requestedFinalityConfig\n ) internal virtual {\n if (address(s_advancedPoolHooks) != address(0)) {\n s_advancedPoolHooks.postflightCheck(releaseOrMintIn, localAmount, requestedFinalityConfig);\n }\n }\n\n // ================================================================\n // \u2502 Token decimals \u2502\n // ================================================================\n\n /// @notice Gets the IERC20 token decimals on the local chain.\n function getTokenDecimals() public view virtual returns (uint8 decimals) {\n return i_tokenDecimals;\n }\n\n function _encodeLocalDecimals() internal view virtual returns (bytes memory) {\n return abi.encode(i_tokenDecimals);\n }\n\n function _parseRemoteDecimals(\n bytes memory sourcePoolData\n ) internal view virtual returns (uint8) {\n // Fallback to the local token decimals if the source pool data is empty. This allows for backwards compatibility.\n if (sourcePoolData.length == 0) {\n return i_tokenDecimals;\n }\n if (sourcePoolData.length != 32) {\n revert InvalidRemoteChainDecimals(sourcePoolData);\n }\n uint256 remoteDecimals = abi.decode(sourcePoolData, (uint256));\n if (remoteDecimals > type(uint8).max) {\n revert InvalidRemoteChainDecimals(sourcePoolData);\n }\n return uint8(remoteDecimals);\n }\n\n /// @notice Calculates the local amount based on the remote amount and decimals.\n /// @param remoteAmount The amount on the remote chain.\n /// @param remoteDecimals The decimals of the token on the remote chain.\n /// @return The local amount.\n /// @dev This function protects against overflows. If there is a transaction that hits the overflow check, it is\n /// probably incorrect as that means the amount cannot be represented on this chain. If the local decimals have been\n /// wrongly configured, the token issuer could redeploy the pool with the correct decimals and manually re-execute the\n /// CCIP tx to fix the issue.\n function _calculateLocalAmount(\n uint256 remoteAmount,\n uint8 remoteDecimals\n ) internal view virtual returns (uint256) {\n if (remoteDecimals == i_tokenDecimals) {\n return remoteAmount;\n }\n if (remoteDecimals > i_tokenDecimals) {\n uint8 decimalsDiff = remoteDecimals - i_tokenDecimals;\n if (decimalsDiff > 77) {\n // This is a safety check to prevent overflow in the next calculation.\n revert OverflowDetected(remoteDecimals, i_tokenDecimals, remoteAmount);\n }\n // Solidity rounds down so there is no risk of minting more tokens than the remote chain sent.\n return remoteAmount / (10 ** decimalsDiff);\n }\n\n // This is a safety check to prevent overflow in the next calculation.\n // More than 77 would never fit in a uint256 and would cause an overflow. We also check if the resulting amount\n // would overflow.\n uint8 diffDecimals = i_tokenDecimals - remoteDecimals;\n if (diffDecimals > 77 || remoteAmount > type(uint256).max / (10 ** diffDecimals)) {\n revert OverflowDetected(remoteDecimals, i_tokenDecimals, remoteAmount);\n }\n\n return remoteAmount * (10 ** diffDecimals);\n }\n\n // ================================================================\n // \u2502 Chain permissions \u2502\n // ================================================================\n\n /// @notice Gets the pool address on the remote chain.\n /// @param remoteChainSelector Remote chain selector.\n /// @dev To support non-evm chains, this value is encoded into bytes\n function getRemotePools(\n uint64 remoteChainSelector\n ) public view virtual returns (bytes[] memory) {\n bytes32[] memory remotePoolHashes = s_remoteChainConfigs[remoteChainSelector].remotePools.values();\n\n bytes[] memory remotePools = new bytes[](remotePoolHashes.length);\n for (uint256 i = 0; i < remotePoolHashes.length; ++i) {\n remotePools[i] = s_remotePoolAddresses[remotePoolHashes[i]];\n }\n\n return remotePools;\n }\n\n /// @notice Checks if the pool address is configured on the remote chain.\n /// @param remoteChainSelector Remote chain selector.\n /// @param remotePoolAddress The address of the remote pool.\n function isRemotePool(\n uint64 remoteChainSelector,\n bytes memory remotePoolAddress\n ) public view virtual returns (bool) {\n return s_remoteChainConfigs[remoteChainSelector].remotePools.contains(keccak256(remotePoolAddress));\n }\n\n /// @inheritdoc IPoolV2\n /// @param remoteChainSelector Remote chain selector.\n function getRemoteToken(\n uint64 remoteChainSelector\n ) public view virtual returns (bytes memory) {\n return s_remoteChainConfigs[remoteChainSelector].remoteTokenAddress;\n }\n\n /// @notice Adds a remote pool for a given chain selector. This could be due to a pool being upgraded on the remote\n /// chain. We don't simply want to replace the old pool as there could still be valid inflight messages from the old\n /// pool. This function allows for multiple pools to be added for a single chain selector.\n /// @param remoteChainSelector The remote chain selector for which the remote pool address is being added.\n /// @param remotePoolAddress The address of the new remote pool.\n function addRemotePool(\n uint64 remoteChainSelector,\n bytes calldata remotePoolAddress\n ) external virtual onlyOwner {\n if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector);\n\n _setRemotePool(remoteChainSelector, remotePoolAddress);\n }\n\n /// @notice Removes the remote pool address for a given chain selector.\n /// @dev All inflight txs from the remote pool will be rejected after it is removed. To ensure no loss of funds, there\n /// should be no inflight txs from the given pool.\n /// @param remoteChainSelector The remote chain selector.\n /// @param remotePoolAddress The remote pool address to remove.\n function removeRemotePool(\n uint64 remoteChainSelector,\n bytes calldata remotePoolAddress\n ) external virtual onlyOwner {\n if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector);\n\n if (!s_remoteChainConfigs[remoteChainSelector].remotePools.remove(keccak256(remotePoolAddress))) {\n revert InvalidRemotePoolForChain(remoteChainSelector, remotePoolAddress);\n }\n\n emit RemotePoolRemoved(remoteChainSelector, remotePoolAddress);\n }\n\n /// @inheritdoc IPoolV1\n /// @param remoteChainSelector The remote chain selector to check.\n function isSupportedChain(\n uint64 remoteChainSelector\n ) public view virtual returns (bool) {\n return s_remoteChainSelectors.contains(remoteChainSelector);\n }\n\n /// @notice Get list of allowed chains\n /// @return list of chains.\n function getSupportedChains() public view virtual returns (uint64[] memory) {\n uint256[] memory uint256ChainSelectors = s_remoteChainSelectors.values();\n uint64[] memory chainSelectors = new uint64[](uint256ChainSelectors.length);\n for (uint256 i = 0; i < uint256ChainSelectors.length; ++i) {\n chainSelectors[i] = uint64(uint256ChainSelectors[i]);\n }\n\n return chainSelectors;\n }\n\n /// @notice Sets the permissions for a list of chains selectors. Actual senders for these chains\n /// need to be allowed on the Router to interact with this pool.\n /// @param remoteChainSelectorsToRemove A list of chain selectors to remove.\n /// @param chainsToAdd A list of chains and their new permission status & rate limits. Rate limits\n /// are only used when the chain is being added through `allowed` being true.\n /// @dev Only callable by the owner\n function applyChainUpdates(\n uint64[] calldata remoteChainSelectorsToRemove,\n ChainUpdate[] calldata chainsToAdd\n ) external virtual onlyOwner {\n for (uint256 i = 0; i < remoteChainSelectorsToRemove.length; ++i) {\n uint64 remoteChainSelectorToRemove = remoteChainSelectorsToRemove[i];\n // If the chain doesn't exist, revert.\n if (!s_remoteChainSelectors.remove(remoteChainSelectorToRemove)) {\n revert NonExistentChain(remoteChainSelectorToRemove);\n }\n\n // Remove all remote pool hashes for the chain.\n bytes32[] memory remotePools = s_remoteChainConfigs[remoteChainSelectorToRemove].remotePools.values();\n for (uint256 j = 0; j < remotePools.length; ++j) {\n s_remoteChainConfigs[remoteChainSelectorToRemove].remotePools.remove(remotePools[j]);\n }\n\n delete s_remoteChainConfigs[remoteChainSelectorToRemove];\n delete s_fastFinalityOutboundRateLimiterConfig[remoteChainSelectorToRemove];\n delete s_fastFinalityInboundRateLimiterConfig[remoteChainSelectorToRemove];\n\n emit ChainRemoved(remoteChainSelectorToRemove);\n }\n\n for (uint256 i = 0; i < chainsToAdd.length; ++i) {\n ChainUpdate memory newChain = chainsToAdd[i];\n if (newChain.remoteTokenAddress.length == 0) {\n revert ZeroAddressInvalid();\n }\n\n // If the chain already exists, revert\n if (!s_remoteChainSelectors.add(newChain.remoteChainSelector)) {\n revert ChainAlreadyExists(newChain.remoteChainSelector);\n }\n\n RemoteChainConfig storage remoteChainConfig = s_remoteChainConfigs[newChain.remoteChainSelector];\n remoteChainConfig.outboundRateLimiterConfig._setTokenBucketConfig(newChain.outboundRateLimiterConfig);\n remoteChainConfig.inboundRateLimiterConfig._setTokenBucketConfig(newChain.inboundRateLimiterConfig);\n\n remoteChainConfig.remoteTokenAddress = newChain.remoteTokenAddress;\n\n for (uint256 j = 0; j < newChain.remotePoolAddresses.length; ++j) {\n _setRemotePool(newChain.remoteChainSelector, newChain.remotePoolAddresses[j]);\n }\n\n emit ChainAdded(\n newChain.remoteChainSelector,\n newChain.remoteTokenAddress,\n newChain.outboundRateLimiterConfig,\n newChain.inboundRateLimiterConfig\n );\n }\n }\n\n /// @notice Adds a pool address to the allowed remote token pools for a particular chain.\n /// @param remoteChainSelector The remote chain selector for which the remote pool address is being added.\n /// @param remotePoolAddress The address of the new remote pool.\n function _setRemotePool(\n uint64 remoteChainSelector,\n bytes memory remotePoolAddress\n ) internal virtual {\n if (remotePoolAddress.length == 0) {\n revert ZeroAddressInvalid();\n }\n\n bytes32 poolHash = keccak256(remotePoolAddress);\n\n // Check if the pool already exists.\n if (!s_remoteChainConfigs[remoteChainSelector].remotePools.add(poolHash)) {\n revert PoolAlreadyAdded(remoteChainSelector, remotePoolAddress);\n }\n\n // Add the pool to the mapping to be able to un-hash it later.\n s_remotePoolAddresses[poolHash] = remotePoolAddress;\n\n emit RemotePoolAdded(remoteChainSelector, remotePoolAddress);\n }\n\n // ================================================================\n // \u2502 Rate limiting \u2502\n // ================================================================\n\n /// @dev The inbound rate limits should be slightly higher than the outbound rate limits. This is because many chains\n /// finalize blocks in batches. CCIP also commits messages in batches: the commit plugin bundles multiple messages in\n /// a single merkle root.\n /// Imagine the following scenario.\n /// - Chain A has an inbound and outbound rate limit of 100 tokens capacity and 1 token per second refill rate.\n /// - Chain B has an inbound and outbound rate limit of 100 tokens capacity and 1 token per second refill rate.\n ///\n /// At time 0:\n /// - Chain A sends 100 tokens to Chain B.\n /// At time 5:\n /// - Chain A sends 5 tokens to Chain B.\n /// At time 6:\n /// The epoch that contains blocks [0-5] is finalized.\n /// Both transactions will be included in the same merkle root and become executable at the same time. This means\n /// the token pool on chain B requires a capacity of 105 to successfully execute both messages at the same time.\n /// The exact additional capacity required depends on the refill rate and the size of the source chain epochs and the\n /// CCIP round time. For simplicity, a 5-10% buffer should be sufficient in most cases.\n\n /// @notice Consumes outbound rate limiting capacity in this pool.\n /// @param remoteChainSelector The remote chain selector.\n /// @param amount The amount of tokens consumed.\n function _consumeOutboundRateLimit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {\n s_remoteChainConfigs[remoteChainSelector].outboundRateLimiterConfig._consume(amount, token);\n\n emit OutboundRateLimitConsumed({token: token, remoteChainSelector: remoteChainSelector, amount: amount});\n }\n\n /// @notice Consumes inbound rate limiting capacity in this pool.\n /// @param remoteChainSelector The remote chain selector.\n /// @param amount The amount of tokens consumed.\n function _consumeInboundRateLimit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {\n s_remoteChainConfigs[remoteChainSelector].inboundRateLimiterConfig._consume(amount, token);\n\n emit InboundRateLimitConsumed({token: token, remoteChainSelector: remoteChainSelector, amount: amount});\n }\n\n /// @notice Consumes fast finality outbound rate limiting capacity in this pool.\n /// @dev If fast finality rate limiter is not enabled for the chain, it will fallback to the default\n /// rate limiter.\n /// @param remoteChainSelector The remote chain selector.\n /// @param amount The amount of tokens consumed.\n function _consumeFastFinalityOutboundRateLimit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {\n if (!s_fastFinalityOutboundRateLimiterConfig[remoteChainSelector].isEnabled) {\n _consumeOutboundRateLimit(token, remoteChainSelector, amount);\n return;\n }\n\n s_fastFinalityOutboundRateLimiterConfig[remoteChainSelector]._consume(amount, token);\n\n emit FastFinalityOutboundRateLimitConsumed({token: token, remoteChainSelector: remoteChainSelector, amount: amount});\n }\n\n /// @notice Consumes fast finality inbound rate limiting capacity in this pool.\n /// @dev If fast finality rate limiter is not enabled for the chain, it will fallback to the default\n /// rate limiter.\n /// @param remoteChainSelector The remote chain selector.\n /// @param amount The amount of tokens consumed.\n function _consumeFastFinalityInboundRateLimit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {\n if (!s_fastFinalityInboundRateLimiterConfig[remoteChainSelector].isEnabled) {\n _consumeInboundRateLimit(token, remoteChainSelector, amount);\n return;\n }\n\n s_fastFinalityInboundRateLimiterConfig[remoteChainSelector]._consume(amount, token);\n\n emit FastFinalityInboundRateLimitConsumed({token: token, remoteChainSelector: remoteChainSelector, amount: amount});\n }\n\n /// @notice Returns the outbound and inbound rate limiter state for the given remote chain at the time of the call.\n /// @param remoteChainSelector The remote chain selector.\n /// @param fastFinality Whether to get the fast finality rate limiter state.\n /// @return outboundRateLimiterState The outbound token bucket.\n /// @return inboundRateLimiterState The inbound token bucket.\n function getCurrentRateLimiterState(\n uint64 remoteChainSelector,\n bool fastFinality\n )\n external\n view\n virtual\n returns (\n RateLimiter.TokenBucket memory outboundRateLimiterState,\n RateLimiter.TokenBucket memory inboundRateLimiterState\n )\n {\n if (fastFinality) {\n return (\n s_fastFinalityOutboundRateLimiterConfig[remoteChainSelector]._currentTokenBucketState(),\n s_fastFinalityInboundRateLimiterConfig[remoteChainSelector]._currentTokenBucketState()\n );\n }\n RemoteChainConfig storage config = s_remoteChainConfigs[remoteChainSelector];\n return (\n config.outboundRateLimiterConfig._currentTokenBucketState(),\n config.inboundRateLimiterConfig._currentTokenBucketState()\n );\n }\n\n /// @notice Sets the rate limit configurations for specified remote chains.\n /// @param rateLimitConfigArgs Array of structs containing remote chain selectors and their rate limiter configs.\n function setRateLimitConfig(\n RateLimitConfigArgs[] calldata rateLimitConfigArgs\n ) external virtual {\n _onlyOwnerOrRateLimitAdmin();\n\n for (uint256 i = 0; i < rateLimitConfigArgs.length; ++i) {\n RateLimitConfigArgs calldata configArgs = rateLimitConfigArgs[i];\n\n uint64 remoteChainSelector = configArgs.remoteChainSelector;\n if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector);\n\n if (configArgs.fastFinality) {\n s_fastFinalityOutboundRateLimiterConfig[remoteChainSelector]._setTokenBucketConfig(\n configArgs.outboundRateLimiterConfig\n );\n s_fastFinalityInboundRateLimiterConfig[remoteChainSelector]._setTokenBucketConfig(\n configArgs.inboundRateLimiterConfig\n );\n } else {\n s_remoteChainConfigs[remoteChainSelector].outboundRateLimiterConfig\n ._setTokenBucketConfig(configArgs.outboundRateLimiterConfig);\n s_remoteChainConfigs[remoteChainSelector].inboundRateLimiterConfig\n ._setTokenBucketConfig(configArgs.inboundRateLimiterConfig);\n }\n\n emit RateLimitConfigured(\n remoteChainSelector,\n configArgs.fastFinality,\n configArgs.outboundRateLimiterConfig,\n configArgs.inboundRateLimiterConfig\n );\n }\n }\n\n // ================================================================\n // \u2502 Access \u2502\n // ================================================================\n\n /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender\n /// is a permissioned onRamp for the given chain on the Router.\n /// @dev This function is marked virtual as other token pools may inherit from this contract, but do\n /// not receive calls from the ramps directly, instead receiving them from a proxy contract. In that\n /// situation this function must be overridden and the ramp-check removed and replaced with a different\n /// access-control scheme.\n /// @param remoteChainSelector The remote chain selector.\n function _onlyOnRamp(\n uint64 remoteChainSelector\n ) internal view virtual {\n if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector);\n if (!(msg.sender == s_router.getOnRamp(remoteChainSelector))) revert CallerIsNotARampOnRouter(msg.sender);\n }\n\n /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender\n /// is a permissioned offRamp for the given chain on the Router.\n /// @dev This function is marked virtual as other token pools may inherit from this contract, but do\n /// not receive calls from the ramps directly, instead receiving them from a proxy contract. In that\n /// situation this function must be overridden and the ramp-check removed and replaced with a different\n /// access-control scheme.\n /// @param remoteChainSelector The remote chain selector.\n function _onlyOffRamp(\n uint64 remoteChainSelector\n ) internal view virtual {\n if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector);\n if (!s_router.isOffRamp(remoteChainSelector, msg.sender)) revert CallerIsNotARampOnRouter(msg.sender);\n }\n\n /// @notice Checks whether the msg.sender is either the owner or the rate limit admin.\n function _onlyOwnerOrRateLimitAdmin() internal view virtual {\n if (msg.sender != s_rateLimitAdmin && msg.sender != owner()) {\n revert Unauthorized(msg.sender);\n }\n }\n\n /// @notice Returns the set of required CCVs for transfers in a specific direction.\n /// @dev This function delegates to AdvancedPoolHooks if configured, otherwise returns an empty array.\n /// @param localToken The address of the local token.\n /// @param remoteChainSelector The remote chain selector for this transfer.\n /// @param sourceDenominatedAmount The amount being transferred, source denominated.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param extraData Direction-specific payload forwarded by the caller (e.g. token args or source pool data).\n /// @param direction The direction of the transfer (Inbound or Outbound).\n /// @return requiredCCVs Set of required CCV addresses.\n function getRequiredCCVs(\n address localToken,\n uint64 remoteChainSelector,\n uint256 sourceDenominatedAmount,\n bytes4 requestedFinalityConfig,\n bytes calldata extraData,\n IPoolV2.MessageDirection direction\n ) public view virtual returns (address[] memory requiredCCVs) {\n if (address(s_advancedPoolHooks) == address(0)) {\n return new address[](0);\n }\n\n // By default, the amount is equal to the source denominated amount.\n uint256 amount = sourceDenominatedAmount;\n\n // The source fee amount is not classified as transferred value, meaning we have to subtract it from the amount\n // before passing it into the hook. The inbound amount is already post-fee so we only need to do this for outbound\n // transfers.\n if (direction == IPoolV2.MessageDirection.Outbound) {\n TokenTransferFeeConfig memory feeConfig = s_tokenTransferFeeConfig[remoteChainSelector];\n if (feeConfig.isEnabled) {\n if (requestedFinalityConfig != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n amount =\n sourceDenominatedAmount - (sourceDenominatedAmount * feeConfig.fastFinalityTransferFeeBps) / BPS_DIVIDER;\n } else {\n amount = sourceDenominatedAmount - (sourceDenominatedAmount * feeConfig.finalityTransferFeeBps) / BPS_DIVIDER;\n }\n }\n } else {\n // For inbound transfers, the amount is already post-fee so we don't need to do any additional calculations to get\n // the amount that will be received by the user. However, we still need to convert it to the local amount based on\n // decimals for the hooks.\n\n // extraData is sourcePoolData for inbound transfers, which contains the remote decimals.\n amount = _calculateLocalAmount(sourceDenominatedAmount, _parseRemoteDecimals(extraData));\n }\n\n return s_advancedPoolHooks.getRequiredCCVs(\n localToken, remoteChainSelector, amount, requestedFinalityConfig, extraData, direction\n );\n }\n\n // ================================================================\n // \u2502 Fee \u2502\n // ================================================================\n\n /// @notice Updates the token transfer fee configurations for specified destination chains.\n /// @param tokenTransferFeeConfigArgs Array of structs containing destination chain selectors and their fee configs.\n /// @param disableTokenTransferFeeConfigs Array of destination chain selectors to disable custom fee configs for.\n function applyTokenTransferFeeConfigUpdates(\n TokenTransferFeeConfigArgs[] calldata tokenTransferFeeConfigArgs,\n uint64[] calldata disableTokenTransferFeeConfigs\n ) external virtual onlyOwner {\n for (uint256 i = 0; i < tokenTransferFeeConfigArgs.length; ++i) {\n uint64 destChainSelector = tokenTransferFeeConfigArgs[i].destChainSelector;\n if (!isSupportedChain(destChainSelector)) revert NonExistentChain(destChainSelector);\n\n TokenTransferFeeConfig calldata tokenTransferFeeConfig = tokenTransferFeeConfigArgs[i].tokenTransferFeeConfig;\n\n // Reject configs with isEnabled: false - use disableTokenTransferFeeConfigs parameter instead.\n if (!tokenTransferFeeConfig.isEnabled) {\n revert InvalidTokenTransferFeeConfig(destChainSelector);\n }\n\n if (tokenTransferFeeConfig.finalityTransferFeeBps >= BPS_DIVIDER) {\n revert InvalidTransferFeeBps(tokenTransferFeeConfig.finalityTransferFeeBps);\n }\n if (tokenTransferFeeConfig.fastFinalityTransferFeeBps >= BPS_DIVIDER) {\n revert InvalidTransferFeeBps(tokenTransferFeeConfig.fastFinalityTransferFeeBps);\n }\n // Gas overhead must be non-zero for proper fee accounting.\n if (tokenTransferFeeConfig.destGasOverhead == 0) {\n revert InvalidTokenTransferFeeConfig(destChainSelector);\n }\n\n s_tokenTransferFeeConfig[destChainSelector] = tokenTransferFeeConfig;\n emit TokenTransferFeeConfigUpdated(destChainSelector, tokenTransferFeeConfig);\n }\n\n for (uint256 i = 0; i < disableTokenTransferFeeConfigs.length; ++i) {\n uint64 destChainSelector = disableTokenTransferFeeConfigs[i];\n delete s_tokenTransferFeeConfig[destChainSelector];\n emit TokenTransferFeeConfigDeleted(destChainSelector);\n }\n }\n\n /// @notice Returns the token transfer fee override for a destination chain.\n /// @param destChainSelector The destination chain selector used for lookup.\n /// @return feeConfig The enabled fee configuration for the lane.\n function getTokenTransferFeeConfig(\n address, // localToken\n uint64 destChainSelector,\n bytes4, // requestedFinalityConfig\n bytes calldata // tokenArgs\n ) external view virtual returns (TokenTransferFeeConfig memory feeConfig) {\n return s_tokenTransferFeeConfig[destChainSelector];\n }\n\n /// @inheritdoc IPoolV2\n /// @notice Returns the pool fee parameters that will apply to a transfer.\n /// @param destChainSelector The destination lane selector.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n function getFee(\n address, // localToken\n uint64 destChainSelector,\n uint256, // amount\n address, // feeToken\n bytes4 requestedFinalityConfig,\n bytes calldata // tokenArgs\n )\n external\n view\n virtual\n returns (uint256 feeUSDCents, uint32 destGasOverhead, uint32 destBytesOverhead, uint16 tokenFeeBps, bool isEnabled)\n {\n FinalityCodec._ensureRequestedFinalityAllowed(requestedFinalityConfig, s_allowedFinalityConfig);\n\n TokenTransferFeeConfig memory feeConfig = s_tokenTransferFeeConfig[destChainSelector];\n\n // If config is disabled, return zeros with isEnabled=false to signal OnRamp to use FeeQuoter defaults.\n if (!feeConfig.isEnabled) {\n return (0, 0, 0, 0, false);\n }\n\n if (requestedFinalityConfig != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n return (\n feeConfig.fastFinalityFeeUSDCents,\n feeConfig.destGasOverhead,\n feeConfig.destBytesOverhead,\n feeConfig.fastFinalityTransferFeeBps,\n true\n );\n }\n return (\n feeConfig.finalityFeeUSDCents,\n feeConfig.destGasOverhead,\n feeConfig.destBytesOverhead,\n feeConfig.finalityTransferFeeBps,\n true\n );\n }\n\n /// @dev Calculates the fee based on the transferred amount, and the configured basis points.\n /// @param lockOrBurnIn The original lock or burn request.\n /// @param requestedFinalityConfig The requested finality encoding (see `FinalityCodec`).\n /// A value of zero (FinalityCodec.WAIT_FOR_FINALITY_FLAG) applies default finality fees.\n /// Returns the fee amount.\n function _getFee(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig\n ) internal view virtual returns (uint256) {\n TokenTransferFeeConfig storage feeConfig = s_tokenTransferFeeConfig[lockOrBurnIn.remoteChainSelector];\n\n // Determine which fee basis points to apply based on finality type.\n if (requestedFinalityConfig != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n return (lockOrBurnIn.amount * feeConfig.fastFinalityTransferFeeBps) / BPS_DIVIDER;\n } else {\n return (lockOrBurnIn.amount * feeConfig.finalityTransferFeeBps) / BPS_DIVIDER;\n }\n }\n\n /// @notice Withdraws accrued fee token balances to the provided `recipient`.\n /// @dev Only callable by the owner or the fee admin.\n /// @dev FeeTokenHandler will revert if `recipient` is zero address.\n /// @dev Pools accrue fees directly on this contract. Lock/release pools send bridge liquidity to their ERC20 lockbox\n /// during the lock flow, which means any balance left on this contract represents fees that have accrued to the pool.\n /// Because user liquidity never resides on `address(this)` for lock/release pools, transferring the full contract\n /// balance is safe and clears only accrued fees.\n /// @param feeTokens The token addresses to withdraw, including the pool token when applicable.\n /// @param recipient The address to withdraw the fee tokens to.\n function withdrawFeeTokens(\n address[] calldata feeTokens,\n address recipient\n ) external virtual {\n if (msg.sender != owner() && msg.sender != s_feeAdmin) {\n revert CallerIsNotOwnerOrFeeAdmin(msg.sender);\n }\n FeeTokenHandler._withdrawFeeTokens(feeTokens, recipient);\n }\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2Step.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {IOwnable} from \"../interfaces/IOwnable.sol\";\n\n/// @notice A minimal contract that implements 2-step ownership transfer and nothing more. It's made to be minimal\n/// to reduce the impact of the bytecode size on any contract that inherits from it.\ncontract Ownable2Step is IOwnable {\n /// @notice The pending owner is the address to which ownership may be transferred.\n address private s_pendingOwner;\n /// @notice The owner is the current owner of the contract.\n /// @dev The owner is the second storage variable so any implementing contract could pack other state with it\n /// instead of the much less used s_pendingOwner.\n address private s_owner;\n\n error OwnerCannotBeZero();\n error MustBeProposedOwner();\n error CannotTransferToSelf();\n error OnlyCallableByOwner();\n\n event OwnershipTransferRequested(address indexed from, address indexed to);\n event OwnershipTransferred(address indexed from, address indexed to);\n\n constructor(address newOwner, address pendingOwner) {\n if (newOwner == address(0)) {\n revert OwnerCannotBeZero();\n }\n\n s_owner = newOwner;\n if (pendingOwner != address(0)) {\n _transferOwnership(pendingOwner);\n }\n }\n\n /// @notice Get the current owner\n function owner() public view override returns (address) {\n return s_owner;\n }\n\n /// @notice Allows an owner to begin transferring ownership to a new address. The new owner needs to call\n /// `acceptOwnership` to accept the transfer before any permissions are changed.\n /// @param to The address to which ownership will be transferred.\n function transferOwnership(\n address to\n ) public override onlyOwner {\n _transferOwnership(to);\n }\n\n /// @notice validate, transfer ownership, and emit relevant events\n /// @param to The address to which ownership will be transferred.\n function _transferOwnership(\n address to\n ) private {\n if (to == msg.sender) {\n revert CannotTransferToSelf();\n }\n\n s_pendingOwner = to;\n\n emit OwnershipTransferRequested(s_owner, to);\n }\n\n /// @notice Allows an ownership transfer to be completed by the recipient.\n function acceptOwnership() external override {\n if (msg.sender != s_pendingOwner) {\n revert MustBeProposedOwner();\n }\n\n address oldOwner = s_owner;\n s_owner = msg.sender;\n s_pendingOwner = address(0);\n\n emit OwnershipTransferred(oldOwner, msg.sender);\n }\n\n /// @notice validate access\n function _validateOwnership() internal view {\n if (msg.sender != s_owner) {\n revert OnlyCallableByOwner();\n }\n }\n\n /// @notice Reverts if called by anyone other than the contract owner.\n modifier onlyOwner() {\n _validateOwnership();\n _;\n }\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {Ownable2Step} from \"./Ownable2Step.sol\";\n\n/// @notice Sets the msg.sender to be the owner of the contract and does not set a pending owner.\ncontract Ownable2StepMsgSender is Ownable2Step {\n constructor() Ownable2Step(msg.sender, address(0)) {}\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/IOwnable.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface IOwnable {\n function owner() external returns (address);\n\n function transferOwnership(\n address recipient\n ) external;\n\n function acceptOwnership() external;\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface ITypeAndVersion {\n function typeAndVersion() external pure returns (string memory);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC1363.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1363.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"./IERC20.sol\";\nimport {IERC165} from \"./IERC165.sol\";\n\n/**\n * @title IERC1363\n * @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].\n *\n * Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract\n * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.\n */\ninterface IERC1363 is IERC20, IERC165 {\n /*\n * Note: the ERC-165 identifier for this interface is 0xb0202a11.\n * 0xb0202a11 ===\n * bytes4(keccak256('transferAndCall(address,uint256)')) ^\n * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^\n * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^\n * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^\n * bytes4(keccak256('approveAndCall(address,uint256)')) ^\n * bytes4(keccak256('approveAndCall(address,uint256,bytes)'))\n */\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferAndCall(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @param data Additional data with no specified format, sent in call to `to`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param from The address which you want to send tokens from.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferFromAndCall(address from, address to, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param from The address which you want to send tokens from.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @param data Additional data with no specified format, sent in call to `to`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.\n * @param spender The address which will spend the funds.\n * @param value The amount of tokens to be spent.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function approveAndCall(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.\n * @param spender The address which will spend the funds.\n * @param value The amount of tokens to be spent.\n * @param data Additional data with no specified format, sent in call to `spender`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC165.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC165.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC165} from \"../utils/introspection/IERC165.sol\";\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC20.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../token/ERC20/IERC20.sol\";\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/IERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-20 standard as defined in the ERC.\n */\ninterface IERC20 {\n /**\n * @dev Emitted when `value` tokens are moved from one account (`from`) to\n * another (`to`).\n *\n * Note that `value` may be zero.\n */\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n /**\n * @dev Emitted when the allowance of a `spender` for an `owner` is set by\n * a call to {approve}. `value` is the new allowance.\n */\n event Approval(address indexed owner, address indexed spender, uint256 value);\n\n /**\n * @dev Returns the value of tokens in existence.\n */\n function totalSupply() external view returns (uint256);\n\n /**\n * @dev Returns the value of tokens owned by `account`.\n */\n function balanceOf(address account) external view returns (uint256);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transfer(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Returns the remaining number of tokens that `spender` will be\n * allowed to spend on behalf of `owner` through {transferFrom}. This is\n * zero by default.\n *\n * This value changes when {approve} or {transferFrom} are called.\n */\n function allowance(address owner, address spender) external view returns (uint256);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * IMPORTANT: Beware that changing an allowance with this method brings the risk\n * that someone may use both the old and the new allowance by unfortunate\n * transaction ordering. One possible solution to mitigate this race\n * condition is to first reduce the spender's allowance to 0 and set the\n * desired value afterwards:\n * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n *\n * Emits an {Approval} event.\n */\n function approve(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the\n * allowance mechanism. `value` is then deducted from the caller's\n * allowance.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(address from, address to, uint256 value) external returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Metadata.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Metadata.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../IERC20.sol\";\n\n/**\n * @dev Interface for the optional metadata functions from the ERC-20 standard.\n */\ninterface IERC20Metadata is IERC20 {\n /**\n * @dev Returns the name of the token.\n */\n function name() external view returns (string memory);\n\n /**\n * @dev Returns the symbol of the token.\n */\n function symbol() external view returns (string memory);\n\n /**\n * @dev Returns the decimals places of the token.\n */\n function decimals() external view returns (uint8);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/utils/SafeERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../IERC20.sol\";\nimport {IERC1363} from \"../../../interfaces/IERC1363.sol\";\n\n/**\n * @title SafeERC20\n * @dev Wrappers around ERC-20 operations that throw on failure (when the token\n * contract returns false). Tokens that return no value (and instead revert or\n * throw on failure) are also supported, non-reverting calls are assumed to be\n * successful.\n * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,\n * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.\n */\nlibrary SafeERC20 {\n /**\n * @dev An operation with an ERC-20 token failed.\n */\n error SafeERC20FailedOperation(address token);\n\n /**\n * @dev Indicates a failed `decreaseAllowance` request.\n */\n error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);\n\n /**\n * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,\n * non-reverting calls are assumed to be successful.\n */\n function safeTransfer(IERC20 token, address to, uint256 value) internal {\n _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));\n }\n\n /**\n * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the\n * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.\n */\n function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {\n _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));\n }\n\n /**\n * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.\n */\n function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {\n return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value)));\n }\n\n /**\n * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.\n */\n function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {\n return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value)));\n }\n\n /**\n * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,\n * non-reverting calls are assumed to be successful.\n *\n * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the \"client\"\n * smart contract uses ERC-7674 to set temporary allowances, then the \"client\" smart contract should avoid using\n * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract\n * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.\n */\n function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {\n uint256 oldAllowance = token.allowance(address(this), spender);\n forceApprove(token, spender, oldAllowance + value);\n }\n\n /**\n * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no\n * value, non-reverting calls are assumed to be successful.\n *\n * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the \"client\"\n * smart contract uses ERC-7674 to set temporary allowances, then the \"client\" smart contract should avoid using\n * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract\n * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.\n */\n function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {\n unchecked {\n uint256 currentAllowance = token.allowance(address(this), spender);\n if (currentAllowance < requestedDecrease) {\n revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);\n }\n forceApprove(token, spender, currentAllowance - requestedDecrease);\n }\n }\n\n /**\n * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,\n * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval\n * to be set to zero before setting it to a non-zero value, such as USDT.\n *\n * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function\n * only sets the \"standard\" allowance. Any temporary allowance will remain active, in addition to the value being\n * set here.\n */\n function forceApprove(IERC20 token, address spender, uint256 value) internal {\n bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));\n\n if (!_callOptionalReturnBool(token, approvalCall)) {\n _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));\n _callOptionalReturn(token, approvalCall);\n }\n }\n\n /**\n * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no\n * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * Reverts if the returned value is other than `true`.\n */\n function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {\n if (to.code.length == 0) {\n safeTransfer(token, to, value);\n } else if (!token.transferAndCall(to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target\n * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * Reverts if the returned value is other than `true`.\n */\n function transferFromAndCallRelaxed(\n IERC1363 token,\n address from,\n address to,\n uint256 value,\n bytes memory data\n ) internal {\n if (to.code.length == 0) {\n safeTransferFrom(token, from, to, value);\n } else if (!token.transferFromAndCall(from, to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no\n * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.\n * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}\n * once without retrying, and relies on the returned value to be true.\n *\n * Reverts if the returned value is other than `true`.\n */\n function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {\n if (to.code.length == 0) {\n forceApprove(token, to, value);\n } else if (!token.approveAndCall(to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement\n * on the return value: the return value is optional (but if data is returned, it must not be false).\n * @param token The token targeted by the call.\n * @param data The call data (encoded using abi.encode or one of its variants).\n *\n * This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.\n */\n function _callOptionalReturn(IERC20 token, bytes memory data) private {\n uint256 returnSize;\n uint256 returnValue;\n assembly (\"memory-safe\") {\n let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)\n // bubble errors\n if iszero(success) {\n let ptr := mload(0x40)\n returndatacopy(ptr, 0, returndatasize())\n revert(ptr, returndatasize())\n }\n returnSize := returndatasize()\n returnValue := mload(0)\n }\n\n if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement\n * on the return value: the return value is optional (but if data is returned, it must not be false).\n * @param token The token targeted by the call.\n * @param data The call data (encoded using abi.encode or one of its variants).\n *\n * This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.\n */\n function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {\n bool success;\n uint256 returnSize;\n uint256 returnValue;\n assembly (\"memory-safe\") {\n success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)\n returnSize := returndatasize()\n returnValue := mload(0)\n }\n return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Arrays.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/Arrays.sol)\n// This file was procedurally generated from scripts/generate/templates/Arrays.js.\n\npragma solidity ^0.8.20;\n\nimport {Comparators} from \"./Comparators.sol\";\nimport {SlotDerivation} from \"./SlotDerivation.sol\";\nimport {StorageSlot} from \"./StorageSlot.sol\";\nimport {Math} from \"./math/Math.sol\";\n\n/**\n * @dev Collection of functions related to array types.\n */\nlibrary Arrays {\n using SlotDerivation for bytes32;\n using StorageSlot for bytes32;\n\n /**\n * @dev Sort an array of uint256 (in memory) following the provided comparator function.\n *\n * This function does the sorting \"in place\", meaning that it overrides the input. The object is returned for\n * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.\n *\n * NOTE: this function's cost is `O(n \u00b7 log(n))` in average and `O(n\u00b2)` in the worst case, with n the length of the\n * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful\n * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may\n * consume more gas than is available in a block, leading to potential DoS.\n *\n * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.\n */\n function sort(\n uint256[] memory array,\n function(uint256, uint256) pure returns (bool) comp\n ) internal pure returns (uint256[] memory) {\n _quickSort(_begin(array), _end(array), comp);\n return array;\n }\n\n /**\n * @dev Variant of {sort} that sorts an array of uint256 in increasing order.\n */\n function sort(uint256[] memory array) internal pure returns (uint256[] memory) {\n sort(array, Comparators.lt);\n return array;\n }\n\n /**\n * @dev Sort an array of address (in memory) following the provided comparator function.\n *\n * This function does the sorting \"in place\", meaning that it overrides the input. The object is returned for\n * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.\n *\n * NOTE: this function's cost is `O(n \u00b7 log(n))` in average and `O(n\u00b2)` in the worst case, with n the length of the\n * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful\n * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may\n * consume more gas than is available in a block, leading to potential DoS.\n *\n * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.\n */\n function sort(\n address[] memory array,\n function(address, address) pure returns (bool) comp\n ) internal pure returns (address[] memory) {\n sort(_castToUint256Array(array), _castToUint256Comp(comp));\n return array;\n }\n\n /**\n * @dev Variant of {sort} that sorts an array of address in increasing order.\n */\n function sort(address[] memory array) internal pure returns (address[] memory) {\n sort(_castToUint256Array(array), Comparators.lt);\n return array;\n }\n\n /**\n * @dev Sort an array of bytes32 (in memory) following the provided comparator function.\n *\n * This function does the sorting \"in place\", meaning that it overrides the input. The object is returned for\n * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.\n *\n * NOTE: this function's cost is `O(n \u00b7 log(n))` in average and `O(n\u00b2)` in the worst case, with n the length of the\n * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful\n * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may\n * consume more gas than is available in a block, leading to potential DoS.\n *\n * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.\n */\n function sort(\n bytes32[] memory array,\n function(bytes32, bytes32) pure returns (bool) comp\n ) internal pure returns (bytes32[] memory) {\n sort(_castToUint256Array(array), _castToUint256Comp(comp));\n return array;\n }\n\n /**\n * @dev Variant of {sort} that sorts an array of bytes32 in increasing order.\n */\n function sort(bytes32[] memory array) internal pure returns (bytes32[] memory) {\n sort(_castToUint256Array(array), Comparators.lt);\n return array;\n }\n\n /**\n * @dev Performs a quick sort of a segment of memory. The segment sorted starts at `begin` (inclusive), and stops\n * at end (exclusive). Sorting follows the `comp` comparator.\n *\n * Invariant: `begin <= end`. This is the case when initially called by {sort} and is preserved in subcalls.\n *\n * IMPORTANT: Memory locations between `begin` and `end` are not validated/zeroed. This function should\n * be used only if the limits are within a memory array.\n */\n function _quickSort(uint256 begin, uint256 end, function(uint256, uint256) pure returns (bool) comp) private pure {\n unchecked {\n if (end - begin < 0x40) return;\n\n // Use first element as pivot\n uint256 pivot = _mload(begin);\n // Position where the pivot should be at the end of the loop\n uint256 pos = begin;\n\n for (uint256 it = begin + 0x20; it < end; it += 0x20) {\n if (comp(_mload(it), pivot)) {\n // If the value stored at the iterator's position comes before the pivot, we increment the\n // position of the pivot and move the value there.\n pos += 0x20;\n _swap(pos, it);\n }\n }\n\n _swap(begin, pos); // Swap pivot into place\n _quickSort(begin, pos, comp); // Sort the left side of the pivot\n _quickSort(pos + 0x20, end, comp); // Sort the right side of the pivot\n }\n }\n\n /**\n * @dev Pointer to the memory location of the first element of `array`.\n */\n function _begin(uint256[] memory array) private pure returns (uint256 ptr) {\n assembly (\"memory-safe\") {\n ptr := add(array, 0x20)\n }\n }\n\n /**\n * @dev Pointer to the memory location of the first memory word (32bytes) after `array`. This is the memory word\n * that comes just after the last element of the array.\n */\n function _end(uint256[] memory array) private pure returns (uint256 ptr) {\n unchecked {\n return _begin(array) + array.length * 0x20;\n }\n }\n\n /**\n * @dev Load memory word (as a uint256) at location `ptr`.\n */\n function _mload(uint256 ptr) private pure returns (uint256 value) {\n assembly {\n value := mload(ptr)\n }\n }\n\n /**\n * @dev Swaps the elements memory location `ptr1` and `ptr2`.\n */\n function _swap(uint256 ptr1, uint256 ptr2) private pure {\n assembly {\n let value1 := mload(ptr1)\n let value2 := mload(ptr2)\n mstore(ptr1, value2)\n mstore(ptr2, value1)\n }\n }\n\n /// @dev Helper: low level cast address memory array to uint256 memory array\n function _castToUint256Array(address[] memory input) private pure returns (uint256[] memory output) {\n assembly {\n output := input\n }\n }\n\n /// @dev Helper: low level cast bytes32 memory array to uint256 memory array\n function _castToUint256Array(bytes32[] memory input) private pure returns (uint256[] memory output) {\n assembly {\n output := input\n }\n }\n\n /// @dev Helper: low level cast address comp function to uint256 comp function\n function _castToUint256Comp(\n function(address, address) pure returns (bool) input\n ) private pure returns (function(uint256, uint256) pure returns (bool) output) {\n assembly {\n output := input\n }\n }\n\n /// @dev Helper: low level cast bytes32 comp function to uint256 comp function\n function _castToUint256Comp(\n function(bytes32, bytes32) pure returns (bool) input\n ) private pure returns (function(uint256, uint256) pure returns (bool) output) {\n assembly {\n output := input\n }\n }\n\n /**\n * @dev Searches a sorted `array` and returns the first index that contains\n * a value greater or equal to `element`. If no such index exists (i.e. all\n * values in the array are strictly less than `element`), the array length is\n * returned. Time complexity O(log n).\n *\n * NOTE: The `array` is expected to be sorted in ascending order, and to\n * contain no repeated elements.\n *\n * IMPORTANT: Deprecated. This implementation behaves as {lowerBound} but lacks\n * support for repeated elements in the array. The {lowerBound} function should\n * be used instead.\n */\n function findUpperBound(uint256[] storage array, uint256 element) internal view returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeAccess(array, mid).value > element) {\n high = mid;\n } else {\n low = mid + 1;\n }\n }\n\n // At this point `low` is the exclusive upper bound. We will return the inclusive upper bound.\n if (low > 0 && unsafeAccess(array, low - 1).value == element) {\n return low - 1;\n } else {\n return low;\n }\n }\n\n /**\n * @dev Searches an `array` sorted in ascending order and returns the first\n * index that contains a value greater or equal than `element`. If no such index\n * exists (i.e. all values in the array are strictly less than `element`), the array\n * length is returned. Time complexity O(log n).\n *\n * See C++'s https://en.cppreference.com/w/cpp/algorithm/lower_bound[lower_bound].\n */\n function lowerBound(uint256[] storage array, uint256 element) internal view returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeAccess(array, mid).value < element) {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n } else {\n high = mid;\n }\n }\n\n return low;\n }\n\n /**\n * @dev Searches an `array` sorted in ascending order and returns the first\n * index that contains a value strictly greater than `element`. If no such index\n * exists (i.e. all values in the array are strictly less than `element`), the array\n * length is returned. Time complexity O(log n).\n *\n * See C++'s https://en.cppreference.com/w/cpp/algorithm/upper_bound[upper_bound].\n */\n function upperBound(uint256[] storage array, uint256 element) internal view returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeAccess(array, mid).value > element) {\n high = mid;\n } else {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n }\n }\n\n return low;\n }\n\n /**\n * @dev Same as {lowerBound}, but with an array in memory.\n */\n function lowerBoundMemory(uint256[] memory array, uint256 element) internal pure returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeMemoryAccess(array, mid) < element) {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n } else {\n high = mid;\n }\n }\n\n return low;\n }\n\n /**\n * @dev Same as {upperBound}, but with an array in memory.\n */\n function upperBoundMemory(uint256[] memory array, uint256 element) internal pure returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeMemoryAccess(array, mid) > element) {\n high = mid;\n } else {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n }\n }\n\n return low;\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeAccess(address[] storage arr, uint256 pos) internal pure returns (StorageSlot.AddressSlot storage) {\n bytes32 slot;\n assembly (\"memory-safe\") {\n slot := arr.slot\n }\n return slot.deriveArray().offset(pos).getAddressSlot();\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeAccess(bytes32[] storage arr, uint256 pos) internal pure returns (StorageSlot.Bytes32Slot storage) {\n bytes32 slot;\n assembly (\"memory-safe\") {\n slot := arr.slot\n }\n return slot.deriveArray().offset(pos).getBytes32Slot();\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeAccess(uint256[] storage arr, uint256 pos) internal pure returns (StorageSlot.Uint256Slot storage) {\n bytes32 slot;\n assembly (\"memory-safe\") {\n slot := arr.slot\n }\n return slot.deriveArray().offset(pos).getUint256Slot();\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeMemoryAccess(address[] memory arr, uint256 pos) internal pure returns (address res) {\n assembly {\n res := mload(add(add(arr, 0x20), mul(pos, 0x20)))\n }\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeMemoryAccess(bytes32[] memory arr, uint256 pos) internal pure returns (bytes32 res) {\n assembly {\n res := mload(add(add(arr, 0x20), mul(pos, 0x20)))\n }\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeMemoryAccess(uint256[] memory arr, uint256 pos) internal pure returns (uint256 res) {\n assembly {\n res := mload(add(add(arr, 0x20), mul(pos, 0x20)))\n }\n }\n\n /**\n * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden.\n *\n * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.\n */\n function unsafeSetLength(address[] storage array, uint256 len) internal {\n assembly (\"memory-safe\") {\n sstore(array.slot, len)\n }\n }\n\n /**\n * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden.\n *\n * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.\n */\n function unsafeSetLength(bytes32[] storage array, uint256 len) internal {\n assembly (\"memory-safe\") {\n sstore(array.slot, len)\n }\n }\n\n /**\n * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden.\n *\n * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.\n */\n function unsafeSetLength(uint256[] storage array, uint256 len) internal {\n assembly (\"memory-safe\") {\n sstore(array.slot, len)\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Comparators.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/Comparators.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Provides a set of functions to compare values.\n *\n * _Available since v5.1._\n */\nlibrary Comparators {\n function lt(uint256 a, uint256 b) internal pure returns (bool) {\n return a < b;\n }\n\n function gt(uint256 a, uint256 b) internal pure returns (bool) {\n return a > b;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Panic.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/Panic.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Helper library for emitting standardized panic codes.\n *\n * ```solidity\n * contract Example {\n * using Panic for uint256;\n *\n * // Use any of the declared internal constants\n * function foo() { Panic.GENERIC.panic(); }\n *\n * // Alternatively\n * function foo() { Panic.panic(Panic.GENERIC); }\n * }\n * ```\n *\n * Follows the list from https://github.com/ethereum/solidity/blob/v0.8.24/libsolutil/ErrorCodes.h[libsolutil].\n *\n * _Available since v5.1._\n */\n// slither-disable-next-line unused-state\nlibrary Panic {\n /// @dev generic / unspecified error\n uint256 internal constant GENERIC = 0x00;\n /// @dev used by the assert() builtin\n uint256 internal constant ASSERT = 0x01;\n /// @dev arithmetic underflow or overflow\n uint256 internal constant UNDER_OVERFLOW = 0x11;\n /// @dev division or modulo by zero\n uint256 internal constant DIVISION_BY_ZERO = 0x12;\n /// @dev enum conversion error\n uint256 internal constant ENUM_CONVERSION_ERROR = 0x21;\n /// @dev invalid encoding in storage\n uint256 internal constant STORAGE_ENCODING_ERROR = 0x22;\n /// @dev empty array pop\n uint256 internal constant EMPTY_ARRAY_POP = 0x31;\n /// @dev array out of bounds access\n uint256 internal constant ARRAY_OUT_OF_BOUNDS = 0x32;\n /// @dev resource error (too large allocation or too large array)\n uint256 internal constant RESOURCE_ERROR = 0x41;\n /// @dev calling invalid internal function\n uint256 internal constant INVALID_INTERNAL_FUNCTION = 0x51;\n\n /// @dev Reverts with a panic code. Recommended to use with\n /// the internal constants with predefined codes.\n function panic(uint256 code) internal pure {\n assembly (\"memory-safe\") {\n mstore(0x00, 0x4e487b71)\n mstore(0x20, code)\n revert(0x1c, 0x24)\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/SlotDerivation.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/SlotDerivation.sol)\n// This file was procedurally generated from scripts/generate/templates/SlotDerivation.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Library for computing storage (and transient storage) locations from namespaces and deriving slots\n * corresponding to standard patterns. The derivation method for array and mapping matches the storage layout used by\n * the solidity language / compiler.\n *\n * See https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays[Solidity docs for mappings and dynamic arrays.].\n *\n * Example usage:\n * ```solidity\n * contract Example {\n * // Add the library methods\n * using StorageSlot for bytes32;\n * using SlotDerivation for bytes32;\n *\n * // Declare a namespace\n * string private constant _NAMESPACE = \"\"; // eg. OpenZeppelin.Slot\n *\n * function setValueInNamespace(uint256 key, address newValue) internal {\n * _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value = newValue;\n * }\n *\n * function getValueInNamespace(uint256 key) internal view returns (address) {\n * return _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value;\n * }\n * }\n * ```\n *\n * TIP: Consider using this library along with {StorageSlot}.\n *\n * NOTE: This library provides a way to manipulate storage locations in a non-standard way. Tooling for checking\n * upgrade safety will ignore the slots accessed through this library.\n *\n * _Available since v5.1._\n */\nlibrary SlotDerivation {\n /**\n * @dev Derive an ERC-7201 slot from a string (namespace).\n */\n function erc7201Slot(string memory namespace) internal pure returns (bytes32 slot) {\n assembly (\"memory-safe\") {\n mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1))\n slot := and(keccak256(0x00, 0x20), not(0xff))\n }\n }\n\n /**\n * @dev Add an offset to a slot to get the n-th element of a structure or an array.\n */\n function offset(bytes32 slot, uint256 pos) internal pure returns (bytes32 result) {\n unchecked {\n return bytes32(uint256(slot) + pos);\n }\n }\n\n /**\n * @dev Derive the location of the first element in an array from the slot where the length is stored.\n */\n function deriveArray(bytes32 slot) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, slot)\n result := keccak256(0x00, 0x20)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, address key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, and(key, shr(96, not(0))))\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, bool key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, iszero(iszero(key)))\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, bytes32 key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, key)\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, uint256 key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, key)\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, int256 key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, key)\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, string memory key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n let length := mload(key)\n let begin := add(key, 0x20)\n let end := add(begin, length)\n let cache := mload(end)\n mstore(end, slot)\n result := keccak256(begin, add(length, 0x20))\n mstore(end, cache)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, bytes memory key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n let length := mload(key)\n let begin := add(key, 0x20)\n let end := add(begin, length)\n let cache := mload(end)\n mstore(end, slot)\n result := keccak256(begin, add(length, 0x20))\n mstore(end, cache)\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/StorageSlot.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/StorageSlot.sol)\n// This file was procedurally generated from scripts/generate/templates/StorageSlot.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Library for reading and writing primitive types to specific storage slots.\n *\n * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts.\n * This library helps with reading and writing to such slots without the need for inline assembly.\n *\n * The functions in this library return Slot structs that contain a `value` member that can be used to read or write.\n *\n * Example usage to set ERC-1967 implementation slot:\n * ```solidity\n * contract ERC1967 {\n * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot.\n * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;\n *\n * function _getImplementation() internal view returns (address) {\n * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;\n * }\n *\n * function _setImplementation(address newImplementation) internal {\n * require(newImplementation.code.length > 0);\n * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;\n * }\n * }\n * ```\n *\n * TIP: Consider using this library along with {SlotDerivation}.\n */\nlibrary StorageSlot {\n struct AddressSlot {\n address value;\n }\n\n struct BooleanSlot {\n bool value;\n }\n\n struct Bytes32Slot {\n bytes32 value;\n }\n\n struct Uint256Slot {\n uint256 value;\n }\n\n struct Int256Slot {\n int256 value;\n }\n\n struct StringSlot {\n string value;\n }\n\n struct BytesSlot {\n bytes value;\n }\n\n /**\n * @dev Returns an `AddressSlot` with member `value` located at `slot`.\n */\n function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `BooleanSlot` with member `value` located at `slot`.\n */\n function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Bytes32Slot` with member `value` located at `slot`.\n */\n function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Uint256Slot` with member `value` located at `slot`.\n */\n function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Int256Slot` with member `value` located at `slot`.\n */\n function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `StringSlot` with member `value` located at `slot`.\n */\n function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns an `StringSlot` representation of the string storage pointer `store`.\n */\n function getStringSlot(string storage store) internal pure returns (StringSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := store.slot\n }\n }\n\n /**\n * @dev Returns a `BytesSlot` with member `value` located at `slot`.\n */\n function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`.\n */\n function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := store.slot\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/introspection/IERC165.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/IERC165.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-165 standard, as defined in the\n * https://eips.ethereum.org/EIPS/eip-165[ERC].\n *\n * Implementers can declare support of contract interfaces, which can then be\n * queried by others ({ERC165Checker}).\n *\n * For an implementation, see {ERC165}.\n */\ninterface IERC165 {\n /**\n * @dev Returns true if this contract implements the interface defined by\n * `interfaceId`. See the corresponding\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]\n * to learn more about how these ids are created.\n *\n * This function call must use less than 30 000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/Math.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/math/Math.sol)\n\npragma solidity ^0.8.20;\n\nimport {Panic} from \"../Panic.sol\";\nimport {SafeCast} from \"./SafeCast.sol\";\n\n/**\n * @dev Standard math utilities missing in the Solidity language.\n */\nlibrary Math {\n enum Rounding {\n Floor, // Toward negative infinity\n Ceil, // Toward positive infinity\n Trunc, // Toward zero\n Expand // Away from zero\n }\n\n /**\n * @dev Return the 512-bit addition of two uint256.\n *\n * The result is stored in two 256 variables such that sum = high * 2\u00b2\u2075\u2076 + low.\n */\n function add512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {\n assembly (\"memory-safe\") {\n low := add(a, b)\n high := lt(low, a)\n }\n }\n\n /**\n * @dev Return the 512-bit multiplication of two uint256.\n *\n * The result is stored in two 256 variables such that product = high * 2\u00b2\u2075\u2076 + low.\n */\n function mul512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {\n // 512-bit multiply [high low] = x * y. Compute the product mod 2\u00b2\u2075\u2076 and mod 2\u00b2\u2075\u2076 - 1, then use\n // the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256\n // variables such that product = high * 2\u00b2\u2075\u2076 + low.\n assembly (\"memory-safe\") {\n let mm := mulmod(a, b, not(0))\n low := mul(a, b)\n high := sub(sub(mm, low), lt(mm, low))\n }\n }\n\n /**\n * @dev Returns the addition of two unsigned integers, with a success flag (no overflow).\n */\n function tryAdd(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a + b;\n success = c >= a;\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the subtraction of two unsigned integers, with a success flag (no overflow).\n */\n function trySub(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a - b;\n success = c <= a;\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the multiplication of two unsigned integers, with a success flag (no overflow).\n */\n function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a * b;\n assembly (\"memory-safe\") {\n // Only true when the multiplication doesn't overflow\n // (c / a == b) || (a == 0)\n success := or(eq(div(c, a), b), iszero(a))\n }\n // equivalent to: success ? c : 0\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the division of two unsigned integers, with a success flag (no division by zero).\n */\n function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n success = b > 0;\n assembly (\"memory-safe\") {\n // The `DIV` opcode returns zero when the denominator is 0.\n result := div(a, b)\n }\n }\n }\n\n /**\n * @dev Returns the remainder of dividing two unsigned integers, with a success flag (no division by zero).\n */\n function tryMod(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n success = b > 0;\n assembly (\"memory-safe\") {\n // The `MOD` opcode returns zero when the denominator is 0.\n result := mod(a, b)\n }\n }\n }\n\n /**\n * @dev Unsigned saturating addition, bounds to `2\u00b2\u2075\u2076 - 1` instead of overflowing.\n */\n function saturatingAdd(uint256 a, uint256 b) internal pure returns (uint256) {\n (bool success, uint256 result) = tryAdd(a, b);\n return ternary(success, result, type(uint256).max);\n }\n\n /**\n * @dev Unsigned saturating subtraction, bounds to zero instead of overflowing.\n */\n function saturatingSub(uint256 a, uint256 b) internal pure returns (uint256) {\n (, uint256 result) = trySub(a, b);\n return result;\n }\n\n /**\n * @dev Unsigned saturating multiplication, bounds to `2\u00b2\u2075\u2076 - 1` instead of overflowing.\n */\n function saturatingMul(uint256 a, uint256 b) internal pure returns (uint256) {\n (bool success, uint256 result) = tryMul(a, b);\n return ternary(success, result, type(uint256).max);\n }\n\n /**\n * @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant.\n *\n * IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone.\n * However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute\n * one branch when needed, making this function more expensive.\n */\n function ternary(bool condition, uint256 a, uint256 b) internal pure returns (uint256) {\n unchecked {\n // branchless ternary works because:\n // b ^ (a ^ b) == a\n // b ^ 0 == b\n return b ^ ((a ^ b) * SafeCast.toUint(condition));\n }\n }\n\n /**\n * @dev Returns the largest of two numbers.\n */\n function max(uint256 a, uint256 b) internal pure returns (uint256) {\n return ternary(a > b, a, b);\n }\n\n /**\n * @dev Returns the smallest of two numbers.\n */\n function min(uint256 a, uint256 b) internal pure returns (uint256) {\n return ternary(a < b, a, b);\n }\n\n /**\n * @dev Returns the average of two numbers. The result is rounded towards\n * zero.\n */\n function average(uint256 a, uint256 b) internal pure returns (uint256) {\n // (a + b) / 2 can overflow.\n return (a & b) + (a ^ b) / 2;\n }\n\n /**\n * @dev Returns the ceiling of the division of two numbers.\n *\n * This differs from standard division with `/` in that it rounds towards infinity instead\n * of rounding towards zero.\n */\n function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {\n if (b == 0) {\n // Guarantee the same behavior as in a regular Solidity division.\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n\n // The following calculation ensures accurate ceiling division without overflow.\n // Since a is non-zero, (a - 1) / b will not overflow.\n // The largest possible result occurs when (a - 1) / b is type(uint256).max,\n // but the largest value we can obtain is type(uint256).max - 1, which happens\n // when a = type(uint256).max and b = 1.\n unchecked {\n return SafeCast.toUint(a > 0) * ((a - 1) / b + 1);\n }\n }\n\n /**\n * @dev Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or\n * denominator == 0.\n *\n * Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by\n * Uniswap Labs also under MIT license.\n */\n function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {\n unchecked {\n (uint256 high, uint256 low) = mul512(x, y);\n\n // Handle non-overflow cases, 256 by 256 division.\n if (high == 0) {\n // Solidity will revert if denominator == 0, unlike the div opcode on its own.\n // The surrounding unchecked block does not change this fact.\n // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.\n return low / denominator;\n }\n\n // Make sure the result is less than 2\u00b2\u2075\u2076. Also prevents denominator == 0.\n if (denominator <= high) {\n Panic.panic(ternary(denominator == 0, Panic.DIVISION_BY_ZERO, Panic.UNDER_OVERFLOW));\n }\n\n ///////////////////////////////////////////////\n // 512 by 256 division.\n ///////////////////////////////////////////////\n\n // Make division exact by subtracting the remainder from [high low].\n uint256 remainder;\n assembly (\"memory-safe\") {\n // Compute remainder using mulmod.\n remainder := mulmod(x, y, denominator)\n\n // Subtract 256 bit number from 512 bit number.\n high := sub(high, gt(remainder, low))\n low := sub(low, remainder)\n }\n\n // Factor powers of two out of denominator and compute largest power of two divisor of denominator.\n // Always >= 1. See https://cs.stackexchange.com/q/138556/92363.\n\n uint256 twos = denominator & (0 - denominator);\n assembly (\"memory-safe\") {\n // Divide denominator by twos.\n denominator := div(denominator, twos)\n\n // Divide [high low] by twos.\n low := div(low, twos)\n\n // Flip twos such that it is 2\u00b2\u2075\u2076 / twos. If twos is zero, then it becomes one.\n twos := add(div(sub(0, twos), twos), 1)\n }\n\n // Shift in bits from high into low.\n low |= high * twos;\n\n // Invert denominator mod 2\u00b2\u2075\u2076. Now that denominator is an odd number, it has an inverse modulo 2\u00b2\u2075\u2076 such\n // that denominator * inv \u2261 1 mod 2\u00b2\u2075\u2076. Compute the inverse by starting with a seed that is correct for\n // four bits. That is, denominator * inv \u2261 1 mod 2\u2074.\n uint256 inverse = (3 * denominator) ^ 2;\n\n // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also\n // works in modular arithmetic, doubling the correct bits in each step.\n inverse *= 2 - denominator * inverse; // inverse mod 2\u2078\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b9\u2076\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b3\u00b2\n inverse *= 2 - denominator * inverse; // inverse mod 2\u2076\u2074\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b9\u00b2\u2078\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b2\u2075\u2076\n\n // Because the division is now exact we can divide by multiplying with the modular inverse of denominator.\n // This will give us the correct result modulo 2\u00b2\u2075\u2076. Since the preconditions guarantee that the outcome is\n // less than 2\u00b2\u2075\u2076, this is the final result. We don't need to compute the high bits of the result and high\n // is no longer required.\n result = low * inverse;\n return result;\n }\n }\n\n /**\n * @dev Calculates x * y / denominator with full precision, following the selected rounding direction.\n */\n function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {\n return mulDiv(x, y, denominator) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0);\n }\n\n /**\n * @dev Calculates floor(x * y >> n) with full precision. Throws if result overflows a uint256.\n */\n function mulShr(uint256 x, uint256 y, uint8 n) internal pure returns (uint256 result) {\n unchecked {\n (uint256 high, uint256 low) = mul512(x, y);\n if (high >= 1 << n) {\n Panic.panic(Panic.UNDER_OVERFLOW);\n }\n return (high << (256 - n)) | (low >> n);\n }\n }\n\n /**\n * @dev Calculates x * y >> n with full precision, following the selected rounding direction.\n */\n function mulShr(uint256 x, uint256 y, uint8 n, Rounding rounding) internal pure returns (uint256) {\n return mulShr(x, y, n) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, 1 << n) > 0);\n }\n\n /**\n * @dev Calculate the modular multiplicative inverse of a number in Z/nZ.\n *\n * If n is a prime, then Z/nZ is a field. In that case all elements are inversible, except 0.\n * If n is not a prime, then Z/nZ is not a field, and some elements might not be inversible.\n *\n * If the input value is not inversible, 0 is returned.\n *\n * NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Fermat's little theorem and get the\n * inverse using `Math.modExp(a, n - 2, n)`. See {invModPrime}.\n */\n function invMod(uint256 a, uint256 n) internal pure returns (uint256) {\n unchecked {\n if (n == 0) return 0;\n\n // The inverse modulo is calculated using the Extended Euclidean Algorithm (iterative version)\n // Used to compute integers x and y such that: ax + ny = gcd(a, n).\n // When the gcd is 1, then the inverse of a modulo n exists and it's x.\n // ax + ny = 1\n // ax = 1 + (-y)n\n // ax \u2261 1 (mod n) # x is the inverse of a modulo n\n\n // If the remainder is 0 the gcd is n right away.\n uint256 remainder = a % n;\n uint256 gcd = n;\n\n // Therefore the initial coefficients are:\n // ax + ny = gcd(a, n) = n\n // 0a + 1n = n\n int256 x = 0;\n int256 y = 1;\n\n while (remainder != 0) {\n uint256 quotient = gcd / remainder;\n\n (gcd, remainder) = (\n // The old remainder is the next gcd to try.\n remainder,\n // Compute the next remainder.\n // Can't overflow given that (a % gcd) * (gcd // (a % gcd)) <= gcd\n // where gcd is at most n (capped to type(uint256).max)\n gcd - remainder * quotient\n );\n\n (x, y) = (\n // Increment the coefficient of a.\n y,\n // Decrement the coefficient of n.\n // Can overflow, but the result is casted to uint256 so that the\n // next value of y is \"wrapped around\" to a value between 0 and n - 1.\n x - y * int256(quotient)\n );\n }\n\n if (gcd != 1) return 0; // No inverse exists.\n return ternary(x < 0, n - uint256(-x), uint256(x)); // Wrap the result if it's negative.\n }\n }\n\n /**\n * @dev Variant of {invMod}. More efficient, but only works if `p` is known to be a prime greater than `2`.\n *\n * From https://en.wikipedia.org/wiki/Fermat%27s_little_theorem[Fermat's little theorem], we know that if p is\n * prime, then `a**(p-1) \u2261 1 mod p`. As a consequence, we have `a * a**(p-2) \u2261 1 mod p`, which means that\n * `a**(p-2)` is the modular multiplicative inverse of a in Fp.\n *\n * NOTE: this function does NOT check that `p` is a prime greater than `2`.\n */\n function invModPrime(uint256 a, uint256 p) internal view returns (uint256) {\n unchecked {\n return Math.modExp(a, p - 2, p);\n }\n }\n\n /**\n * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m)\n *\n * Requirements:\n * - modulus can't be zero\n * - underlying staticcall to precompile must succeed\n *\n * IMPORTANT: The result is only valid if the underlying call succeeds. When using this function, make\n * sure the chain you're using it on supports the precompiled contract for modular exponentiation\n * at address 0x05 as specified in https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise,\n * the underlying function will succeed given the lack of a revert, but the result may be incorrectly\n * interpreted as 0.\n */\n function modExp(uint256 b, uint256 e, uint256 m) internal view returns (uint256) {\n (bool success, uint256 result) = tryModExp(b, e, m);\n if (!success) {\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n return result;\n }\n\n /**\n * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m).\n * It includes a success flag indicating if the operation succeeded. Operation will be marked as failed if trying\n * to operate modulo 0 or if the underlying precompile reverted.\n *\n * IMPORTANT: The result is only valid if the success flag is true. When using this function, make sure the chain\n * you're using it on supports the precompiled contract for modular exponentiation at address 0x05 as specified in\n * https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise, the underlying function will succeed given the lack\n * of a revert, but the result may be incorrectly interpreted as 0.\n */\n function tryModExp(uint256 b, uint256 e, uint256 m) internal view returns (bool success, uint256 result) {\n if (m == 0) return (false, 0);\n assembly (\"memory-safe\") {\n let ptr := mload(0x40)\n // | Offset | Content | Content (Hex) |\n // |-----------|------------|--------------------------------------------------------------------|\n // | 0x00:0x1f | size of b | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x20:0x3f | size of e | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x40:0x5f | size of m | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x60:0x7f | value of b | 0x<.............................................................b> |\n // | 0x80:0x9f | value of e | 0x<.............................................................e> |\n // | 0xa0:0xbf | value of m | 0x<.............................................................m> |\n mstore(ptr, 0x20)\n mstore(add(ptr, 0x20), 0x20)\n mstore(add(ptr, 0x40), 0x20)\n mstore(add(ptr, 0x60), b)\n mstore(add(ptr, 0x80), e)\n mstore(add(ptr, 0xa0), m)\n\n // Given the result < m, it's guaranteed to fit in 32 bytes,\n // so we can use the memory scratch space located at offset 0.\n success := staticcall(gas(), 0x05, ptr, 0xc0, 0x00, 0x20)\n result := mload(0x00)\n }\n }\n\n /**\n * @dev Variant of {modExp} that supports inputs of arbitrary length.\n */\n function modExp(bytes memory b, bytes memory e, bytes memory m) internal view returns (bytes memory) {\n (bool success, bytes memory result) = tryModExp(b, e, m);\n if (!success) {\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n return result;\n }\n\n /**\n * @dev Variant of {tryModExp} that supports inputs of arbitrary length.\n */\n function tryModExp(\n bytes memory b,\n bytes memory e,\n bytes memory m\n ) internal view returns (bool success, bytes memory result) {\n if (_zeroBytes(m)) return (false, new bytes(0));\n\n uint256 mLen = m.length;\n\n // Encode call args in result and move the free memory pointer\n result = abi.encodePacked(b.length, e.length, mLen, b, e, m);\n\n assembly (\"memory-safe\") {\n let dataPtr := add(result, 0x20)\n // Write result on top of args to avoid allocating extra memory.\n success := staticcall(gas(), 0x05, dataPtr, mload(result), dataPtr, mLen)\n // Overwrite the length.\n // result.length > returndatasize() is guaranteed because returndatasize() == m.length\n mstore(result, mLen)\n // Set the memory pointer after the returned data.\n mstore(0x40, add(dataPtr, mLen))\n }\n }\n\n /**\n * @dev Returns whether the provided byte array is zero.\n */\n function _zeroBytes(bytes memory byteArray) private pure returns (bool) {\n for (uint256 i = 0; i < byteArray.length; ++i) {\n if (byteArray[i] != 0) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded\n * towards zero.\n *\n * This method is based on Newton's method for computing square roots; the algorithm is restricted to only\n * using integer operations.\n */\n function sqrt(uint256 a) internal pure returns (uint256) {\n unchecked {\n // Take care of easy edge cases when a == 0 or a == 1\n if (a <= 1) {\n return a;\n }\n\n // In this function, we use Newton's method to get a root of `f(x) := x\u00b2 - a`. It involves building a\n // sequence x_n that converges toward sqrt(a). For each iteration x_n, we also define the error between\n // the current value as `\u03b5_n = | x_n - sqrt(a) |`.\n //\n // For our first estimation, we consider `e` the smallest power of 2 which is bigger than the square root\n // of the target. (i.e. `2**(e-1) \u2264 sqrt(a) < 2**e`). We know that `e \u2264 128` because `(2\u00b9\u00b2\u2078)\u00b2 = 2\u00b2\u2075\u2076` is\n // bigger than any uint256.\n //\n // By noticing that\n // `2**(e-1) \u2264 sqrt(a) < 2**e \u2192 (2**(e-1))\u00b2 \u2264 a < (2**e)\u00b2 \u2192 2**(2*e-2) \u2264 a < 2**(2*e)`\n // we can deduce that `e - 1` is `log2(a) / 2`. We can thus compute `x_n = 2**(e-1)` using a method similar\n // to the msb function.\n uint256 aa = a;\n uint256 xn = 1;\n\n if (aa >= (1 << 128)) {\n aa >>= 128;\n xn <<= 64;\n }\n if (aa >= (1 << 64)) {\n aa >>= 64;\n xn <<= 32;\n }\n if (aa >= (1 << 32)) {\n aa >>= 32;\n xn <<= 16;\n }\n if (aa >= (1 << 16)) {\n aa >>= 16;\n xn <<= 8;\n }\n if (aa >= (1 << 8)) {\n aa >>= 8;\n xn <<= 4;\n }\n if (aa >= (1 << 4)) {\n aa >>= 4;\n xn <<= 2;\n }\n if (aa >= (1 << 2)) {\n xn <<= 1;\n }\n\n // We now have x_n such that `x_n = 2**(e-1) \u2264 sqrt(a) < 2**e = 2 * x_n`. This implies \u03b5_n \u2264 2**(e-1).\n //\n // We can refine our estimation by noticing that the middle of that interval minimizes the error.\n // If we move x_n to equal 2**(e-1) + 2**(e-2), then we reduce the error to \u03b5_n \u2264 2**(e-2).\n // This is going to be our x_0 (and \u03b5_0)\n xn = (3 * xn) >> 1; // \u03b5_0 := | x_0 - sqrt(a) | \u2264 2**(e-2)\n\n // From here, Newton's method give us:\n // x_{n+1} = (x_n + a / x_n) / 2\n //\n // One should note that:\n // x_{n+1}\u00b2 - a = ((x_n + a / x_n) / 2)\u00b2 - a\n // = ((x_n\u00b2 + a) / (2 * x_n))\u00b2 - a\n // = (x_n\u2074 + 2 * a * x_n\u00b2 + a\u00b2) / (4 * x_n\u00b2) - a\n // = (x_n\u2074 + 2 * a * x_n\u00b2 + a\u00b2 - 4 * a * x_n\u00b2) / (4 * x_n\u00b2)\n // = (x_n\u2074 - 2 * a * x_n\u00b2 + a\u00b2) / (4 * x_n\u00b2)\n // = (x_n\u00b2 - a)\u00b2 / (2 * x_n)\u00b2\n // = ((x_n\u00b2 - a) / (2 * x_n))\u00b2\n // \u2265 0\n // Which proves that for all n \u2265 1, sqrt(a) \u2264 x_n\n //\n // This gives us the proof of quadratic convergence of the sequence:\n // \u03b5_{n+1} = | x_{n+1} - sqrt(a) |\n // = | (x_n + a / x_n) / 2 - sqrt(a) |\n // = | (x_n\u00b2 + a - 2*x_n*sqrt(a)) / (2 * x_n) |\n // = | (x_n - sqrt(a))\u00b2 / (2 * x_n) |\n // = | \u03b5_n\u00b2 / (2 * x_n) |\n // = \u03b5_n\u00b2 / | (2 * x_n) |\n //\n // For the first iteration, we have a special case where x_0 is known:\n // \u03b5_1 = \u03b5_0\u00b2 / | (2 * x_0) |\n // \u2264 (2**(e-2))\u00b2 / (2 * (2**(e-1) + 2**(e-2)))\n // \u2264 2**(2*e-4) / (3 * 2**(e-1))\n // \u2264 2**(e-3) / 3\n // \u2264 2**(e-3-log2(3))\n // \u2264 2**(e-4.5)\n //\n // For the following iterations, we use the fact that, 2**(e-1) \u2264 sqrt(a) \u2264 x_n:\n // \u03b5_{n+1} = \u03b5_n\u00b2 / | (2 * x_n) |\n // \u2264 (2**(e-k))\u00b2 / (2 * 2**(e-1))\n // \u2264 2**(2*e-2*k) / 2**e\n // \u2264 2**(e-2*k)\n xn = (xn + a / xn) >> 1; // \u03b5_1 := | x_1 - sqrt(a) | \u2264 2**(e-4.5) -- special case, see above\n xn = (xn + a / xn) >> 1; // \u03b5_2 := | x_2 - sqrt(a) | \u2264 2**(e-9) -- general case with k = 4.5\n xn = (xn + a / xn) >> 1; // \u03b5_3 := | x_3 - sqrt(a) | \u2264 2**(e-18) -- general case with k = 9\n xn = (xn + a / xn) >> 1; // \u03b5_4 := | x_4 - sqrt(a) | \u2264 2**(e-36) -- general case with k = 18\n xn = (xn + a / xn) >> 1; // \u03b5_5 := | x_5 - sqrt(a) | \u2264 2**(e-72) -- general case with k = 36\n xn = (xn + a / xn) >> 1; // \u03b5_6 := | x_6 - sqrt(a) | \u2264 2**(e-144) -- general case with k = 72\n\n // Because e \u2264 128 (as discussed during the first estimation phase), we know have reached a precision\n // \u03b5_6 \u2264 2**(e-144) < 1. Given we're operating on integers, then we can ensure that xn is now either\n // sqrt(a) or sqrt(a) + 1.\n return xn - SafeCast.toUint(xn > a / xn);\n }\n }\n\n /**\n * @dev Calculates sqrt(a), following the selected rounding direction.\n */\n function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = sqrt(a);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && result * result < a);\n }\n }\n\n /**\n * @dev Return the log in base 2 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n */\n function log2(uint256 x) internal pure returns (uint256 r) {\n // If value has upper 128 bits set, log2 result is at least 128\n r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7;\n // If upper 64 bits of 128-bit half set, add 64 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6;\n // If upper 32 bits of 64-bit half set, add 32 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5;\n // If upper 16 bits of 32-bit half set, add 16 to result\n r |= SafeCast.toUint((x >> r) > 0xffff) << 4;\n // If upper 8 bits of 16-bit half set, add 8 to result\n r |= SafeCast.toUint((x >> r) > 0xff) << 3;\n // If upper 4 bits of 8-bit half set, add 4 to result\n r |= SafeCast.toUint((x >> r) > 0xf) << 2;\n\n // Shifts value right by the current result and use it as an index into this lookup table:\n //\n // | x (4 bits) | index | table[index] = MSB position |\n // |------------|---------|-----------------------------|\n // | 0000 | 0 | table[0] = 0 |\n // | 0001 | 1 | table[1] = 0 |\n // | 0010 | 2 | table[2] = 1 |\n // | 0011 | 3 | table[3] = 1 |\n // | 0100 | 4 | table[4] = 2 |\n // | 0101 | 5 | table[5] = 2 |\n // | 0110 | 6 | table[6] = 2 |\n // | 0111 | 7 | table[7] = 2 |\n // | 1000 | 8 | table[8] = 3 |\n // | 1001 | 9 | table[9] = 3 |\n // | 1010 | 10 | table[10] = 3 |\n // | 1011 | 11 | table[11] = 3 |\n // | 1100 | 12 | table[12] = 3 |\n // | 1101 | 13 | table[13] = 3 |\n // | 1110 | 14 | table[14] = 3 |\n // | 1111 | 15 | table[15] = 3 |\n //\n // The lookup table is represented as a 32-byte value with the MSB positions for 0-15 in the last 16 bytes.\n assembly (\"memory-safe\") {\n r := or(r, byte(shr(r, x), 0x0000010102020202030303030303030300000000000000000000000000000000))\n }\n }\n\n /**\n * @dev Return the log in base 2, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log2(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << result < value);\n }\n }\n\n /**\n * @dev Return the log in base 10 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n */\n function log10(uint256 value) internal pure returns (uint256) {\n uint256 result = 0;\n unchecked {\n if (value >= 10 ** 64) {\n value /= 10 ** 64;\n result += 64;\n }\n if (value >= 10 ** 32) {\n value /= 10 ** 32;\n result += 32;\n }\n if (value >= 10 ** 16) {\n value /= 10 ** 16;\n result += 16;\n }\n if (value >= 10 ** 8) {\n value /= 10 ** 8;\n result += 8;\n }\n if (value >= 10 ** 4) {\n value /= 10 ** 4;\n result += 4;\n }\n if (value >= 10 ** 2) {\n value /= 10 ** 2;\n result += 2;\n }\n if (value >= 10 ** 1) {\n result += 1;\n }\n }\n return result;\n }\n\n /**\n * @dev Return the log in base 10, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log10(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 10 ** result < value);\n }\n }\n\n /**\n * @dev Return the log in base 256 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n *\n * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.\n */\n function log256(uint256 x) internal pure returns (uint256 r) {\n // If value has upper 128 bits set, log2 result is at least 128\n r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7;\n // If upper 64 bits of 128-bit half set, add 64 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6;\n // If upper 32 bits of 64-bit half set, add 32 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5;\n // If upper 16 bits of 32-bit half set, add 16 to result\n r |= SafeCast.toUint((x >> r) > 0xffff) << 4;\n // Add 1 if upper 8 bits of 16-bit half set, and divide accumulated result by 8\n return (r >> 3) | SafeCast.toUint((x >> r) > 0xff);\n }\n\n /**\n * @dev Return the log in base 256, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log256(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << (result << 3) < value);\n }\n }\n\n /**\n * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.\n */\n function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {\n return uint8(rounding) % 2 == 1;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/SafeCast.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SafeCast.sol)\n// This file was procedurally generated from scripts/generate/templates/SafeCast.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Wrappers over Solidity's uintXX/intXX/bool casting operators with added overflow\n * checks.\n *\n * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can\n * easily result in undesired exploitation or bugs, since developers usually\n * assume that overflows raise errors. `SafeCast` restores this intuition by\n * reverting the transaction when such an operation overflows.\n *\n * Using this library instead of the unchecked operations eliminates an entire\n * class of bugs, so it's recommended to use it always.\n */\nlibrary SafeCast {\n /**\n * @dev Value doesn't fit in an uint of `bits` size.\n */\n error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);\n\n /**\n * @dev An int value doesn't fit in an uint of `bits` size.\n */\n error SafeCastOverflowedIntToUint(int256 value);\n\n /**\n * @dev Value doesn't fit in an int of `bits` size.\n */\n error SafeCastOverflowedIntDowncast(uint8 bits, int256 value);\n\n /**\n * @dev An uint value doesn't fit in an int of `bits` size.\n */\n error SafeCastOverflowedUintToInt(uint256 value);\n\n /**\n * @dev Returns the downcasted uint248 from uint256, reverting on\n * overflow (when the input is greater than largest uint248).\n *\n * Counterpart to Solidity's `uint248` operator.\n *\n * Requirements:\n *\n * - input must fit into 248 bits\n */\n function toUint248(uint256 value) internal pure returns (uint248) {\n if (value > type(uint248).max) {\n revert SafeCastOverflowedUintDowncast(248, value);\n }\n return uint248(value);\n }\n\n /**\n * @dev Returns the downcasted uint240 from uint256, reverting on\n * overflow (when the input is greater than largest uint240).\n *\n * Counterpart to Solidity's `uint240` operator.\n *\n * Requirements:\n *\n * - input must fit into 240 bits\n */\n function toUint240(uint256 value) internal pure returns (uint240) {\n if (value > type(uint240).max) {\n revert SafeCastOverflowedUintDowncast(240, value);\n }\n return uint240(value);\n }\n\n /**\n * @dev Returns the downcasted uint232 from uint256, reverting on\n * overflow (when the input is greater than largest uint232).\n *\n * Counterpart to Solidity's `uint232` operator.\n *\n * Requirements:\n *\n * - input must fit into 232 bits\n */\n function toUint232(uint256 value) internal pure returns (uint232) {\n if (value > type(uint232).max) {\n revert SafeCastOverflowedUintDowncast(232, value);\n }\n return uint232(value);\n }\n\n /**\n * @dev Returns the downcasted uint224 from uint256, reverting on\n * overflow (when the input is greater than largest uint224).\n *\n * Counterpart to Solidity's `uint224` operator.\n *\n * Requirements:\n *\n * - input must fit into 224 bits\n */\n function toUint224(uint256 value) internal pure returns (uint224) {\n if (value > type(uint224).max) {\n revert SafeCastOverflowedUintDowncast(224, value);\n }\n return uint224(value);\n }\n\n /**\n * @dev Returns the downcasted uint216 from uint256, reverting on\n * overflow (when the input is greater than largest uint216).\n *\n * Counterpart to Solidity's `uint216` operator.\n *\n * Requirements:\n *\n * - input must fit into 216 bits\n */\n function toUint216(uint256 value) internal pure returns (uint216) {\n if (value > type(uint216).max) {\n revert SafeCastOverflowedUintDowncast(216, value);\n }\n return uint216(value);\n }\n\n /**\n * @dev Returns the downcasted uint208 from uint256, reverting on\n * overflow (when the input is greater than largest uint208).\n *\n * Counterpart to Solidity's `uint208` operator.\n *\n * Requirements:\n *\n * - input must fit into 208 bits\n */\n function toUint208(uint256 value) internal pure returns (uint208) {\n if (value > type(uint208).max) {\n revert SafeCastOverflowedUintDowncast(208, value);\n }\n return uint208(value);\n }\n\n /**\n * @dev Returns the downcasted uint200 from uint256, reverting on\n * overflow (when the input is greater than largest uint200).\n *\n * Counterpart to Solidity's `uint200` operator.\n *\n * Requirements:\n *\n * - input must fit into 200 bits\n */\n function toUint200(uint256 value) internal pure returns (uint200) {\n if (value > type(uint200).max) {\n revert SafeCastOverflowedUintDowncast(200, value);\n }\n return uint200(value);\n }\n\n /**\n * @dev Returns the downcasted uint192 from uint256, reverting on\n * overflow (when the input is greater than largest uint192).\n *\n * Counterpart to Solidity's `uint192` operator.\n *\n * Requirements:\n *\n * - input must fit into 192 bits\n */\n function toUint192(uint256 value) internal pure returns (uint192) {\n if (value > type(uint192).max) {\n revert SafeCastOverflowedUintDowncast(192, value);\n }\n return uint192(value);\n }\n\n /**\n * @dev Returns the downcasted uint184 from uint256, reverting on\n * overflow (when the input is greater than largest uint184).\n *\n * Counterpart to Solidity's `uint184` operator.\n *\n * Requirements:\n *\n * - input must fit into 184 bits\n */\n function toUint184(uint256 value) internal pure returns (uint184) {\n if (value > type(uint184).max) {\n revert SafeCastOverflowedUintDowncast(184, value);\n }\n return uint184(value);\n }\n\n /**\n * @dev Returns the downcasted uint176 from uint256, reverting on\n * overflow (when the input is greater than largest uint176).\n *\n * Counterpart to Solidity's `uint176` operator.\n *\n * Requirements:\n *\n * - input must fit into 176 bits\n */\n function toUint176(uint256 value) internal pure returns (uint176) {\n if (value > type(uint176).max) {\n revert SafeCastOverflowedUintDowncast(176, value);\n }\n return uint176(value);\n }\n\n /**\n * @dev Returns the downcasted uint168 from uint256, reverting on\n * overflow (when the input is greater than largest uint168).\n *\n * Counterpart to Solidity's `uint168` operator.\n *\n * Requirements:\n *\n * - input must fit into 168 bits\n */\n function toUint168(uint256 value) internal pure returns (uint168) {\n if (value > type(uint168).max) {\n revert SafeCastOverflowedUintDowncast(168, value);\n }\n return uint168(value);\n }\n\n /**\n * @dev Returns the downcasted uint160 from uint256, reverting on\n * overflow (when the input is greater than largest uint160).\n *\n * Counterpart to Solidity's `uint160` operator.\n *\n * Requirements:\n *\n * - input must fit into 160 bits\n */\n function toUint160(uint256 value) internal pure returns (uint160) {\n if (value > type(uint160).max) {\n revert SafeCastOverflowedUintDowncast(160, value);\n }\n return uint160(value);\n }\n\n /**\n * @dev Returns the downcasted uint152 from uint256, reverting on\n * overflow (when the input is greater than largest uint152).\n *\n * Counterpart to Solidity's `uint152` operator.\n *\n * Requirements:\n *\n * - input must fit into 152 bits\n */\n function toUint152(uint256 value) internal pure returns (uint152) {\n if (value > type(uint152).max) {\n revert SafeCastOverflowedUintDowncast(152, value);\n }\n return uint152(value);\n }\n\n /**\n * @dev Returns the downcasted uint144 from uint256, reverting on\n * overflow (when the input is greater than largest uint144).\n *\n * Counterpart to Solidity's `uint144` operator.\n *\n * Requirements:\n *\n * - input must fit into 144 bits\n */\n function toUint144(uint256 value) internal pure returns (uint144) {\n if (value > type(uint144).max) {\n revert SafeCastOverflowedUintDowncast(144, value);\n }\n return uint144(value);\n }\n\n /**\n * @dev Returns the downcasted uint136 from uint256, reverting on\n * overflow (when the input is greater than largest uint136).\n *\n * Counterpart to Solidity's `uint136` operator.\n *\n * Requirements:\n *\n * - input must fit into 136 bits\n */\n function toUint136(uint256 value) internal pure returns (uint136) {\n if (value > type(uint136).max) {\n revert SafeCastOverflowedUintDowncast(136, value);\n }\n return uint136(value);\n }\n\n /**\n * @dev Returns the downcasted uint128 from uint256, reverting on\n * overflow (when the input is greater than largest uint128).\n *\n * Counterpart to Solidity's `uint128` operator.\n *\n * Requirements:\n *\n * - input must fit into 128 bits\n */\n function toUint128(uint256 value) internal pure returns (uint128) {\n if (value > type(uint128).max) {\n revert SafeCastOverflowedUintDowncast(128, value);\n }\n return uint128(value);\n }\n\n /**\n * @dev Returns the downcasted uint120 from uint256, reverting on\n * overflow (when the input is greater than largest uint120).\n *\n * Counterpart to Solidity's `uint120` operator.\n *\n * Requirements:\n *\n * - input must fit into 120 bits\n */\n function toUint120(uint256 value) internal pure returns (uint120) {\n if (value > type(uint120).max) {\n revert SafeCastOverflowedUintDowncast(120, value);\n }\n return uint120(value);\n }\n\n /**\n * @dev Returns the downcasted uint112 from uint256, reverting on\n * overflow (when the input is greater than largest uint112).\n *\n * Counterpart to Solidity's `uint112` operator.\n *\n * Requirements:\n *\n * - input must fit into 112 bits\n */\n function toUint112(uint256 value) internal pure returns (uint112) {\n if (value > type(uint112).max) {\n revert SafeCastOverflowedUintDowncast(112, value);\n }\n return uint112(value);\n }\n\n /**\n * @dev Returns the downcasted uint104 from uint256, reverting on\n * overflow (when the input is greater than largest uint104).\n *\n * Counterpart to Solidity's `uint104` operator.\n *\n * Requirements:\n *\n * - input must fit into 104 bits\n */\n function toUint104(uint256 value) internal pure returns (uint104) {\n if (value > type(uint104).max) {\n revert SafeCastOverflowedUintDowncast(104, value);\n }\n return uint104(value);\n }\n\n /**\n * @dev Returns the downcasted uint96 from uint256, reverting on\n * overflow (when the input is greater than largest uint96).\n *\n * Counterpart to Solidity's `uint96` operator.\n *\n * Requirements:\n *\n * - input must fit into 96 bits\n */\n function toUint96(uint256 value) internal pure returns (uint96) {\n if (value > type(uint96).max) {\n revert SafeCastOverflowedUintDowncast(96, value);\n }\n return uint96(value);\n }\n\n /**\n * @dev Returns the downcasted uint88 from uint256, reverting on\n * overflow (when the input is greater than largest uint88).\n *\n * Counterpart to Solidity's `uint88` operator.\n *\n * Requirements:\n *\n * - input must fit into 88 bits\n */\n function toUint88(uint256 value) internal pure returns (uint88) {\n if (value > type(uint88).max) {\n revert SafeCastOverflowedUintDowncast(88, value);\n }\n return uint88(value);\n }\n\n /**\n * @dev Returns the downcasted uint80 from uint256, reverting on\n * overflow (when the input is greater than largest uint80).\n *\n * Counterpart to Solidity's `uint80` operator.\n *\n * Requirements:\n *\n * - input must fit into 80 bits\n */\n function toUint80(uint256 value) internal pure returns (uint80) {\n if (value > type(uint80).max) {\n revert SafeCastOverflowedUintDowncast(80, value);\n }\n return uint80(value);\n }\n\n /**\n * @dev Returns the downcasted uint72 from uint256, reverting on\n * overflow (when the input is greater than largest uint72).\n *\n * Counterpart to Solidity's `uint72` operator.\n *\n * Requirements:\n *\n * - input must fit into 72 bits\n */\n function toUint72(uint256 value) internal pure returns (uint72) {\n if (value > type(uint72).max) {\n revert SafeCastOverflowedUintDowncast(72, value);\n }\n return uint72(value);\n }\n\n /**\n * @dev Returns the downcasted uint64 from uint256, reverting on\n * overflow (when the input is greater than largest uint64).\n *\n * Counterpart to Solidity's `uint64` operator.\n *\n * Requirements:\n *\n * - input must fit into 64 bits\n */\n function toUint64(uint256 value) internal pure returns (uint64) {\n if (value > type(uint64).max) {\n revert SafeCastOverflowedUintDowncast(64, value);\n }\n return uint64(value);\n }\n\n /**\n * @dev Returns the downcasted uint56 from uint256, reverting on\n * overflow (when the input is greater than largest uint56).\n *\n * Counterpart to Solidity's `uint56` operator.\n *\n * Requirements:\n *\n * - input must fit into 56 bits\n */\n function toUint56(uint256 value) internal pure returns (uint56) {\n if (value > type(uint56).max) {\n revert SafeCastOverflowedUintDowncast(56, value);\n }\n return uint56(value);\n }\n\n /**\n * @dev Returns the downcasted uint48 from uint256, reverting on\n * overflow (when the input is greater than largest uint48).\n *\n * Counterpart to Solidity's `uint48` operator.\n *\n * Requirements:\n *\n * - input must fit into 48 bits\n */\n function toUint48(uint256 value) internal pure returns (uint48) {\n if (value > type(uint48).max) {\n revert SafeCastOverflowedUintDowncast(48, value);\n }\n return uint48(value);\n }\n\n /**\n * @dev Returns the downcasted uint40 from uint256, reverting on\n * overflow (when the input is greater than largest uint40).\n *\n * Counterpart to Solidity's `uint40` operator.\n *\n * Requirements:\n *\n * - input must fit into 40 bits\n */\n function toUint40(uint256 value) internal pure returns (uint40) {\n if (value > type(uint40).max) {\n revert SafeCastOverflowedUintDowncast(40, value);\n }\n return uint40(value);\n }\n\n /**\n * @dev Returns the downcasted uint32 from uint256, reverting on\n * overflow (when the input is greater than largest uint32).\n *\n * Counterpart to Solidity's `uint32` operator.\n *\n * Requirements:\n *\n * - input must fit into 32 bits\n */\n function toUint32(uint256 value) internal pure returns (uint32) {\n if (value > type(uint32).max) {\n revert SafeCastOverflowedUintDowncast(32, value);\n }\n return uint32(value);\n }\n\n /**\n * @dev Returns the downcasted uint24 from uint256, reverting on\n * overflow (when the input is greater than largest uint24).\n *\n * Counterpart to Solidity's `uint24` operator.\n *\n * Requirements:\n *\n * - input must fit into 24 bits\n */\n function toUint24(uint256 value) internal pure returns (uint24) {\n if (value > type(uint24).max) {\n revert SafeCastOverflowedUintDowncast(24, value);\n }\n return uint24(value);\n }\n\n /**\n * @dev Returns the downcasted uint16 from uint256, reverting on\n * overflow (when the input is greater than largest uint16).\n *\n * Counterpart to Solidity's `uint16` operator.\n *\n * Requirements:\n *\n * - input must fit into 16 bits\n */\n function toUint16(uint256 value) internal pure returns (uint16) {\n if (value > type(uint16).max) {\n revert SafeCastOverflowedUintDowncast(16, value);\n }\n return uint16(value);\n }\n\n /**\n * @dev Returns the downcasted uint8 from uint256, reverting on\n * overflow (when the input is greater than largest uint8).\n *\n * Counterpart to Solidity's `uint8` operator.\n *\n * Requirements:\n *\n * - input must fit into 8 bits\n */\n function toUint8(uint256 value) internal pure returns (uint8) {\n if (value > type(uint8).max) {\n revert SafeCastOverflowedUintDowncast(8, value);\n }\n return uint8(value);\n }\n\n /**\n * @dev Converts a signed int256 into an unsigned uint256.\n *\n * Requirements:\n *\n * - input must be greater than or equal to 0.\n */\n function toUint256(int256 value) internal pure returns (uint256) {\n if (value < 0) {\n revert SafeCastOverflowedIntToUint(value);\n }\n return uint256(value);\n }\n\n /**\n * @dev Returns the downcasted int248 from int256, reverting on\n * overflow (when the input is less than smallest int248 or\n * greater than largest int248).\n *\n * Counterpart to Solidity's `int248` operator.\n *\n * Requirements:\n *\n * - input must fit into 248 bits\n */\n function toInt248(int256 value) internal pure returns (int248 downcasted) {\n downcasted = int248(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(248, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int240 from int256, reverting on\n * overflow (when the input is less than smallest int240 or\n * greater than largest int240).\n *\n * Counterpart to Solidity's `int240` operator.\n *\n * Requirements:\n *\n * - input must fit into 240 bits\n */\n function toInt240(int256 value) internal pure returns (int240 downcasted) {\n downcasted = int240(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(240, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int232 from int256, reverting on\n * overflow (when the input is less than smallest int232 or\n * greater than largest int232).\n *\n * Counterpart to Solidity's `int232` operator.\n *\n * Requirements:\n *\n * - input must fit into 232 bits\n */\n function toInt232(int256 value) internal pure returns (int232 downcasted) {\n downcasted = int232(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(232, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int224 from int256, reverting on\n * overflow (when the input is less than smallest int224 or\n * greater than largest int224).\n *\n * Counterpart to Solidity's `int224` operator.\n *\n * Requirements:\n *\n * - input must fit into 224 bits\n */\n function toInt224(int256 value) internal pure returns (int224 downcasted) {\n downcasted = int224(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(224, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int216 from int256, reverting on\n * overflow (when the input is less than smallest int216 or\n * greater than largest int216).\n *\n * Counterpart to Solidity's `int216` operator.\n *\n * Requirements:\n *\n * - input must fit into 216 bits\n */\n function toInt216(int256 value) internal pure returns (int216 downcasted) {\n downcasted = int216(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(216, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int208 from int256, reverting on\n * overflow (when the input is less than smallest int208 or\n * greater than largest int208).\n *\n * Counterpart to Solidity's `int208` operator.\n *\n * Requirements:\n *\n * - input must fit into 208 bits\n */\n function toInt208(int256 value) internal pure returns (int208 downcasted) {\n downcasted = int208(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(208, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int200 from int256, reverting on\n * overflow (when the input is less than smallest int200 or\n * greater than largest int200).\n *\n * Counterpart to Solidity's `int200` operator.\n *\n * Requirements:\n *\n * - input must fit into 200 bits\n */\n function toInt200(int256 value) internal pure returns (int200 downcasted) {\n downcasted = int200(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(200, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int192 from int256, reverting on\n * overflow (when the input is less than smallest int192 or\n * greater than largest int192).\n *\n * Counterpart to Solidity's `int192` operator.\n *\n * Requirements:\n *\n * - input must fit into 192 bits\n */\n function toInt192(int256 value) internal pure returns (int192 downcasted) {\n downcasted = int192(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(192, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int184 from int256, reverting on\n * overflow (when the input is less than smallest int184 or\n * greater than largest int184).\n *\n * Counterpart to Solidity's `int184` operator.\n *\n * Requirements:\n *\n * - input must fit into 184 bits\n */\n function toInt184(int256 value) internal pure returns (int184 downcasted) {\n downcasted = int184(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(184, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int176 from int256, reverting on\n * overflow (when the input is less than smallest int176 or\n * greater than largest int176).\n *\n * Counterpart to Solidity's `int176` operator.\n *\n * Requirements:\n *\n * - input must fit into 176 bits\n */\n function toInt176(int256 value) internal pure returns (int176 downcasted) {\n downcasted = int176(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(176, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int168 from int256, reverting on\n * overflow (when the input is less than smallest int168 or\n * greater than largest int168).\n *\n * Counterpart to Solidity's `int168` operator.\n *\n * Requirements:\n *\n * - input must fit into 168 bits\n */\n function toInt168(int256 value) internal pure returns (int168 downcasted) {\n downcasted = int168(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(168, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int160 from int256, reverting on\n * overflow (when the input is less than smallest int160 or\n * greater than largest int160).\n *\n * Counterpart to Solidity's `int160` operator.\n *\n * Requirements:\n *\n * - input must fit into 160 bits\n */\n function toInt160(int256 value) internal pure returns (int160 downcasted) {\n downcasted = int160(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(160, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int152 from int256, reverting on\n * overflow (when the input is less than smallest int152 or\n * greater than largest int152).\n *\n * Counterpart to Solidity's `int152` operator.\n *\n * Requirements:\n *\n * - input must fit into 152 bits\n */\n function toInt152(int256 value) internal pure returns (int152 downcasted) {\n downcasted = int152(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(152, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int144 from int256, reverting on\n * overflow (when the input is less than smallest int144 or\n * greater than largest int144).\n *\n * Counterpart to Solidity's `int144` operator.\n *\n * Requirements:\n *\n * - input must fit into 144 bits\n */\n function toInt144(int256 value) internal pure returns (int144 downcasted) {\n downcasted = int144(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(144, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int136 from int256, reverting on\n * overflow (when the input is less than smallest int136 or\n * greater than largest int136).\n *\n * Counterpart to Solidity's `int136` operator.\n *\n * Requirements:\n *\n * - input must fit into 136 bits\n */\n function toInt136(int256 value) internal pure returns (int136 downcasted) {\n downcasted = int136(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(136, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int128 from int256, reverting on\n * overflow (when the input is less than smallest int128 or\n * greater than largest int128).\n *\n * Counterpart to Solidity's `int128` operator.\n *\n * Requirements:\n *\n * - input must fit into 128 bits\n */\n function toInt128(int256 value) internal pure returns (int128 downcasted) {\n downcasted = int128(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(128, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int120 from int256, reverting on\n * overflow (when the input is less than smallest int120 or\n * greater than largest int120).\n *\n * Counterpart to Solidity's `int120` operator.\n *\n * Requirements:\n *\n * - input must fit into 120 bits\n */\n function toInt120(int256 value) internal pure returns (int120 downcasted) {\n downcasted = int120(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(120, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int112 from int256, reverting on\n * overflow (when the input is less than smallest int112 or\n * greater than largest int112).\n *\n * Counterpart to Solidity's `int112` operator.\n *\n * Requirements:\n *\n * - input must fit into 112 bits\n */\n function toInt112(int256 value) internal pure returns (int112 downcasted) {\n downcasted = int112(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(112, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int104 from int256, reverting on\n * overflow (when the input is less than smallest int104 or\n * greater than largest int104).\n *\n * Counterpart to Solidity's `int104` operator.\n *\n * Requirements:\n *\n * - input must fit into 104 bits\n */\n function toInt104(int256 value) internal pure returns (int104 downcasted) {\n downcasted = int104(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(104, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int96 from int256, reverting on\n * overflow (when the input is less than smallest int96 or\n * greater than largest int96).\n *\n * Counterpart to Solidity's `int96` operator.\n *\n * Requirements:\n *\n * - input must fit into 96 bits\n */\n function toInt96(int256 value) internal pure returns (int96 downcasted) {\n downcasted = int96(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(96, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int88 from int256, reverting on\n * overflow (when the input is less than smallest int88 or\n * greater than largest int88).\n *\n * Counterpart to Solidity's `int88` operator.\n *\n * Requirements:\n *\n * - input must fit into 88 bits\n */\n function toInt88(int256 value) internal pure returns (int88 downcasted) {\n downcasted = int88(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(88, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int80 from int256, reverting on\n * overflow (when the input is less than smallest int80 or\n * greater than largest int80).\n *\n * Counterpart to Solidity's `int80` operator.\n *\n * Requirements:\n *\n * - input must fit into 80 bits\n */\n function toInt80(int256 value) internal pure returns (int80 downcasted) {\n downcasted = int80(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(80, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int72 from int256, reverting on\n * overflow (when the input is less than smallest int72 or\n * greater than largest int72).\n *\n * Counterpart to Solidity's `int72` operator.\n *\n * Requirements:\n *\n * - input must fit into 72 bits\n */\n function toInt72(int256 value) internal pure returns (int72 downcasted) {\n downcasted = int72(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(72, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int64 from int256, reverting on\n * overflow (when the input is less than smallest int64 or\n * greater than largest int64).\n *\n * Counterpart to Solidity's `int64` operator.\n *\n * Requirements:\n *\n * - input must fit into 64 bits\n */\n function toInt64(int256 value) internal pure returns (int64 downcasted) {\n downcasted = int64(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(64, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int56 from int256, reverting on\n * overflow (when the input is less than smallest int56 or\n * greater than largest int56).\n *\n * Counterpart to Solidity's `int56` operator.\n *\n * Requirements:\n *\n * - input must fit into 56 bits\n */\n function toInt56(int256 value) internal pure returns (int56 downcasted) {\n downcasted = int56(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(56, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int48 from int256, reverting on\n * overflow (when the input is less than smallest int48 or\n * greater than largest int48).\n *\n * Counterpart to Solidity's `int48` operator.\n *\n * Requirements:\n *\n * - input must fit into 48 bits\n */\n function toInt48(int256 value) internal pure returns (int48 downcasted) {\n downcasted = int48(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(48, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int40 from int256, reverting on\n * overflow (when the input is less than smallest int40 or\n * greater than largest int40).\n *\n * Counterpart to Solidity's `int40` operator.\n *\n * Requirements:\n *\n * - input must fit into 40 bits\n */\n function toInt40(int256 value) internal pure returns (int40 downcasted) {\n downcasted = int40(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(40, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int32 from int256, reverting on\n * overflow (when the input is less than smallest int32 or\n * greater than largest int32).\n *\n * Counterpart to Solidity's `int32` operator.\n *\n * Requirements:\n *\n * - input must fit into 32 bits\n */\n function toInt32(int256 value) internal pure returns (int32 downcasted) {\n downcasted = int32(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(32, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int24 from int256, reverting on\n * overflow (when the input is less than smallest int24 or\n * greater than largest int24).\n *\n * Counterpart to Solidity's `int24` operator.\n *\n * Requirements:\n *\n * - input must fit into 24 bits\n */\n function toInt24(int256 value) internal pure returns (int24 downcasted) {\n downcasted = int24(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(24, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int16 from int256, reverting on\n * overflow (when the input is less than smallest int16 or\n * greater than largest int16).\n *\n * Counterpart to Solidity's `int16` operator.\n *\n * Requirements:\n *\n * - input must fit into 16 bits\n */\n function toInt16(int256 value) internal pure returns (int16 downcasted) {\n downcasted = int16(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(16, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int8 from int256, reverting on\n * overflow (when the input is less than smallest int8 or\n * greater than largest int8).\n *\n * Counterpart to Solidity's `int8` operator.\n *\n * Requirements:\n *\n * - input must fit into 8 bits\n */\n function toInt8(int256 value) internal pure returns (int8 downcasted) {\n downcasted = int8(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(8, value);\n }\n }\n\n /**\n * @dev Converts an unsigned uint256 into a signed int256.\n *\n * Requirements:\n *\n * - input must be less than or equal to maxInt256.\n */\n function toInt256(uint256 value) internal pure returns (int256) {\n // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive\n if (value > uint256(type(int256).max)) {\n revert SafeCastOverflowedUintToInt(value);\n }\n return int256(value);\n }\n\n /**\n * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump.\n */\n function toUint(bool b) internal pure returns (uint256 u) {\n assembly (\"memory-safe\") {\n u := iszero(iszero(b))\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/structs/EnumerableSet.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/structs/EnumerableSet.sol)\n// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.\n\npragma solidity ^0.8.20;\n\nimport {Arrays} from \"../Arrays.sol\";\n\n/**\n * @dev Library for managing\n * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive\n * types.\n *\n * Sets have the following properties:\n *\n * - Elements are added, removed, and checked for existence in constant time\n * (O(1)).\n * - Elements are enumerated in O(n). No guarantees are made on the ordering.\n * - Set can be cleared (all elements removed) in O(n).\n *\n * ```solidity\n * contract Example {\n * // Add the library methods\n * using EnumerableSet for EnumerableSet.AddressSet;\n *\n * // Declare a set state variable\n * EnumerableSet.AddressSet private mySet;\n * }\n * ```\n *\n * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)\n * and `uint256` (`UintSet`) are supported.\n *\n * [WARNING]\n * ====\n * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure\n * unusable.\n * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.\n *\n * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an\n * array of EnumerableSet.\n * ====\n */\nlibrary EnumerableSet {\n // To implement this library for multiple types with as little code\n // repetition as possible, we write it in terms of a generic Set type with\n // bytes32 values.\n // The Set implementation uses private functions, and user-facing\n // implementations (such as AddressSet) are just wrappers around the\n // underlying Set.\n // This means that we can only create new EnumerableSets for types that fit\n // in bytes32.\n\n struct Set {\n // Storage of set values\n bytes32[] _values;\n // Position is the index of the value in the `values` array plus 1.\n // Position 0 is used to mean a value is not in the set.\n mapping(bytes32 value => uint256) _positions;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function _add(Set storage set, bytes32 value) private returns (bool) {\n if (!_contains(set, value)) {\n set._values.push(value);\n // The value is stored at length-1, but we add 1 to all indexes\n // and use 0 as a sentinel value\n set._positions[value] = set._values.length;\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function _remove(Set storage set, bytes32 value) private returns (bool) {\n // We cache the value's position to prevent multiple reads from the same storage slot\n uint256 position = set._positions[value];\n\n if (position != 0) {\n // Equivalent to contains(set, value)\n // To delete an element from the _values array in O(1), we swap the element to delete with the last one in\n // the array, and then remove the last element (sometimes called as 'swap and pop').\n // This modifies the order of the array, as noted in {at}.\n\n uint256 valueIndex = position - 1;\n uint256 lastIndex = set._values.length - 1;\n\n if (valueIndex != lastIndex) {\n bytes32 lastValue = set._values[lastIndex];\n\n // Move the lastValue to the index where the value to delete is\n set._values[valueIndex] = lastValue;\n // Update the tracked position of the lastValue (that was just moved)\n set._positions[lastValue] = position;\n }\n\n // Delete the slot where the moved value was stored\n set._values.pop();\n\n // Delete the tracked position for the deleted slot\n delete set._positions[value];\n\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function _clear(Set storage set) private {\n uint256 len = _length(set);\n for (uint256 i = 0; i < len; ++i) {\n delete set._positions[set._values[i]];\n }\n Arrays.unsafeSetLength(set._values, 0);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function _contains(Set storage set, bytes32 value) private view returns (bool) {\n return set._positions[value] != 0;\n }\n\n /**\n * @dev Returns the number of values on the set. O(1).\n */\n function _length(Set storage set) private view returns (uint256) {\n return set._values.length;\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function _at(Set storage set, uint256 index) private view returns (bytes32) {\n return set._values[index];\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function _values(Set storage set) private view returns (bytes32[] memory) {\n return set._values;\n }\n\n // Bytes32Set\n\n struct Bytes32Set {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _add(set._inner, value);\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _remove(set._inner, value);\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function clear(Bytes32Set storage set) internal {\n _clear(set._inner);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {\n return _contains(set._inner, value);\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(Bytes32Set storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {\n return _at(set._inner, index);\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {\n bytes32[] memory store = _values(set._inner);\n bytes32[] memory result;\n\n assembly (\"memory-safe\") {\n result := store\n }\n\n return result;\n }\n\n // AddressSet\n\n struct AddressSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(AddressSet storage set, address value) internal returns (bool) {\n return _add(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(AddressSet storage set, address value) internal returns (bool) {\n return _remove(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function clear(AddressSet storage set) internal {\n _clear(set._inner);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(AddressSet storage set, address value) internal view returns (bool) {\n return _contains(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(AddressSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(AddressSet storage set, uint256 index) internal view returns (address) {\n return address(uint160(uint256(_at(set._inner, index))));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(AddressSet storage set) internal view returns (address[] memory) {\n bytes32[] memory store = _values(set._inner);\n address[] memory result;\n\n assembly (\"memory-safe\") {\n result := store\n }\n\n return result;\n }\n\n // UintSet\n\n struct UintSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(UintSet storage set, uint256 value) internal returns (bool) {\n return _add(set._inner, bytes32(value));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(UintSet storage set, uint256 value) internal returns (bool) {\n return _remove(set._inner, bytes32(value));\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function clear(UintSet storage set) internal {\n _clear(set._inner);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(UintSet storage set, uint256 value) internal view returns (bool) {\n return _contains(set._inner, bytes32(value));\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(UintSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(UintSet storage set, uint256 index) internal view returns (uint256) {\n return uint256(_at(set._inner, index));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(UintSet storage set) internal view returns (uint256[] memory) {\n bytes32[] memory store = _values(set._inner);\n uint256[] memory result;\n\n assembly (\"memory-safe\") {\n result := store\n }\n\n return result;\n }\n}\n" + } + }, + "settings": { + "evmVersion": "paris", + "libraries": {}, + "metadata": { "appendCBOR": true, "bytecodeHash": "none", "useLiteralContent": false }, + "optimizer": { "enabled": true, "runs": 50000 }, + "outputSelection": { + "contracts/interfaces/IAdvancedPoolHooks.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/IBurnMintERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/IPool.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/IPoolV1V2.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/IPoolV2.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/IRMN.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/IRouter.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/libraries/Client.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/libraries/FeeTokenHandler.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/libraries/FinalityCodec.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/libraries/Pool.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/libraries/RateLimiter.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/pools/BurnMintTokenPool.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/pools/BurnMintTokenPoolAbstract.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/pools/TokenPool.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2Step.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/IOwnable.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC1363.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC165.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/IERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Metadata.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/utils/SafeERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Arrays.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Comparators.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Panic.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/SlotDerivation.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/StorageSlot.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/introspection/IERC165.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/Math.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/SafeCast.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/structs/EnumerableSet.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + } + }, + "remappings": [ + "forge-std/=node_modules/@chainlink/contracts/src/v0.8/vendor/forge-std/src/", + "@chainlink/policy-management/=node_modules/@chainlink/ace/packages/policy-management/src/", + "@chainlink/contracts/=node_modules/@chainlink/contracts/", + "@openzeppelin/contracts@4.8.3/=node_modules/@openzeppelin/contracts-4.8.3/", + "@openzeppelin/contracts@5.3.0/=node_modules/@openzeppelin/contracts-5.3.0/" + ], + "viaIR": true + } +} diff --git a/ccip-sdk/src/verify/fixtures/CrossChainPoolToken.abi.json b/ccip-sdk/src/verify/fixtures/CrossChainPoolToken.abi.json new file mode 100644 index 00000000..adc5a2cc --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/CrossChainPoolToken.abi.json @@ -0,0 +1,2495 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "tokenParams", + "type": "tuple", + "internalType": "struct BaseERC20.ConstructorParams", + "components": [ + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "symbol", + "type": "string", + "internalType": "string" + }, + { + "name": "maxSupply", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "preMint", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "preMintRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "decimals", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "ccipAdmin", + "type": "address", + "internalType": "address" + } + ] + }, + { + "name": "advancedPoolHooks", + "type": "address", + "internalType": "address" + }, + { + "name": "rmnProxy", + "type": "address", + "internalType": "address" + }, + { + "name": "router", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addRemotePool", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "allowance", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "applyChainUpdates", + "inputs": [ + { + "name": "remoteChainSelectorsToRemove", + "type": "uint64[]", + "internalType": "uint64[]" + }, + { + "name": "chainsToAdd", + "type": "tuple[]", + "internalType": "struct TokenPool.ChainUpdate[]", + "components": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddresses", + "type": "bytes[]", + "internalType": "bytes[]" + }, + { + "name": "remoteTokenAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "outboundRateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "applyTokenTransferFeeConfigUpdates", + "inputs": [ + { + "name": "tokenTransferFeeConfigArgs", + "type": "tuple[]", + "internalType": "struct TokenPool.TokenTransferFeeConfigArgs[]", + "components": [ + { + "name": "destChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "tokenTransferFeeConfig", + "type": "tuple", + "internalType": "struct IPoolV2.TokenTransferFeeConfig", + "components": [ + { + "name": "destGasOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destBytesOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "fastFinalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "fastFinalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + } + ] + } + ] + }, + { + "name": "disableTokenTransferFeeConfigs", + "type": "uint64[]", + "internalType": "uint64[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [ + { + "name": "_decimals", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAdvancedPoolHooks", + "inputs": [], + "outputs": [ + { + "name": "advancedPoolHook", + "type": "address", + "internalType": "contract IAdvancedPoolHooks" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAllowedFinalityConfig", + "inputs": [], + "outputs": [ + { + "name": "allowedFinality", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getCCIPAdmin", + "inputs": [], + "outputs": [ + { + "name": "ccipAdmin", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getCurrentRateLimiterState", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "fastFinality", + "type": "bool", + "internalType": "bool" + } + ], + "outputs": [ + { + "name": "outboundRateLimiterState", + "type": "tuple", + "internalType": "struct RateLimiter.TokenBucket", + "components": [ + { + "name": "tokens", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "lastUpdated", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterState", + "type": "tuple", + "internalType": "struct RateLimiter.TokenBucket", + "components": [ + { + "name": "tokens", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "lastUpdated", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getDynamicConfig", + "inputs": [], + "outputs": [ + { + "name": "router", + "type": "address", + "internalType": "address" + }, + { + "name": "rateLimitAdmin", + "type": "address", + "internalType": "address" + }, + { + "name": "feeAdmin", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getFee", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "destChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "requestedFinalityConfig", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "feeUSDCents", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "destGasOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destBytesOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "tokenFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRemotePools", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes[]", + "internalType": "bytes[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRemoteToken", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRequiredCCVs", + "inputs": [ + { + "name": "localToken", + "type": "address", + "internalType": "address" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "sourceDenominatedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "requestedFinalityConfig", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "direction", + "type": "uint8", + "internalType": "enum IPoolV2.MessageDirection" + } + ], + "outputs": [ + { + "name": "requiredCCVs", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRmnProxy", + "inputs": [], + "outputs": [ + { + "name": "rmnProxy", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getSupportedChains", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint64[]", + "internalType": "uint64[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getToken", + "inputs": [], + "outputs": [ + { + "name": "token", + "type": "address", + "internalType": "contract IERC20" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTokenDecimals", + "inputs": [], + "outputs": [ + { + "name": "decimals", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTokenTransferFeeConfig", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "destChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "feeConfig", + "type": "tuple", + "internalType": "struct IPoolV2.TokenTransferFeeConfig", + "components": [ + { + "name": "destGasOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destBytesOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "fastFinalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "fastFinalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isRemotePool", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isSupportedChain", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isSupportedToken", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "lockOrBurn", + "inputs": [ + { + "name": "lockOrBurnIn", + "type": "tuple", + "internalType": "struct Pool.LockOrBurnInV1", + "components": [ + { + "name": "receiver", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "originalSender", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + } + ] + } + ], + "outputs": [ + { + "name": "lockOrBurnOutV1", + "type": "tuple", + "internalType": "struct Pool.LockOrBurnOutV1", + "components": [ + { + "name": "destTokenAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "destPoolData", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "lockOrBurn", + "inputs": [ + { + "name": "lockOrBurnIn", + "type": "tuple", + "internalType": "struct Pool.LockOrBurnInV1", + "components": [ + { + "name": "receiver", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "originalSender", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + } + ] + }, + { + "name": "requestedFinalityConfig", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "tokenArgs", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct Pool.LockOrBurnOutV1", + "components": [ + { + "name": "destTokenAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "destPoolData", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "destTokenAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "maxSupply", + "inputs": [], + "outputs": [ + { + "name": "_maxSupply", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "releaseOrMint", + "inputs": [ + { + "name": "releaseOrMintIn", + "type": "tuple", + "internalType": "struct Pool.ReleaseOrMintInV1", + "components": [ + { + "name": "originalSender", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "sourceDenominatedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + }, + { + "name": "sourcePoolAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "sourcePoolData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "offchainTokenData", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "requestedFinalityConfig", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct Pool.ReleaseOrMintOutV1", + "components": [ + { + "name": "destinationAmount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "releaseOrMint", + "inputs": [ + { + "name": "releaseOrMintIn", + "type": "tuple", + "internalType": "struct Pool.ReleaseOrMintInV1", + "components": [ + { + "name": "originalSender", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "sourceDenominatedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + }, + { + "name": "sourcePoolAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "sourcePoolData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "offchainTokenData", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct Pool.ReleaseOrMintOutV1", + "components": [ + { + "name": "destinationAmount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeRemotePool", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setAllowedFinalityConfig", + "inputs": [ + { + "name": "allowedFinality", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setCCIPAdmin", + "inputs": [ + { + "name": "newAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setDynamicConfig", + "inputs": [ + { + "name": "router", + "type": "address", + "internalType": "address" + }, + { + "name": "rateLimitAdmin", + "type": "address", + "internalType": "address" + }, + { + "name": "feeAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setRateLimitConfig", + "inputs": [ + { + "name": "rateLimitConfigArgs", + "type": "tuple[]", + "internalType": "struct TokenPool.RateLimitConfigArgs[]", + "components": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "fastFinality", + "type": "bool", + "internalType": "bool" + }, + { + "name": "outboundRateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transfer", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "typeAndVersion", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "updateAdvancedPoolHooks", + "inputs": [ + { + "name": "newHook", + "type": "address", + "internalType": "contract IAdvancedPoolHooks" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "withdrawFeeTokens", + "inputs": [ + { + "name": "feeTokens", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "AdvancedPoolHooksUpdated", + "inputs": [ + { + "name": "oldHook", + "type": "address", + "indexed": false, + "internalType": "contract IAdvancedPoolHooks" + }, + { + "name": "newHook", + "type": "address", + "indexed": false, + "internalType": "contract IAdvancedPoolHooks" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Approval", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "CCIPAdminTransferred", + "inputs": [ + { + "name": "previousAdmin", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newAdmin", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ChainAdded", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + }, + { + "name": "remoteToken", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "outboundRateLimiterConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ChainRemoved", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DynamicConfigSet", + "inputs": [ + { + "name": "router", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "rateLimitAdmin", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "feeAdmin", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FastFinalityInboundRateLimitConsumed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FastFinalityOutboundRateLimitConsumed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FeeTokenWithdrawn", + "inputs": [ + { + "name": "receiver", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "feeToken", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FinalityConfigSet", + "inputs": [ + { + "name": "allowedFinality", + "type": "bytes4", + "indexed": false, + "internalType": "bytes4" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "InboundRateLimitConsumed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "LockedOrBurned", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OutboundRateLimitConsumed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferRequested", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RateLimitConfigured", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "fastFinality", + "type": "bool", + "indexed": false, + "internalType": "bool" + }, + { + "name": "outboundRateLimiterConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ReleasedOrMinted", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "recipient", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RemotePoolAdded", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RemotePoolRemoved", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "TokenTransferFeeConfigDeleted", + "inputs": [ + { + "name": "destChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "TokenTransferFeeConfigUpdated", + "inputs": [ + { + "name": "destChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "tokenTransferFeeConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct IPoolV2.TokenTransferFeeConfig", + "components": [ + { + "name": "destGasOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destBytesOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "fastFinalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "fastFinalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "BucketOverfilled", + "inputs": [] + }, + { + "type": "error", + "name": "CallerIsNotARampOnRouter", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "CallerIsNotOwnerOrFeeAdmin", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "CannotRenounceCCIPAdmin", + "inputs": [] + }, + { + "type": "error", + "name": "CannotTransferToSelf", + "inputs": [] + }, + { + "type": "error", + "name": "ChainAlreadyExists", + "inputs": [ + { + "name": "chainSelector", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "ChainNotAllowed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "CursedByRMN", + "inputs": [] + }, + { + "type": "error", + "name": "DisabledNonZeroRateLimit", + "inputs": [ + { + "name": "config", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ] + }, + { + "type": "error", + "name": "ERC20InsufficientAllowance", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "allowance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "needed", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ERC20InsufficientBalance", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + }, + { + "name": "balance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "needed", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidApprover", + "inputs": [ + { + "name": "approver", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidReceiver", + "inputs": [ + { + "name": "receiver", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidSender", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidSpender", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "InvalidDecimalArgs", + "inputs": [ + { + "name": "expected", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "actual", + "type": "uint8", + "internalType": "uint8" + } + ] + }, + { + "type": "error", + "name": "InvalidRateLimitRate", + "inputs": [ + { + "name": "rateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ] + }, + { + "type": "error", + "name": "InvalidRemoteChainDecimals", + "inputs": [ + { + "name": "sourcePoolData", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "InvalidRemotePoolForChain", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "InvalidRequestedFinality", + "inputs": [ + { + "name": "requestedFinality", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "allowedFinality", + "type": "bytes4", + "internalType": "bytes4" + } + ] + }, + { + "type": "error", + "name": "InvalidSourcePoolAddress", + "inputs": [ + { + "name": "sourcePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "InvalidToken", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "InvalidTokenTransferFeeConfig", + "inputs": [ + { + "name": "destChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "InvalidTransferFeeBps", + "inputs": [ + { + "name": "bps", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "MaxSupplyExceeded", + "inputs": [ + { + "name": "supplyAfterMint", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxSupply", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "MustBeProposedOwner", + "inputs": [] + }, + { + "type": "error", + "name": "NonExistentChain", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "OnlyCCIPAdmin", + "inputs": [] + }, + { + "type": "error", + "name": "OnlyCallableByOwner", + "inputs": [] + }, + { + "type": "error", + "name": "OverflowDetected", + "inputs": [ + { + "name": "remoteDecimals", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "localDecimals", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "remoteAmount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "OwnerCannotBeZero", + "inputs": [] + }, + { + "type": "error", + "name": "PoolAlreadyAdded", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "PreMintAddressNotSet", + "inputs": [] + }, + { + "type": "error", + "name": "PreMintRecipientSetWithZeroPreMint", + "inputs": [ + { + "name": "preMintRecipient", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "RequestedFinalityCanOnlyHaveOneMode", + "inputs": [ + { + "name": "encodedFinality", + "type": "bytes4", + "internalType": "bytes4" + } + ] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "TokenMaxCapacityExceeded", + "inputs": [ + { + "name": "capacity", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "tokenAddress", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "TokenRateLimitReached", + "inputs": [ + { + "name": "minWaitInSeconds", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "tokenAddress", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "Unauthorized", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ZeroAddressInvalid", + "inputs": [] + }, + { + "type": "error", + "name": "ZeroAddressNotAllowed", + "inputs": [] + } +] diff --git a/ccip-sdk/src/verify/fixtures/CrossChainPoolToken.standard-input.json b/ccip-sdk/src/verify/fixtures/CrossChainPoolToken.standard-input.json new file mode 100644 index 00000000..29048b65 --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/CrossChainPoolToken.standard-input.json @@ -0,0 +1,147 @@ +{ + "language": "Solidity", + "sources": { + "contracts/interfaces/IAdvancedPoolHooks.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.24;\n\nimport {Pool} from \"../libraries/Pool.sol\";\nimport {IPoolV2} from \"./IPoolV2.sol\";\n\n/// @notice Interface for AdvancedPoolHooks contract. Implementations may contain no-op logic.\ninterface IAdvancedPoolHooks {\n /// @notice Preflight check before lock or burn operation.\n /// @param lockOrBurnIn The lock or burn input parameters.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Additional token arguments.\n /// @param amountPostFee The amount after token pool bps-based fees have been deducted.\n /// @dev This function may revert if the preflight check fails. This means the transaction is rolled back on source.\n function preflightCheck(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs,\n uint256 amountPostFee\n ) external;\n\n /// @notice Postflight check before releasing or minting tokens.\n /// @param releaseOrMintIn The release or mint output parameters.\n /// @param localAmount The local amount to be released or minted.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @dev This function may revert if the postflight check fails. This means the transaction is unexecutable until\n /// the issue is resolved.\n function postflightCheck(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n uint256 localAmount,\n bytes4 requestedFinalityConfig\n ) external;\n\n /// @notice Returns the set of required CCVs for transfers in a specific direction.\n /// @param localToken The address of the local token.\n /// @param remoteChainSelector The remote chain selector for this transfer.\n /// @param amount The amount being transferred.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param extraData Direction-specific payload forwarded by the caller (e.g. token args or source pool data).\n /// @param direction The direction of the transfer (Inbound or Outbound).\n /// @return requiredCCVs Set of required CCV addresses.\n function getRequiredCCVs(\n address localToken,\n uint64 remoteChainSelector,\n uint256 amount,\n bytes4 requestedFinalityConfig,\n bytes calldata extraData,\n IPoolV2.MessageDirection direction\n ) external view returns (address[] memory requiredCCVs);\n}\n" + }, + "contracts/interfaces/IGetCCIPAdmin.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface IGetCCIPAdmin {\n /// @notice Returns the admin of the token.\n /// @dev This method is named to never conflict with existing methods.\n function getCCIPAdmin() external view returns (address);\n}\n" + }, + "contracts/interfaces/IPool.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport {Pool} from \"../libraries/Pool.sol\";\n\nimport {IERC165} from \"@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol\";\n\n/// @notice Shared public interface for multiple V1 pool types.\n/// Each pool type handles a different child token model e.g. lock/unlock, mint/burn.\ninterface IPoolV1 is IERC165 {\n /// @notice Lock tokens into the pool or burn the tokens.\n /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain.\n /// @return lockOrBurnOut Encoded data fields for the processing of tokens on the destination chain.\n function lockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn\n ) external returns (Pool.LockOrBurnOutV1 memory lockOrBurnOut);\n\n /// @notice Releases or mints tokens to the receiver address.\n /// @param releaseOrMintIn All data required to release or mint tokens.\n /// @return releaseOrMintOut The amount of tokens released or minted on the local chain, denominated\n /// in the local token's decimals.\n /// @dev The offRamp asserts that the balanceOf of the receiver has been incremented by exactly the number\n /// of tokens that is returned in ReleaseOrMintOutV1.destinationAmount. If the amounts do not match, the tx reverts.\n function releaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn\n ) external returns (Pool.ReleaseOrMintOutV1 memory);\n\n /// @notice Checks whether a remote chain is supported in the token pool.\n /// @param remoteChainSelector The selector of the remote chain.\n /// @return true if the given chain is a permissioned remote chain.\n function isSupportedChain(\n uint64 remoteChainSelector\n ) external view returns (bool);\n\n /// @notice Returns if the token pool supports the given token.\n /// @param token The address of the token.\n /// @return true if the token is supported by the pool.\n function isSupportedToken(\n address token\n ) external view returns (bool);\n}\n" + }, + "contracts/interfaces/IPoolV1V2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport {IPoolV1} from \"./IPool.sol\";\nimport {IPoolV2} from \"./IPoolV2.sol\";\n\ninterface IPoolV1V2 is IPoolV1, IPoolV2 {}\n" + }, + "contracts/interfaces/IPoolV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport {Pool} from \"../libraries/Pool.sol\";\n\nimport {IERC165} from \"@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol\";\n\n/// @notice Shared public interface for multiple V2 pool types.\n/// Each pool type handles a different child token model e.g. lock/release, mint/burn.\ninterface IPoolV2 is IERC165 {\n struct TokenTransferFeeConfig {\n uint32 destGasOverhead; // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e Gas charged to execute the token transfer on the destination chain.\n uint32 destBytesOverhead; // \u2502 Data availability bytes.\n uint32 finalityFeeUSDCents; // \u2502 Fee to charge for token transfer with default (wait-for-finality) finality, multiples of 0.01 USD.\n uint32 fastFinalityFeeUSDCents; // \u2502 Fee to charge for token transfer with fast finality (FTF), multiples of 0.01 USD.\n // \u2502 The following two fee is deducted from the transferred asset, not added on top.\n uint16 finalityTransferFeeBps; // \u2502 Fee in basis points for default finality transfers [0-10_000].\n uint16 fastFinalityTransferFeeBps; //\u2502 Fee in basis points for custom finality transfers [0-10_000].\n bool isEnabled; // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f Whether this config is enabled.\n }\n\n enum MessageDirection {\n Outbound,\n Inbound\n }\n\n /// @notice Lock tokens into the pool or burn the tokens.\n /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Additional token arguments.\n /// @return lockOrBurnOut Encoded data fields for the processing of tokens on the destination chain.\n /// @return destTokenAmount The amount of tokens that will be set in TokenTransferV1.amount to be released/mint on destination.\n function lockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n ) external returns (Pool.LockOrBurnOutV1 memory lockOrBurnOut, uint256 destTokenAmount);\n\n /// @notice Releases or mints tokens on the destination chain.\n /// @param releaseOrMintIn Encoded data fields for the processing of tokens on the destination chain.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @return releaseOrMintOut Encoded data fields describing the result of the release or mint.\n function releaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n bytes4 requestedFinalityConfig\n ) external returns (Pool.ReleaseOrMintOutV1 memory releaseOrMintOut);\n\n /// @notice Returns the set of required CCVs for transfers in a given direction.\n /// @param localToken The address of the local token.\n /// @param remoteChainSelector The chain selector of the remote chain.\n /// @param sourceAmount The source-denominated amount of tokens to be transferred.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param extraData Direction-specific payload forwarded by the caller (e.g. token args or source pool data).\n /// @param direction Whether CCVs are required for outbound (source -> remote) or inbound (remote -> destination) transfers.\n /// @return requiredCCVs A set of addresses representing the required CCVs.\n function getRequiredCCVs(\n address localToken,\n uint64 remoteChainSelector,\n uint256 sourceAmount,\n bytes4 requestedFinalityConfig,\n bytes calldata extraData,\n MessageDirection direction\n ) external view returns (address[] memory requiredCCVs);\n\n /// @notice Returns the fee overrides for transferring the pool's token to a destination chain.\n /// @param localToken The address of the local token.\n /// @param destChainSelector The chain selector of the destination chain.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Additional token argument from the CCIP message.\n /// @return feeConfig the fee configuration for transferring the token to the destination chain.\n function getTokenTransferFeeConfig(\n address localToken,\n uint64 destChainSelector,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n ) external view returns (TokenTransferFeeConfig memory feeConfig);\n\n /// @notice Returns the pool fee parameters that will apply to a transfer.\n /// @param localToken The local asset being transferred.\n /// @param destChainSelector The destination lane selector.\n /// @param amount The amount of tokens being bridged on this lane.\n /// @param feeToken The token used to pay feeUSDCents.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Opaque token arguments supplied by the caller.\n /// @return feeUSDCents Flat fee charged in USD cents (crumbs) for this transfer.\n /// @return destGasOverhead Destination gas charged for accounting in the cost model.\n /// @return destBytesOverhead Destination calldata size attributed to the transfer.\n /// @return tokenFeeBps Bps charged in token units. Value of zero implies no in-token fee.\n /// @return isEnabled Whether the pool's fee config is enabled. If false, OnRamp should use FeeQuoter defaults.\n function getFee(\n address localToken,\n uint64 destChainSelector,\n uint256 amount,\n address feeToken,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n )\n external\n view\n returns (uint256 feeUSDCents, uint32 destGasOverhead, uint32 destBytesOverhead, uint16 tokenFeeBps, bool isEnabled);\n\n /// @notice Gets the token address on the remote chain.\n /// @param remoteChainSelector Remote chain selector.\n /// @dev To support non-evm chains, this value is encoded into bytes.\n function getRemoteToken(\n uint64 remoteChainSelector\n ) external view returns (bytes memory);\n}\n" + }, + "contracts/interfaces/IRMN.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n/// @notice This interface contains the only RMN-related functions that might be used on-chain by other CCIP contracts.\ninterface IRMN {\n /// @notice gets the current set of cursed subjects.\n /// @return subjects the list of cursed subjects.\n function getCursedSubjects() external view returns (bytes16[] memory subjects);\n\n /// @notice Iff there is an active global or legacy curse, this function returns true.\n /// @return bool true if there is an active global curse.\n function isCursed() external view returns (bool);\n\n /// @notice Iff there is an active global curse, or an active curse for `subject`, this function returns true.\n /// @param subject To check whether a particular chain is cursed, set to bytes16(uint128(chainSelector)).\n /// @return bool true if the provided subject is cursed *or* if there is an active global curse.\n function isCursed(\n bytes16 subject\n ) external view returns (bool);\n}\n" + }, + "contracts/interfaces/IRouter.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport {Client} from \"../libraries/Client.sol\";\n\ninterface IRouter {\n error OnlyOffRamp();\n\n /// @notice Route the message to its intended receiver contract.\n /// @param message Client.Any2EVMMessage struct.\n /// @param gasForCallExactCheck of params for exec.\n /// @param gasLimit set of params for exec.\n /// @param receiver set of params for exec.\n /// @dev if the receiver is a contracts that signals support for CCIP execution through EIP-165.\n /// the contract is called. If not, only tokens are transferred.\n /// @return success A boolean value indicating whether the ccip message was received without errors.\n /// @return retBytes A bytes array containing return data form CCIP receiver.\n /// @return gasUsed the gas used by the external customer call. Does not include any overhead.\n function routeMessage(\n Client.Any2EVMMessage calldata message,\n uint16 gasForCallExactCheck,\n uint256 gasLimit,\n address receiver\n ) external returns (bool success, bytes memory retBytes, uint256 gasUsed);\n\n /// @notice Returns the configured onRamp for a specific destination chain.\n /// @param destChainSelector The destination chain Id to get the onRamp for.\n /// @return onRampAddress The address of the onRamp.\n function getOnRamp(\n uint64 destChainSelector\n ) external view returns (address onRampAddress);\n\n /// @notice Return true if the given offRamp is a configured offRamp for the given source chain.\n /// @param sourceChainSelector The source chain selector to check.\n /// @param offRamp The address of the offRamp to check.\n function isOffRamp(\n uint64 sourceChainSelector,\n address offRamp\n ) external view returns (bool isOffRamp);\n}\n" + }, + "contracts/libraries/Client.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n// End consumer library.\nlibrary Client {\n struct EVMTokenAmount {\n address token; // token address on the local chain.\n uint256 amount; // Amount of tokens.\n }\n\n struct Any2EVMMessage {\n bytes32 messageId; // MessageId corresponding to ccipSend on source.\n uint64 sourceChainSelector; // Source chain selector.\n bytes sender; // abi.encode(address) on EVM source chains; abi.decode(sender, (address)) to recover.\n bytes data; // payload sent in original message.\n EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation.\n }\n\n // If extraArgs is empty bytes, the default is 200k gas limit.\n struct EVM2AnyMessage {\n bytes receiver; // abi.encode(receiver address) for dest EVM chains.\n bytes data; // Data payload.\n EVMTokenAmount[] tokenAmounts; // Token transfers.\n address feeToken; // Address of feeToken. address(0) means you will send msg.value.\n bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV3).\n }\n\n /// @notice Tag to indicate no execution on the destination chain. Execution will need to be done manually.\n /// @dev Preimage for this tag is: keccak256(\"NO_EXECUTION_TAG\")[:4]\n bytes4 public constant NO_EXECUTION_TAG = 0xeba517d2;\n address public constant NO_EXECUTION_ADDRESS = address(bytes20(NO_EXECUTION_TAG));\n\n // ================================================================\n // \u2502 Legacy \u2502\n // ================================================================\n\n // Tag to indicate only a gas limit. Only usable for EVM as destination chain.\n bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9;\n\n struct EVMExtraArgsV1 {\n uint256 gasLimit;\n }\n\n function _argsToBytes(\n EVMExtraArgsV1 memory extraArgs\n ) internal pure returns (bytes memory bts) {\n return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs);\n }\n\n // Tag to indicate a gas limit (or dest chain equivalent processing units) and Out Of Order Execution. This tag is\n // available for multiple chain families. If there is no chain family specific tag, this is the default available\n // for a chain.\n // Note: not available for Solana or Sui VM based chains.\n bytes4 public constant GENERIC_EXTRA_ARGS_V2_TAG = 0x181dcf10;\n\n /// @param gasLimit: gas limit for the callback on the destination chain.\n /// @param allowOutOfOrderExecution: if true, it indicates that the message can be executed in any order relative to\n /// other messages from the same sender. This value's default varies by chain. On some chains, a particular value is\n /// enforced, meaning if the expected value is not set, the message request will revert.\n /// @dev Fully compatible with the previously existing EVMExtraArgsV2.\n struct GenericExtraArgsV2 {\n uint256 gasLimit;\n bool allowOutOfOrderExecution;\n }\n\n // Extra args tag for chains that use the Sui VM.\n bytes4 public constant SUI_EXTRA_ARGS_V1_TAG = 0x21ea4ca9;\n\n // Extra args tag for chains that use the Solana VM.\n bytes4 public constant SVM_EXTRA_ARGS_V1_TAG = 0x1f3b3aba;\n\n struct SVMExtraArgsV1 {\n uint32 computeUnits;\n uint64 accountIsWritableBitmap;\n bool allowOutOfOrderExecution;\n bytes32 tokenReceiver;\n // Additional accounts needed for execution of CCIP receiver. Must be empty if message.receiver is zero.\n // Token transfer related accounts are specified in the token pool lookup table on SVM.\n bytes32[] accounts;\n }\n\n /// @dev The maximum number of accounts that can be passed in SVMExtraArgs.\n uint256 public constant SVM_EXTRA_ARGS_MAX_ACCOUNTS = 64;\n\n /// @dev The expected static payload size of a token transfer when Borsh encoded and submitted to SVM.\n /// TokenPool extra data and offchain data sizes are dynamic, and should be accounted for separately.\n uint256 public constant SVM_TOKEN_TRANSFER_DATA_OVERHEAD = (4 + 32) // source_pool\n + 32 // token_address\n + 4 // gas_amount\n + 4 // extra_data overhead\n + 32 // amount\n + 32 // size of the token lookup table account\n + 32 // token-related accounts in the lookup table, over-estimated to 32, typically between 11 - 13\n + 32 // token account belonging to the token receiver, e.g ATA, not included in the token lookup table\n + 32 // per-chain token pool config, not included in the token lookup table\n + 32 // per-chain token billing config, not always included in the token lookup table\n + 32; // OffRamp pool signer PDA, not included in the token lookup table\n\n /// @dev Number of overhead accounts needed for message execution on SVM.\n /// @dev These are message.receiver, and the OffRamp Signer PDA specific to the receiver.\n uint256 public constant SVM_MESSAGING_ACCOUNTS_OVERHEAD = 2;\n\n /// @dev The size of each SVM account address in bytes.\n uint256 public constant SVM_ACCOUNT_BYTE_SIZE = 32;\n\n struct SuiExtraArgsV1 {\n uint256 gasLimit;\n bool allowOutOfOrderExecution;\n bytes32 tokenReceiver;\n bytes32[] receiverObjectIds;\n }\n\n /// @dev The expected static payload size of a token transfer when BCS encoded and submitted to SUI.\n /// TokenPool extra data and offchain data sizes are dynamic, and should be accounted for separately.\n uint256 public constant SUI_TOKEN_TRANSFER_DATA_OVERHEAD = (4 + 32) // source_pool, 4 bytes for length, 32 bytes for address\n + 32 // dest_token_address\n + 4 // dest_gas_amount\n + 4 // extra_data length, the contents are calculated separately\n + 32; // amount\n\n /// @dev Number of overhead accounts needed for message execution on SUI.\n /// @dev This is the message.receiver.\n uint256 public constant SUI_MESSAGING_ACCOUNTS_OVERHEAD = 1;\n\n /// @dev The maximum number of receiver object ids that can be passed in SuiExtraArgs.\n uint256 public constant SUI_EXTRA_ARGS_MAX_RECEIVER_OBJECT_IDS = 64;\n\n /// @dev The size of each SUI account address in bytes.\n uint256 public constant SUI_ACCOUNT_BYTE_SIZE = 32;\n\n function _argsToBytes(\n GenericExtraArgsV2 memory extraArgs\n ) internal pure returns (bytes memory bts) {\n return abi.encodeWithSelector(GENERIC_EXTRA_ARGS_V2_TAG, extraArgs);\n }\n\n function _svmArgsToBytes(\n SVMExtraArgsV1 memory extraArgs\n ) internal pure returns (bytes memory bts) {\n return abi.encodeWithSelector(SVM_EXTRA_ARGS_V1_TAG, extraArgs);\n }\n\n function _suiArgsToBytes(\n SuiExtraArgsV1 memory extraArgs\n ) internal pure returns (bytes memory bts) {\n return abi.encodeWithSelector(SUI_EXTRA_ARGS_V1_TAG, extraArgs);\n }\n}\n" + }, + "contracts/libraries/FeeTokenHandler.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {IERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/utils/SafeERC20.sol\";\n\nlibrary FeeTokenHandler {\n using SafeERC20 for IERC20;\n\n error ZeroAddressNotAllowed();\n\n event FeeTokenWithdrawn(address indexed receiver, address indexed feeToken, uint256 amount);\n\n /// @notice Withdraws the outstanding fee token balances to the fee aggregator.\n /// @param feeTokens The fee tokens to withdraw.\n /// @param feeAggregator The address to withdraw the fee tokens to, cannot be the zero address.\n function _withdrawFeeTokens(\n address[] calldata feeTokens,\n address feeAggregator\n ) internal {\n if (feeAggregator == address(0)) revert ZeroAddressNotAllowed();\n\n for (uint256 i = 0; i < feeTokens.length; ++i) {\n IERC20 feeToken = IERC20(feeTokens[i]);\n uint256 feeTokenBalance = feeToken.balanceOf(address(this));\n\n if (feeTokenBalance > 0) {\n feeToken.safeTransfer(feeAggregator, feeTokenBalance);\n\n emit FeeTokenWithdrawn(feeAggregator, address(feeToken), feeTokenBalance);\n }\n }\n }\n}\n" + }, + "contracts/libraries/FinalityCodec.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\n/// @notice This library provides encoding and validation for finality parameters used in cross-chain transfers.\n/// @dev this codec supports all the bit flags, even though some might not be assigned any meaning yet. This is\n/// intentional to allow for future flexibility.\n///\n/// @dev Bit layout of the `bytes4` finality value (32 bits, MSB on the left):\n///\n/// Bit: 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 | 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0\n/// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+\n/// | R | R | R | R | R | R | R | R | R | R | R | R | R | R | R | S | block depth (16 bits) |\n/// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+\n/// \\_______________________________ _____________________________/ \\______________________________ _____________________________/\n/// \\/ \\/\n/// flags (16 bits) depth (16 bits)\n/// max = 65535 (0xFFFF)\n///\n/// S (bit 16) = WAIT_FOR_SAFE_FLAG \u2014 wait for the `safe` tag.\n/// R (bits 17-31) = Reserved for future flags (currently unassigned; accepted on the wire).\n/// Reserved bits may be assigned in the future, read the docs for the latest bit definitions.\n///\n/// Special values:\n/// 0x00000000 WAIT_FOR_FINALITY_FLAG \u2014 wait for full finality (safest, default).\n/// 0x00010000 WAIT_FOR_SAFE_FLAG \u2014 wait for the `safe` head (bit 16 set, no depth).\n/// 0x00000001..0x0000FFFF \u2014 wait for N blocks.\nlibrary FinalityCodec {\n error InvalidRequestedFinality(bytes4 requestedFinality, bytes4 allowedFinality);\n /// @notice Requested finality must be exactly one mode: any of the flag bits or a block depth with no upper flag bits.\n /// It cannot combine a flag with a block depth.\n error RequestedFinalityCanOnlyHaveOneMode(bytes4 encodedFinality);\n\n /// @notice The block depth is stored in the lower 16 bits, leaving the upper 16 bits for flags.\n /// For more security, users should wait for finality instead (bytes4(0)).\n uint256 public constant BLOCK_DEPTH_BITS = 16;\n /// @notice The maximum block depth that can be encoded in the finality params.\n uint16 public constant MAX_BLOCK_DEPTH = type(uint16).max;\n /// @notice The block depth mask to extract the block depth from the finality params.\n bytes4 public constant BLOCK_DEPTH_MASK = bytes4(uint32(MAX_BLOCK_DEPTH));\n\n /// @notice The finality flag for waiting for finality is 0, this is the safest option. Any block depth that's deeper\n /// than finality will fall back to finality, meaning a very deep block depth will not be more secure than finality.\n bytes4 public constant WAIT_FOR_FINALITY_FLAG = bytes4(0);\n /// @notice Signals to wait for the `safe` tag.\n bytes4 public constant WAIT_FOR_SAFE_FLAG = bytes4(uint32(1 << BLOCK_DEPTH_BITS));\n\n /// @notice Helper to encode block depth into the finality params. Returns WAIT_FOR_FINALITY_FLAG if the block depth\n /// is zero.\n /// @param blockDepth The block depth to encode into the finality params.\n /// @return The encoded finality params with the block depth.\n function _encodeBlockDepth(\n uint16 blockDepth\n ) internal pure returns (bytes4) {\n return bytes4(uint32(blockDepth));\n }\n\n /// @notice Helper to encode the `safe` tag plus a block depth into the finality params.\n /// NOTE: this format is only allowed for allowed finality, not requested finality, as requested finality can only\n /// contain a single flag or block depth, but allowed finality can contain multiple.\n /// @param blockDepth The block depth to encode into the finality params.\n /// @return The encoded finality params with the `safe` tag and block depth.\n function _encodeBlockDepthAndSafeFlag(\n uint16 blockDepth\n ) internal pure returns (bytes4) {\n return _encodeBlockDepth(blockDepth) | WAIT_FOR_SAFE_FLAG;\n }\n\n /// @notice Validates requested finality: either `bytes4(0)`, exactly one set bit among the upper flag bits, or a pure\n /// block depth (no flag bits, depth in `1..MAX_BLOCK_DEPTH`). Never a flag combined with a non-zero depth. Unknown\n /// flags are accepted here for wire compatibility; pools/CCVs reject modes they do not implement.\n /// @param encodedFinality The encoded finality params to validate.\n function _validateRequestedFinality(\n bytes4 encodedFinality\n ) internal pure {\n // Waiting for finality is always valid.\n if (encodedFinality == WAIT_FOR_FINALITY_FLAG) {\n return;\n }\n bool hasBlockDepth = encodedFinality & BLOCK_DEPTH_MASK != 0;\n uint256 activeModes = hasBlockDepth ? 1 : 0; // If it has depth, it counts as one active mode.\n\n uint32 flags = uint32(encodedFinality) >> BLOCK_DEPTH_BITS;\n if (flags != 0) {\n for (uint256 i = 0; i < 16; ++i) {\n if ((flags & (1 << i)) != 0) {\n activeModes += 1;\n }\n }\n }\n // There must be exactly one active mode: either a block depth or a single flag. Selecting multiple modes is only\n // allowed for `allowedFinality` set by Pools, CCVs, etc., but not for `requestedFinality` set by senders.\n if (activeModes != 1) {\n revert RequestedFinalityCanOnlyHaveOneMode(encodedFinality);\n }\n }\n\n /// @notice Validates that `requestedFinality` is well-formed and permitted by `allowedFinality`.\n /// @param requestedFinality The requested finality params to check.\n /// @param allowedFinality The allowed finality params to check against.\n function _ensureRequestedFinalityAllowed(\n bytes4 requestedFinality,\n bytes4 allowedFinality\n ) internal pure {\n // Finality is always allowed.\n if (requestedFinality == WAIT_FOR_FINALITY_FLAG) {\n return;\n }\n\n // Validate the structural shape of the requested finality, as it is only allowed to signal one mode.\n _validateRequestedFinality(requestedFinality);\n\n // If any of the flags match, the request is allowed only when it has no depth field (flag-only request).\n if (((requestedFinality >> BLOCK_DEPTH_BITS) & (allowedFinality >> BLOCK_DEPTH_BITS)) != 0) {\n return;\n }\n // Otherwise, it must be block-depth based.\n uint32 requestedBlockDepth = uint32(requestedFinality & BLOCK_DEPTH_MASK);\n uint32 allowedBlockDepth = uint32(allowedFinality & BLOCK_DEPTH_MASK);\n if (allowedBlockDepth == 0 || requestedBlockDepth < allowedBlockDepth) {\n revert InvalidRequestedFinality(requestedFinality, allowedFinality);\n }\n }\n}\n" + }, + "contracts/libraries/Pool.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n/// @notice This library contains various token pool functions to aid constructing the return data.\nlibrary Pool {\n // The tag used to signal support for the pool v1 standard.\n // bytes4(keccak256(\"CCIP_POOL_V1\"))\n bytes4 public constant CCIP_POOL_V1 = 0xaff2afbf;\n\n // The number of bytes in the return data for a pool v1 releaseOrMint call.\n // This should match the size of the ReleaseOrMintOutV1 struct.\n uint16 public constant CCIP_POOL_V1_RET_BYTES = 32;\n\n // The default max number of bytes in the return data for a pool v1 lockOrBurn call.\n // This data can be used to send information to the destination chain token pool. Can be overwritten\n // in the TokenTransferFeeConfig.destBytesOverhead if more data is required.\n uint32 public constant CCIP_LOCK_OR_BURN_V1_RET_BYTES = 32;\n\n struct LockOrBurnInV1 {\n bytes receiver; // The recipient of the tokens on the destination chain. For EVM source chains, this is abi-encoded (32 bytes).\n uint64 remoteChainSelector; // \u2500\u256e The chain ID of the destination chain.\n address originalSender; // \u2500\u2500\u2500\u2500\u2500\u256f The original sender of the tx on the source chain.\n uint256 amount; // The amount of tokens to lock or burn, denominated in the source token's decimals.\n address localToken; // The address on this chain of the token to lock or burn.\n }\n\n struct LockOrBurnOutV1 {\n // The address of the destination token, abi encoded in the case of EVM chains.\n // This value is UNTRUSTED as any pool owner can return whatever value they want.\n bytes destTokenAddress;\n // Optional pool data to be transferred to the destination chain. Be default this is capped at\n // CCIP_LOCK_OR_BURN_V1_RET_BYTES bytes. If more data is required, the TokenTransferFeeConfig.destBytesOverhead\n // has to be set for the specific token.\n bytes destPoolData;\n }\n\n struct ReleaseOrMintInV1 {\n bytes originalSender; // The original sender of the tx on the source chain.\n uint64 remoteChainSelector; // \u2500\u2500\u2500\u256e The chain ID of the source chain.\n address receiver; // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f The recipient of the tokens on the destination chain.\n uint256 sourceDenominatedAmount; // The amount of tokens to release or mint, denominated in the source token's decimals.\n address localToken; // The address on this chain of the token to release or mint.\n /// @dev WARNING: sourcePoolAddress should be checked prior to any processing of funds. Make sure it matches the\n /// expected pool address for the given remoteChainSelector.\n bytes sourcePoolAddress; // The address of the source pool, abi encoded in the case of EVM chains.\n bytes sourcePoolData; // The data received from the source pool to process the release or mint.\n /// @dev WARNING: offchainTokenData is untrusted data.\n bytes offchainTokenData; // The offchain data to process the release or mint.\n }\n\n struct ReleaseOrMintOutV1 {\n // The number of tokens released or minted on the destination chain, denominated in the local token's decimals.\n // This value is expected to be equal to the ReleaseOrMintInV1.amount in the case where the source and destination\n // chain have the same number of decimals.\n uint256 destinationAmount;\n }\n}\n" + }, + "contracts/libraries/RateLimiter.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.4;\n\n/// @notice Implements Token Bucket rate limiting.\n/// @dev uint128 is safe for rate limiter state.\n/// - For USD value rate limiting, it can adequately store USD value in 18 decimals.\n/// - For ERC20 token amount rate limiting, all tokens that will be listed will have at most a supply of uint128.max\n/// tokens, and it will therefore not overflow the bucket. In exceptional scenarios where tokens consumed may be larger\n/// than uint128, e.g. compromised issuer, an enabled RateLimiter will check and revert.\nlibrary RateLimiter {\n error BucketOverfilled();\n error TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress);\n error TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress);\n error InvalidRateLimitRate(Config rateLimiterConfig);\n error DisabledNonZeroRateLimit(Config config);\n\n struct TokenBucket {\n uint128 tokens; // \u2500\u2500\u2500\u2500\u256e Current number of tokens that are in the bucket.\n uint32 lastUpdated; // \u2502 Timestamp in seconds of the last token refill, good for 100+ years.\n bool isEnabled; // \u2500\u2500\u2500\u2500\u256f Indication whether the rate limiting is enabled or not.\n uint128 capacity; // \u2500\u2500\u256e Maximum number of tokens that can be in the bucket.\n uint128 rate; // \u2500\u2500\u2500\u2500\u2500\u2500\u256f Number of tokens per second that the bucket is refilled.\n }\n\n struct Config {\n bool isEnabled; // Indication whether the rate limiting should be enabled.\n uint128 capacity; // \u2500\u2500\u256e Specifies the capacity of the rate limiter.\n uint128 rate; // \u2500\u2500\u2500\u2500\u2500\u256f Specifies the rate of the rate limiter.\n }\n\n /// @notice _consume removes the given tokens from the pool, lowering the rate tokens allowed to be\n /// consumed for subsequent calls.\n /// @param requestTokens The total tokens to be consumed from the bucket.\n /// @param tokenAddress The token to consume capacity for, use 0x0 to indicate aggregate value capacity.\n /// @dev Reverts when requestTokens exceeds bucket capacity or available tokens in the bucket.\n /// @dev emits removal of requestTokens if requestTokens is > 0.\n function _consume(\n TokenBucket storage s_bucket,\n uint256 requestTokens,\n address tokenAddress\n ) internal {\n // If there is no value to remove or rate limiting is turned off, skip this step to reduce gas usage.\n if (!s_bucket.isEnabled || requestTokens == 0) {\n return;\n }\n\n uint256 tokens = s_bucket.tokens;\n uint256 capacity = s_bucket.capacity;\n uint256 timeDiff = block.timestamp - s_bucket.lastUpdated;\n\n if (timeDiff != 0) {\n if (tokens > capacity) revert BucketOverfilled();\n\n // Refill tokens when arriving at a new block time.\n tokens = _calculateRefill(capacity, tokens, timeDiff, s_bucket.rate);\n\n s_bucket.lastUpdated = uint32(block.timestamp);\n }\n\n if (capacity < requestTokens) {\n revert TokenMaxCapacityExceeded(capacity, requestTokens, tokenAddress);\n }\n if (tokens < requestTokens) {\n uint256 rate = s_bucket.rate;\n if (rate == 0) {\n // No tokens will ever be refilled. Check is required to avoid division by zero later.\n revert TokenRateLimitReached(type(uint256).max, tokens, tokenAddress);\n }\n // Wait required until the bucket is refilled enough to accept this value, round up to next higher second.\n // Consume is not guaranteed to succeed after wait time passes if there is competing traffic.\n // This acts as a lower bound of wait time.\n uint256 minWaitInSeconds = ((requestTokens - tokens) + (rate - 1)) / rate;\n\n revert TokenRateLimitReached(minWaitInSeconds, tokens, tokenAddress);\n }\n tokens -= requestTokens;\n\n // Downcast is safe here, as tokens is not larger than capacity.\n s_bucket.tokens = uint128(tokens);\n }\n\n /// @notice Gets the token bucket with its values for the block it was requested at.\n /// @return The token bucket.\n function _currentTokenBucketState(\n TokenBucket memory bucket\n ) internal view returns (TokenBucket memory) {\n // We update the bucket to reflect the status at the exact time of the call. This means we might need to refill a\n // part of the bucket based on the time that has passed since the last update.\n bucket.tokens =\n uint128(_calculateRefill(bucket.capacity, bucket.tokens, block.timestamp - bucket.lastUpdated, bucket.rate));\n bucket.lastUpdated = uint32(block.timestamp);\n return bucket;\n }\n\n /// @notice Sets the rate limited config.\n /// @param s_bucket The token bucket.\n /// @param config The new config.\n function _setTokenBucketConfig(\n TokenBucket storage s_bucket,\n Config memory config\n ) internal {\n if (config.isEnabled) {\n if (config.rate > config.capacity) {\n revert InvalidRateLimitRate(config);\n }\n } else {\n if (config.rate != 0 || config.capacity != 0) {\n revert DisabledNonZeroRateLimit(config);\n }\n }\n\n s_bucket.isEnabled = config.isEnabled;\n s_bucket.tokens = config.capacity;\n s_bucket.capacity = config.capacity;\n s_bucket.rate = config.rate;\n s_bucket.lastUpdated = uint32(block.timestamp);\n }\n\n /// @notice Calculate refilled tokens.\n /// @param capacity bucket capacity.\n /// @param tokens current bucket tokens.\n /// @param timeDiff block time difference since last refill.\n /// @param rate bucket refill rate.\n /// @return the value of tokens after refill.\n function _calculateRefill(\n uint256 capacity,\n uint256 tokens,\n uint256 timeDiff,\n uint256 rate\n ) private pure returns (uint256) {\n return _min(capacity, tokens + timeDiff * rate);\n }\n\n /// @notice Return the smallest of two integers.\n /// @param a first int.\n /// @param b second int.\n /// @return smallest.\n function _min(\n uint256 a,\n uint256 b\n ) internal pure returns (uint256) {\n return a < b ? a : b;\n }\n}\n" + }, + "contracts/pools/CrossChainPoolToken.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.24;\n\nimport {BaseERC20} from \"../tokens/BaseERC20.sol\";\nimport {TokenPool} from \"./TokenPool.sol\";\n\nimport {ERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol\";\n\n/// @notice A CCIP token pool that is also an ERC20 token. This allows the pool to burn/mint without needing to manage\n/// roles for a separate token contract.\n/// @dev This contract inherits its access control from TokenPool, meaning it uses an `owner` role with 2-step ownership\n/// transfers. There's also a separate `ccipAdmin` role which can be used to register with the CCIP token admin registry\n/// but has no other special powers, and can only be transferred by the owner. The owner role can also be used to\n/// register the token in the token admin registry.\ncontract CrossChainPoolToken is TokenPool, BaseERC20 {\n function typeAndVersion() external pure virtual override returns (string memory) {\n return \"CrossChainPoolToken 2.0.0\";\n }\n\n constructor(\n BaseERC20.ConstructorParams memory tokenParams,\n address advancedPoolHooks,\n address rmnProxy,\n address router\n )\n BaseERC20(tokenParams)\n TokenPool(IERC20(address(this)), tokenParams.decimals, advancedPoolHooks, rmnProxy, router)\n {}\n\n /// @notice Burns tokens held by the pool. The Router transfers tokens to\n /// this contract before the OnRamp calls lockOrBurn, so the burn is from self.\n /// @param amount The amount of tokens to burn.\n function _lockOrBurn(\n uint64, // remoteChainSelector\n uint256 amount\n ) internal virtual override {\n _burn(address(this), amount);\n }\n\n /// @notice Mints tokens to the receiver.\n /// @param receiver The address to mint tokens to.\n /// @param amount The amount of tokens to mint.\n function _releaseOrMint(\n address receiver,\n uint256 amount,\n uint64 // remoteChainSelector\n ) internal virtual override {\n _mint(receiver, amount);\n }\n\n /// @dev Overrides BaseERC20._update to allow this contract to receive its own tokens.\n /// The CCIP Router transfers tokens to the pool (which IS this contract) before\n /// lockOrBurn is called, so transfers to address(this) must be permitted.\n /// @dev This function must reflect any changes made in BaseERC20._update, which it currently does by adding the\n /// supply check.\n function _update(\n address from,\n address to,\n uint256 value\n ) internal virtual override {\n // Update first, then check the total supply.\n ERC20._update(from, to, value);\n\n // If `from` is address(0), this is a mint, so we need to check the total supply against the max supply.\n if (from == address(0)) {\n _assertMaxSupply();\n }\n }\n\n /// @notice Signals which version of the pool interface is supported.\n /// @param interfaceId The interface identifier, as specified in ERC-165.\n /// @return True if the contract implements the requested interface, false otherwise.\n function supportsInterface(\n bytes4 interfaceId\n ) public view virtual override(BaseERC20, TokenPool) returns (bool) {\n return BaseERC20.supportsInterface(interfaceId) || TokenPool.supportsInterface(interfaceId);\n }\n\n /// @notice Overrides the default CCIP admin role setter to require the caller to be the owner.\n /// @param newAdmin The address of the new CCIP admin.\n function setCCIPAdmin(\n address newAdmin\n ) external virtual override onlyOwner {\n _setCCIPAdmin(newAdmin);\n }\n}\n" + }, + "contracts/pools/TokenPool.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.24;\n\nimport {IAdvancedPoolHooks} from \"../interfaces/IAdvancedPoolHooks.sol\";\nimport {IPoolV1} from \"../interfaces/IPool.sol\";\nimport {IPoolV1V2} from \"../interfaces/IPoolV1V2.sol\";\nimport {IPoolV2} from \"../interfaces/IPoolV2.sol\";\nimport {IRMN} from \"../interfaces/IRMN.sol\";\nimport {IRouter} from \"../interfaces/IRouter.sol\";\n\nimport {FeeTokenHandler} from \"../libraries/FeeTokenHandler.sol\";\nimport {FinalityCodec} from \"../libraries/FinalityCodec.sol\";\nimport {Pool} from \"../libraries/Pool.sol\";\nimport {RateLimiter} from \"../libraries/RateLimiter.sol\";\nimport {Ownable2StepMsgSender} from \"@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol\";\n\nimport {IERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol\";\nimport {IERC20Metadata} from \"@openzeppelin/contracts@5.3.0/token/ERC20/extensions/IERC20Metadata.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/utils/SafeERC20.sol\";\nimport {IERC165} from \"@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol\";\nimport {EnumerableSet} from \"@openzeppelin/contracts@5.3.0/utils/structs/EnumerableSet.sol\";\n\n/// @notice Base abstract class with common functions for all token pools.\n/// A token pool serves as isolated place for holding tokens and token specific logic\n/// that may execute as tokens move across the bridge.\n/// @dev This pool supports different decimals on different chains but using this feature could impact the total number\n/// of tokens in circulation. Since all of the tokens are locked/burned on the source, and a rounded amount is\n/// minted/released on the destination, the number of tokens minted/released could be less than the number of tokens\n/// burned/locked. This is because the source chain does not know about the destination token decimals. This is not a\n/// problem if the decimals are the same on both chains.\n///\n/// Example:\n/// Assume there is a token with 6 decimals on chain A and 3 decimals on chain B.\n/// - 1.234567 tokens are burned on chain A.\n/// - 1.234 tokens are minted on chain B.\n/// When sending the 1.234 tokens back to chain A, you will receive 1.234000 tokens on chain A, effectively losing\n/// 0.000567 tokens.\n/// In the case of a burnMint pool on chain A, these funds are burned in the pool on chain A.\n/// In the case of a lockRelease pool on chain A, these funds accumulate in the pool on chain A.\nabstract contract TokenPool is IPoolV1V2, Ownable2StepMsgSender {\n using EnumerableSet for EnumerableSet.Bytes32Set;\n using EnumerableSet for EnumerableSet.UintSet;\n using RateLimiter for RateLimiter.TokenBucket;\n using SafeERC20 for IERC20;\n\n error InvalidTransferFeeBps(uint256 bps);\n error InvalidTokenTransferFeeConfig(uint64 destChainSelector);\n error CallerIsNotARampOnRouter(address caller);\n error ZeroAddressInvalid();\n error NonExistentChain(uint64 remoteChainSelector);\n error ChainNotAllowed(uint64 remoteChainSelector);\n error CursedByRMN();\n error ChainAlreadyExists(uint64 chainSelector);\n error InvalidSourcePoolAddress(bytes sourcePoolAddress);\n error InvalidToken(address token);\n error Unauthorized(address caller);\n error PoolAlreadyAdded(uint64 remoteChainSelector, bytes remotePoolAddress);\n error InvalidRemotePoolForChain(uint64 remoteChainSelector, bytes remotePoolAddress);\n error InvalidRemoteChainDecimals(bytes sourcePoolData);\n error OverflowDetected(uint8 remoteDecimals, uint8 localDecimals, uint256 remoteAmount);\n error InvalidDecimalArgs(uint8 expected, uint8 actual);\n error CallerIsNotOwnerOrFeeAdmin(address caller);\n\n event LockedOrBurned(uint64 indexed remoteChainSelector, address token, address sender, uint256 amount);\n event ReleasedOrMinted(\n uint64 indexed remoteChainSelector, address token, address sender, address recipient, uint256 amount\n );\n event ChainAdded(\n uint64 remoteChainSelector,\n bytes remoteToken,\n RateLimiter.Config outboundRateLimiterConfig,\n RateLimiter.Config inboundRateLimiterConfig\n );\n event ChainRemoved(uint64 remoteChainSelector);\n event RemotePoolAdded(uint64 indexed remoteChainSelector, bytes remotePoolAddress);\n event RemotePoolRemoved(uint64 indexed remoteChainSelector, bytes remotePoolAddress);\n event DynamicConfigSet(address router, address rateLimitAdmin, address feeAdmin);\n event OutboundRateLimitConsumed(uint64 indexed remoteChainSelector, address token, uint256 amount);\n event InboundRateLimitConsumed(uint64 indexed remoteChainSelector, address token, uint256 amount);\n event TokenTransferFeeConfigUpdated(uint64 indexed destChainSelector, TokenTransferFeeConfig tokenTransferFeeConfig);\n event TokenTransferFeeConfigDeleted(uint64 indexed destChainSelector);\n event FastFinalityOutboundRateLimitConsumed(uint64 indexed remoteChainSelector, address token, uint256 amount);\n event FastFinalityInboundRateLimitConsumed(uint64 indexed remoteChainSelector, address token, uint256 amount);\n event RateLimitConfigured(\n uint64 indexed remoteChainSelector,\n bool fastFinality,\n RateLimiter.Config outboundRateLimiterConfig,\n RateLimiter.Config inboundRateLimiterConfig\n );\n event FinalityConfigSet(bytes4 allowedFinality);\n event AdvancedPoolHooksUpdated(IAdvancedPoolHooks oldHook, IAdvancedPoolHooks newHook);\n\n struct ChainUpdate {\n uint64 remoteChainSelector; // Remote chain selector.\n bytes[] remotePoolAddresses; // Address of the remote pool, ABI encoded in the case of a remote EVM chain.\n bytes remoteTokenAddress; // Address of the remote token, ABI encoded in the case of a remote EVM chain.\n RateLimiter.Config outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain.\n RateLimiter.Config inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain.\n }\n\n struct RemoteChainConfig {\n RateLimiter.TokenBucket outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain.\n RateLimiter.TokenBucket inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain.\n bytes remoteTokenAddress; // Address of the remote token, ABI encoded in the case of a remote EVM chain.\n EnumerableSet.Bytes32Set remotePools; // Set of remote pool hashes, ABI encoded in the case of a remote EVM chain.\n }\n\n struct RateLimitConfigArgs {\n uint64 remoteChainSelector; // Remote chain selector.\n bool fastFinality; // Whether the rate limit config is for fast finality transfers.\n RateLimiter.Config outboundRateLimiterConfig; // Outbound rate limiter configuration.\n RateLimiter.Config inboundRateLimiterConfig; // Inbound rate limiter configuration.\n }\n\n /// @dev Struct with args for setting the token transfer fee configurations for a destination chain and a set of tokens.\n struct TokenTransferFeeConfigArgs {\n uint64 destChainSelector; // Destination chain selector.\n TokenTransferFeeConfig tokenTransferFeeConfig; // Token transfer fee configuration.\n }\n\n /// @notice The division factor for bps. This also represents the maximum bps fee.\n uint256 internal constant BPS_DIVIDER = 10_000;\n /// @dev The bridgeable token that is managed by this pool. Pools could support multiple tokens at the same time if\n /// required, but this implementation only supports one token.\n IERC20 internal immutable i_token;\n /// @dev The number of decimals of the token managed by this pool.\n uint8 internal immutable i_tokenDecimals;\n /// @dev The address of the RMN proxy.\n address internal immutable i_rmnProxy;\n\n /// @dev The address of the router.\n IRouter internal s_router;\n /// @dev Allowed finality config for fast finality transfers (see `FinalityCodec`).\n /// FinalityCodec.WAIT_FOR_FINALITY_FLAG means wait for finality.\n bytes4 internal s_allowedFinalityConfig;\n /// @dev Optional advanced pool hooks contract for additional features like allowlists and CCV management.\n IAdvancedPoolHooks internal s_advancedPoolHooks;\n /// @dev Separate buckets provide isolated rate limits for fast finality transfers, as their risk\n /// profiles differ from default transfers. When these are not configured, the default buckets are used for all\n /// transfers regardless of the finality requirements.\n mapping(uint64 remoteChainSelector => RateLimiter.TokenBucket tokenBucketOutbound) internal\n s_fastFinalityOutboundRateLimiterConfig;\n mapping(uint64 remoteChainSelector => RateLimiter.TokenBucket tokenBucketInbound) internal\n s_fastFinalityInboundRateLimiterConfig;\n /// @dev A set of allowed chain selectors. We want the allowlist to be enumerable to\n /// be able to quickly determine (without parsing logs) who can access the pool.\n /// @dev The chain selectors are in uint256 format because of the EnumerableSet implementation.\n EnumerableSet.UintSet internal s_remoteChainSelectors;\n mapping(uint64 remoteChainSelector => RemoteChainConfig) internal s_remoteChainConfigs;\n /// @notice A mapping of hashed pool addresses to their unhashed form. This is used to be able to find the actually\n /// configured pools and not just their hashed versions.\n mapping(bytes32 poolAddressHash => bytes poolAddress) internal s_remotePoolAddresses;\n /// @notice The address of the rate limiter admin.\n /// @dev Can be address(0) if none is configured.\n address internal s_rateLimitAdmin;\n /// @dev Optional token-transfer fee overrides keyed by destination chain selector.\n mapping(uint64 destChainSelector => TokenTransferFeeConfig tokenTransferFeeConfig) internal s_tokenTransferFeeConfig;\n /// @notice The address of the fee admin.\n /// @dev Constructor does not set this value so it is opt in only.\n address internal s_feeAdmin;\n\n constructor(\n IERC20 token,\n uint8 localTokenDecimals,\n address advancedPoolHooks,\n address rmnProxy,\n address router\n ) {\n if (address(token) == address(0) || router == address(0) || rmnProxy == address(0)) {\n revert ZeroAddressInvalid();\n }\n i_token = token;\n i_rmnProxy = rmnProxy;\n\n // In the case the token is also the pool, it won't exist yet so we skip this check.\n if (address(token) != address(this)) {\n try IERC20Metadata(address(token)).decimals() returns (uint8 actualTokenDecimals) {\n if (localTokenDecimals != actualTokenDecimals) {\n revert InvalidDecimalArgs(localTokenDecimals, actualTokenDecimals);\n }\n } catch {\n // The decimals function doesn't exist, which is possible since it's optional in the ERC20 spec. We skip the\n // check and assume the supplied token decimals are correct.\n }\n }\n i_tokenDecimals = localTokenDecimals;\n s_advancedPoolHooks = IAdvancedPoolHooks(advancedPoolHooks);\n\n s_router = IRouter(router);\n }\n\n /// @inheritdoc IPoolV1\n /// @param token The token address to check.\n function isSupportedToken(\n address token\n ) public view virtual returns (bool) {\n return token == address(i_token);\n }\n\n /// @notice Gets the IERC20 token that this pool can lock or burn.\n /// @return token The IERC20 token representation.\n function getToken() public view virtual returns (IERC20 token) {\n return i_token;\n }\n\n /// @notice Get RMN proxy address.\n /// @return rmnProxy Address of RMN proxy.\n function getRmnProxy() public view virtual returns (address rmnProxy) {\n return i_rmnProxy;\n }\n\n /// @notice Gets the pools dynamic configuration.\n function getDynamicConfig() public view virtual returns (address router, address rateLimitAdmin, address feeAdmin) {\n return (address(s_router), s_rateLimitAdmin, s_feeAdmin);\n }\n\n /// @notice Gets the finality config as defined in the FinalityCodec library. This value does NOT 1:1 translate to\n /// a block depth. The finality config contains special flags and should only be encoded/decoded using the\n /// FinalityCodec library. Checks must happen by calling `FinalityCodec._ensureRequestedFinalityAllowed`.\n function getAllowedFinalityConfig() public view virtual returns (bytes4 allowedFinality) {\n return s_allowedFinalityConfig;\n }\n\n /// @notice Gets the advanced pool hook contract address used by this pool.\n function getAdvancedPoolHooks() public view virtual returns (IAdvancedPoolHooks advancedPoolHook) {\n return s_advancedPoolHooks;\n }\n\n /// @notice Sets the dynamic configuration for the pool.\n /// @param router The address of the router contract.\n /// @param rateLimitAdmin The address of the rate limiter admin.\n /// @param feeAdmin An additional address that can withdraw fees from this contract.\n /// @dev FeeTokenHandler will revert if feeAdmin is zero when withdrawing fees.\n /// @dev If only the owner can withdraw fees, set feeAdmin to address(0).\n function setDynamicConfig(\n address router,\n address rateLimitAdmin,\n address feeAdmin\n ) public virtual onlyOwner {\n if (router == address(0)) revert ZeroAddressInvalid();\n s_router = IRouter(router);\n s_rateLimitAdmin = rateLimitAdmin;\n s_feeAdmin = feeAdmin;\n\n emit DynamicConfigSet(router, rateLimitAdmin, feeAdmin);\n }\n\n /// @notice Sets the finality config according to the FinalityCodec library encoding.\n /// @param allowedFinality The finality settings allowed in this pool, according to the FinalityCodec encoding.\n function setAllowedFinalityConfig(\n bytes4 allowedFinality\n ) public virtual onlyOwner {\n // Any bytes4 value is accepted as allowedFinality; the FinalityCodec semantics are enforced when requests are\n // checked against this value via FinalityCodec._ensureRequestedFinalityAllowed.\n s_allowedFinalityConfig = allowedFinality;\n\n emit FinalityConfigSet(allowedFinality);\n }\n\n /// @notice Updates the advanced pool hook.\n /// @param newHook The new advanced pool hooks contract.\n function updateAdvancedPoolHooks(\n IAdvancedPoolHooks newHook\n ) public virtual onlyOwner {\n emit AdvancedPoolHooksUpdated(s_advancedPoolHooks, newHook);\n s_advancedPoolHooks = newHook;\n }\n\n /// @notice Signals which version of the pool interface is supported.\n /// @param interfaceId The interface identifier, as specified in ERC-165.\n function supportsInterface(\n bytes4 interfaceId\n ) public view virtual override returns (bool) {\n return interfaceId == Pool.CCIP_POOL_V1 || interfaceId == type(IPoolV2).interfaceId\n || interfaceId == type(IPoolV1).interfaceId || interfaceId == type(IERC165).interfaceId;\n }\n\n // ================================================================\n // \u2502 Lock or Burn \u2502\n // ================================================================\n\n /// @inheritdoc IPoolV2\n /// @dev The _validateLockOrBurn check is an essential security check.\n /// @dev The _getFee function deducts the fee from the amount and returns the amount after fee deduction.\n /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain.\n /// @param requestedFinalityConfig Requested finality config according to the FinalityCodec.\n /// @param tokenArgs Additional token arguments.\n function lockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n ) public virtual returns (Pool.LockOrBurnOutV1 memory, uint256 destTokenAmount) {\n uint256 feeAmount = _getFee(lockOrBurnIn, requestedFinalityConfig);\n _validateLockOrBurn(lockOrBurnIn, requestedFinalityConfig, tokenArgs, feeAmount);\n destTokenAmount = lockOrBurnIn.amount - feeAmount;\n _lockOrBurn(lockOrBurnIn.remoteChainSelector, destTokenAmount);\n\n emit LockedOrBurned({\n remoteChainSelector: lockOrBurnIn.remoteChainSelector,\n token: lockOrBurnIn.localToken,\n sender: msg.sender,\n amount: destTokenAmount\n });\n\n return (\n Pool.LockOrBurnOutV1({\n destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: _encodeLocalDecimals()\n }),\n destTokenAmount\n );\n }\n\n /// @inheritdoc IPoolV1\n /// @dev The _validateLockOrBurn check is an essential security check.\n /// @dev _getFee is not called in this legacy method, so the full amount is locked or burned.\n /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain.\n function lockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn\n ) public virtual returns (Pool.LockOrBurnOutV1 memory lockOrBurnOutV1) {\n _validateLockOrBurn(lockOrBurnIn, FinalityCodec.WAIT_FOR_FINALITY_FLAG, \"\", 0); // feeAmount is zero\n _lockOrBurn(lockOrBurnIn.remoteChainSelector, lockOrBurnIn.amount);\n\n emit LockedOrBurned({\n remoteChainSelector: lockOrBurnIn.remoteChainSelector,\n token: lockOrBurnIn.localToken,\n sender: msg.sender,\n amount: lockOrBurnIn.amount\n });\n\n return Pool.LockOrBurnOutV1({\n destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: _encodeLocalDecimals()\n });\n }\n\n /// @notice Contains the specific lock or burn token logic for a pool.\n /// @dev overriding this method allows us to create pools with different lock/burn signatures\n /// without duplicating the underlying logic.\n /// @param remoteChainSelector The selector of the remote chain.\n /// @param amount The amount of tokens to lock or burn.\n function _lockOrBurn(\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {}\n\n // ================================================================\n // \u2502 Release or Mint \u2502\n // ================================================================\n\n /// @inheritdoc IPoolV2\n /// @dev The _validateReleaseOrMint check is an essential security check.\n /// @param releaseOrMintIn Encoded data fields for the processing of tokens on the destination chain.\n /// @param requestedFinalityConfig Requested finality config according to the FinalityCodec.\n function releaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n bytes4 requestedFinalityConfig\n ) public virtual override(IPoolV2) returns (Pool.ReleaseOrMintOutV1 memory) {\n uint256 localAmount = _calculateLocalAmount(\n releaseOrMintIn.sourceDenominatedAmount, _parseRemoteDecimals(releaseOrMintIn.sourcePoolData)\n );\n\n _validateReleaseOrMint(releaseOrMintIn, localAmount, requestedFinalityConfig);\n\n _releaseOrMint(releaseOrMintIn.receiver, localAmount, releaseOrMintIn.remoteChainSelector);\n\n emit ReleasedOrMinted({\n remoteChainSelector: releaseOrMintIn.remoteChainSelector,\n token: releaseOrMintIn.localToken,\n sender: msg.sender,\n recipient: releaseOrMintIn.receiver,\n amount: localAmount\n });\n\n return Pool.ReleaseOrMintOutV1({destinationAmount: localAmount});\n }\n\n /// @inheritdoc IPoolV1\n /// @dev calls IPoolV2.releaseOrMint with default finality.\n /// @param releaseOrMintIn Encoded data fields for the processing of tokens on the destination chain.\n function releaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn\n ) public virtual override returns (Pool.ReleaseOrMintOutV1 memory) {\n return releaseOrMint(releaseOrMintIn, FinalityCodec.WAIT_FOR_FINALITY_FLAG);\n }\n\n /// @notice Contains the specific release or mint token logic for a pool.\n /// @dev overriding this method allows us to create pools with different release/mint signatures\n /// without duplicating the underlying logic.\n /// @param receiver The address to receive the tokens.\n /// @param amount The amount of tokens to release or mint.\n /// @param remoteChainSelector The selector of the remote chain.\n function _releaseOrMint(\n address receiver,\n uint256 amount,\n uint64 remoteChainSelector\n ) internal virtual {}\n\n // ================================================================\n // \u2502 Validation \u2502\n // ================================================================\n\n /// @notice Validates the lock or burn input for correctness on\n /// - token to be locked or burned\n /// - RMN curse status\n /// - if the sender is a valid onRamp\n /// - rate limiting for either default or FTF transfer messages.\n /// - preflight checks hooks (if enabled)\n /// @param lockOrBurnIn The input to validate.\n /// @param requestedFinality The requested finality speed according to the FinalityCodec encoding.\n /// @param tokenArgs Additional token arguments passed in by the sender of the message.\n /// @param feeAmount The fee amount deducted from the transfer amount.\n /// @dev This function should always be called before executing a lock or burn. Not doing so would allow\n /// for various exploits.\n function _validateLockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinality,\n bytes memory tokenArgs,\n uint256 feeAmount\n ) internal virtual {\n if (!isSupportedToken(lockOrBurnIn.localToken)) {\n revert InvalidToken(lockOrBurnIn.localToken);\n }\n if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(lockOrBurnIn.remoteChainSelector)))) revert CursedByRMN();\n\n _onlyOnRamp(lockOrBurnIn.remoteChainSelector);\n\n uint256 amount = lockOrBurnIn.amount - feeAmount;\n\n // If FTF is requested, validate against the allowed and apply the custom rate limit.\n if (requestedFinality != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n // Use the codec to validate that the requested finality is allowed by the pool's configuration. This will revert\n // if the requested finality is not allowed.\n FinalityCodec._ensureRequestedFinalityAllowed(requestedFinality, s_allowedFinalityConfig);\n _consumeFastFinalityOutboundRateLimit(lockOrBurnIn.localToken, lockOrBurnIn.remoteChainSelector, amount);\n } else {\n _consumeOutboundRateLimit(lockOrBurnIn.localToken, lockOrBurnIn.remoteChainSelector, amount);\n }\n\n _preflightCheck(lockOrBurnIn, requestedFinality, tokenArgs, amount);\n }\n\n /// @notice Hook for pre-flight checks on lock or burn.\n /// @dev These hooks are optional but take up a lot of space in the contracts bytecode. To avoid this overhead when\n /// not needed, you can override this function in the derived contract with an empty implementation. This will result\n /// in the compiler removing the function and all related code, saving close to 1KB.\n /// @param lockOrBurnIn The input to validate.\n /// @param requestedFinalityConfig The requested finality config according to the FinalityCodec encoding.\n /// @param tokenArgs Additional token arguments passed in by the sender of the message.\n /// @param amountPostFee The amount after token pool bps-based fees have been deducted.\n function _preflightCheck(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes memory tokenArgs,\n uint256 amountPostFee\n ) internal virtual {\n if (address(s_advancedPoolHooks) != address(0)) {\n s_advancedPoolHooks.preflightCheck(lockOrBurnIn, requestedFinalityConfig, tokenArgs, amountPostFee);\n }\n }\n\n /// @notice Validates the release or mint input for correctness on\n /// - token to be released or minted\n /// - RMN curse status\n /// - if the sender is a valid offRamp\n /// - if the source pool is configured for the remote chain\n /// - rate limiting for either default or FTF transfer messages.\n /// @param releaseOrMintIn The input to validate.\n /// @param localAmount The local amount to be released or minted.\n /// @param requestedFinalityConfig The requested finality encoding (see `FinalityCodec`).\n /// @dev This function should always be called before executing a release or mint. Not doing so would allow\n /// for various exploits.\n function _validateReleaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n uint256 localAmount,\n bytes4 requestedFinalityConfig\n ) internal virtual {\n if (!isSupportedToken(releaseOrMintIn.localToken)) {\n revert InvalidToken(releaseOrMintIn.localToken);\n }\n if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(releaseOrMintIn.remoteChainSelector)))) revert CursedByRMN();\n _onlyOffRamp(releaseOrMintIn.remoteChainSelector);\n\n // Validates that the source pool address is configured on this pool.\n if (!isRemotePool(releaseOrMintIn.remoteChainSelector, releaseOrMintIn.sourcePoolAddress)) {\n revert InvalidSourcePoolAddress(releaseOrMintIn.sourcePoolAddress);\n }\n if (requestedFinalityConfig != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n _consumeFastFinalityInboundRateLimit(releaseOrMintIn.localToken, releaseOrMintIn.remoteChainSelector, localAmount);\n } else {\n _consumeInboundRateLimit(releaseOrMintIn.localToken, releaseOrMintIn.remoteChainSelector, localAmount);\n }\n\n _postflightCheck(releaseOrMintIn, localAmount, requestedFinalityConfig);\n }\n\n /// @notice Hook for post-flight checks on release or mint.\n /// @dev These hooks are optional but take up a lot of space in the contracts bytecode. To avoid this overhead when\n /// not needed, you can override this function in the derived contract with an empty implementation. This will result\n /// in the compiler removing the function and all related code, saving close to 1KB.\n /// @param releaseOrMintIn The input to validate.\n /// @param localAmount The local amount to be released or minted.\n /// @param requestedFinalityConfig The requested finality encoding (see `FinalityCodec`).\n function _postflightCheck(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n uint256 localAmount,\n bytes4 requestedFinalityConfig\n ) internal virtual {\n if (address(s_advancedPoolHooks) != address(0)) {\n s_advancedPoolHooks.postflightCheck(releaseOrMintIn, localAmount, requestedFinalityConfig);\n }\n }\n\n // ================================================================\n // \u2502 Token decimals \u2502\n // ================================================================\n\n /// @notice Gets the IERC20 token decimals on the local chain.\n function getTokenDecimals() public view virtual returns (uint8 decimals) {\n return i_tokenDecimals;\n }\n\n function _encodeLocalDecimals() internal view virtual returns (bytes memory) {\n return abi.encode(i_tokenDecimals);\n }\n\n function _parseRemoteDecimals(\n bytes memory sourcePoolData\n ) internal view virtual returns (uint8) {\n // Fallback to the local token decimals if the source pool data is empty. This allows for backwards compatibility.\n if (sourcePoolData.length == 0) {\n return i_tokenDecimals;\n }\n if (sourcePoolData.length != 32) {\n revert InvalidRemoteChainDecimals(sourcePoolData);\n }\n uint256 remoteDecimals = abi.decode(sourcePoolData, (uint256));\n if (remoteDecimals > type(uint8).max) {\n revert InvalidRemoteChainDecimals(sourcePoolData);\n }\n return uint8(remoteDecimals);\n }\n\n /// @notice Calculates the local amount based on the remote amount and decimals.\n /// @param remoteAmount The amount on the remote chain.\n /// @param remoteDecimals The decimals of the token on the remote chain.\n /// @return The local amount.\n /// @dev This function protects against overflows. If there is a transaction that hits the overflow check, it is\n /// probably incorrect as that means the amount cannot be represented on this chain. If the local decimals have been\n /// wrongly configured, the token issuer could redeploy the pool with the correct decimals and manually re-execute the\n /// CCIP tx to fix the issue.\n function _calculateLocalAmount(\n uint256 remoteAmount,\n uint8 remoteDecimals\n ) internal view virtual returns (uint256) {\n if (remoteDecimals == i_tokenDecimals) {\n return remoteAmount;\n }\n if (remoteDecimals > i_tokenDecimals) {\n uint8 decimalsDiff = remoteDecimals - i_tokenDecimals;\n if (decimalsDiff > 77) {\n // This is a safety check to prevent overflow in the next calculation.\n revert OverflowDetected(remoteDecimals, i_tokenDecimals, remoteAmount);\n }\n // Solidity rounds down so there is no risk of minting more tokens than the remote chain sent.\n return remoteAmount / (10 ** decimalsDiff);\n }\n\n // This is a safety check to prevent overflow in the next calculation.\n // More than 77 would never fit in a uint256 and would cause an overflow. We also check if the resulting amount\n // would overflow.\n uint8 diffDecimals = i_tokenDecimals - remoteDecimals;\n if (diffDecimals > 77 || remoteAmount > type(uint256).max / (10 ** diffDecimals)) {\n revert OverflowDetected(remoteDecimals, i_tokenDecimals, remoteAmount);\n }\n\n return remoteAmount * (10 ** diffDecimals);\n }\n\n // ================================================================\n // \u2502 Chain permissions \u2502\n // ================================================================\n\n /// @notice Gets the pool address on the remote chain.\n /// @param remoteChainSelector Remote chain selector.\n /// @dev To support non-evm chains, this value is encoded into bytes\n function getRemotePools(\n uint64 remoteChainSelector\n ) public view virtual returns (bytes[] memory) {\n bytes32[] memory remotePoolHashes = s_remoteChainConfigs[remoteChainSelector].remotePools.values();\n\n bytes[] memory remotePools = new bytes[](remotePoolHashes.length);\n for (uint256 i = 0; i < remotePoolHashes.length; ++i) {\n remotePools[i] = s_remotePoolAddresses[remotePoolHashes[i]];\n }\n\n return remotePools;\n }\n\n /// @notice Checks if the pool address is configured on the remote chain.\n /// @param remoteChainSelector Remote chain selector.\n /// @param remotePoolAddress The address of the remote pool.\n function isRemotePool(\n uint64 remoteChainSelector,\n bytes memory remotePoolAddress\n ) public view virtual returns (bool) {\n return s_remoteChainConfigs[remoteChainSelector].remotePools.contains(keccak256(remotePoolAddress));\n }\n\n /// @inheritdoc IPoolV2\n /// @param remoteChainSelector Remote chain selector.\n function getRemoteToken(\n uint64 remoteChainSelector\n ) public view virtual returns (bytes memory) {\n return s_remoteChainConfigs[remoteChainSelector].remoteTokenAddress;\n }\n\n /// @notice Adds a remote pool for a given chain selector. This could be due to a pool being upgraded on the remote\n /// chain. We don't simply want to replace the old pool as there could still be valid inflight messages from the old\n /// pool. This function allows for multiple pools to be added for a single chain selector.\n /// @param remoteChainSelector The remote chain selector for which the remote pool address is being added.\n /// @param remotePoolAddress The address of the new remote pool.\n function addRemotePool(\n uint64 remoteChainSelector,\n bytes calldata remotePoolAddress\n ) external virtual onlyOwner {\n if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector);\n\n _setRemotePool(remoteChainSelector, remotePoolAddress);\n }\n\n /// @notice Removes the remote pool address for a given chain selector.\n /// @dev All inflight txs from the remote pool will be rejected after it is removed. To ensure no loss of funds, there\n /// should be no inflight txs from the given pool.\n /// @param remoteChainSelector The remote chain selector.\n /// @param remotePoolAddress The remote pool address to remove.\n function removeRemotePool(\n uint64 remoteChainSelector,\n bytes calldata remotePoolAddress\n ) external virtual onlyOwner {\n if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector);\n\n if (!s_remoteChainConfigs[remoteChainSelector].remotePools.remove(keccak256(remotePoolAddress))) {\n revert InvalidRemotePoolForChain(remoteChainSelector, remotePoolAddress);\n }\n\n emit RemotePoolRemoved(remoteChainSelector, remotePoolAddress);\n }\n\n /// @inheritdoc IPoolV1\n /// @param remoteChainSelector The remote chain selector to check.\n function isSupportedChain(\n uint64 remoteChainSelector\n ) public view virtual returns (bool) {\n return s_remoteChainSelectors.contains(remoteChainSelector);\n }\n\n /// @notice Get list of allowed chains\n /// @return list of chains.\n function getSupportedChains() public view virtual returns (uint64[] memory) {\n uint256[] memory uint256ChainSelectors = s_remoteChainSelectors.values();\n uint64[] memory chainSelectors = new uint64[](uint256ChainSelectors.length);\n for (uint256 i = 0; i < uint256ChainSelectors.length; ++i) {\n chainSelectors[i] = uint64(uint256ChainSelectors[i]);\n }\n\n return chainSelectors;\n }\n\n /// @notice Sets the permissions for a list of chains selectors. Actual senders for these chains\n /// need to be allowed on the Router to interact with this pool.\n /// @param remoteChainSelectorsToRemove A list of chain selectors to remove.\n /// @param chainsToAdd A list of chains and their new permission status & rate limits. Rate limits\n /// are only used when the chain is being added through `allowed` being true.\n /// @dev Only callable by the owner\n function applyChainUpdates(\n uint64[] calldata remoteChainSelectorsToRemove,\n ChainUpdate[] calldata chainsToAdd\n ) external virtual onlyOwner {\n for (uint256 i = 0; i < remoteChainSelectorsToRemove.length; ++i) {\n uint64 remoteChainSelectorToRemove = remoteChainSelectorsToRemove[i];\n // If the chain doesn't exist, revert.\n if (!s_remoteChainSelectors.remove(remoteChainSelectorToRemove)) {\n revert NonExistentChain(remoteChainSelectorToRemove);\n }\n\n // Remove all remote pool hashes for the chain.\n bytes32[] memory remotePools = s_remoteChainConfigs[remoteChainSelectorToRemove].remotePools.values();\n for (uint256 j = 0; j < remotePools.length; ++j) {\n s_remoteChainConfigs[remoteChainSelectorToRemove].remotePools.remove(remotePools[j]);\n }\n\n delete s_remoteChainConfigs[remoteChainSelectorToRemove];\n delete s_fastFinalityOutboundRateLimiterConfig[remoteChainSelectorToRemove];\n delete s_fastFinalityInboundRateLimiterConfig[remoteChainSelectorToRemove];\n\n emit ChainRemoved(remoteChainSelectorToRemove);\n }\n\n for (uint256 i = 0; i < chainsToAdd.length; ++i) {\n ChainUpdate memory newChain = chainsToAdd[i];\n if (newChain.remoteTokenAddress.length == 0) {\n revert ZeroAddressInvalid();\n }\n\n // If the chain already exists, revert\n if (!s_remoteChainSelectors.add(newChain.remoteChainSelector)) {\n revert ChainAlreadyExists(newChain.remoteChainSelector);\n }\n\n RemoteChainConfig storage remoteChainConfig = s_remoteChainConfigs[newChain.remoteChainSelector];\n remoteChainConfig.outboundRateLimiterConfig._setTokenBucketConfig(newChain.outboundRateLimiterConfig);\n remoteChainConfig.inboundRateLimiterConfig._setTokenBucketConfig(newChain.inboundRateLimiterConfig);\n\n remoteChainConfig.remoteTokenAddress = newChain.remoteTokenAddress;\n\n for (uint256 j = 0; j < newChain.remotePoolAddresses.length; ++j) {\n _setRemotePool(newChain.remoteChainSelector, newChain.remotePoolAddresses[j]);\n }\n\n emit ChainAdded(\n newChain.remoteChainSelector,\n newChain.remoteTokenAddress,\n newChain.outboundRateLimiterConfig,\n newChain.inboundRateLimiterConfig\n );\n }\n }\n\n /// @notice Adds a pool address to the allowed remote token pools for a particular chain.\n /// @param remoteChainSelector The remote chain selector for which the remote pool address is being added.\n /// @param remotePoolAddress The address of the new remote pool.\n function _setRemotePool(\n uint64 remoteChainSelector,\n bytes memory remotePoolAddress\n ) internal virtual {\n if (remotePoolAddress.length == 0) {\n revert ZeroAddressInvalid();\n }\n\n bytes32 poolHash = keccak256(remotePoolAddress);\n\n // Check if the pool already exists.\n if (!s_remoteChainConfigs[remoteChainSelector].remotePools.add(poolHash)) {\n revert PoolAlreadyAdded(remoteChainSelector, remotePoolAddress);\n }\n\n // Add the pool to the mapping to be able to un-hash it later.\n s_remotePoolAddresses[poolHash] = remotePoolAddress;\n\n emit RemotePoolAdded(remoteChainSelector, remotePoolAddress);\n }\n\n // ================================================================\n // \u2502 Rate limiting \u2502\n // ================================================================\n\n /// @dev The inbound rate limits should be slightly higher than the outbound rate limits. This is because many chains\n /// finalize blocks in batches. CCIP also commits messages in batches: the commit plugin bundles multiple messages in\n /// a single merkle root.\n /// Imagine the following scenario.\n /// - Chain A has an inbound and outbound rate limit of 100 tokens capacity and 1 token per second refill rate.\n /// - Chain B has an inbound and outbound rate limit of 100 tokens capacity and 1 token per second refill rate.\n ///\n /// At time 0:\n /// - Chain A sends 100 tokens to Chain B.\n /// At time 5:\n /// - Chain A sends 5 tokens to Chain B.\n /// At time 6:\n /// The epoch that contains blocks [0-5] is finalized.\n /// Both transactions will be included in the same merkle root and become executable at the same time. This means\n /// the token pool on chain B requires a capacity of 105 to successfully execute both messages at the same time.\n /// The exact additional capacity required depends on the refill rate and the size of the source chain epochs and the\n /// CCIP round time. For simplicity, a 5-10% buffer should be sufficient in most cases.\n\n /// @notice Consumes outbound rate limiting capacity in this pool.\n /// @param remoteChainSelector The remote chain selector.\n /// @param amount The amount of tokens consumed.\n function _consumeOutboundRateLimit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {\n s_remoteChainConfigs[remoteChainSelector].outboundRateLimiterConfig._consume(amount, token);\n\n emit OutboundRateLimitConsumed({token: token, remoteChainSelector: remoteChainSelector, amount: amount});\n }\n\n /// @notice Consumes inbound rate limiting capacity in this pool.\n /// @param remoteChainSelector The remote chain selector.\n /// @param amount The amount of tokens consumed.\n function _consumeInboundRateLimit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {\n s_remoteChainConfigs[remoteChainSelector].inboundRateLimiterConfig._consume(amount, token);\n\n emit InboundRateLimitConsumed({token: token, remoteChainSelector: remoteChainSelector, amount: amount});\n }\n\n /// @notice Consumes fast finality outbound rate limiting capacity in this pool.\n /// @dev If fast finality rate limiter is not enabled for the chain, it will fallback to the default\n /// rate limiter.\n /// @param remoteChainSelector The remote chain selector.\n /// @param amount The amount of tokens consumed.\n function _consumeFastFinalityOutboundRateLimit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {\n if (!s_fastFinalityOutboundRateLimiterConfig[remoteChainSelector].isEnabled) {\n _consumeOutboundRateLimit(token, remoteChainSelector, amount);\n return;\n }\n\n s_fastFinalityOutboundRateLimiterConfig[remoteChainSelector]._consume(amount, token);\n\n emit FastFinalityOutboundRateLimitConsumed({token: token, remoteChainSelector: remoteChainSelector, amount: amount});\n }\n\n /// @notice Consumes fast finality inbound rate limiting capacity in this pool.\n /// @dev If fast finality rate limiter is not enabled for the chain, it will fallback to the default\n /// rate limiter.\n /// @param remoteChainSelector The remote chain selector.\n /// @param amount The amount of tokens consumed.\n function _consumeFastFinalityInboundRateLimit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {\n if (!s_fastFinalityInboundRateLimiterConfig[remoteChainSelector].isEnabled) {\n _consumeInboundRateLimit(token, remoteChainSelector, amount);\n return;\n }\n\n s_fastFinalityInboundRateLimiterConfig[remoteChainSelector]._consume(amount, token);\n\n emit FastFinalityInboundRateLimitConsumed({token: token, remoteChainSelector: remoteChainSelector, amount: amount});\n }\n\n /// @notice Returns the outbound and inbound rate limiter state for the given remote chain at the time of the call.\n /// @param remoteChainSelector The remote chain selector.\n /// @param fastFinality Whether to get the fast finality rate limiter state.\n /// @return outboundRateLimiterState The outbound token bucket.\n /// @return inboundRateLimiterState The inbound token bucket.\n function getCurrentRateLimiterState(\n uint64 remoteChainSelector,\n bool fastFinality\n )\n external\n view\n virtual\n returns (\n RateLimiter.TokenBucket memory outboundRateLimiterState,\n RateLimiter.TokenBucket memory inboundRateLimiterState\n )\n {\n if (fastFinality) {\n return (\n s_fastFinalityOutboundRateLimiterConfig[remoteChainSelector]._currentTokenBucketState(),\n s_fastFinalityInboundRateLimiterConfig[remoteChainSelector]._currentTokenBucketState()\n );\n }\n RemoteChainConfig storage config = s_remoteChainConfigs[remoteChainSelector];\n return (\n config.outboundRateLimiterConfig._currentTokenBucketState(),\n config.inboundRateLimiterConfig._currentTokenBucketState()\n );\n }\n\n /// @notice Sets the rate limit configurations for specified remote chains.\n /// @param rateLimitConfigArgs Array of structs containing remote chain selectors and their rate limiter configs.\n function setRateLimitConfig(\n RateLimitConfigArgs[] calldata rateLimitConfigArgs\n ) external virtual {\n _onlyOwnerOrRateLimitAdmin();\n\n for (uint256 i = 0; i < rateLimitConfigArgs.length; ++i) {\n RateLimitConfigArgs calldata configArgs = rateLimitConfigArgs[i];\n\n uint64 remoteChainSelector = configArgs.remoteChainSelector;\n if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector);\n\n if (configArgs.fastFinality) {\n s_fastFinalityOutboundRateLimiterConfig[remoteChainSelector]._setTokenBucketConfig(\n configArgs.outboundRateLimiterConfig\n );\n s_fastFinalityInboundRateLimiterConfig[remoteChainSelector]._setTokenBucketConfig(\n configArgs.inboundRateLimiterConfig\n );\n } else {\n s_remoteChainConfigs[remoteChainSelector].outboundRateLimiterConfig\n ._setTokenBucketConfig(configArgs.outboundRateLimiterConfig);\n s_remoteChainConfigs[remoteChainSelector].inboundRateLimiterConfig\n ._setTokenBucketConfig(configArgs.inboundRateLimiterConfig);\n }\n\n emit RateLimitConfigured(\n remoteChainSelector,\n configArgs.fastFinality,\n configArgs.outboundRateLimiterConfig,\n configArgs.inboundRateLimiterConfig\n );\n }\n }\n\n // ================================================================\n // \u2502 Access \u2502\n // ================================================================\n\n /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender\n /// is a permissioned onRamp for the given chain on the Router.\n /// @dev This function is marked virtual as other token pools may inherit from this contract, but do\n /// not receive calls from the ramps directly, instead receiving them from a proxy contract. In that\n /// situation this function must be overridden and the ramp-check removed and replaced with a different\n /// access-control scheme.\n /// @param remoteChainSelector The remote chain selector.\n function _onlyOnRamp(\n uint64 remoteChainSelector\n ) internal view virtual {\n if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector);\n if (!(msg.sender == s_router.getOnRamp(remoteChainSelector))) revert CallerIsNotARampOnRouter(msg.sender);\n }\n\n /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender\n /// is a permissioned offRamp for the given chain on the Router.\n /// @dev This function is marked virtual as other token pools may inherit from this contract, but do\n /// not receive calls from the ramps directly, instead receiving them from a proxy contract. In that\n /// situation this function must be overridden and the ramp-check removed and replaced with a different\n /// access-control scheme.\n /// @param remoteChainSelector The remote chain selector.\n function _onlyOffRamp(\n uint64 remoteChainSelector\n ) internal view virtual {\n if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector);\n if (!s_router.isOffRamp(remoteChainSelector, msg.sender)) revert CallerIsNotARampOnRouter(msg.sender);\n }\n\n /// @notice Checks whether the msg.sender is either the owner or the rate limit admin.\n function _onlyOwnerOrRateLimitAdmin() internal view virtual {\n if (msg.sender != s_rateLimitAdmin && msg.sender != owner()) {\n revert Unauthorized(msg.sender);\n }\n }\n\n /// @notice Returns the set of required CCVs for transfers in a specific direction.\n /// @dev This function delegates to AdvancedPoolHooks if configured, otherwise returns an empty array.\n /// @param localToken The address of the local token.\n /// @param remoteChainSelector The remote chain selector for this transfer.\n /// @param sourceDenominatedAmount The amount being transferred, source denominated.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param extraData Direction-specific payload forwarded by the caller (e.g. token args or source pool data).\n /// @param direction The direction of the transfer (Inbound or Outbound).\n /// @return requiredCCVs Set of required CCV addresses.\n function getRequiredCCVs(\n address localToken,\n uint64 remoteChainSelector,\n uint256 sourceDenominatedAmount,\n bytes4 requestedFinalityConfig,\n bytes calldata extraData,\n IPoolV2.MessageDirection direction\n ) public view virtual returns (address[] memory requiredCCVs) {\n if (address(s_advancedPoolHooks) == address(0)) {\n return new address[](0);\n }\n\n // By default, the amount is equal to the source denominated amount.\n uint256 amount = sourceDenominatedAmount;\n\n // The source fee amount is not classified as transferred value, meaning we have to subtract it from the amount\n // before passing it into the hook. The inbound amount is already post-fee so we only need to do this for outbound\n // transfers.\n if (direction == IPoolV2.MessageDirection.Outbound) {\n TokenTransferFeeConfig memory feeConfig = s_tokenTransferFeeConfig[remoteChainSelector];\n if (feeConfig.isEnabled) {\n if (requestedFinalityConfig != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n amount =\n sourceDenominatedAmount - (sourceDenominatedAmount * feeConfig.fastFinalityTransferFeeBps) / BPS_DIVIDER;\n } else {\n amount = sourceDenominatedAmount - (sourceDenominatedAmount * feeConfig.finalityTransferFeeBps) / BPS_DIVIDER;\n }\n }\n } else {\n // For inbound transfers, the amount is already post-fee so we don't need to do any additional calculations to get\n // the amount that will be received by the user. However, we still need to convert it to the local amount based on\n // decimals for the hooks.\n\n // extraData is sourcePoolData for inbound transfers, which contains the remote decimals.\n amount = _calculateLocalAmount(sourceDenominatedAmount, _parseRemoteDecimals(extraData));\n }\n\n return s_advancedPoolHooks.getRequiredCCVs(\n localToken, remoteChainSelector, amount, requestedFinalityConfig, extraData, direction\n );\n }\n\n // ================================================================\n // \u2502 Fee \u2502\n // ================================================================\n\n /// @notice Updates the token transfer fee configurations for specified destination chains.\n /// @param tokenTransferFeeConfigArgs Array of structs containing destination chain selectors and their fee configs.\n /// @param disableTokenTransferFeeConfigs Array of destination chain selectors to disable custom fee configs for.\n function applyTokenTransferFeeConfigUpdates(\n TokenTransferFeeConfigArgs[] calldata tokenTransferFeeConfigArgs,\n uint64[] calldata disableTokenTransferFeeConfigs\n ) external virtual onlyOwner {\n for (uint256 i = 0; i < tokenTransferFeeConfigArgs.length; ++i) {\n uint64 destChainSelector = tokenTransferFeeConfigArgs[i].destChainSelector;\n if (!isSupportedChain(destChainSelector)) revert NonExistentChain(destChainSelector);\n\n TokenTransferFeeConfig calldata tokenTransferFeeConfig = tokenTransferFeeConfigArgs[i].tokenTransferFeeConfig;\n\n // Reject configs with isEnabled: false - use disableTokenTransferFeeConfigs parameter instead.\n if (!tokenTransferFeeConfig.isEnabled) {\n revert InvalidTokenTransferFeeConfig(destChainSelector);\n }\n\n if (tokenTransferFeeConfig.finalityTransferFeeBps >= BPS_DIVIDER) {\n revert InvalidTransferFeeBps(tokenTransferFeeConfig.finalityTransferFeeBps);\n }\n if (tokenTransferFeeConfig.fastFinalityTransferFeeBps >= BPS_DIVIDER) {\n revert InvalidTransferFeeBps(tokenTransferFeeConfig.fastFinalityTransferFeeBps);\n }\n // Gas overhead must be non-zero for proper fee accounting.\n if (tokenTransferFeeConfig.destGasOverhead == 0) {\n revert InvalidTokenTransferFeeConfig(destChainSelector);\n }\n\n s_tokenTransferFeeConfig[destChainSelector] = tokenTransferFeeConfig;\n emit TokenTransferFeeConfigUpdated(destChainSelector, tokenTransferFeeConfig);\n }\n\n for (uint256 i = 0; i < disableTokenTransferFeeConfigs.length; ++i) {\n uint64 destChainSelector = disableTokenTransferFeeConfigs[i];\n delete s_tokenTransferFeeConfig[destChainSelector];\n emit TokenTransferFeeConfigDeleted(destChainSelector);\n }\n }\n\n /// @notice Returns the token transfer fee override for a destination chain.\n /// @param destChainSelector The destination chain selector used for lookup.\n /// @return feeConfig The enabled fee configuration for the lane.\n function getTokenTransferFeeConfig(\n address, // localToken\n uint64 destChainSelector,\n bytes4, // requestedFinalityConfig\n bytes calldata // tokenArgs\n ) external view virtual returns (TokenTransferFeeConfig memory feeConfig) {\n return s_tokenTransferFeeConfig[destChainSelector];\n }\n\n /// @inheritdoc IPoolV2\n /// @notice Returns the pool fee parameters that will apply to a transfer.\n /// @param destChainSelector The destination lane selector.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n function getFee(\n address, // localToken\n uint64 destChainSelector,\n uint256, // amount\n address, // feeToken\n bytes4 requestedFinalityConfig,\n bytes calldata // tokenArgs\n )\n external\n view\n virtual\n returns (uint256 feeUSDCents, uint32 destGasOverhead, uint32 destBytesOverhead, uint16 tokenFeeBps, bool isEnabled)\n {\n FinalityCodec._ensureRequestedFinalityAllowed(requestedFinalityConfig, s_allowedFinalityConfig);\n\n TokenTransferFeeConfig memory feeConfig = s_tokenTransferFeeConfig[destChainSelector];\n\n // If config is disabled, return zeros with isEnabled=false to signal OnRamp to use FeeQuoter defaults.\n if (!feeConfig.isEnabled) {\n return (0, 0, 0, 0, false);\n }\n\n if (requestedFinalityConfig != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n return (\n feeConfig.fastFinalityFeeUSDCents,\n feeConfig.destGasOverhead,\n feeConfig.destBytesOverhead,\n feeConfig.fastFinalityTransferFeeBps,\n true\n );\n }\n return (\n feeConfig.finalityFeeUSDCents,\n feeConfig.destGasOverhead,\n feeConfig.destBytesOverhead,\n feeConfig.finalityTransferFeeBps,\n true\n );\n }\n\n /// @dev Calculates the fee based on the transferred amount, and the configured basis points.\n /// @param lockOrBurnIn The original lock or burn request.\n /// @param requestedFinalityConfig The requested finality encoding (see `FinalityCodec`).\n /// A value of zero (FinalityCodec.WAIT_FOR_FINALITY_FLAG) applies default finality fees.\n /// Returns the fee amount.\n function _getFee(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig\n ) internal view virtual returns (uint256) {\n TokenTransferFeeConfig storage feeConfig = s_tokenTransferFeeConfig[lockOrBurnIn.remoteChainSelector];\n\n // Determine which fee basis points to apply based on finality type.\n if (requestedFinalityConfig != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n return (lockOrBurnIn.amount * feeConfig.fastFinalityTransferFeeBps) / BPS_DIVIDER;\n } else {\n return (lockOrBurnIn.amount * feeConfig.finalityTransferFeeBps) / BPS_DIVIDER;\n }\n }\n\n /// @notice Withdraws accrued fee token balances to the provided `recipient`.\n /// @dev Only callable by the owner or the fee admin.\n /// @dev FeeTokenHandler will revert if `recipient` is zero address.\n /// @dev Pools accrue fees directly on this contract. Lock/release pools send bridge liquidity to their ERC20 lockbox\n /// during the lock flow, which means any balance left on this contract represents fees that have accrued to the pool.\n /// Because user liquidity never resides on `address(this)` for lock/release pools, transferring the full contract\n /// balance is safe and clears only accrued fees.\n /// @param feeTokens The token addresses to withdraw, including the pool token when applicable.\n /// @param recipient The address to withdraw the fee tokens to.\n function withdrawFeeTokens(\n address[] calldata feeTokens,\n address recipient\n ) external virtual {\n if (msg.sender != owner() && msg.sender != s_feeAdmin) {\n revert CallerIsNotOwnerOrFeeAdmin(msg.sender);\n }\n FeeTokenHandler._withdrawFeeTokens(feeTokens, recipient);\n }\n}\n" + }, + "contracts/tokens/BaseERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {IGetCCIPAdmin} from \"../interfaces/IGetCCIPAdmin.sol\";\nimport {ITypeAndVersion} from \"@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol\";\n\nimport {ERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol\";\nimport {IERC20Metadata} from \"@openzeppelin/contracts@5.3.0/token/ERC20/extensions/IERC20Metadata.sol\";\nimport {IERC165} from \"@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol\";\n\n/// @notice A basic ERC20 that has no burn/mint functions.\n/// @dev If this contract is deployed with a pre-mint of 0, it is effectively useless as no mint functionality is\n/// exposed.\ncontract BaseERC20 is IGetCCIPAdmin, ERC20, ITypeAndVersion, IERC165 {\n function typeAndVersion() external pure virtual override returns (string memory) {\n return \"BaseERC20 2.0.0\";\n }\n\n error CannotRenounceCCIPAdmin();\n error MaxSupplyExceeded(uint256 supplyAfterMint, uint256 maxSupply);\n error OnlyCCIPAdmin();\n error PreMintAddressNotSet();\n error PreMintRecipientSetWithZeroPreMint(address preMintRecipient);\n\n /// @notice Emitted when the CCIPAdmin role is transferred to a new address.\n /// @param previousAdmin The address of the previous CCIPAdmin.\n /// @param newAdmin The address of the new CCIPAdmin.\n event CCIPAdminTransferred(address indexed previousAdmin, address indexed newAdmin);\n\n /// @param name The name of the token\n /// @param symbol The symbol of the token\n /// @param maxSupply_ The maximum supply of the token, 0 if unlimited\n /// @param preMint The amount of tokens to mint upon construction. Must be zero, or preMintRecipient must be set.\n /// NOTE: the base version of this contract does not support minting additional tokens after deployment, so this\n /// should be set to the full supply.\n /// @param preMintRecipient The address to receive the pre-mint amount. Must be non-zero if preMint is non-zero.\n /// @param decimals The number of decimals the token uses\n /// @param ccipAdmin The initial CCIPAdmin. If set to address(0), the deployer will be set as the initial CCIPAdmin.\n struct ConstructorParams {\n string name;\n string symbol;\n uint256 maxSupply;\n uint256 preMint;\n address preMintRecipient;\n uint8 decimals;\n address ccipAdmin;\n }\n\n /// @dev The number of decimals for the token\n uint8 internal immutable i_decimals;\n\n /// @dev The maximum supply of the token, 0 if unlimited\n uint256 internal immutable i_maxSupply;\n\n /// @dev the CCIPAdmin can be used to register with the CCIP token admin registry, but has no other special powers.\n address internal s_ccipAdmin;\n\n constructor(\n ConstructorParams memory args\n ) ERC20(args.name, args.symbol) {\n i_decimals = args.decimals;\n i_maxSupply = args.maxSupply;\n\n // Mint the initial supply to the preMintRecipient, saving gas by not calling if the mint amount is zero.\n if (args.preMint != 0) {\n if (args.preMintRecipient == address(0)) revert PreMintAddressNotSet();\n\n _mint(args.preMintRecipient, args.preMint);\n } else if (args.preMintRecipient != address(0)) {\n revert PreMintRecipientSetWithZeroPreMint(args.preMintRecipient);\n }\n\n _setCCIPAdmin(args.ccipAdmin == address(0) ? msg.sender : args.ccipAdmin);\n }\n\n /// @inheritdoc IERC165\n function supportsInterface(\n bytes4 interfaceId\n ) public view virtual returns (bool) {\n return interfaceId == type(IERC20).interfaceId || interfaceId == type(IGetCCIPAdmin).interfaceId\n || interfaceId == type(IERC20Metadata).interfaceId || interfaceId == type(IERC165).interfaceId;\n }\n\n // ================================================================\n // \u2502 ERC20 \u2502\n // ================================================================\n\n /// @notice Returns the number of decimals for this token.\n /// @return _decimals The number of decimals for this token.\n function decimals() public view virtual override returns (uint8 _decimals) {\n return i_decimals;\n }\n\n /// @notice Returns the max supply of the token, 0 if unlimited.\n /// @return _maxSupply The max supply of the token, 0 if unlimited.\n function maxSupply() public view virtual returns (uint256 _maxSupply) {\n return i_maxSupply;\n }\n\n /// @inheritdoc ERC20\n /// @dev Uses OZ ERC20 _approve to disallow approving for address(0).\n /// @dev Disallows approving for address(this).\n function _approve(\n address owner,\n address spender,\n uint256 value,\n bool emitEvent\n ) internal virtual override {\n if (spender == address(this)) revert ERC20InvalidSpender(spender);\n\n super._approve(owner, spender, value, emitEvent);\n }\n\n /// @inheritdoc ERC20\n /// @dev This check applies to transfer, minting, and burning.\n /// @dev Disallows transferring/minting to address(this).\n function _update(\n address from,\n address to,\n uint256 value\n ) internal virtual override {\n if (to == address(this)) revert ERC20InvalidReceiver(to);\n\n // Update first, then check the total supply.\n super._update(from, to, value);\n\n // If `from` is address(0), this is a mint, so we need to check the total supply against the max supply.\n if (from == address(0)) {\n _assertMaxSupply();\n }\n }\n\n /// @notice Asserts that the total supply does not exceed the max supply. Reverts if it does.\n function _assertMaxSupply() internal view virtual {\n if (i_maxSupply != 0) {\n uint256 supply = totalSupply();\n if (supply > i_maxSupply) {\n revert MaxSupplyExceeded(supply, i_maxSupply);\n }\n }\n }\n\n // ================================================================\n // \u2502 Roles \u2502\n // ================================================================\n\n /// @notice Gets the current CCIPAdmin.\n /// @return ccipAdmin The address of the current CCIPAdmin.\n function getCCIPAdmin() external view virtual returns (address ccipAdmin) {\n return s_ccipAdmin;\n }\n\n /// @notice Transfers the CCIPAdmin role to a new address.\n /// @param newAdmin The address of the new CCIPAdmin. Setting this to address(0) is not allowed.\n /// @dev The BaseERC20 has no notion of ownership, so this function can be called by the CCIP admin. Tokens expanding\n /// from this base contract can choose to restrict this function to other roles instead.\n function setCCIPAdmin(\n address newAdmin\n ) external virtual {\n if (msg.sender != s_ccipAdmin) {\n revert OnlyCCIPAdmin();\n }\n\n if (newAdmin == address(0)) revert CannotRenounceCCIPAdmin();\n\n _setCCIPAdmin(newAdmin);\n }\n\n /// @dev Internal function to set the CCIPAdmin, emits an event with the previous and new admin.\n /// @param newAdmin The address of the new CCIPAdmin.\n function _setCCIPAdmin(\n address newAdmin\n ) internal virtual {\n address currentAdmin = s_ccipAdmin;\n\n s_ccipAdmin = newAdmin;\n\n emit CCIPAdminTransferred(currentAdmin, newAdmin);\n }\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2Step.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {IOwnable} from \"../interfaces/IOwnable.sol\";\n\n/// @notice A minimal contract that implements 2-step ownership transfer and nothing more. It's made to be minimal\n/// to reduce the impact of the bytecode size on any contract that inherits from it.\ncontract Ownable2Step is IOwnable {\n /// @notice The pending owner is the address to which ownership may be transferred.\n address private s_pendingOwner;\n /// @notice The owner is the current owner of the contract.\n /// @dev The owner is the second storage variable so any implementing contract could pack other state with it\n /// instead of the much less used s_pendingOwner.\n address private s_owner;\n\n error OwnerCannotBeZero();\n error MustBeProposedOwner();\n error CannotTransferToSelf();\n error OnlyCallableByOwner();\n\n event OwnershipTransferRequested(address indexed from, address indexed to);\n event OwnershipTransferred(address indexed from, address indexed to);\n\n constructor(address newOwner, address pendingOwner) {\n if (newOwner == address(0)) {\n revert OwnerCannotBeZero();\n }\n\n s_owner = newOwner;\n if (pendingOwner != address(0)) {\n _transferOwnership(pendingOwner);\n }\n }\n\n /// @notice Get the current owner\n function owner() public view override returns (address) {\n return s_owner;\n }\n\n /// @notice Allows an owner to begin transferring ownership to a new address. The new owner needs to call\n /// `acceptOwnership` to accept the transfer before any permissions are changed.\n /// @param to The address to which ownership will be transferred.\n function transferOwnership(\n address to\n ) public override onlyOwner {\n _transferOwnership(to);\n }\n\n /// @notice validate, transfer ownership, and emit relevant events\n /// @param to The address to which ownership will be transferred.\n function _transferOwnership(\n address to\n ) private {\n if (to == msg.sender) {\n revert CannotTransferToSelf();\n }\n\n s_pendingOwner = to;\n\n emit OwnershipTransferRequested(s_owner, to);\n }\n\n /// @notice Allows an ownership transfer to be completed by the recipient.\n function acceptOwnership() external override {\n if (msg.sender != s_pendingOwner) {\n revert MustBeProposedOwner();\n }\n\n address oldOwner = s_owner;\n s_owner = msg.sender;\n s_pendingOwner = address(0);\n\n emit OwnershipTransferred(oldOwner, msg.sender);\n }\n\n /// @notice validate access\n function _validateOwnership() internal view {\n if (msg.sender != s_owner) {\n revert OnlyCallableByOwner();\n }\n }\n\n /// @notice Reverts if called by anyone other than the contract owner.\n modifier onlyOwner() {\n _validateOwnership();\n _;\n }\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {Ownable2Step} from \"./Ownable2Step.sol\";\n\n/// @notice Sets the msg.sender to be the owner of the contract and does not set a pending owner.\ncontract Ownable2StepMsgSender is Ownable2Step {\n constructor() Ownable2Step(msg.sender, address(0)) {}\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/IOwnable.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface IOwnable {\n function owner() external returns (address);\n\n function transferOwnership(\n address recipient\n ) external;\n\n function acceptOwnership() external;\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface ITypeAndVersion {\n function typeAndVersion() external pure returns (string memory);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC1363.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1363.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"./IERC20.sol\";\nimport {IERC165} from \"./IERC165.sol\";\n\n/**\n * @title IERC1363\n * @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].\n *\n * Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract\n * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.\n */\ninterface IERC1363 is IERC20, IERC165 {\n /*\n * Note: the ERC-165 identifier for this interface is 0xb0202a11.\n * 0xb0202a11 ===\n * bytes4(keccak256('transferAndCall(address,uint256)')) ^\n * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^\n * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^\n * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^\n * bytes4(keccak256('approveAndCall(address,uint256)')) ^\n * bytes4(keccak256('approveAndCall(address,uint256,bytes)'))\n */\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferAndCall(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @param data Additional data with no specified format, sent in call to `to`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param from The address which you want to send tokens from.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferFromAndCall(address from, address to, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param from The address which you want to send tokens from.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @param data Additional data with no specified format, sent in call to `to`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.\n * @param spender The address which will spend the funds.\n * @param value The amount of tokens to be spent.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function approveAndCall(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.\n * @param spender The address which will spend the funds.\n * @param value The amount of tokens to be spent.\n * @param data Additional data with no specified format, sent in call to `spender`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC165.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC165.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC165} from \"../utils/introspection/IERC165.sol\";\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC20.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../token/ERC20/IERC20.sol\";\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/draft-IERC6093.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/draft-IERC6093.sol)\npragma solidity ^0.8.20;\n\n/**\n * @dev Standard ERC-20 Errors\n * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-20 tokens.\n */\ninterface IERC20Errors {\n /**\n * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n * @param balance Current balance for the interacting account.\n * @param needed Minimum amount required to perform a transfer.\n */\n error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);\n\n /**\n * @dev Indicates a failure with the token `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n */\n error ERC20InvalidSender(address sender);\n\n /**\n * @dev Indicates a failure with the token `receiver`. Used in transfers.\n * @param receiver Address to which tokens are being transferred.\n */\n error ERC20InvalidReceiver(address receiver);\n\n /**\n * @dev Indicates a failure with the `spender`\u2019s `allowance`. Used in transfers.\n * @param spender Address that may be allowed to operate on tokens without being their owner.\n * @param allowance Amount of tokens a `spender` is allowed to operate with.\n * @param needed Minimum amount required to perform a transfer.\n */\n error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);\n\n /**\n * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.\n * @param approver Address initiating an approval operation.\n */\n error ERC20InvalidApprover(address approver);\n\n /**\n * @dev Indicates a failure with the `spender` to be approved. Used in approvals.\n * @param spender Address that may be allowed to operate on tokens without being their owner.\n */\n error ERC20InvalidSpender(address spender);\n}\n\n/**\n * @dev Standard ERC-721 Errors\n * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-721 tokens.\n */\ninterface IERC721Errors {\n /**\n * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in ERC-20.\n * Used in balance queries.\n * @param owner Address of the current owner of a token.\n */\n error ERC721InvalidOwner(address owner);\n\n /**\n * @dev Indicates a `tokenId` whose `owner` is the zero address.\n * @param tokenId Identifier number of a token.\n */\n error ERC721NonexistentToken(uint256 tokenId);\n\n /**\n * @dev Indicates an error related to the ownership over a particular token. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n * @param tokenId Identifier number of a token.\n * @param owner Address of the current owner of a token.\n */\n error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner);\n\n /**\n * @dev Indicates a failure with the token `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n */\n error ERC721InvalidSender(address sender);\n\n /**\n * @dev Indicates a failure with the token `receiver`. Used in transfers.\n * @param receiver Address to which tokens are being transferred.\n */\n error ERC721InvalidReceiver(address receiver);\n\n /**\n * @dev Indicates a failure with the `operator`\u2019s approval. Used in transfers.\n * @param operator Address that may be allowed to operate on tokens without being their owner.\n * @param tokenId Identifier number of a token.\n */\n error ERC721InsufficientApproval(address operator, uint256 tokenId);\n\n /**\n * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.\n * @param approver Address initiating an approval operation.\n */\n error ERC721InvalidApprover(address approver);\n\n /**\n * @dev Indicates a failure with the `operator` to be approved. Used in approvals.\n * @param operator Address that may be allowed to operate on tokens without being their owner.\n */\n error ERC721InvalidOperator(address operator);\n}\n\n/**\n * @dev Standard ERC-1155 Errors\n * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-1155 tokens.\n */\ninterface IERC1155Errors {\n /**\n * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n * @param balance Current balance for the interacting account.\n * @param needed Minimum amount required to perform a transfer.\n * @param tokenId Identifier number of a token.\n */\n error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId);\n\n /**\n * @dev Indicates a failure with the token `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n */\n error ERC1155InvalidSender(address sender);\n\n /**\n * @dev Indicates a failure with the token `receiver`. Used in transfers.\n * @param receiver Address to which tokens are being transferred.\n */\n error ERC1155InvalidReceiver(address receiver);\n\n /**\n * @dev Indicates a failure with the `operator`\u2019s approval. Used in transfers.\n * @param operator Address that may be allowed to operate on tokens without being their owner.\n * @param owner Address of the current owner of a token.\n */\n error ERC1155MissingApprovalForAll(address operator, address owner);\n\n /**\n * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.\n * @param approver Address initiating an approval operation.\n */\n error ERC1155InvalidApprover(address approver);\n\n /**\n * @dev Indicates a failure with the `operator` to be approved. Used in approvals.\n * @param operator Address that may be allowed to operate on tokens without being their owner.\n */\n error ERC1155InvalidOperator(address operator);\n\n /**\n * @dev Indicates an array length mismatch between ids and values in a safeBatchTransferFrom operation.\n * Used in batch transfers.\n * @param idsLength Length of the array of token identifiers\n * @param valuesLength Length of the array of token amounts\n */\n error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/ERC20.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"./IERC20.sol\";\nimport {IERC20Metadata} from \"./extensions/IERC20Metadata.sol\";\nimport {Context} from \"../../utils/Context.sol\";\nimport {IERC20Errors} from \"../../interfaces/draft-IERC6093.sol\";\n\n/**\n * @dev Implementation of the {IERC20} interface.\n *\n * This implementation is agnostic to the way tokens are created. This means\n * that a supply mechanism has to be added in a derived contract using {_mint}.\n *\n * TIP: For a detailed writeup see our guide\n * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How\n * to implement supply mechanisms].\n *\n * The default value of {decimals} is 18. To change this, you should override\n * this function so it returns a different value.\n *\n * We have followed general OpenZeppelin Contracts guidelines: functions revert\n * instead returning `false` on failure. This behavior is nonetheless\n * conventional and does not conflict with the expectations of ERC-20\n * applications.\n */\nabstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {\n mapping(address account => uint256) private _balances;\n\n mapping(address account => mapping(address spender => uint256)) private _allowances;\n\n uint256 private _totalSupply;\n\n string private _name;\n string private _symbol;\n\n /**\n * @dev Sets the values for {name} and {symbol}.\n *\n * Both values are immutable: they can only be set once during construction.\n */\n constructor(string memory name_, string memory symbol_) {\n _name = name_;\n _symbol = symbol_;\n }\n\n /**\n * @dev Returns the name of the token.\n */\n function name() public view virtual returns (string memory) {\n return _name;\n }\n\n /**\n * @dev Returns the symbol of the token, usually a shorter version of the\n * name.\n */\n function symbol() public view virtual returns (string memory) {\n return _symbol;\n }\n\n /**\n * @dev Returns the number of decimals used to get its user representation.\n * For example, if `decimals` equals `2`, a balance of `505` tokens should\n * be displayed to a user as `5.05` (`505 / 10 ** 2`).\n *\n * Tokens usually opt for a value of 18, imitating the relationship between\n * Ether and Wei. This is the default value returned by this function, unless\n * it's overridden.\n *\n * NOTE: This information is only used for _display_ purposes: it in\n * no way affects any of the arithmetic of the contract, including\n * {IERC20-balanceOf} and {IERC20-transfer}.\n */\n function decimals() public view virtual returns (uint8) {\n return 18;\n }\n\n /**\n * @dev See {IERC20-totalSupply}.\n */\n function totalSupply() public view virtual returns (uint256) {\n return _totalSupply;\n }\n\n /**\n * @dev See {IERC20-balanceOf}.\n */\n function balanceOf(address account) public view virtual returns (uint256) {\n return _balances[account];\n }\n\n /**\n * @dev See {IERC20-transfer}.\n *\n * Requirements:\n *\n * - `to` cannot be the zero address.\n * - the caller must have a balance of at least `value`.\n */\n function transfer(address to, uint256 value) public virtual returns (bool) {\n address owner = _msgSender();\n _transfer(owner, to, value);\n return true;\n }\n\n /**\n * @dev See {IERC20-allowance}.\n */\n function allowance(address owner, address spender) public view virtual returns (uint256) {\n return _allowances[owner][spender];\n }\n\n /**\n * @dev See {IERC20-approve}.\n *\n * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on\n * `transferFrom`. This is semantically equivalent to an infinite approval.\n *\n * Requirements:\n *\n * - `spender` cannot be the zero address.\n */\n function approve(address spender, uint256 value) public virtual returns (bool) {\n address owner = _msgSender();\n _approve(owner, spender, value);\n return true;\n }\n\n /**\n * @dev See {IERC20-transferFrom}.\n *\n * Skips emitting an {Approval} event indicating an allowance update. This is not\n * required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve].\n *\n * NOTE: Does not update the allowance if the current allowance\n * is the maximum `uint256`.\n *\n * Requirements:\n *\n * - `from` and `to` cannot be the zero address.\n * - `from` must have a balance of at least `value`.\n * - the caller must have allowance for ``from``'s tokens of at least\n * `value`.\n */\n function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {\n address spender = _msgSender();\n _spendAllowance(from, spender, value);\n _transfer(from, to, value);\n return true;\n }\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to`.\n *\n * This internal function is equivalent to {transfer}, and can be used to\n * e.g. implement automatic token fees, slashing mechanisms, etc.\n *\n * Emits a {Transfer} event.\n *\n * NOTE: This function is not virtual, {_update} should be overridden instead.\n */\n function _transfer(address from, address to, uint256 value) internal {\n if (from == address(0)) {\n revert ERC20InvalidSender(address(0));\n }\n if (to == address(0)) {\n revert ERC20InvalidReceiver(address(0));\n }\n _update(from, to, value);\n }\n\n /**\n * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`\n * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding\n * this function.\n *\n * Emits a {Transfer} event.\n */\n function _update(address from, address to, uint256 value) internal virtual {\n if (from == address(0)) {\n // Overflow check required: The rest of the code assumes that totalSupply never overflows\n _totalSupply += value;\n } else {\n uint256 fromBalance = _balances[from];\n if (fromBalance < value) {\n revert ERC20InsufficientBalance(from, fromBalance, value);\n }\n unchecked {\n // Overflow not possible: value <= fromBalance <= totalSupply.\n _balances[from] = fromBalance - value;\n }\n }\n\n if (to == address(0)) {\n unchecked {\n // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.\n _totalSupply -= value;\n }\n } else {\n unchecked {\n // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.\n _balances[to] += value;\n }\n }\n\n emit Transfer(from, to, value);\n }\n\n /**\n * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).\n * Relies on the `_update` mechanism\n *\n * Emits a {Transfer} event with `from` set to the zero address.\n *\n * NOTE: This function is not virtual, {_update} should be overridden instead.\n */\n function _mint(address account, uint256 value) internal {\n if (account == address(0)) {\n revert ERC20InvalidReceiver(address(0));\n }\n _update(address(0), account, value);\n }\n\n /**\n * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.\n * Relies on the `_update` mechanism.\n *\n * Emits a {Transfer} event with `to` set to the zero address.\n *\n * NOTE: This function is not virtual, {_update} should be overridden instead\n */\n function _burn(address account, uint256 value) internal {\n if (account == address(0)) {\n revert ERC20InvalidSender(address(0));\n }\n _update(account, address(0), value);\n }\n\n /**\n * @dev Sets `value` as the allowance of `spender` over the `owner`'s tokens.\n *\n * This internal function is equivalent to `approve`, and can be used to\n * e.g. set automatic allowances for certain subsystems, etc.\n *\n * Emits an {Approval} event.\n *\n * Requirements:\n *\n * - `owner` cannot be the zero address.\n * - `spender` cannot be the zero address.\n *\n * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.\n */\n function _approve(address owner, address spender, uint256 value) internal {\n _approve(owner, spender, value, true);\n }\n\n /**\n * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.\n *\n * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by\n * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any\n * `Approval` event during `transferFrom` operations.\n *\n * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to\n * true using the following override:\n *\n * ```solidity\n * function _approve(address owner, address spender, uint256 value, bool) internal virtual override {\n * super._approve(owner, spender, value, true);\n * }\n * ```\n *\n * Requirements are the same as {_approve}.\n */\n function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {\n if (owner == address(0)) {\n revert ERC20InvalidApprover(address(0));\n }\n if (spender == address(0)) {\n revert ERC20InvalidSpender(address(0));\n }\n _allowances[owner][spender] = value;\n if (emitEvent) {\n emit Approval(owner, spender, value);\n }\n }\n\n /**\n * @dev Updates `owner`'s allowance for `spender` based on spent `value`.\n *\n * Does not update the allowance value in case of infinite allowance.\n * Revert if not enough allowance is available.\n *\n * Does not emit an {Approval} event.\n */\n function _spendAllowance(address owner, address spender, uint256 value) internal virtual {\n uint256 currentAllowance = allowance(owner, spender);\n if (currentAllowance < type(uint256).max) {\n if (currentAllowance < value) {\n revert ERC20InsufficientAllowance(spender, currentAllowance, value);\n }\n unchecked {\n _approve(owner, spender, currentAllowance - value, false);\n }\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/IERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-20 standard as defined in the ERC.\n */\ninterface IERC20 {\n /**\n * @dev Emitted when `value` tokens are moved from one account (`from`) to\n * another (`to`).\n *\n * Note that `value` may be zero.\n */\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n /**\n * @dev Emitted when the allowance of a `spender` for an `owner` is set by\n * a call to {approve}. `value` is the new allowance.\n */\n event Approval(address indexed owner, address indexed spender, uint256 value);\n\n /**\n * @dev Returns the value of tokens in existence.\n */\n function totalSupply() external view returns (uint256);\n\n /**\n * @dev Returns the value of tokens owned by `account`.\n */\n function balanceOf(address account) external view returns (uint256);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transfer(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Returns the remaining number of tokens that `spender` will be\n * allowed to spend on behalf of `owner` through {transferFrom}. This is\n * zero by default.\n *\n * This value changes when {approve} or {transferFrom} are called.\n */\n function allowance(address owner, address spender) external view returns (uint256);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * IMPORTANT: Beware that changing an allowance with this method brings the risk\n * that someone may use both the old and the new allowance by unfortunate\n * transaction ordering. One possible solution to mitigate this race\n * condition is to first reduce the spender's allowance to 0 and set the\n * desired value afterwards:\n * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n *\n * Emits an {Approval} event.\n */\n function approve(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the\n * allowance mechanism. `value` is then deducted from the caller's\n * allowance.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(address from, address to, uint256 value) external returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Metadata.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Metadata.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../IERC20.sol\";\n\n/**\n * @dev Interface for the optional metadata functions from the ERC-20 standard.\n */\ninterface IERC20Metadata is IERC20 {\n /**\n * @dev Returns the name of the token.\n */\n function name() external view returns (string memory);\n\n /**\n * @dev Returns the symbol of the token.\n */\n function symbol() external view returns (string memory);\n\n /**\n * @dev Returns the decimals places of the token.\n */\n function decimals() external view returns (uint8);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/utils/SafeERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../IERC20.sol\";\nimport {IERC1363} from \"../../../interfaces/IERC1363.sol\";\n\n/**\n * @title SafeERC20\n * @dev Wrappers around ERC-20 operations that throw on failure (when the token\n * contract returns false). Tokens that return no value (and instead revert or\n * throw on failure) are also supported, non-reverting calls are assumed to be\n * successful.\n * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,\n * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.\n */\nlibrary SafeERC20 {\n /**\n * @dev An operation with an ERC-20 token failed.\n */\n error SafeERC20FailedOperation(address token);\n\n /**\n * @dev Indicates a failed `decreaseAllowance` request.\n */\n error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);\n\n /**\n * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,\n * non-reverting calls are assumed to be successful.\n */\n function safeTransfer(IERC20 token, address to, uint256 value) internal {\n _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));\n }\n\n /**\n * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the\n * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.\n */\n function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {\n _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));\n }\n\n /**\n * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.\n */\n function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {\n return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value)));\n }\n\n /**\n * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.\n */\n function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {\n return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value)));\n }\n\n /**\n * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,\n * non-reverting calls are assumed to be successful.\n *\n * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the \"client\"\n * smart contract uses ERC-7674 to set temporary allowances, then the \"client\" smart contract should avoid using\n * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract\n * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.\n */\n function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {\n uint256 oldAllowance = token.allowance(address(this), spender);\n forceApprove(token, spender, oldAllowance + value);\n }\n\n /**\n * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no\n * value, non-reverting calls are assumed to be successful.\n *\n * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the \"client\"\n * smart contract uses ERC-7674 to set temporary allowances, then the \"client\" smart contract should avoid using\n * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract\n * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.\n */\n function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {\n unchecked {\n uint256 currentAllowance = token.allowance(address(this), spender);\n if (currentAllowance < requestedDecrease) {\n revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);\n }\n forceApprove(token, spender, currentAllowance - requestedDecrease);\n }\n }\n\n /**\n * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,\n * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval\n * to be set to zero before setting it to a non-zero value, such as USDT.\n *\n * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function\n * only sets the \"standard\" allowance. Any temporary allowance will remain active, in addition to the value being\n * set here.\n */\n function forceApprove(IERC20 token, address spender, uint256 value) internal {\n bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));\n\n if (!_callOptionalReturnBool(token, approvalCall)) {\n _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));\n _callOptionalReturn(token, approvalCall);\n }\n }\n\n /**\n * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no\n * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * Reverts if the returned value is other than `true`.\n */\n function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {\n if (to.code.length == 0) {\n safeTransfer(token, to, value);\n } else if (!token.transferAndCall(to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target\n * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * Reverts if the returned value is other than `true`.\n */\n function transferFromAndCallRelaxed(\n IERC1363 token,\n address from,\n address to,\n uint256 value,\n bytes memory data\n ) internal {\n if (to.code.length == 0) {\n safeTransferFrom(token, from, to, value);\n } else if (!token.transferFromAndCall(from, to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no\n * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.\n * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}\n * once without retrying, and relies on the returned value to be true.\n *\n * Reverts if the returned value is other than `true`.\n */\n function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {\n if (to.code.length == 0) {\n forceApprove(token, to, value);\n } else if (!token.approveAndCall(to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement\n * on the return value: the return value is optional (but if data is returned, it must not be false).\n * @param token The token targeted by the call.\n * @param data The call data (encoded using abi.encode or one of its variants).\n *\n * This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.\n */\n function _callOptionalReturn(IERC20 token, bytes memory data) private {\n uint256 returnSize;\n uint256 returnValue;\n assembly (\"memory-safe\") {\n let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)\n // bubble errors\n if iszero(success) {\n let ptr := mload(0x40)\n returndatacopy(ptr, 0, returndatasize())\n revert(ptr, returndatasize())\n }\n returnSize := returndatasize()\n returnValue := mload(0)\n }\n\n if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement\n * on the return value: the return value is optional (but if data is returned, it must not be false).\n * @param token The token targeted by the call.\n * @param data The call data (encoded using abi.encode or one of its variants).\n *\n * This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.\n */\n function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {\n bool success;\n uint256 returnSize;\n uint256 returnValue;\n assembly (\"memory-safe\") {\n success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)\n returnSize := returndatasize()\n returnValue := mload(0)\n }\n return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Arrays.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/Arrays.sol)\n// This file was procedurally generated from scripts/generate/templates/Arrays.js.\n\npragma solidity ^0.8.20;\n\nimport {Comparators} from \"./Comparators.sol\";\nimport {SlotDerivation} from \"./SlotDerivation.sol\";\nimport {StorageSlot} from \"./StorageSlot.sol\";\nimport {Math} from \"./math/Math.sol\";\n\n/**\n * @dev Collection of functions related to array types.\n */\nlibrary Arrays {\n using SlotDerivation for bytes32;\n using StorageSlot for bytes32;\n\n /**\n * @dev Sort an array of uint256 (in memory) following the provided comparator function.\n *\n * This function does the sorting \"in place\", meaning that it overrides the input. The object is returned for\n * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.\n *\n * NOTE: this function's cost is `O(n \u00b7 log(n))` in average and `O(n\u00b2)` in the worst case, with n the length of the\n * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful\n * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may\n * consume more gas than is available in a block, leading to potential DoS.\n *\n * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.\n */\n function sort(\n uint256[] memory array,\n function(uint256, uint256) pure returns (bool) comp\n ) internal pure returns (uint256[] memory) {\n _quickSort(_begin(array), _end(array), comp);\n return array;\n }\n\n /**\n * @dev Variant of {sort} that sorts an array of uint256 in increasing order.\n */\n function sort(uint256[] memory array) internal pure returns (uint256[] memory) {\n sort(array, Comparators.lt);\n return array;\n }\n\n /**\n * @dev Sort an array of address (in memory) following the provided comparator function.\n *\n * This function does the sorting \"in place\", meaning that it overrides the input. The object is returned for\n * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.\n *\n * NOTE: this function's cost is `O(n \u00b7 log(n))` in average and `O(n\u00b2)` in the worst case, with n the length of the\n * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful\n * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may\n * consume more gas than is available in a block, leading to potential DoS.\n *\n * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.\n */\n function sort(\n address[] memory array,\n function(address, address) pure returns (bool) comp\n ) internal pure returns (address[] memory) {\n sort(_castToUint256Array(array), _castToUint256Comp(comp));\n return array;\n }\n\n /**\n * @dev Variant of {sort} that sorts an array of address in increasing order.\n */\n function sort(address[] memory array) internal pure returns (address[] memory) {\n sort(_castToUint256Array(array), Comparators.lt);\n return array;\n }\n\n /**\n * @dev Sort an array of bytes32 (in memory) following the provided comparator function.\n *\n * This function does the sorting \"in place\", meaning that it overrides the input. The object is returned for\n * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.\n *\n * NOTE: this function's cost is `O(n \u00b7 log(n))` in average and `O(n\u00b2)` in the worst case, with n the length of the\n * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful\n * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may\n * consume more gas than is available in a block, leading to potential DoS.\n *\n * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.\n */\n function sort(\n bytes32[] memory array,\n function(bytes32, bytes32) pure returns (bool) comp\n ) internal pure returns (bytes32[] memory) {\n sort(_castToUint256Array(array), _castToUint256Comp(comp));\n return array;\n }\n\n /**\n * @dev Variant of {sort} that sorts an array of bytes32 in increasing order.\n */\n function sort(bytes32[] memory array) internal pure returns (bytes32[] memory) {\n sort(_castToUint256Array(array), Comparators.lt);\n return array;\n }\n\n /**\n * @dev Performs a quick sort of a segment of memory. The segment sorted starts at `begin` (inclusive), and stops\n * at end (exclusive). Sorting follows the `comp` comparator.\n *\n * Invariant: `begin <= end`. This is the case when initially called by {sort} and is preserved in subcalls.\n *\n * IMPORTANT: Memory locations between `begin` and `end` are not validated/zeroed. This function should\n * be used only if the limits are within a memory array.\n */\n function _quickSort(uint256 begin, uint256 end, function(uint256, uint256) pure returns (bool) comp) private pure {\n unchecked {\n if (end - begin < 0x40) return;\n\n // Use first element as pivot\n uint256 pivot = _mload(begin);\n // Position where the pivot should be at the end of the loop\n uint256 pos = begin;\n\n for (uint256 it = begin + 0x20; it < end; it += 0x20) {\n if (comp(_mload(it), pivot)) {\n // If the value stored at the iterator's position comes before the pivot, we increment the\n // position of the pivot and move the value there.\n pos += 0x20;\n _swap(pos, it);\n }\n }\n\n _swap(begin, pos); // Swap pivot into place\n _quickSort(begin, pos, comp); // Sort the left side of the pivot\n _quickSort(pos + 0x20, end, comp); // Sort the right side of the pivot\n }\n }\n\n /**\n * @dev Pointer to the memory location of the first element of `array`.\n */\n function _begin(uint256[] memory array) private pure returns (uint256 ptr) {\n assembly (\"memory-safe\") {\n ptr := add(array, 0x20)\n }\n }\n\n /**\n * @dev Pointer to the memory location of the first memory word (32bytes) after `array`. This is the memory word\n * that comes just after the last element of the array.\n */\n function _end(uint256[] memory array) private pure returns (uint256 ptr) {\n unchecked {\n return _begin(array) + array.length * 0x20;\n }\n }\n\n /**\n * @dev Load memory word (as a uint256) at location `ptr`.\n */\n function _mload(uint256 ptr) private pure returns (uint256 value) {\n assembly {\n value := mload(ptr)\n }\n }\n\n /**\n * @dev Swaps the elements memory location `ptr1` and `ptr2`.\n */\n function _swap(uint256 ptr1, uint256 ptr2) private pure {\n assembly {\n let value1 := mload(ptr1)\n let value2 := mload(ptr2)\n mstore(ptr1, value2)\n mstore(ptr2, value1)\n }\n }\n\n /// @dev Helper: low level cast address memory array to uint256 memory array\n function _castToUint256Array(address[] memory input) private pure returns (uint256[] memory output) {\n assembly {\n output := input\n }\n }\n\n /// @dev Helper: low level cast bytes32 memory array to uint256 memory array\n function _castToUint256Array(bytes32[] memory input) private pure returns (uint256[] memory output) {\n assembly {\n output := input\n }\n }\n\n /// @dev Helper: low level cast address comp function to uint256 comp function\n function _castToUint256Comp(\n function(address, address) pure returns (bool) input\n ) private pure returns (function(uint256, uint256) pure returns (bool) output) {\n assembly {\n output := input\n }\n }\n\n /// @dev Helper: low level cast bytes32 comp function to uint256 comp function\n function _castToUint256Comp(\n function(bytes32, bytes32) pure returns (bool) input\n ) private pure returns (function(uint256, uint256) pure returns (bool) output) {\n assembly {\n output := input\n }\n }\n\n /**\n * @dev Searches a sorted `array` and returns the first index that contains\n * a value greater or equal to `element`. If no such index exists (i.e. all\n * values in the array are strictly less than `element`), the array length is\n * returned. Time complexity O(log n).\n *\n * NOTE: The `array` is expected to be sorted in ascending order, and to\n * contain no repeated elements.\n *\n * IMPORTANT: Deprecated. This implementation behaves as {lowerBound} but lacks\n * support for repeated elements in the array. The {lowerBound} function should\n * be used instead.\n */\n function findUpperBound(uint256[] storage array, uint256 element) internal view returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeAccess(array, mid).value > element) {\n high = mid;\n } else {\n low = mid + 1;\n }\n }\n\n // At this point `low` is the exclusive upper bound. We will return the inclusive upper bound.\n if (low > 0 && unsafeAccess(array, low - 1).value == element) {\n return low - 1;\n } else {\n return low;\n }\n }\n\n /**\n * @dev Searches an `array` sorted in ascending order and returns the first\n * index that contains a value greater or equal than `element`. If no such index\n * exists (i.e. all values in the array are strictly less than `element`), the array\n * length is returned. Time complexity O(log n).\n *\n * See C++'s https://en.cppreference.com/w/cpp/algorithm/lower_bound[lower_bound].\n */\n function lowerBound(uint256[] storage array, uint256 element) internal view returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeAccess(array, mid).value < element) {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n } else {\n high = mid;\n }\n }\n\n return low;\n }\n\n /**\n * @dev Searches an `array` sorted in ascending order and returns the first\n * index that contains a value strictly greater than `element`. If no such index\n * exists (i.e. all values in the array are strictly less than `element`), the array\n * length is returned. Time complexity O(log n).\n *\n * See C++'s https://en.cppreference.com/w/cpp/algorithm/upper_bound[upper_bound].\n */\n function upperBound(uint256[] storage array, uint256 element) internal view returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeAccess(array, mid).value > element) {\n high = mid;\n } else {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n }\n }\n\n return low;\n }\n\n /**\n * @dev Same as {lowerBound}, but with an array in memory.\n */\n function lowerBoundMemory(uint256[] memory array, uint256 element) internal pure returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeMemoryAccess(array, mid) < element) {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n } else {\n high = mid;\n }\n }\n\n return low;\n }\n\n /**\n * @dev Same as {upperBound}, but with an array in memory.\n */\n function upperBoundMemory(uint256[] memory array, uint256 element) internal pure returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeMemoryAccess(array, mid) > element) {\n high = mid;\n } else {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n }\n }\n\n return low;\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeAccess(address[] storage arr, uint256 pos) internal pure returns (StorageSlot.AddressSlot storage) {\n bytes32 slot;\n assembly (\"memory-safe\") {\n slot := arr.slot\n }\n return slot.deriveArray().offset(pos).getAddressSlot();\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeAccess(bytes32[] storage arr, uint256 pos) internal pure returns (StorageSlot.Bytes32Slot storage) {\n bytes32 slot;\n assembly (\"memory-safe\") {\n slot := arr.slot\n }\n return slot.deriveArray().offset(pos).getBytes32Slot();\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeAccess(uint256[] storage arr, uint256 pos) internal pure returns (StorageSlot.Uint256Slot storage) {\n bytes32 slot;\n assembly (\"memory-safe\") {\n slot := arr.slot\n }\n return slot.deriveArray().offset(pos).getUint256Slot();\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeMemoryAccess(address[] memory arr, uint256 pos) internal pure returns (address res) {\n assembly {\n res := mload(add(add(arr, 0x20), mul(pos, 0x20)))\n }\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeMemoryAccess(bytes32[] memory arr, uint256 pos) internal pure returns (bytes32 res) {\n assembly {\n res := mload(add(add(arr, 0x20), mul(pos, 0x20)))\n }\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeMemoryAccess(uint256[] memory arr, uint256 pos) internal pure returns (uint256 res) {\n assembly {\n res := mload(add(add(arr, 0x20), mul(pos, 0x20)))\n }\n }\n\n /**\n * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden.\n *\n * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.\n */\n function unsafeSetLength(address[] storage array, uint256 len) internal {\n assembly (\"memory-safe\") {\n sstore(array.slot, len)\n }\n }\n\n /**\n * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden.\n *\n * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.\n */\n function unsafeSetLength(bytes32[] storage array, uint256 len) internal {\n assembly (\"memory-safe\") {\n sstore(array.slot, len)\n }\n }\n\n /**\n * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden.\n *\n * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.\n */\n function unsafeSetLength(uint256[] storage array, uint256 len) internal {\n assembly (\"memory-safe\") {\n sstore(array.slot, len)\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Comparators.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/Comparators.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Provides a set of functions to compare values.\n *\n * _Available since v5.1._\n */\nlibrary Comparators {\n function lt(uint256 a, uint256 b) internal pure returns (bool) {\n return a < b;\n }\n\n function gt(uint256 a, uint256 b) internal pure returns (bool) {\n return a > b;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Context.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Provides information about the current execution context, including the\n * sender of the transaction and its data. While these are generally available\n * via msg.sender and msg.data, they should not be accessed in such a direct\n * manner, since when dealing with meta-transactions the account sending and\n * paying for execution may not be the actual sender (as far as an application\n * is concerned).\n *\n * This contract is only required for intermediate, library-like contracts.\n */\nabstract contract Context {\n function _msgSender() internal view virtual returns (address) {\n return msg.sender;\n }\n\n function _msgData() internal view virtual returns (bytes calldata) {\n return msg.data;\n }\n\n function _contextSuffixLength() internal view virtual returns (uint256) {\n return 0;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Panic.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/Panic.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Helper library for emitting standardized panic codes.\n *\n * ```solidity\n * contract Example {\n * using Panic for uint256;\n *\n * // Use any of the declared internal constants\n * function foo() { Panic.GENERIC.panic(); }\n *\n * // Alternatively\n * function foo() { Panic.panic(Panic.GENERIC); }\n * }\n * ```\n *\n * Follows the list from https://github.com/ethereum/solidity/blob/v0.8.24/libsolutil/ErrorCodes.h[libsolutil].\n *\n * _Available since v5.1._\n */\n// slither-disable-next-line unused-state\nlibrary Panic {\n /// @dev generic / unspecified error\n uint256 internal constant GENERIC = 0x00;\n /// @dev used by the assert() builtin\n uint256 internal constant ASSERT = 0x01;\n /// @dev arithmetic underflow or overflow\n uint256 internal constant UNDER_OVERFLOW = 0x11;\n /// @dev division or modulo by zero\n uint256 internal constant DIVISION_BY_ZERO = 0x12;\n /// @dev enum conversion error\n uint256 internal constant ENUM_CONVERSION_ERROR = 0x21;\n /// @dev invalid encoding in storage\n uint256 internal constant STORAGE_ENCODING_ERROR = 0x22;\n /// @dev empty array pop\n uint256 internal constant EMPTY_ARRAY_POP = 0x31;\n /// @dev array out of bounds access\n uint256 internal constant ARRAY_OUT_OF_BOUNDS = 0x32;\n /// @dev resource error (too large allocation or too large array)\n uint256 internal constant RESOURCE_ERROR = 0x41;\n /// @dev calling invalid internal function\n uint256 internal constant INVALID_INTERNAL_FUNCTION = 0x51;\n\n /// @dev Reverts with a panic code. Recommended to use with\n /// the internal constants with predefined codes.\n function panic(uint256 code) internal pure {\n assembly (\"memory-safe\") {\n mstore(0x00, 0x4e487b71)\n mstore(0x20, code)\n revert(0x1c, 0x24)\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/SlotDerivation.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/SlotDerivation.sol)\n// This file was procedurally generated from scripts/generate/templates/SlotDerivation.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Library for computing storage (and transient storage) locations from namespaces and deriving slots\n * corresponding to standard patterns. The derivation method for array and mapping matches the storage layout used by\n * the solidity language / compiler.\n *\n * See https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays[Solidity docs for mappings and dynamic arrays.].\n *\n * Example usage:\n * ```solidity\n * contract Example {\n * // Add the library methods\n * using StorageSlot for bytes32;\n * using SlotDerivation for bytes32;\n *\n * // Declare a namespace\n * string private constant _NAMESPACE = \"\"; // eg. OpenZeppelin.Slot\n *\n * function setValueInNamespace(uint256 key, address newValue) internal {\n * _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value = newValue;\n * }\n *\n * function getValueInNamespace(uint256 key) internal view returns (address) {\n * return _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value;\n * }\n * }\n * ```\n *\n * TIP: Consider using this library along with {StorageSlot}.\n *\n * NOTE: This library provides a way to manipulate storage locations in a non-standard way. Tooling for checking\n * upgrade safety will ignore the slots accessed through this library.\n *\n * _Available since v5.1._\n */\nlibrary SlotDerivation {\n /**\n * @dev Derive an ERC-7201 slot from a string (namespace).\n */\n function erc7201Slot(string memory namespace) internal pure returns (bytes32 slot) {\n assembly (\"memory-safe\") {\n mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1))\n slot := and(keccak256(0x00, 0x20), not(0xff))\n }\n }\n\n /**\n * @dev Add an offset to a slot to get the n-th element of a structure or an array.\n */\n function offset(bytes32 slot, uint256 pos) internal pure returns (bytes32 result) {\n unchecked {\n return bytes32(uint256(slot) + pos);\n }\n }\n\n /**\n * @dev Derive the location of the first element in an array from the slot where the length is stored.\n */\n function deriveArray(bytes32 slot) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, slot)\n result := keccak256(0x00, 0x20)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, address key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, and(key, shr(96, not(0))))\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, bool key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, iszero(iszero(key)))\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, bytes32 key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, key)\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, uint256 key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, key)\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, int256 key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, key)\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, string memory key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n let length := mload(key)\n let begin := add(key, 0x20)\n let end := add(begin, length)\n let cache := mload(end)\n mstore(end, slot)\n result := keccak256(begin, add(length, 0x20))\n mstore(end, cache)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, bytes memory key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n let length := mload(key)\n let begin := add(key, 0x20)\n let end := add(begin, length)\n let cache := mload(end)\n mstore(end, slot)\n result := keccak256(begin, add(length, 0x20))\n mstore(end, cache)\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/StorageSlot.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/StorageSlot.sol)\n// This file was procedurally generated from scripts/generate/templates/StorageSlot.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Library for reading and writing primitive types to specific storage slots.\n *\n * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts.\n * This library helps with reading and writing to such slots without the need for inline assembly.\n *\n * The functions in this library return Slot structs that contain a `value` member that can be used to read or write.\n *\n * Example usage to set ERC-1967 implementation slot:\n * ```solidity\n * contract ERC1967 {\n * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot.\n * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;\n *\n * function _getImplementation() internal view returns (address) {\n * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;\n * }\n *\n * function _setImplementation(address newImplementation) internal {\n * require(newImplementation.code.length > 0);\n * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;\n * }\n * }\n * ```\n *\n * TIP: Consider using this library along with {SlotDerivation}.\n */\nlibrary StorageSlot {\n struct AddressSlot {\n address value;\n }\n\n struct BooleanSlot {\n bool value;\n }\n\n struct Bytes32Slot {\n bytes32 value;\n }\n\n struct Uint256Slot {\n uint256 value;\n }\n\n struct Int256Slot {\n int256 value;\n }\n\n struct StringSlot {\n string value;\n }\n\n struct BytesSlot {\n bytes value;\n }\n\n /**\n * @dev Returns an `AddressSlot` with member `value` located at `slot`.\n */\n function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `BooleanSlot` with member `value` located at `slot`.\n */\n function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Bytes32Slot` with member `value` located at `slot`.\n */\n function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Uint256Slot` with member `value` located at `slot`.\n */\n function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Int256Slot` with member `value` located at `slot`.\n */\n function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `StringSlot` with member `value` located at `slot`.\n */\n function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns an `StringSlot` representation of the string storage pointer `store`.\n */\n function getStringSlot(string storage store) internal pure returns (StringSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := store.slot\n }\n }\n\n /**\n * @dev Returns a `BytesSlot` with member `value` located at `slot`.\n */\n function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`.\n */\n function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := store.slot\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/introspection/IERC165.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/IERC165.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-165 standard, as defined in the\n * https://eips.ethereum.org/EIPS/eip-165[ERC].\n *\n * Implementers can declare support of contract interfaces, which can then be\n * queried by others ({ERC165Checker}).\n *\n * For an implementation, see {ERC165}.\n */\ninterface IERC165 {\n /**\n * @dev Returns true if this contract implements the interface defined by\n * `interfaceId`. See the corresponding\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]\n * to learn more about how these ids are created.\n *\n * This function call must use less than 30 000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/Math.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/math/Math.sol)\n\npragma solidity ^0.8.20;\n\nimport {Panic} from \"../Panic.sol\";\nimport {SafeCast} from \"./SafeCast.sol\";\n\n/**\n * @dev Standard math utilities missing in the Solidity language.\n */\nlibrary Math {\n enum Rounding {\n Floor, // Toward negative infinity\n Ceil, // Toward positive infinity\n Trunc, // Toward zero\n Expand // Away from zero\n }\n\n /**\n * @dev Return the 512-bit addition of two uint256.\n *\n * The result is stored in two 256 variables such that sum = high * 2\u00b2\u2075\u2076 + low.\n */\n function add512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {\n assembly (\"memory-safe\") {\n low := add(a, b)\n high := lt(low, a)\n }\n }\n\n /**\n * @dev Return the 512-bit multiplication of two uint256.\n *\n * The result is stored in two 256 variables such that product = high * 2\u00b2\u2075\u2076 + low.\n */\n function mul512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {\n // 512-bit multiply [high low] = x * y. Compute the product mod 2\u00b2\u2075\u2076 and mod 2\u00b2\u2075\u2076 - 1, then use\n // the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256\n // variables such that product = high * 2\u00b2\u2075\u2076 + low.\n assembly (\"memory-safe\") {\n let mm := mulmod(a, b, not(0))\n low := mul(a, b)\n high := sub(sub(mm, low), lt(mm, low))\n }\n }\n\n /**\n * @dev Returns the addition of two unsigned integers, with a success flag (no overflow).\n */\n function tryAdd(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a + b;\n success = c >= a;\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the subtraction of two unsigned integers, with a success flag (no overflow).\n */\n function trySub(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a - b;\n success = c <= a;\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the multiplication of two unsigned integers, with a success flag (no overflow).\n */\n function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a * b;\n assembly (\"memory-safe\") {\n // Only true when the multiplication doesn't overflow\n // (c / a == b) || (a == 0)\n success := or(eq(div(c, a), b), iszero(a))\n }\n // equivalent to: success ? c : 0\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the division of two unsigned integers, with a success flag (no division by zero).\n */\n function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n success = b > 0;\n assembly (\"memory-safe\") {\n // The `DIV` opcode returns zero when the denominator is 0.\n result := div(a, b)\n }\n }\n }\n\n /**\n * @dev Returns the remainder of dividing two unsigned integers, with a success flag (no division by zero).\n */\n function tryMod(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n success = b > 0;\n assembly (\"memory-safe\") {\n // The `MOD` opcode returns zero when the denominator is 0.\n result := mod(a, b)\n }\n }\n }\n\n /**\n * @dev Unsigned saturating addition, bounds to `2\u00b2\u2075\u2076 - 1` instead of overflowing.\n */\n function saturatingAdd(uint256 a, uint256 b) internal pure returns (uint256) {\n (bool success, uint256 result) = tryAdd(a, b);\n return ternary(success, result, type(uint256).max);\n }\n\n /**\n * @dev Unsigned saturating subtraction, bounds to zero instead of overflowing.\n */\n function saturatingSub(uint256 a, uint256 b) internal pure returns (uint256) {\n (, uint256 result) = trySub(a, b);\n return result;\n }\n\n /**\n * @dev Unsigned saturating multiplication, bounds to `2\u00b2\u2075\u2076 - 1` instead of overflowing.\n */\n function saturatingMul(uint256 a, uint256 b) internal pure returns (uint256) {\n (bool success, uint256 result) = tryMul(a, b);\n return ternary(success, result, type(uint256).max);\n }\n\n /**\n * @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant.\n *\n * IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone.\n * However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute\n * one branch when needed, making this function more expensive.\n */\n function ternary(bool condition, uint256 a, uint256 b) internal pure returns (uint256) {\n unchecked {\n // branchless ternary works because:\n // b ^ (a ^ b) == a\n // b ^ 0 == b\n return b ^ ((a ^ b) * SafeCast.toUint(condition));\n }\n }\n\n /**\n * @dev Returns the largest of two numbers.\n */\n function max(uint256 a, uint256 b) internal pure returns (uint256) {\n return ternary(a > b, a, b);\n }\n\n /**\n * @dev Returns the smallest of two numbers.\n */\n function min(uint256 a, uint256 b) internal pure returns (uint256) {\n return ternary(a < b, a, b);\n }\n\n /**\n * @dev Returns the average of two numbers. The result is rounded towards\n * zero.\n */\n function average(uint256 a, uint256 b) internal pure returns (uint256) {\n // (a + b) / 2 can overflow.\n return (a & b) + (a ^ b) / 2;\n }\n\n /**\n * @dev Returns the ceiling of the division of two numbers.\n *\n * This differs from standard division with `/` in that it rounds towards infinity instead\n * of rounding towards zero.\n */\n function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {\n if (b == 0) {\n // Guarantee the same behavior as in a regular Solidity division.\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n\n // The following calculation ensures accurate ceiling division without overflow.\n // Since a is non-zero, (a - 1) / b will not overflow.\n // The largest possible result occurs when (a - 1) / b is type(uint256).max,\n // but the largest value we can obtain is type(uint256).max - 1, which happens\n // when a = type(uint256).max and b = 1.\n unchecked {\n return SafeCast.toUint(a > 0) * ((a - 1) / b + 1);\n }\n }\n\n /**\n * @dev Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or\n * denominator == 0.\n *\n * Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by\n * Uniswap Labs also under MIT license.\n */\n function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {\n unchecked {\n (uint256 high, uint256 low) = mul512(x, y);\n\n // Handle non-overflow cases, 256 by 256 division.\n if (high == 0) {\n // Solidity will revert if denominator == 0, unlike the div opcode on its own.\n // The surrounding unchecked block does not change this fact.\n // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.\n return low / denominator;\n }\n\n // Make sure the result is less than 2\u00b2\u2075\u2076. Also prevents denominator == 0.\n if (denominator <= high) {\n Panic.panic(ternary(denominator == 0, Panic.DIVISION_BY_ZERO, Panic.UNDER_OVERFLOW));\n }\n\n ///////////////////////////////////////////////\n // 512 by 256 division.\n ///////////////////////////////////////////////\n\n // Make division exact by subtracting the remainder from [high low].\n uint256 remainder;\n assembly (\"memory-safe\") {\n // Compute remainder using mulmod.\n remainder := mulmod(x, y, denominator)\n\n // Subtract 256 bit number from 512 bit number.\n high := sub(high, gt(remainder, low))\n low := sub(low, remainder)\n }\n\n // Factor powers of two out of denominator and compute largest power of two divisor of denominator.\n // Always >= 1. See https://cs.stackexchange.com/q/138556/92363.\n\n uint256 twos = denominator & (0 - denominator);\n assembly (\"memory-safe\") {\n // Divide denominator by twos.\n denominator := div(denominator, twos)\n\n // Divide [high low] by twos.\n low := div(low, twos)\n\n // Flip twos such that it is 2\u00b2\u2075\u2076 / twos. If twos is zero, then it becomes one.\n twos := add(div(sub(0, twos), twos), 1)\n }\n\n // Shift in bits from high into low.\n low |= high * twos;\n\n // Invert denominator mod 2\u00b2\u2075\u2076. Now that denominator is an odd number, it has an inverse modulo 2\u00b2\u2075\u2076 such\n // that denominator * inv \u2261 1 mod 2\u00b2\u2075\u2076. Compute the inverse by starting with a seed that is correct for\n // four bits. That is, denominator * inv \u2261 1 mod 2\u2074.\n uint256 inverse = (3 * denominator) ^ 2;\n\n // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also\n // works in modular arithmetic, doubling the correct bits in each step.\n inverse *= 2 - denominator * inverse; // inverse mod 2\u2078\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b9\u2076\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b3\u00b2\n inverse *= 2 - denominator * inverse; // inverse mod 2\u2076\u2074\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b9\u00b2\u2078\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b2\u2075\u2076\n\n // Because the division is now exact we can divide by multiplying with the modular inverse of denominator.\n // This will give us the correct result modulo 2\u00b2\u2075\u2076. Since the preconditions guarantee that the outcome is\n // less than 2\u00b2\u2075\u2076, this is the final result. We don't need to compute the high bits of the result and high\n // is no longer required.\n result = low * inverse;\n return result;\n }\n }\n\n /**\n * @dev Calculates x * y / denominator with full precision, following the selected rounding direction.\n */\n function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {\n return mulDiv(x, y, denominator) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0);\n }\n\n /**\n * @dev Calculates floor(x * y >> n) with full precision. Throws if result overflows a uint256.\n */\n function mulShr(uint256 x, uint256 y, uint8 n) internal pure returns (uint256 result) {\n unchecked {\n (uint256 high, uint256 low) = mul512(x, y);\n if (high >= 1 << n) {\n Panic.panic(Panic.UNDER_OVERFLOW);\n }\n return (high << (256 - n)) | (low >> n);\n }\n }\n\n /**\n * @dev Calculates x * y >> n with full precision, following the selected rounding direction.\n */\n function mulShr(uint256 x, uint256 y, uint8 n, Rounding rounding) internal pure returns (uint256) {\n return mulShr(x, y, n) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, 1 << n) > 0);\n }\n\n /**\n * @dev Calculate the modular multiplicative inverse of a number in Z/nZ.\n *\n * If n is a prime, then Z/nZ is a field. In that case all elements are inversible, except 0.\n * If n is not a prime, then Z/nZ is not a field, and some elements might not be inversible.\n *\n * If the input value is not inversible, 0 is returned.\n *\n * NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Fermat's little theorem and get the\n * inverse using `Math.modExp(a, n - 2, n)`. See {invModPrime}.\n */\n function invMod(uint256 a, uint256 n) internal pure returns (uint256) {\n unchecked {\n if (n == 0) return 0;\n\n // The inverse modulo is calculated using the Extended Euclidean Algorithm (iterative version)\n // Used to compute integers x and y such that: ax + ny = gcd(a, n).\n // When the gcd is 1, then the inverse of a modulo n exists and it's x.\n // ax + ny = 1\n // ax = 1 + (-y)n\n // ax \u2261 1 (mod n) # x is the inverse of a modulo n\n\n // If the remainder is 0 the gcd is n right away.\n uint256 remainder = a % n;\n uint256 gcd = n;\n\n // Therefore the initial coefficients are:\n // ax + ny = gcd(a, n) = n\n // 0a + 1n = n\n int256 x = 0;\n int256 y = 1;\n\n while (remainder != 0) {\n uint256 quotient = gcd / remainder;\n\n (gcd, remainder) = (\n // The old remainder is the next gcd to try.\n remainder,\n // Compute the next remainder.\n // Can't overflow given that (a % gcd) * (gcd // (a % gcd)) <= gcd\n // where gcd is at most n (capped to type(uint256).max)\n gcd - remainder * quotient\n );\n\n (x, y) = (\n // Increment the coefficient of a.\n y,\n // Decrement the coefficient of n.\n // Can overflow, but the result is casted to uint256 so that the\n // next value of y is \"wrapped around\" to a value between 0 and n - 1.\n x - y * int256(quotient)\n );\n }\n\n if (gcd != 1) return 0; // No inverse exists.\n return ternary(x < 0, n - uint256(-x), uint256(x)); // Wrap the result if it's negative.\n }\n }\n\n /**\n * @dev Variant of {invMod}. More efficient, but only works if `p` is known to be a prime greater than `2`.\n *\n * From https://en.wikipedia.org/wiki/Fermat%27s_little_theorem[Fermat's little theorem], we know that if p is\n * prime, then `a**(p-1) \u2261 1 mod p`. As a consequence, we have `a * a**(p-2) \u2261 1 mod p`, which means that\n * `a**(p-2)` is the modular multiplicative inverse of a in Fp.\n *\n * NOTE: this function does NOT check that `p` is a prime greater than `2`.\n */\n function invModPrime(uint256 a, uint256 p) internal view returns (uint256) {\n unchecked {\n return Math.modExp(a, p - 2, p);\n }\n }\n\n /**\n * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m)\n *\n * Requirements:\n * - modulus can't be zero\n * - underlying staticcall to precompile must succeed\n *\n * IMPORTANT: The result is only valid if the underlying call succeeds. When using this function, make\n * sure the chain you're using it on supports the precompiled contract for modular exponentiation\n * at address 0x05 as specified in https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise,\n * the underlying function will succeed given the lack of a revert, but the result may be incorrectly\n * interpreted as 0.\n */\n function modExp(uint256 b, uint256 e, uint256 m) internal view returns (uint256) {\n (bool success, uint256 result) = tryModExp(b, e, m);\n if (!success) {\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n return result;\n }\n\n /**\n * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m).\n * It includes a success flag indicating if the operation succeeded. Operation will be marked as failed if trying\n * to operate modulo 0 or if the underlying precompile reverted.\n *\n * IMPORTANT: The result is only valid if the success flag is true. When using this function, make sure the chain\n * you're using it on supports the precompiled contract for modular exponentiation at address 0x05 as specified in\n * https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise, the underlying function will succeed given the lack\n * of a revert, but the result may be incorrectly interpreted as 0.\n */\n function tryModExp(uint256 b, uint256 e, uint256 m) internal view returns (bool success, uint256 result) {\n if (m == 0) return (false, 0);\n assembly (\"memory-safe\") {\n let ptr := mload(0x40)\n // | Offset | Content | Content (Hex) |\n // |-----------|------------|--------------------------------------------------------------------|\n // | 0x00:0x1f | size of b | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x20:0x3f | size of e | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x40:0x5f | size of m | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x60:0x7f | value of b | 0x<.............................................................b> |\n // | 0x80:0x9f | value of e | 0x<.............................................................e> |\n // | 0xa0:0xbf | value of m | 0x<.............................................................m> |\n mstore(ptr, 0x20)\n mstore(add(ptr, 0x20), 0x20)\n mstore(add(ptr, 0x40), 0x20)\n mstore(add(ptr, 0x60), b)\n mstore(add(ptr, 0x80), e)\n mstore(add(ptr, 0xa0), m)\n\n // Given the result < m, it's guaranteed to fit in 32 bytes,\n // so we can use the memory scratch space located at offset 0.\n success := staticcall(gas(), 0x05, ptr, 0xc0, 0x00, 0x20)\n result := mload(0x00)\n }\n }\n\n /**\n * @dev Variant of {modExp} that supports inputs of arbitrary length.\n */\n function modExp(bytes memory b, bytes memory e, bytes memory m) internal view returns (bytes memory) {\n (bool success, bytes memory result) = tryModExp(b, e, m);\n if (!success) {\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n return result;\n }\n\n /**\n * @dev Variant of {tryModExp} that supports inputs of arbitrary length.\n */\n function tryModExp(\n bytes memory b,\n bytes memory e,\n bytes memory m\n ) internal view returns (bool success, bytes memory result) {\n if (_zeroBytes(m)) return (false, new bytes(0));\n\n uint256 mLen = m.length;\n\n // Encode call args in result and move the free memory pointer\n result = abi.encodePacked(b.length, e.length, mLen, b, e, m);\n\n assembly (\"memory-safe\") {\n let dataPtr := add(result, 0x20)\n // Write result on top of args to avoid allocating extra memory.\n success := staticcall(gas(), 0x05, dataPtr, mload(result), dataPtr, mLen)\n // Overwrite the length.\n // result.length > returndatasize() is guaranteed because returndatasize() == m.length\n mstore(result, mLen)\n // Set the memory pointer after the returned data.\n mstore(0x40, add(dataPtr, mLen))\n }\n }\n\n /**\n * @dev Returns whether the provided byte array is zero.\n */\n function _zeroBytes(bytes memory byteArray) private pure returns (bool) {\n for (uint256 i = 0; i < byteArray.length; ++i) {\n if (byteArray[i] != 0) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded\n * towards zero.\n *\n * This method is based on Newton's method for computing square roots; the algorithm is restricted to only\n * using integer operations.\n */\n function sqrt(uint256 a) internal pure returns (uint256) {\n unchecked {\n // Take care of easy edge cases when a == 0 or a == 1\n if (a <= 1) {\n return a;\n }\n\n // In this function, we use Newton's method to get a root of `f(x) := x\u00b2 - a`. It involves building a\n // sequence x_n that converges toward sqrt(a). For each iteration x_n, we also define the error between\n // the current value as `\u03b5_n = | x_n - sqrt(a) |`.\n //\n // For our first estimation, we consider `e` the smallest power of 2 which is bigger than the square root\n // of the target. (i.e. `2**(e-1) \u2264 sqrt(a) < 2**e`). We know that `e \u2264 128` because `(2\u00b9\u00b2\u2078)\u00b2 = 2\u00b2\u2075\u2076` is\n // bigger than any uint256.\n //\n // By noticing that\n // `2**(e-1) \u2264 sqrt(a) < 2**e \u2192 (2**(e-1))\u00b2 \u2264 a < (2**e)\u00b2 \u2192 2**(2*e-2) \u2264 a < 2**(2*e)`\n // we can deduce that `e - 1` is `log2(a) / 2`. We can thus compute `x_n = 2**(e-1)` using a method similar\n // to the msb function.\n uint256 aa = a;\n uint256 xn = 1;\n\n if (aa >= (1 << 128)) {\n aa >>= 128;\n xn <<= 64;\n }\n if (aa >= (1 << 64)) {\n aa >>= 64;\n xn <<= 32;\n }\n if (aa >= (1 << 32)) {\n aa >>= 32;\n xn <<= 16;\n }\n if (aa >= (1 << 16)) {\n aa >>= 16;\n xn <<= 8;\n }\n if (aa >= (1 << 8)) {\n aa >>= 8;\n xn <<= 4;\n }\n if (aa >= (1 << 4)) {\n aa >>= 4;\n xn <<= 2;\n }\n if (aa >= (1 << 2)) {\n xn <<= 1;\n }\n\n // We now have x_n such that `x_n = 2**(e-1) \u2264 sqrt(a) < 2**e = 2 * x_n`. This implies \u03b5_n \u2264 2**(e-1).\n //\n // We can refine our estimation by noticing that the middle of that interval minimizes the error.\n // If we move x_n to equal 2**(e-1) + 2**(e-2), then we reduce the error to \u03b5_n \u2264 2**(e-2).\n // This is going to be our x_0 (and \u03b5_0)\n xn = (3 * xn) >> 1; // \u03b5_0 := | x_0 - sqrt(a) | \u2264 2**(e-2)\n\n // From here, Newton's method give us:\n // x_{n+1} = (x_n + a / x_n) / 2\n //\n // One should note that:\n // x_{n+1}\u00b2 - a = ((x_n + a / x_n) / 2)\u00b2 - a\n // = ((x_n\u00b2 + a) / (2 * x_n))\u00b2 - a\n // = (x_n\u2074 + 2 * a * x_n\u00b2 + a\u00b2) / (4 * x_n\u00b2) - a\n // = (x_n\u2074 + 2 * a * x_n\u00b2 + a\u00b2 - 4 * a * x_n\u00b2) / (4 * x_n\u00b2)\n // = (x_n\u2074 - 2 * a * x_n\u00b2 + a\u00b2) / (4 * x_n\u00b2)\n // = (x_n\u00b2 - a)\u00b2 / (2 * x_n)\u00b2\n // = ((x_n\u00b2 - a) / (2 * x_n))\u00b2\n // \u2265 0\n // Which proves that for all n \u2265 1, sqrt(a) \u2264 x_n\n //\n // This gives us the proof of quadratic convergence of the sequence:\n // \u03b5_{n+1} = | x_{n+1} - sqrt(a) |\n // = | (x_n + a / x_n) / 2 - sqrt(a) |\n // = | (x_n\u00b2 + a - 2*x_n*sqrt(a)) / (2 * x_n) |\n // = | (x_n - sqrt(a))\u00b2 / (2 * x_n) |\n // = | \u03b5_n\u00b2 / (2 * x_n) |\n // = \u03b5_n\u00b2 / | (2 * x_n) |\n //\n // For the first iteration, we have a special case where x_0 is known:\n // \u03b5_1 = \u03b5_0\u00b2 / | (2 * x_0) |\n // \u2264 (2**(e-2))\u00b2 / (2 * (2**(e-1) + 2**(e-2)))\n // \u2264 2**(2*e-4) / (3 * 2**(e-1))\n // \u2264 2**(e-3) / 3\n // \u2264 2**(e-3-log2(3))\n // \u2264 2**(e-4.5)\n //\n // For the following iterations, we use the fact that, 2**(e-1) \u2264 sqrt(a) \u2264 x_n:\n // \u03b5_{n+1} = \u03b5_n\u00b2 / | (2 * x_n) |\n // \u2264 (2**(e-k))\u00b2 / (2 * 2**(e-1))\n // \u2264 2**(2*e-2*k) / 2**e\n // \u2264 2**(e-2*k)\n xn = (xn + a / xn) >> 1; // \u03b5_1 := | x_1 - sqrt(a) | \u2264 2**(e-4.5) -- special case, see above\n xn = (xn + a / xn) >> 1; // \u03b5_2 := | x_2 - sqrt(a) | \u2264 2**(e-9) -- general case with k = 4.5\n xn = (xn + a / xn) >> 1; // \u03b5_3 := | x_3 - sqrt(a) | \u2264 2**(e-18) -- general case with k = 9\n xn = (xn + a / xn) >> 1; // \u03b5_4 := | x_4 - sqrt(a) | \u2264 2**(e-36) -- general case with k = 18\n xn = (xn + a / xn) >> 1; // \u03b5_5 := | x_5 - sqrt(a) | \u2264 2**(e-72) -- general case with k = 36\n xn = (xn + a / xn) >> 1; // \u03b5_6 := | x_6 - sqrt(a) | \u2264 2**(e-144) -- general case with k = 72\n\n // Because e \u2264 128 (as discussed during the first estimation phase), we know have reached a precision\n // \u03b5_6 \u2264 2**(e-144) < 1. Given we're operating on integers, then we can ensure that xn is now either\n // sqrt(a) or sqrt(a) + 1.\n return xn - SafeCast.toUint(xn > a / xn);\n }\n }\n\n /**\n * @dev Calculates sqrt(a), following the selected rounding direction.\n */\n function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = sqrt(a);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && result * result < a);\n }\n }\n\n /**\n * @dev Return the log in base 2 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n */\n function log2(uint256 x) internal pure returns (uint256 r) {\n // If value has upper 128 bits set, log2 result is at least 128\n r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7;\n // If upper 64 bits of 128-bit half set, add 64 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6;\n // If upper 32 bits of 64-bit half set, add 32 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5;\n // If upper 16 bits of 32-bit half set, add 16 to result\n r |= SafeCast.toUint((x >> r) > 0xffff) << 4;\n // If upper 8 bits of 16-bit half set, add 8 to result\n r |= SafeCast.toUint((x >> r) > 0xff) << 3;\n // If upper 4 bits of 8-bit half set, add 4 to result\n r |= SafeCast.toUint((x >> r) > 0xf) << 2;\n\n // Shifts value right by the current result and use it as an index into this lookup table:\n //\n // | x (4 bits) | index | table[index] = MSB position |\n // |------------|---------|-----------------------------|\n // | 0000 | 0 | table[0] = 0 |\n // | 0001 | 1 | table[1] = 0 |\n // | 0010 | 2 | table[2] = 1 |\n // | 0011 | 3 | table[3] = 1 |\n // | 0100 | 4 | table[4] = 2 |\n // | 0101 | 5 | table[5] = 2 |\n // | 0110 | 6 | table[6] = 2 |\n // | 0111 | 7 | table[7] = 2 |\n // | 1000 | 8 | table[8] = 3 |\n // | 1001 | 9 | table[9] = 3 |\n // | 1010 | 10 | table[10] = 3 |\n // | 1011 | 11 | table[11] = 3 |\n // | 1100 | 12 | table[12] = 3 |\n // | 1101 | 13 | table[13] = 3 |\n // | 1110 | 14 | table[14] = 3 |\n // | 1111 | 15 | table[15] = 3 |\n //\n // The lookup table is represented as a 32-byte value with the MSB positions for 0-15 in the last 16 bytes.\n assembly (\"memory-safe\") {\n r := or(r, byte(shr(r, x), 0x0000010102020202030303030303030300000000000000000000000000000000))\n }\n }\n\n /**\n * @dev Return the log in base 2, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log2(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << result < value);\n }\n }\n\n /**\n * @dev Return the log in base 10 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n */\n function log10(uint256 value) internal pure returns (uint256) {\n uint256 result = 0;\n unchecked {\n if (value >= 10 ** 64) {\n value /= 10 ** 64;\n result += 64;\n }\n if (value >= 10 ** 32) {\n value /= 10 ** 32;\n result += 32;\n }\n if (value >= 10 ** 16) {\n value /= 10 ** 16;\n result += 16;\n }\n if (value >= 10 ** 8) {\n value /= 10 ** 8;\n result += 8;\n }\n if (value >= 10 ** 4) {\n value /= 10 ** 4;\n result += 4;\n }\n if (value >= 10 ** 2) {\n value /= 10 ** 2;\n result += 2;\n }\n if (value >= 10 ** 1) {\n result += 1;\n }\n }\n return result;\n }\n\n /**\n * @dev Return the log in base 10, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log10(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 10 ** result < value);\n }\n }\n\n /**\n * @dev Return the log in base 256 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n *\n * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.\n */\n function log256(uint256 x) internal pure returns (uint256 r) {\n // If value has upper 128 bits set, log2 result is at least 128\n r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7;\n // If upper 64 bits of 128-bit half set, add 64 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6;\n // If upper 32 bits of 64-bit half set, add 32 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5;\n // If upper 16 bits of 32-bit half set, add 16 to result\n r |= SafeCast.toUint((x >> r) > 0xffff) << 4;\n // Add 1 if upper 8 bits of 16-bit half set, and divide accumulated result by 8\n return (r >> 3) | SafeCast.toUint((x >> r) > 0xff);\n }\n\n /**\n * @dev Return the log in base 256, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log256(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << (result << 3) < value);\n }\n }\n\n /**\n * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.\n */\n function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {\n return uint8(rounding) % 2 == 1;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/SafeCast.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SafeCast.sol)\n// This file was procedurally generated from scripts/generate/templates/SafeCast.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Wrappers over Solidity's uintXX/intXX/bool casting operators with added overflow\n * checks.\n *\n * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can\n * easily result in undesired exploitation or bugs, since developers usually\n * assume that overflows raise errors. `SafeCast` restores this intuition by\n * reverting the transaction when such an operation overflows.\n *\n * Using this library instead of the unchecked operations eliminates an entire\n * class of bugs, so it's recommended to use it always.\n */\nlibrary SafeCast {\n /**\n * @dev Value doesn't fit in an uint of `bits` size.\n */\n error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);\n\n /**\n * @dev An int value doesn't fit in an uint of `bits` size.\n */\n error SafeCastOverflowedIntToUint(int256 value);\n\n /**\n * @dev Value doesn't fit in an int of `bits` size.\n */\n error SafeCastOverflowedIntDowncast(uint8 bits, int256 value);\n\n /**\n * @dev An uint value doesn't fit in an int of `bits` size.\n */\n error SafeCastOverflowedUintToInt(uint256 value);\n\n /**\n * @dev Returns the downcasted uint248 from uint256, reverting on\n * overflow (when the input is greater than largest uint248).\n *\n * Counterpart to Solidity's `uint248` operator.\n *\n * Requirements:\n *\n * - input must fit into 248 bits\n */\n function toUint248(uint256 value) internal pure returns (uint248) {\n if (value > type(uint248).max) {\n revert SafeCastOverflowedUintDowncast(248, value);\n }\n return uint248(value);\n }\n\n /**\n * @dev Returns the downcasted uint240 from uint256, reverting on\n * overflow (when the input is greater than largest uint240).\n *\n * Counterpart to Solidity's `uint240` operator.\n *\n * Requirements:\n *\n * - input must fit into 240 bits\n */\n function toUint240(uint256 value) internal pure returns (uint240) {\n if (value > type(uint240).max) {\n revert SafeCastOverflowedUintDowncast(240, value);\n }\n return uint240(value);\n }\n\n /**\n * @dev Returns the downcasted uint232 from uint256, reverting on\n * overflow (when the input is greater than largest uint232).\n *\n * Counterpart to Solidity's `uint232` operator.\n *\n * Requirements:\n *\n * - input must fit into 232 bits\n */\n function toUint232(uint256 value) internal pure returns (uint232) {\n if (value > type(uint232).max) {\n revert SafeCastOverflowedUintDowncast(232, value);\n }\n return uint232(value);\n }\n\n /**\n * @dev Returns the downcasted uint224 from uint256, reverting on\n * overflow (when the input is greater than largest uint224).\n *\n * Counterpart to Solidity's `uint224` operator.\n *\n * Requirements:\n *\n * - input must fit into 224 bits\n */\n function toUint224(uint256 value) internal pure returns (uint224) {\n if (value > type(uint224).max) {\n revert SafeCastOverflowedUintDowncast(224, value);\n }\n return uint224(value);\n }\n\n /**\n * @dev Returns the downcasted uint216 from uint256, reverting on\n * overflow (when the input is greater than largest uint216).\n *\n * Counterpart to Solidity's `uint216` operator.\n *\n * Requirements:\n *\n * - input must fit into 216 bits\n */\n function toUint216(uint256 value) internal pure returns (uint216) {\n if (value > type(uint216).max) {\n revert SafeCastOverflowedUintDowncast(216, value);\n }\n return uint216(value);\n }\n\n /**\n * @dev Returns the downcasted uint208 from uint256, reverting on\n * overflow (when the input is greater than largest uint208).\n *\n * Counterpart to Solidity's `uint208` operator.\n *\n * Requirements:\n *\n * - input must fit into 208 bits\n */\n function toUint208(uint256 value) internal pure returns (uint208) {\n if (value > type(uint208).max) {\n revert SafeCastOverflowedUintDowncast(208, value);\n }\n return uint208(value);\n }\n\n /**\n * @dev Returns the downcasted uint200 from uint256, reverting on\n * overflow (when the input is greater than largest uint200).\n *\n * Counterpart to Solidity's `uint200` operator.\n *\n * Requirements:\n *\n * - input must fit into 200 bits\n */\n function toUint200(uint256 value) internal pure returns (uint200) {\n if (value > type(uint200).max) {\n revert SafeCastOverflowedUintDowncast(200, value);\n }\n return uint200(value);\n }\n\n /**\n * @dev Returns the downcasted uint192 from uint256, reverting on\n * overflow (when the input is greater than largest uint192).\n *\n * Counterpart to Solidity's `uint192` operator.\n *\n * Requirements:\n *\n * - input must fit into 192 bits\n */\n function toUint192(uint256 value) internal pure returns (uint192) {\n if (value > type(uint192).max) {\n revert SafeCastOverflowedUintDowncast(192, value);\n }\n return uint192(value);\n }\n\n /**\n * @dev Returns the downcasted uint184 from uint256, reverting on\n * overflow (when the input is greater than largest uint184).\n *\n * Counterpart to Solidity's `uint184` operator.\n *\n * Requirements:\n *\n * - input must fit into 184 bits\n */\n function toUint184(uint256 value) internal pure returns (uint184) {\n if (value > type(uint184).max) {\n revert SafeCastOverflowedUintDowncast(184, value);\n }\n return uint184(value);\n }\n\n /**\n * @dev Returns the downcasted uint176 from uint256, reverting on\n * overflow (when the input is greater than largest uint176).\n *\n * Counterpart to Solidity's `uint176` operator.\n *\n * Requirements:\n *\n * - input must fit into 176 bits\n */\n function toUint176(uint256 value) internal pure returns (uint176) {\n if (value > type(uint176).max) {\n revert SafeCastOverflowedUintDowncast(176, value);\n }\n return uint176(value);\n }\n\n /**\n * @dev Returns the downcasted uint168 from uint256, reverting on\n * overflow (when the input is greater than largest uint168).\n *\n * Counterpart to Solidity's `uint168` operator.\n *\n * Requirements:\n *\n * - input must fit into 168 bits\n */\n function toUint168(uint256 value) internal pure returns (uint168) {\n if (value > type(uint168).max) {\n revert SafeCastOverflowedUintDowncast(168, value);\n }\n return uint168(value);\n }\n\n /**\n * @dev Returns the downcasted uint160 from uint256, reverting on\n * overflow (when the input is greater than largest uint160).\n *\n * Counterpart to Solidity's `uint160` operator.\n *\n * Requirements:\n *\n * - input must fit into 160 bits\n */\n function toUint160(uint256 value) internal pure returns (uint160) {\n if (value > type(uint160).max) {\n revert SafeCastOverflowedUintDowncast(160, value);\n }\n return uint160(value);\n }\n\n /**\n * @dev Returns the downcasted uint152 from uint256, reverting on\n * overflow (when the input is greater than largest uint152).\n *\n * Counterpart to Solidity's `uint152` operator.\n *\n * Requirements:\n *\n * - input must fit into 152 bits\n */\n function toUint152(uint256 value) internal pure returns (uint152) {\n if (value > type(uint152).max) {\n revert SafeCastOverflowedUintDowncast(152, value);\n }\n return uint152(value);\n }\n\n /**\n * @dev Returns the downcasted uint144 from uint256, reverting on\n * overflow (when the input is greater than largest uint144).\n *\n * Counterpart to Solidity's `uint144` operator.\n *\n * Requirements:\n *\n * - input must fit into 144 bits\n */\n function toUint144(uint256 value) internal pure returns (uint144) {\n if (value > type(uint144).max) {\n revert SafeCastOverflowedUintDowncast(144, value);\n }\n return uint144(value);\n }\n\n /**\n * @dev Returns the downcasted uint136 from uint256, reverting on\n * overflow (when the input is greater than largest uint136).\n *\n * Counterpart to Solidity's `uint136` operator.\n *\n * Requirements:\n *\n * - input must fit into 136 bits\n */\n function toUint136(uint256 value) internal pure returns (uint136) {\n if (value > type(uint136).max) {\n revert SafeCastOverflowedUintDowncast(136, value);\n }\n return uint136(value);\n }\n\n /**\n * @dev Returns the downcasted uint128 from uint256, reverting on\n * overflow (when the input is greater than largest uint128).\n *\n * Counterpart to Solidity's `uint128` operator.\n *\n * Requirements:\n *\n * - input must fit into 128 bits\n */\n function toUint128(uint256 value) internal pure returns (uint128) {\n if (value > type(uint128).max) {\n revert SafeCastOverflowedUintDowncast(128, value);\n }\n return uint128(value);\n }\n\n /**\n * @dev Returns the downcasted uint120 from uint256, reverting on\n * overflow (when the input is greater than largest uint120).\n *\n * Counterpart to Solidity's `uint120` operator.\n *\n * Requirements:\n *\n * - input must fit into 120 bits\n */\n function toUint120(uint256 value) internal pure returns (uint120) {\n if (value > type(uint120).max) {\n revert SafeCastOverflowedUintDowncast(120, value);\n }\n return uint120(value);\n }\n\n /**\n * @dev Returns the downcasted uint112 from uint256, reverting on\n * overflow (when the input is greater than largest uint112).\n *\n * Counterpart to Solidity's `uint112` operator.\n *\n * Requirements:\n *\n * - input must fit into 112 bits\n */\n function toUint112(uint256 value) internal pure returns (uint112) {\n if (value > type(uint112).max) {\n revert SafeCastOverflowedUintDowncast(112, value);\n }\n return uint112(value);\n }\n\n /**\n * @dev Returns the downcasted uint104 from uint256, reverting on\n * overflow (when the input is greater than largest uint104).\n *\n * Counterpart to Solidity's `uint104` operator.\n *\n * Requirements:\n *\n * - input must fit into 104 bits\n */\n function toUint104(uint256 value) internal pure returns (uint104) {\n if (value > type(uint104).max) {\n revert SafeCastOverflowedUintDowncast(104, value);\n }\n return uint104(value);\n }\n\n /**\n * @dev Returns the downcasted uint96 from uint256, reverting on\n * overflow (when the input is greater than largest uint96).\n *\n * Counterpart to Solidity's `uint96` operator.\n *\n * Requirements:\n *\n * - input must fit into 96 bits\n */\n function toUint96(uint256 value) internal pure returns (uint96) {\n if (value > type(uint96).max) {\n revert SafeCastOverflowedUintDowncast(96, value);\n }\n return uint96(value);\n }\n\n /**\n * @dev Returns the downcasted uint88 from uint256, reverting on\n * overflow (when the input is greater than largest uint88).\n *\n * Counterpart to Solidity's `uint88` operator.\n *\n * Requirements:\n *\n * - input must fit into 88 bits\n */\n function toUint88(uint256 value) internal pure returns (uint88) {\n if (value > type(uint88).max) {\n revert SafeCastOverflowedUintDowncast(88, value);\n }\n return uint88(value);\n }\n\n /**\n * @dev Returns the downcasted uint80 from uint256, reverting on\n * overflow (when the input is greater than largest uint80).\n *\n * Counterpart to Solidity's `uint80` operator.\n *\n * Requirements:\n *\n * - input must fit into 80 bits\n */\n function toUint80(uint256 value) internal pure returns (uint80) {\n if (value > type(uint80).max) {\n revert SafeCastOverflowedUintDowncast(80, value);\n }\n return uint80(value);\n }\n\n /**\n * @dev Returns the downcasted uint72 from uint256, reverting on\n * overflow (when the input is greater than largest uint72).\n *\n * Counterpart to Solidity's `uint72` operator.\n *\n * Requirements:\n *\n * - input must fit into 72 bits\n */\n function toUint72(uint256 value) internal pure returns (uint72) {\n if (value > type(uint72).max) {\n revert SafeCastOverflowedUintDowncast(72, value);\n }\n return uint72(value);\n }\n\n /**\n * @dev Returns the downcasted uint64 from uint256, reverting on\n * overflow (when the input is greater than largest uint64).\n *\n * Counterpart to Solidity's `uint64` operator.\n *\n * Requirements:\n *\n * - input must fit into 64 bits\n */\n function toUint64(uint256 value) internal pure returns (uint64) {\n if (value > type(uint64).max) {\n revert SafeCastOverflowedUintDowncast(64, value);\n }\n return uint64(value);\n }\n\n /**\n * @dev Returns the downcasted uint56 from uint256, reverting on\n * overflow (when the input is greater than largest uint56).\n *\n * Counterpart to Solidity's `uint56` operator.\n *\n * Requirements:\n *\n * - input must fit into 56 bits\n */\n function toUint56(uint256 value) internal pure returns (uint56) {\n if (value > type(uint56).max) {\n revert SafeCastOverflowedUintDowncast(56, value);\n }\n return uint56(value);\n }\n\n /**\n * @dev Returns the downcasted uint48 from uint256, reverting on\n * overflow (when the input is greater than largest uint48).\n *\n * Counterpart to Solidity's `uint48` operator.\n *\n * Requirements:\n *\n * - input must fit into 48 bits\n */\n function toUint48(uint256 value) internal pure returns (uint48) {\n if (value > type(uint48).max) {\n revert SafeCastOverflowedUintDowncast(48, value);\n }\n return uint48(value);\n }\n\n /**\n * @dev Returns the downcasted uint40 from uint256, reverting on\n * overflow (when the input is greater than largest uint40).\n *\n * Counterpart to Solidity's `uint40` operator.\n *\n * Requirements:\n *\n * - input must fit into 40 bits\n */\n function toUint40(uint256 value) internal pure returns (uint40) {\n if (value > type(uint40).max) {\n revert SafeCastOverflowedUintDowncast(40, value);\n }\n return uint40(value);\n }\n\n /**\n * @dev Returns the downcasted uint32 from uint256, reverting on\n * overflow (when the input is greater than largest uint32).\n *\n * Counterpart to Solidity's `uint32` operator.\n *\n * Requirements:\n *\n * - input must fit into 32 bits\n */\n function toUint32(uint256 value) internal pure returns (uint32) {\n if (value > type(uint32).max) {\n revert SafeCastOverflowedUintDowncast(32, value);\n }\n return uint32(value);\n }\n\n /**\n * @dev Returns the downcasted uint24 from uint256, reverting on\n * overflow (when the input is greater than largest uint24).\n *\n * Counterpart to Solidity's `uint24` operator.\n *\n * Requirements:\n *\n * - input must fit into 24 bits\n */\n function toUint24(uint256 value) internal pure returns (uint24) {\n if (value > type(uint24).max) {\n revert SafeCastOverflowedUintDowncast(24, value);\n }\n return uint24(value);\n }\n\n /**\n * @dev Returns the downcasted uint16 from uint256, reverting on\n * overflow (when the input is greater than largest uint16).\n *\n * Counterpart to Solidity's `uint16` operator.\n *\n * Requirements:\n *\n * - input must fit into 16 bits\n */\n function toUint16(uint256 value) internal pure returns (uint16) {\n if (value > type(uint16).max) {\n revert SafeCastOverflowedUintDowncast(16, value);\n }\n return uint16(value);\n }\n\n /**\n * @dev Returns the downcasted uint8 from uint256, reverting on\n * overflow (when the input is greater than largest uint8).\n *\n * Counterpart to Solidity's `uint8` operator.\n *\n * Requirements:\n *\n * - input must fit into 8 bits\n */\n function toUint8(uint256 value) internal pure returns (uint8) {\n if (value > type(uint8).max) {\n revert SafeCastOverflowedUintDowncast(8, value);\n }\n return uint8(value);\n }\n\n /**\n * @dev Converts a signed int256 into an unsigned uint256.\n *\n * Requirements:\n *\n * - input must be greater than or equal to 0.\n */\n function toUint256(int256 value) internal pure returns (uint256) {\n if (value < 0) {\n revert SafeCastOverflowedIntToUint(value);\n }\n return uint256(value);\n }\n\n /**\n * @dev Returns the downcasted int248 from int256, reverting on\n * overflow (when the input is less than smallest int248 or\n * greater than largest int248).\n *\n * Counterpart to Solidity's `int248` operator.\n *\n * Requirements:\n *\n * - input must fit into 248 bits\n */\n function toInt248(int256 value) internal pure returns (int248 downcasted) {\n downcasted = int248(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(248, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int240 from int256, reverting on\n * overflow (when the input is less than smallest int240 or\n * greater than largest int240).\n *\n * Counterpart to Solidity's `int240` operator.\n *\n * Requirements:\n *\n * - input must fit into 240 bits\n */\n function toInt240(int256 value) internal pure returns (int240 downcasted) {\n downcasted = int240(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(240, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int232 from int256, reverting on\n * overflow (when the input is less than smallest int232 or\n * greater than largest int232).\n *\n * Counterpart to Solidity's `int232` operator.\n *\n * Requirements:\n *\n * - input must fit into 232 bits\n */\n function toInt232(int256 value) internal pure returns (int232 downcasted) {\n downcasted = int232(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(232, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int224 from int256, reverting on\n * overflow (when the input is less than smallest int224 or\n * greater than largest int224).\n *\n * Counterpart to Solidity's `int224` operator.\n *\n * Requirements:\n *\n * - input must fit into 224 bits\n */\n function toInt224(int256 value) internal pure returns (int224 downcasted) {\n downcasted = int224(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(224, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int216 from int256, reverting on\n * overflow (when the input is less than smallest int216 or\n * greater than largest int216).\n *\n * Counterpart to Solidity's `int216` operator.\n *\n * Requirements:\n *\n * - input must fit into 216 bits\n */\n function toInt216(int256 value) internal pure returns (int216 downcasted) {\n downcasted = int216(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(216, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int208 from int256, reverting on\n * overflow (when the input is less than smallest int208 or\n * greater than largest int208).\n *\n * Counterpart to Solidity's `int208` operator.\n *\n * Requirements:\n *\n * - input must fit into 208 bits\n */\n function toInt208(int256 value) internal pure returns (int208 downcasted) {\n downcasted = int208(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(208, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int200 from int256, reverting on\n * overflow (when the input is less than smallest int200 or\n * greater than largest int200).\n *\n * Counterpart to Solidity's `int200` operator.\n *\n * Requirements:\n *\n * - input must fit into 200 bits\n */\n function toInt200(int256 value) internal pure returns (int200 downcasted) {\n downcasted = int200(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(200, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int192 from int256, reverting on\n * overflow (when the input is less than smallest int192 or\n * greater than largest int192).\n *\n * Counterpart to Solidity's `int192` operator.\n *\n * Requirements:\n *\n * - input must fit into 192 bits\n */\n function toInt192(int256 value) internal pure returns (int192 downcasted) {\n downcasted = int192(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(192, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int184 from int256, reverting on\n * overflow (when the input is less than smallest int184 or\n * greater than largest int184).\n *\n * Counterpart to Solidity's `int184` operator.\n *\n * Requirements:\n *\n * - input must fit into 184 bits\n */\n function toInt184(int256 value) internal pure returns (int184 downcasted) {\n downcasted = int184(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(184, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int176 from int256, reverting on\n * overflow (when the input is less than smallest int176 or\n * greater than largest int176).\n *\n * Counterpart to Solidity's `int176` operator.\n *\n * Requirements:\n *\n * - input must fit into 176 bits\n */\n function toInt176(int256 value) internal pure returns (int176 downcasted) {\n downcasted = int176(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(176, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int168 from int256, reverting on\n * overflow (when the input is less than smallest int168 or\n * greater than largest int168).\n *\n * Counterpart to Solidity's `int168` operator.\n *\n * Requirements:\n *\n * - input must fit into 168 bits\n */\n function toInt168(int256 value) internal pure returns (int168 downcasted) {\n downcasted = int168(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(168, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int160 from int256, reverting on\n * overflow (when the input is less than smallest int160 or\n * greater than largest int160).\n *\n * Counterpart to Solidity's `int160` operator.\n *\n * Requirements:\n *\n * - input must fit into 160 bits\n */\n function toInt160(int256 value) internal pure returns (int160 downcasted) {\n downcasted = int160(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(160, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int152 from int256, reverting on\n * overflow (when the input is less than smallest int152 or\n * greater than largest int152).\n *\n * Counterpart to Solidity's `int152` operator.\n *\n * Requirements:\n *\n * - input must fit into 152 bits\n */\n function toInt152(int256 value) internal pure returns (int152 downcasted) {\n downcasted = int152(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(152, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int144 from int256, reverting on\n * overflow (when the input is less than smallest int144 or\n * greater than largest int144).\n *\n * Counterpart to Solidity's `int144` operator.\n *\n * Requirements:\n *\n * - input must fit into 144 bits\n */\n function toInt144(int256 value) internal pure returns (int144 downcasted) {\n downcasted = int144(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(144, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int136 from int256, reverting on\n * overflow (when the input is less than smallest int136 or\n * greater than largest int136).\n *\n * Counterpart to Solidity's `int136` operator.\n *\n * Requirements:\n *\n * - input must fit into 136 bits\n */\n function toInt136(int256 value) internal pure returns (int136 downcasted) {\n downcasted = int136(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(136, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int128 from int256, reverting on\n * overflow (when the input is less than smallest int128 or\n * greater than largest int128).\n *\n * Counterpart to Solidity's `int128` operator.\n *\n * Requirements:\n *\n * - input must fit into 128 bits\n */\n function toInt128(int256 value) internal pure returns (int128 downcasted) {\n downcasted = int128(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(128, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int120 from int256, reverting on\n * overflow (when the input is less than smallest int120 or\n * greater than largest int120).\n *\n * Counterpart to Solidity's `int120` operator.\n *\n * Requirements:\n *\n * - input must fit into 120 bits\n */\n function toInt120(int256 value) internal pure returns (int120 downcasted) {\n downcasted = int120(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(120, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int112 from int256, reverting on\n * overflow (when the input is less than smallest int112 or\n * greater than largest int112).\n *\n * Counterpart to Solidity's `int112` operator.\n *\n * Requirements:\n *\n * - input must fit into 112 bits\n */\n function toInt112(int256 value) internal pure returns (int112 downcasted) {\n downcasted = int112(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(112, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int104 from int256, reverting on\n * overflow (when the input is less than smallest int104 or\n * greater than largest int104).\n *\n * Counterpart to Solidity's `int104` operator.\n *\n * Requirements:\n *\n * - input must fit into 104 bits\n */\n function toInt104(int256 value) internal pure returns (int104 downcasted) {\n downcasted = int104(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(104, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int96 from int256, reverting on\n * overflow (when the input is less than smallest int96 or\n * greater than largest int96).\n *\n * Counterpart to Solidity's `int96` operator.\n *\n * Requirements:\n *\n * - input must fit into 96 bits\n */\n function toInt96(int256 value) internal pure returns (int96 downcasted) {\n downcasted = int96(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(96, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int88 from int256, reverting on\n * overflow (when the input is less than smallest int88 or\n * greater than largest int88).\n *\n * Counterpart to Solidity's `int88` operator.\n *\n * Requirements:\n *\n * - input must fit into 88 bits\n */\n function toInt88(int256 value) internal pure returns (int88 downcasted) {\n downcasted = int88(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(88, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int80 from int256, reverting on\n * overflow (when the input is less than smallest int80 or\n * greater than largest int80).\n *\n * Counterpart to Solidity's `int80` operator.\n *\n * Requirements:\n *\n * - input must fit into 80 bits\n */\n function toInt80(int256 value) internal pure returns (int80 downcasted) {\n downcasted = int80(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(80, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int72 from int256, reverting on\n * overflow (when the input is less than smallest int72 or\n * greater than largest int72).\n *\n * Counterpart to Solidity's `int72` operator.\n *\n * Requirements:\n *\n * - input must fit into 72 bits\n */\n function toInt72(int256 value) internal pure returns (int72 downcasted) {\n downcasted = int72(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(72, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int64 from int256, reverting on\n * overflow (when the input is less than smallest int64 or\n * greater than largest int64).\n *\n * Counterpart to Solidity's `int64` operator.\n *\n * Requirements:\n *\n * - input must fit into 64 bits\n */\n function toInt64(int256 value) internal pure returns (int64 downcasted) {\n downcasted = int64(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(64, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int56 from int256, reverting on\n * overflow (when the input is less than smallest int56 or\n * greater than largest int56).\n *\n * Counterpart to Solidity's `int56` operator.\n *\n * Requirements:\n *\n * - input must fit into 56 bits\n */\n function toInt56(int256 value) internal pure returns (int56 downcasted) {\n downcasted = int56(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(56, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int48 from int256, reverting on\n * overflow (when the input is less than smallest int48 or\n * greater than largest int48).\n *\n * Counterpart to Solidity's `int48` operator.\n *\n * Requirements:\n *\n * - input must fit into 48 bits\n */\n function toInt48(int256 value) internal pure returns (int48 downcasted) {\n downcasted = int48(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(48, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int40 from int256, reverting on\n * overflow (when the input is less than smallest int40 or\n * greater than largest int40).\n *\n * Counterpart to Solidity's `int40` operator.\n *\n * Requirements:\n *\n * - input must fit into 40 bits\n */\n function toInt40(int256 value) internal pure returns (int40 downcasted) {\n downcasted = int40(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(40, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int32 from int256, reverting on\n * overflow (when the input is less than smallest int32 or\n * greater than largest int32).\n *\n * Counterpart to Solidity's `int32` operator.\n *\n * Requirements:\n *\n * - input must fit into 32 bits\n */\n function toInt32(int256 value) internal pure returns (int32 downcasted) {\n downcasted = int32(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(32, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int24 from int256, reverting on\n * overflow (when the input is less than smallest int24 or\n * greater than largest int24).\n *\n * Counterpart to Solidity's `int24` operator.\n *\n * Requirements:\n *\n * - input must fit into 24 bits\n */\n function toInt24(int256 value) internal pure returns (int24 downcasted) {\n downcasted = int24(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(24, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int16 from int256, reverting on\n * overflow (when the input is less than smallest int16 or\n * greater than largest int16).\n *\n * Counterpart to Solidity's `int16` operator.\n *\n * Requirements:\n *\n * - input must fit into 16 bits\n */\n function toInt16(int256 value) internal pure returns (int16 downcasted) {\n downcasted = int16(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(16, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int8 from int256, reverting on\n * overflow (when the input is less than smallest int8 or\n * greater than largest int8).\n *\n * Counterpart to Solidity's `int8` operator.\n *\n * Requirements:\n *\n * - input must fit into 8 bits\n */\n function toInt8(int256 value) internal pure returns (int8 downcasted) {\n downcasted = int8(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(8, value);\n }\n }\n\n /**\n * @dev Converts an unsigned uint256 into a signed int256.\n *\n * Requirements:\n *\n * - input must be less than or equal to maxInt256.\n */\n function toInt256(uint256 value) internal pure returns (int256) {\n // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive\n if (value > uint256(type(int256).max)) {\n revert SafeCastOverflowedUintToInt(value);\n }\n return int256(value);\n }\n\n /**\n * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump.\n */\n function toUint(bool b) internal pure returns (uint256 u) {\n assembly (\"memory-safe\") {\n u := iszero(iszero(b))\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/structs/EnumerableSet.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/structs/EnumerableSet.sol)\n// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.\n\npragma solidity ^0.8.20;\n\nimport {Arrays} from \"../Arrays.sol\";\n\n/**\n * @dev Library for managing\n * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive\n * types.\n *\n * Sets have the following properties:\n *\n * - Elements are added, removed, and checked for existence in constant time\n * (O(1)).\n * - Elements are enumerated in O(n). No guarantees are made on the ordering.\n * - Set can be cleared (all elements removed) in O(n).\n *\n * ```solidity\n * contract Example {\n * // Add the library methods\n * using EnumerableSet for EnumerableSet.AddressSet;\n *\n * // Declare a set state variable\n * EnumerableSet.AddressSet private mySet;\n * }\n * ```\n *\n * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)\n * and `uint256` (`UintSet`) are supported.\n *\n * [WARNING]\n * ====\n * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure\n * unusable.\n * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.\n *\n * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an\n * array of EnumerableSet.\n * ====\n */\nlibrary EnumerableSet {\n // To implement this library for multiple types with as little code\n // repetition as possible, we write it in terms of a generic Set type with\n // bytes32 values.\n // The Set implementation uses private functions, and user-facing\n // implementations (such as AddressSet) are just wrappers around the\n // underlying Set.\n // This means that we can only create new EnumerableSets for types that fit\n // in bytes32.\n\n struct Set {\n // Storage of set values\n bytes32[] _values;\n // Position is the index of the value in the `values` array plus 1.\n // Position 0 is used to mean a value is not in the set.\n mapping(bytes32 value => uint256) _positions;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function _add(Set storage set, bytes32 value) private returns (bool) {\n if (!_contains(set, value)) {\n set._values.push(value);\n // The value is stored at length-1, but we add 1 to all indexes\n // and use 0 as a sentinel value\n set._positions[value] = set._values.length;\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function _remove(Set storage set, bytes32 value) private returns (bool) {\n // We cache the value's position to prevent multiple reads from the same storage slot\n uint256 position = set._positions[value];\n\n if (position != 0) {\n // Equivalent to contains(set, value)\n // To delete an element from the _values array in O(1), we swap the element to delete with the last one in\n // the array, and then remove the last element (sometimes called as 'swap and pop').\n // This modifies the order of the array, as noted in {at}.\n\n uint256 valueIndex = position - 1;\n uint256 lastIndex = set._values.length - 1;\n\n if (valueIndex != lastIndex) {\n bytes32 lastValue = set._values[lastIndex];\n\n // Move the lastValue to the index where the value to delete is\n set._values[valueIndex] = lastValue;\n // Update the tracked position of the lastValue (that was just moved)\n set._positions[lastValue] = position;\n }\n\n // Delete the slot where the moved value was stored\n set._values.pop();\n\n // Delete the tracked position for the deleted slot\n delete set._positions[value];\n\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function _clear(Set storage set) private {\n uint256 len = _length(set);\n for (uint256 i = 0; i < len; ++i) {\n delete set._positions[set._values[i]];\n }\n Arrays.unsafeSetLength(set._values, 0);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function _contains(Set storage set, bytes32 value) private view returns (bool) {\n return set._positions[value] != 0;\n }\n\n /**\n * @dev Returns the number of values on the set. O(1).\n */\n function _length(Set storage set) private view returns (uint256) {\n return set._values.length;\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function _at(Set storage set, uint256 index) private view returns (bytes32) {\n return set._values[index];\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function _values(Set storage set) private view returns (bytes32[] memory) {\n return set._values;\n }\n\n // Bytes32Set\n\n struct Bytes32Set {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _add(set._inner, value);\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _remove(set._inner, value);\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function clear(Bytes32Set storage set) internal {\n _clear(set._inner);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {\n return _contains(set._inner, value);\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(Bytes32Set storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {\n return _at(set._inner, index);\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {\n bytes32[] memory store = _values(set._inner);\n bytes32[] memory result;\n\n assembly (\"memory-safe\") {\n result := store\n }\n\n return result;\n }\n\n // AddressSet\n\n struct AddressSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(AddressSet storage set, address value) internal returns (bool) {\n return _add(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(AddressSet storage set, address value) internal returns (bool) {\n return _remove(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function clear(AddressSet storage set) internal {\n _clear(set._inner);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(AddressSet storage set, address value) internal view returns (bool) {\n return _contains(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(AddressSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(AddressSet storage set, uint256 index) internal view returns (address) {\n return address(uint160(uint256(_at(set._inner, index))));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(AddressSet storage set) internal view returns (address[] memory) {\n bytes32[] memory store = _values(set._inner);\n address[] memory result;\n\n assembly (\"memory-safe\") {\n result := store\n }\n\n return result;\n }\n\n // UintSet\n\n struct UintSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(UintSet storage set, uint256 value) internal returns (bool) {\n return _add(set._inner, bytes32(value));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(UintSet storage set, uint256 value) internal returns (bool) {\n return _remove(set._inner, bytes32(value));\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function clear(UintSet storage set) internal {\n _clear(set._inner);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(UintSet storage set, uint256 value) internal view returns (bool) {\n return _contains(set._inner, bytes32(value));\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(UintSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(UintSet storage set, uint256 index) internal view returns (uint256) {\n return uint256(_at(set._inner, index));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(UintSet storage set) internal view returns (uint256[] memory) {\n bytes32[] memory store = _values(set._inner);\n uint256[] memory result;\n\n assembly (\"memory-safe\") {\n result := store\n }\n\n return result;\n }\n}\n" + } + }, + "settings": { + "evmVersion": "paris", + "libraries": {}, + "metadata": { "appendCBOR": true, "bytecodeHash": "none", "useLiteralContent": false }, + "optimizer": { "enabled": true, "runs": 17000 }, + "outputSelection": { + "*": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + } + }, + "remappings": [ + "forge-std/=node_modules/@chainlink/contracts/src/v0.8/vendor/forge-std/src/", + "@chainlink/policy-management/=node_modules/@chainlink/ace/packages/policy-management/src/", + "@chainlink/contracts/=node_modules/@chainlink/contracts/", + "@openzeppelin/contracts@4.8.3/=node_modules/@openzeppelin/contracts-4.8.3/", + "@openzeppelin/contracts@5.3.0/=node_modules/@openzeppelin/contracts-5.3.0/" + ], + "viaIR": true + } +} diff --git a/ccip-sdk/src/verify/fixtures/CrossChainToken.abi.json b/ccip-sdk/src/verify/fixtures/CrossChainToken.abi.json new file mode 100644 index 00000000..858c4636 --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/CrossChainToken.abi.json @@ -0,0 +1,1053 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "args", + "type": "tuple", + "internalType": "struct BaseERC20.ConstructorParams", + "components": [ + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "symbol", + "type": "string", + "internalType": "string" + }, + { + "name": "maxSupply", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "preMint", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "preMintRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "decimals", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "ccipAdmin", + "type": "address", + "internalType": "address" + } + ] + }, + { + "name": "burnMintRoleAdmin", + "type": "address", + "internalType": "address" + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "BURNER_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "BURN_MINT_ADMIN_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "DEFAULT_ADMIN_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MINTER_ROLE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "acceptDefaultAdminTransfer", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "allowance", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "beginDefaultAdminTransfer", + "inputs": [ + { + "name": "newAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "burn", + "inputs": [ + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "burn", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "burnFrom", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "cancelDefaultAdminTransfer", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "changeDefaultAdminDelay", + "inputs": [ + { + "name": "newDelay", + "type": "uint48", + "internalType": "uint48" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [ + { + "name": "_decimals", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "defaultAdmin", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "defaultAdminDelay", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint48", + "internalType": "uint48" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "defaultAdminDelayIncreaseWait", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint48", + "internalType": "uint48" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getCCIPAdmin", + "inputs": [], + "outputs": [ + { + "name": "ccipAdmin", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRoleAdmin", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "grantMintAndBurnRoles", + "inputs": [ + { + "name": "burnAndMinter", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "grantRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "hasRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "maxSupply", + "inputs": [], + "outputs": [ + { + "name": "_maxSupply", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "mint", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pendingDefaultAdmin", + "inputs": [], + "outputs": [ + { + "name": "newAdmin", + "type": "address", + "internalType": "address" + }, + { + "name": "schedule", + "type": "uint48", + "internalType": "uint48" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pendingDefaultAdminDelay", + "inputs": [], + "outputs": [ + { + "name": "newDelay", + "type": "uint48", + "internalType": "uint48" + }, + { + "name": "schedule", + "type": "uint48", + "internalType": "uint48" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "renounceRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeRole", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "rollbackDefaultAdminDelay", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setCCIPAdmin", + "inputs": [ + { + "name": "newAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transfer", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "typeAndVersion", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "pure" + }, + { + "type": "event", + "name": "Approval", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "CCIPAdminTransferred", + "inputs": [ + { + "name": "previousAdmin", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newAdmin", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DefaultAdminDelayChangeCanceled", + "inputs": [], + "anonymous": false + }, + { + "type": "event", + "name": "DefaultAdminDelayChangeScheduled", + "inputs": [ + { + "name": "newDelay", + "type": "uint48", + "indexed": false, + "internalType": "uint48" + }, + { + "name": "effectSchedule", + "type": "uint48", + "indexed": false, + "internalType": "uint48" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DefaultAdminTransferCanceled", + "inputs": [], + "anonymous": false + }, + { + "type": "event", + "name": "DefaultAdminTransferScheduled", + "inputs": [ + { + "name": "newAdmin", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "acceptSchedule", + "type": "uint48", + "indexed": false, + "internalType": "uint48" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleAdminChanged", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "previousAdminRole", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "newAdminRole", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleGranted", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleRevoked", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AccessControlBadConfirmation", + "inputs": [] + }, + { + "type": "error", + "name": "AccessControlEnforcedDefaultAdminDelay", + "inputs": [ + { + "name": "schedule", + "type": "uint48", + "internalType": "uint48" + } + ] + }, + { + "type": "error", + "name": "AccessControlEnforcedDefaultAdminRules", + "inputs": [] + }, + { + "type": "error", + "name": "AccessControlInvalidDefaultAdmin", + "inputs": [ + { + "name": "defaultAdmin", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AccessControlUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "neededRole", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "type": "error", + "name": "CannotRenounceCCIPAdmin", + "inputs": [] + }, + { + "type": "error", + "name": "ERC20InsufficientAllowance", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "allowance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "needed", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ERC20InsufficientBalance", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + }, + { + "name": "balance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "needed", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidApprover", + "inputs": [ + { + "name": "approver", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidReceiver", + "inputs": [ + { + "name": "receiver", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidSender", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidSpender", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "MaxSupplyExceeded", + "inputs": [ + { + "name": "supplyAfterMint", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxSupply", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "OnlyCCIPAdmin", + "inputs": [] + }, + { + "type": "error", + "name": "PreMintAddressNotSet", + "inputs": [] + }, + { + "type": "error", + "name": "PreMintRecipientSetWithZeroPreMint", + "inputs": [ + { + "name": "preMintRecipient", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "SafeCastOverflowedUintDowncast", + "inputs": [ + { + "name": "bits", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ] + } +] diff --git a/ccip-sdk/src/verify/fixtures/CrossChainToken.standard-input.json b/ccip-sdk/src/verify/fixtures/CrossChainToken.standard-input.json new file mode 100644 index 00000000..d7bb54d1 --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/CrossChainToken.standard-input.json @@ -0,0 +1,381 @@ +{ + "language": "Solidity", + "sources": { + "contracts/interfaces/IBurnMintERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol\";\n\n/// @notice Minimal ERC20 interface with mint/burn extensions used across CCIP.\n/// @dev Mirrors the Chainlink `IBurnMintERC20` interface but targets OpenZeppelin Contracts v5.3.0.\ninterface IBurnMintERC20 is IERC20 {\n /// @notice Mints new tokens for a given address.\n /// @param account The address to mint the new tokens to.\n /// @param amount The number of tokens to be minted.\n /// @dev This function increases the total supply.\n function mint(\n address account,\n uint256 amount\n ) external;\n\n /// @notice Burns tokens from the sender.\n /// @param amount The number of tokens to be burned.\n /// @dev This function decreases the total supply.\n function burn(\n uint256 amount\n ) external;\n\n /// @notice Burns tokens from a given address.\n /// @param account The address to burn tokens from.\n /// @param amount The number of tokens to be burned.\n /// @dev This function decreases the total supply.\n function burn(\n address account,\n uint256 amount\n ) external;\n\n /// @notice Burns tokens from a given address.\n /// @param account The address to burn tokens from.\n /// @param amount The number of tokens to be burned.\n /// @dev This function decreases the total supply.\n function burnFrom(\n address account,\n uint256 amount\n ) external;\n}\n\n" + }, + "contracts/interfaces/IGetCCIPAdmin.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface IGetCCIPAdmin {\n /// @notice Returns the admin of the token.\n /// @dev This method is named to never conflict with existing methods.\n function getCCIPAdmin() external view returns (address);\n}\n" + }, + "contracts/tokens/BaseERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {IGetCCIPAdmin} from \"../interfaces/IGetCCIPAdmin.sol\";\nimport {ITypeAndVersion} from \"@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol\";\n\nimport {ERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol\";\nimport {IERC20Metadata} from \"@openzeppelin/contracts@5.3.0/token/ERC20/extensions/IERC20Metadata.sol\";\nimport {IERC165} from \"@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol\";\n\n/// @notice A basic ERC20 that has no burn/mint functions.\n/// @dev If this contract is deployed with a pre-mint of 0, it is effectively useless as no mint functionality is\n/// exposed.\ncontract BaseERC20 is IGetCCIPAdmin, ERC20, ITypeAndVersion, IERC165 {\n function typeAndVersion() external pure virtual override returns (string memory) {\n return \"BaseERC20 2.0.0\";\n }\n\n error CannotRenounceCCIPAdmin();\n error MaxSupplyExceeded(uint256 supplyAfterMint, uint256 maxSupply);\n error OnlyCCIPAdmin();\n error PreMintAddressNotSet();\n error PreMintRecipientSetWithZeroPreMint(address preMintRecipient);\n\n /// @notice Emitted when the CCIPAdmin role is transferred to a new address.\n /// @param previousAdmin The address of the previous CCIPAdmin.\n /// @param newAdmin The address of the new CCIPAdmin.\n event CCIPAdminTransferred(address indexed previousAdmin, address indexed newAdmin);\n\n /// @param name The name of the token\n /// @param symbol The symbol of the token\n /// @param maxSupply_ The maximum supply of the token, 0 if unlimited\n /// @param preMint The amount of tokens to mint upon construction. Must be zero, or preMintRecipient must be set.\n /// NOTE: the base version of this contract does not support minting additional tokens after deployment, so this\n /// should be set to the full supply.\n /// @param preMintRecipient The address to receive the pre-mint amount. Must be non-zero if preMint is non-zero.\n /// @param decimals The number of decimals the token uses\n /// @param ccipAdmin The initial CCIPAdmin. If set to address(0), the deployer will be set as the initial CCIPAdmin.\n struct ConstructorParams {\n string name;\n string symbol;\n uint256 maxSupply;\n uint256 preMint;\n address preMintRecipient;\n uint8 decimals;\n address ccipAdmin;\n }\n\n /// @dev The number of decimals for the token\n uint8 internal immutable i_decimals;\n\n /// @dev The maximum supply of the token, 0 if unlimited\n uint256 internal immutable i_maxSupply;\n\n /// @dev the CCIPAdmin can be used to register with the CCIP token admin registry, but has no other special powers.\n address internal s_ccipAdmin;\n\n constructor(\n ConstructorParams memory args\n ) ERC20(args.name, args.symbol) {\n i_decimals = args.decimals;\n i_maxSupply = args.maxSupply;\n\n // Mint the initial supply to the preMintRecipient, saving gas by not calling if the mint amount is zero.\n if (args.preMint != 0) {\n if (args.preMintRecipient == address(0)) revert PreMintAddressNotSet();\n\n _mint(args.preMintRecipient, args.preMint);\n } else if (args.preMintRecipient != address(0)) {\n revert PreMintRecipientSetWithZeroPreMint(args.preMintRecipient);\n }\n\n _setCCIPAdmin(args.ccipAdmin == address(0) ? msg.sender : args.ccipAdmin);\n }\n\n /// @inheritdoc IERC165\n function supportsInterface(\n bytes4 interfaceId\n ) public view virtual returns (bool) {\n return interfaceId == type(IERC20).interfaceId || interfaceId == type(IGetCCIPAdmin).interfaceId\n || interfaceId == type(IERC20Metadata).interfaceId || interfaceId == type(IERC165).interfaceId;\n }\n\n // ================================================================\n // \u2502 ERC20 \u2502\n // ================================================================\n\n /// @notice Returns the number of decimals for this token.\n /// @return _decimals The number of decimals for this token.\n function decimals() public view virtual override returns (uint8 _decimals) {\n return i_decimals;\n }\n\n /// @notice Returns the max supply of the token, 0 if unlimited.\n /// @return _maxSupply The max supply of the token, 0 if unlimited.\n function maxSupply() public view virtual returns (uint256 _maxSupply) {\n return i_maxSupply;\n }\n\n /// @inheritdoc ERC20\n /// @dev Uses OZ ERC20 _approve to disallow approving for address(0).\n /// @dev Disallows approving for address(this).\n function _approve(\n address owner,\n address spender,\n uint256 value,\n bool emitEvent\n ) internal virtual override {\n if (spender == address(this)) revert ERC20InvalidSpender(spender);\n\n super._approve(owner, spender, value, emitEvent);\n }\n\n /// @inheritdoc ERC20\n /// @dev This check applies to transfer, minting, and burning.\n /// @dev Disallows transferring/minting to address(this).\n function _update(\n address from,\n address to,\n uint256 value\n ) internal virtual override {\n if (to == address(this)) revert ERC20InvalidReceiver(to);\n\n // Update first, then check the total supply.\n super._update(from, to, value);\n\n // If `from` is address(0), this is a mint, so we need to check the total supply against the max supply.\n if (from == address(0)) {\n _assertMaxSupply();\n }\n }\n\n /// @notice Asserts that the total supply does not exceed the max supply. Reverts if it does.\n function _assertMaxSupply() internal view virtual {\n if (i_maxSupply != 0) {\n uint256 supply = totalSupply();\n if (supply > i_maxSupply) {\n revert MaxSupplyExceeded(supply, i_maxSupply);\n }\n }\n }\n\n // ================================================================\n // \u2502 Roles \u2502\n // ================================================================\n\n /// @notice Gets the current CCIPAdmin.\n /// @return ccipAdmin The address of the current CCIPAdmin.\n function getCCIPAdmin() external view virtual returns (address ccipAdmin) {\n return s_ccipAdmin;\n }\n\n /// @notice Transfers the CCIPAdmin role to a new address.\n /// @param newAdmin The address of the new CCIPAdmin. Setting this to address(0) is not allowed.\n /// @dev The BaseERC20 has no notion of ownership, so this function can be called by the CCIP admin. Tokens expanding\n /// from this base contract can choose to restrict this function to other roles instead.\n function setCCIPAdmin(\n address newAdmin\n ) external virtual {\n if (msg.sender != s_ccipAdmin) {\n revert OnlyCCIPAdmin();\n }\n\n if (newAdmin == address(0)) revert CannotRenounceCCIPAdmin();\n\n _setCCIPAdmin(newAdmin);\n }\n\n /// @dev Internal function to set the CCIPAdmin, emits an event with the previous and new admin.\n /// @param newAdmin The address of the new CCIPAdmin.\n function _setCCIPAdmin(\n address newAdmin\n ) internal virtual {\n address currentAdmin = s_ccipAdmin;\n\n s_ccipAdmin = newAdmin;\n\n emit CCIPAdminTransferred(currentAdmin, newAdmin);\n }\n}\n" + }, + "contracts/tokens/CrossChainToken.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {BaseERC20} from \"./BaseERC20.sol\";\n\nimport {IBurnMintERC20} from \"../interfaces/IBurnMintERC20.sol\";\nimport {\n AccessControlDefaultAdminRules\n} from \"@openzeppelin/contracts@5.3.0/access/extensions/AccessControlDefaultAdminRules.sol\";\nimport {IERC165} from \"@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol\";\n\n/// @notice A basic ERC20 compatible token contract with burn and minting roles.\n/// @dev The total supply can be limited during deployment.\n/// @dev This contract inherits its access control from AccessControlDefaultAdminRules, meaning it relies on OZ\n/// AccessControl with 2-step ownership transfers. There's also a separate `ccipAdmin` role which can be used to\n/// register with the CCIP token admin registry but has no other special powers, and can only be transferred by the\n/// DEFAULT_ADMIN_ROLE. The DEFAULT_ADMIN_ROLE holder can also be used to register the token in the token admin registry.\ncontract CrossChainToken is BaseERC20, AccessControlDefaultAdminRules, IBurnMintERC20 {\n function typeAndVersion() external pure virtual override returns (string memory) {\n return \"CrossChainToken 2.0.0\";\n }\n\n /// @notice The holder of this role can mint tokens.\n bytes32 public constant MINTER_ROLE = keccak256(\"MINTER_ROLE\");\n /// @notice The holder of this role can burn tokens.\n bytes32 public constant BURNER_ROLE = keccak256(\"BURNER_ROLE\");\n /// @notice The holder of this role can grant/revoke both the MINTER_ROLE and the BURNER_ROLE.\n bytes32 public constant BURN_MINT_ADMIN_ROLE = keccak256(\"BURN_MINT_ADMIN_ROLE\");\n\n /// @param args The parameters for the ERC20 token, including name, symbol, decimals, max supply, and pre-mint amount.\n /// @param burnMintRoleAdmin The address to grant the BURN_MINT_ADMIN_ROLE. If set to address(0), no address will be\n /// granted the role.\n /// @param owner The address to set as the owner of the contract, which has the default admin role. If set to\n /// address(0), the deployer will be set as the owner.\n constructor(\n ConstructorParams memory args,\n address burnMintRoleAdmin,\n address owner\n ) BaseERC20(args) AccessControlDefaultAdminRules(0, owner == address(0) ? msg.sender : owner) {\n if (burnMintRoleAdmin != address(0)) {\n _grantRole(BURN_MINT_ADMIN_ROLE, burnMintRoleAdmin);\n }\n\n _setRoleAdmin(MINTER_ROLE, BURN_MINT_ADMIN_ROLE);\n _setRoleAdmin(BURNER_ROLE, BURN_MINT_ADMIN_ROLE);\n }\n\n /// @inheritdoc IERC165\n function supportsInterface(\n bytes4 interfaceId\n ) public view virtual override(AccessControlDefaultAdminRules, BaseERC20) returns (bool) {\n return AccessControlDefaultAdminRules.supportsInterface(interfaceId) || BaseERC20.supportsInterface(interfaceId)\n || interfaceId == type(IBurnMintERC20).interfaceId;\n }\n\n // ================================================================\n // \u2502 Burning & minting \u2502\n // ================================================================\n\n /// @inheritdoc IBurnMintERC20\n /// @dev Uses OZ ERC20 _burn to disallow burning from address(0).\n function burn(\n uint256 amount\n ) public virtual override onlyRole(BURNER_ROLE) {\n _burn(_msgSender(), amount);\n }\n\n /// @inheritdoc IBurnMintERC20\n /// @dev Alias for BurnFrom for compatibility with the older naming convention.\n /// @dev Uses burnFrom for all validation & logic.\n function burn(\n address account,\n uint256 amount\n ) public virtual override {\n burnFrom(account, amount);\n }\n\n /// @inheritdoc IBurnMintERC20\n /// @dev Uses OZ ERC20 _burn to disallow burning from address(0).\n function burnFrom(\n address account,\n uint256 amount\n ) public virtual override onlyRole(BURNER_ROLE) {\n _spendAllowance(account, _msgSender(), amount);\n _burn(account, amount);\n }\n\n /// @inheritdoc IBurnMintERC20\n /// @dev Uses OZ ERC20 _mint to disallow minting to address(0), and BaseERC20 to disallow minting to address(this).\n /// @dev Uses BaseERC20's max supply logic.\n function mint(\n address account,\n uint256 amount\n ) public virtual override onlyRole(MINTER_ROLE) {\n _mint(account, amount);\n }\n\n // ================================================================\n // \u2502 Roles \u2502\n // ================================================================\n\n /// @notice grants both mint and burn roles to `burnAndMinter`.\n /// @param burnAndMinter The address to be granted both the MINTER_ROLE and BURNER_ROLE.\n /// @dev calls public functions so this function does not require\n /// access controls. This is handled in the inner functions.\n function grantMintAndBurnRoles(\n address burnAndMinter\n ) public virtual {\n grantRole(MINTER_ROLE, burnAndMinter);\n grantRole(BURNER_ROLE, burnAndMinter);\n }\n\n /// @notice Sets the CCIP admin role to `newAdmin`.\n /// @dev Overrides the default CCIP admin role setter to require the caller to have the DEFAULT_ADMIN_ROLE.\n /// @param newAdmin The address of the new CCIP admin.\n function setCCIPAdmin(\n address newAdmin\n ) external virtual override onlyRole(DEFAULT_ADMIN_ROLE) {\n _setCCIPAdmin(newAdmin);\n }\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface ITypeAndVersion {\n function typeAndVersion() external pure returns (string memory);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/access/AccessControl.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (access/AccessControl.sol)\n\npragma solidity ^0.8.20;\n\nimport {IAccessControl} from \"./IAccessControl.sol\";\nimport {Context} from \"../utils/Context.sol\";\nimport {ERC165} from \"../utils/introspection/ERC165.sol\";\n\n/**\n * @dev Contract module that allows children to implement role-based access\n * control mechanisms. This is a lightweight version that doesn't allow enumerating role\n * members except through off-chain means by accessing the contract event logs. Some\n * applications may benefit from on-chain enumerability, for those cases see\n * {AccessControlEnumerable}.\n *\n * Roles are referred to by their `bytes32` identifier. These should be exposed\n * in the external API and be unique. The best way to achieve this is by\n * using `public constant` hash digests:\n *\n * ```solidity\n * bytes32 public constant MY_ROLE = keccak256(\"MY_ROLE\");\n * ```\n *\n * Roles can be used to represent a set of permissions. To restrict access to a\n * function call, use {hasRole}:\n *\n * ```solidity\n * function foo() public {\n * require(hasRole(MY_ROLE, msg.sender));\n * ...\n * }\n * ```\n *\n * Roles can be granted and revoked dynamically via the {grantRole} and\n * {revokeRole} functions. Each role has an associated admin role, and only\n * accounts that have a role's admin role can call {grantRole} and {revokeRole}.\n *\n * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means\n * that only accounts with this role will be able to grant or revoke other\n * roles. More complex role relationships can be created by using\n * {_setRoleAdmin}.\n *\n * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to\n * grant and revoke this role. Extra precautions should be taken to secure\n * accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}\n * to enforce additional security measures for this role.\n */\nabstract contract AccessControl is Context, IAccessControl, ERC165 {\n struct RoleData {\n mapping(address account => bool) hasRole;\n bytes32 adminRole;\n }\n\n mapping(bytes32 role => RoleData) private _roles;\n\n bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;\n\n /**\n * @dev Modifier that checks that an account has a specific role. Reverts\n * with an {AccessControlUnauthorizedAccount} error including the required role.\n */\n modifier onlyRole(bytes32 role) {\n _checkRole(role);\n _;\n }\n\n /**\n * @dev See {IERC165-supportsInterface}.\n */\n function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {\n return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);\n }\n\n /**\n * @dev Returns `true` if `account` has been granted `role`.\n */\n function hasRole(bytes32 role, address account) public view virtual returns (bool) {\n return _roles[role].hasRole[account];\n }\n\n /**\n * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`\n * is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.\n */\n function _checkRole(bytes32 role) internal view virtual {\n _checkRole(role, _msgSender());\n }\n\n /**\n * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`\n * is missing `role`.\n */\n function _checkRole(bytes32 role, address account) internal view virtual {\n if (!hasRole(role, account)) {\n revert AccessControlUnauthorizedAccount(account, role);\n }\n }\n\n /**\n * @dev Returns the admin role that controls `role`. See {grantRole} and\n * {revokeRole}.\n *\n * To change a role's admin, use {_setRoleAdmin}.\n */\n function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {\n return _roles[role].adminRole;\n }\n\n /**\n * @dev Grants `role` to `account`.\n *\n * If `account` had not been already granted `role`, emits a {RoleGranted}\n * event.\n *\n * Requirements:\n *\n * - the caller must have ``role``'s admin role.\n *\n * May emit a {RoleGranted} event.\n */\n function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {\n _grantRole(role, account);\n }\n\n /**\n * @dev Revokes `role` from `account`.\n *\n * If `account` had been granted `role`, emits a {RoleRevoked} event.\n *\n * Requirements:\n *\n * - the caller must have ``role``'s admin role.\n *\n * May emit a {RoleRevoked} event.\n */\n function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {\n _revokeRole(role, account);\n }\n\n /**\n * @dev Revokes `role` from the calling account.\n *\n * Roles are often managed via {grantRole} and {revokeRole}: this function's\n * purpose is to provide a mechanism for accounts to lose their privileges\n * if they are compromised (such as when a trusted device is misplaced).\n *\n * If the calling account had been revoked `role`, emits a {RoleRevoked}\n * event.\n *\n * Requirements:\n *\n * - the caller must be `callerConfirmation`.\n *\n * May emit a {RoleRevoked} event.\n */\n function renounceRole(bytes32 role, address callerConfirmation) public virtual {\n if (callerConfirmation != _msgSender()) {\n revert AccessControlBadConfirmation();\n }\n\n _revokeRole(role, callerConfirmation);\n }\n\n /**\n * @dev Sets `adminRole` as ``role``'s admin role.\n *\n * Emits a {RoleAdminChanged} event.\n */\n function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {\n bytes32 previousAdminRole = getRoleAdmin(role);\n _roles[role].adminRole = adminRole;\n emit RoleAdminChanged(role, previousAdminRole, adminRole);\n }\n\n /**\n * @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.\n *\n * Internal function without access restriction.\n *\n * May emit a {RoleGranted} event.\n */\n function _grantRole(bytes32 role, address account) internal virtual returns (bool) {\n if (!hasRole(role, account)) {\n _roles[role].hasRole[account] = true;\n emit RoleGranted(role, account, _msgSender());\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Attempts to revoke `role` from `account` and returns a boolean indicating if `role` was revoked.\n *\n * Internal function without access restriction.\n *\n * May emit a {RoleRevoked} event.\n */\n function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {\n if (hasRole(role, account)) {\n _roles[role].hasRole[account] = false;\n emit RoleRevoked(role, account, _msgSender());\n return true;\n } else {\n return false;\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/access/IAccessControl.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (access/IAccessControl.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev External interface of AccessControl declared to support ERC-165 detection.\n */\ninterface IAccessControl {\n /**\n * @dev The `account` is missing a role.\n */\n error AccessControlUnauthorizedAccount(address account, bytes32 neededRole);\n\n /**\n * @dev The caller of a function is not the expected one.\n *\n * NOTE: Don't confuse with {AccessControlUnauthorizedAccount}.\n */\n error AccessControlBadConfirmation();\n\n /**\n * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole`\n *\n * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite\n * {RoleAdminChanged} not being emitted to signal this.\n */\n event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);\n\n /**\n * @dev Emitted when `account` is granted `role`.\n *\n * `sender` is the account that originated the contract call. This account bears the admin role (for the granted role).\n * Expected in cases where the role was granted using the internal {AccessControl-_grantRole}.\n */\n event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);\n\n /**\n * @dev Emitted when `account` is revoked `role`.\n *\n * `sender` is the account that originated the contract call:\n * - if using `revokeRole`, it is the admin role bearer\n * - if using `renounceRole`, it is the role bearer (i.e. `account`)\n */\n event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);\n\n /**\n * @dev Returns `true` if `account` has been granted `role`.\n */\n function hasRole(bytes32 role, address account) external view returns (bool);\n\n /**\n * @dev Returns the admin role that controls `role`. See {grantRole} and\n * {revokeRole}.\n *\n * To change a role's admin, use {AccessControl-_setRoleAdmin}.\n */\n function getRoleAdmin(bytes32 role) external view returns (bytes32);\n\n /**\n * @dev Grants `role` to `account`.\n *\n * If `account` had not been already granted `role`, emits a {RoleGranted}\n * event.\n *\n * Requirements:\n *\n * - the caller must have ``role``'s admin role.\n */\n function grantRole(bytes32 role, address account) external;\n\n /**\n * @dev Revokes `role` from `account`.\n *\n * If `account` had been granted `role`, emits a {RoleRevoked} event.\n *\n * Requirements:\n *\n * - the caller must have ``role``'s admin role.\n */\n function revokeRole(bytes32 role, address account) external;\n\n /**\n * @dev Revokes `role` from the calling account.\n *\n * Roles are often managed via {grantRole} and {revokeRole}: this function's\n * purpose is to provide a mechanism for accounts to lose their privileges\n * if they are compromised (such as when a trusted device is misplaced).\n *\n * If the calling account had been granted `role`, emits a {RoleRevoked}\n * event.\n *\n * Requirements:\n *\n * - the caller must be `callerConfirmation`.\n */\n function renounceRole(bytes32 role, address callerConfirmation) external;\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/access/extensions/AccessControlDefaultAdminRules.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (access/extensions/AccessControlDefaultAdminRules.sol)\n\npragma solidity ^0.8.20;\n\nimport {IAccessControlDefaultAdminRules} from \"./IAccessControlDefaultAdminRules.sol\";\nimport {AccessControl, IAccessControl} from \"../AccessControl.sol\";\nimport {SafeCast} from \"../../utils/math/SafeCast.sol\";\nimport {Math} from \"../../utils/math/Math.sol\";\nimport {IERC5313} from \"../../interfaces/IERC5313.sol\";\n\n/**\n * @dev Extension of {AccessControl} that allows specifying special rules to manage\n * the `DEFAULT_ADMIN_ROLE` holder, which is a sensitive role with special permissions\n * over other roles that may potentially have privileged rights in the system.\n *\n * If a specific role doesn't have an admin role assigned, the holder of the\n * `DEFAULT_ADMIN_ROLE` will have the ability to grant it and revoke it.\n *\n * This contract implements the following risk mitigations on top of {AccessControl}:\n *\n * * Only one account holds the `DEFAULT_ADMIN_ROLE` since deployment until it's potentially renounced.\n * * Enforces a 2-step process to transfer the `DEFAULT_ADMIN_ROLE` to another account.\n * * Enforces a configurable delay between the two steps, with the ability to cancel before the transfer is accepted.\n * * The delay can be changed by scheduling, see {changeDefaultAdminDelay}.\n * * It is not possible to use another role to manage the `DEFAULT_ADMIN_ROLE`.\n *\n * Example usage:\n *\n * ```solidity\n * contract MyToken is AccessControlDefaultAdminRules {\n * constructor() AccessControlDefaultAdminRules(\n * 3 days,\n * msg.sender // Explicit initial `DEFAULT_ADMIN_ROLE` holder\n * ) {}\n * }\n * ```\n */\nabstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRules, IERC5313, AccessControl {\n // pending admin pair read/written together frequently\n address private _pendingDefaultAdmin;\n uint48 private _pendingDefaultAdminSchedule; // 0 == unset\n\n uint48 private _currentDelay;\n address private _currentDefaultAdmin;\n\n // pending delay pair read/written together frequently\n uint48 private _pendingDelay;\n uint48 private _pendingDelaySchedule; // 0 == unset\n\n /**\n * @dev Sets the initial values for {defaultAdminDelay} and {defaultAdmin} address.\n */\n constructor(uint48 initialDelay, address initialDefaultAdmin) {\n if (initialDefaultAdmin == address(0)) {\n revert AccessControlInvalidDefaultAdmin(address(0));\n }\n _currentDelay = initialDelay;\n _grantRole(DEFAULT_ADMIN_ROLE, initialDefaultAdmin);\n }\n\n /**\n * @dev See {IERC165-supportsInterface}.\n */\n function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {\n return interfaceId == type(IAccessControlDefaultAdminRules).interfaceId || super.supportsInterface(interfaceId);\n }\n\n /**\n * @dev See {IERC5313-owner}.\n */\n function owner() public view virtual returns (address) {\n return defaultAdmin();\n }\n\n ///\n /// Override AccessControl role management\n ///\n\n /**\n * @dev See {AccessControl-grantRole}. Reverts for `DEFAULT_ADMIN_ROLE`.\n */\n function grantRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) {\n if (role == DEFAULT_ADMIN_ROLE) {\n revert AccessControlEnforcedDefaultAdminRules();\n }\n super.grantRole(role, account);\n }\n\n /**\n * @dev See {AccessControl-revokeRole}. Reverts for `DEFAULT_ADMIN_ROLE`.\n */\n function revokeRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) {\n if (role == DEFAULT_ADMIN_ROLE) {\n revert AccessControlEnforcedDefaultAdminRules();\n }\n super.revokeRole(role, account);\n }\n\n /**\n * @dev See {AccessControl-renounceRole}.\n *\n * For the `DEFAULT_ADMIN_ROLE`, it only allows renouncing in two steps by first calling\n * {beginDefaultAdminTransfer} to the `address(0)`, so it's required that the {pendingDefaultAdmin} schedule\n * has also passed when calling this function.\n *\n * After its execution, it will not be possible to call `onlyRole(DEFAULT_ADMIN_ROLE)` functions.\n *\n * NOTE: Renouncing `DEFAULT_ADMIN_ROLE` will leave the contract without a {defaultAdmin},\n * thereby disabling any functionality that is only available for it, and the possibility of reassigning a\n * non-administrated role.\n */\n function renounceRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) {\n if (role == DEFAULT_ADMIN_ROLE && account == defaultAdmin()) {\n (address newDefaultAdmin, uint48 schedule) = pendingDefaultAdmin();\n if (newDefaultAdmin != address(0) || !_isScheduleSet(schedule) || !_hasSchedulePassed(schedule)) {\n revert AccessControlEnforcedDefaultAdminDelay(schedule);\n }\n delete _pendingDefaultAdminSchedule;\n }\n super.renounceRole(role, account);\n }\n\n /**\n * @dev See {AccessControl-_grantRole}.\n *\n * For `DEFAULT_ADMIN_ROLE`, it only allows granting if there isn't already a {defaultAdmin} or if the\n * role has been previously renounced.\n *\n * NOTE: Exposing this function through another mechanism may make the `DEFAULT_ADMIN_ROLE`\n * assignable again. Make sure to guarantee this is the expected behavior in your implementation.\n */\n function _grantRole(bytes32 role, address account) internal virtual override returns (bool) {\n if (role == DEFAULT_ADMIN_ROLE) {\n if (defaultAdmin() != address(0)) {\n revert AccessControlEnforcedDefaultAdminRules();\n }\n _currentDefaultAdmin = account;\n }\n return super._grantRole(role, account);\n }\n\n /**\n * @dev See {AccessControl-_revokeRole}.\n */\n function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) {\n if (role == DEFAULT_ADMIN_ROLE && account == defaultAdmin()) {\n delete _currentDefaultAdmin;\n }\n return super._revokeRole(role, account);\n }\n\n /**\n * @dev See {AccessControl-_setRoleAdmin}. Reverts for `DEFAULT_ADMIN_ROLE`.\n */\n function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual override {\n if (role == DEFAULT_ADMIN_ROLE) {\n revert AccessControlEnforcedDefaultAdminRules();\n }\n super._setRoleAdmin(role, adminRole);\n }\n\n ///\n /// AccessControlDefaultAdminRules accessors\n ///\n\n /**\n * @inheritdoc IAccessControlDefaultAdminRules\n */\n function defaultAdmin() public view virtual returns (address) {\n return _currentDefaultAdmin;\n }\n\n /**\n * @inheritdoc IAccessControlDefaultAdminRules\n */\n function pendingDefaultAdmin() public view virtual returns (address newAdmin, uint48 schedule) {\n return (_pendingDefaultAdmin, _pendingDefaultAdminSchedule);\n }\n\n /**\n * @inheritdoc IAccessControlDefaultAdminRules\n */\n function defaultAdminDelay() public view virtual returns (uint48) {\n uint48 schedule = _pendingDelaySchedule;\n return (_isScheduleSet(schedule) && _hasSchedulePassed(schedule)) ? _pendingDelay : _currentDelay;\n }\n\n /**\n * @inheritdoc IAccessControlDefaultAdminRules\n */\n function pendingDefaultAdminDelay() public view virtual returns (uint48 newDelay, uint48 schedule) {\n schedule = _pendingDelaySchedule;\n return (_isScheduleSet(schedule) && !_hasSchedulePassed(schedule)) ? (_pendingDelay, schedule) : (0, 0);\n }\n\n /**\n * @inheritdoc IAccessControlDefaultAdminRules\n */\n function defaultAdminDelayIncreaseWait() public view virtual returns (uint48) {\n return 5 days;\n }\n\n ///\n /// AccessControlDefaultAdminRules public and internal setters for defaultAdmin/pendingDefaultAdmin\n ///\n\n /**\n * @inheritdoc IAccessControlDefaultAdminRules\n */\n function beginDefaultAdminTransfer(address newAdmin) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {\n _beginDefaultAdminTransfer(newAdmin);\n }\n\n /**\n * @dev See {beginDefaultAdminTransfer}.\n *\n * Internal function without access restriction.\n */\n function _beginDefaultAdminTransfer(address newAdmin) internal virtual {\n uint48 newSchedule = SafeCast.toUint48(block.timestamp) + defaultAdminDelay();\n _setPendingDefaultAdmin(newAdmin, newSchedule);\n emit DefaultAdminTransferScheduled(newAdmin, newSchedule);\n }\n\n /**\n * @inheritdoc IAccessControlDefaultAdminRules\n */\n function cancelDefaultAdminTransfer() public virtual onlyRole(DEFAULT_ADMIN_ROLE) {\n _cancelDefaultAdminTransfer();\n }\n\n /**\n * @dev See {cancelDefaultAdminTransfer}.\n *\n * Internal function without access restriction.\n */\n function _cancelDefaultAdminTransfer() internal virtual {\n _setPendingDefaultAdmin(address(0), 0);\n }\n\n /**\n * @inheritdoc IAccessControlDefaultAdminRules\n */\n function acceptDefaultAdminTransfer() public virtual {\n (address newDefaultAdmin, ) = pendingDefaultAdmin();\n if (_msgSender() != newDefaultAdmin) {\n // Enforce newDefaultAdmin explicit acceptance.\n revert AccessControlInvalidDefaultAdmin(_msgSender());\n }\n _acceptDefaultAdminTransfer();\n }\n\n /**\n * @dev See {acceptDefaultAdminTransfer}.\n *\n * Internal function without access restriction.\n */\n function _acceptDefaultAdminTransfer() internal virtual {\n (address newAdmin, uint48 schedule) = pendingDefaultAdmin();\n if (!_isScheduleSet(schedule) || !_hasSchedulePassed(schedule)) {\n revert AccessControlEnforcedDefaultAdminDelay(schedule);\n }\n _revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin());\n _grantRole(DEFAULT_ADMIN_ROLE, newAdmin);\n delete _pendingDefaultAdmin;\n delete _pendingDefaultAdminSchedule;\n }\n\n ///\n /// AccessControlDefaultAdminRules public and internal setters for defaultAdminDelay/pendingDefaultAdminDelay\n ///\n\n /**\n * @inheritdoc IAccessControlDefaultAdminRules\n */\n function changeDefaultAdminDelay(uint48 newDelay) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {\n _changeDefaultAdminDelay(newDelay);\n }\n\n /**\n * @dev See {changeDefaultAdminDelay}.\n *\n * Internal function without access restriction.\n */\n function _changeDefaultAdminDelay(uint48 newDelay) internal virtual {\n uint48 newSchedule = SafeCast.toUint48(block.timestamp) + _delayChangeWait(newDelay);\n _setPendingDelay(newDelay, newSchedule);\n emit DefaultAdminDelayChangeScheduled(newDelay, newSchedule);\n }\n\n /**\n * @inheritdoc IAccessControlDefaultAdminRules\n */\n function rollbackDefaultAdminDelay() public virtual onlyRole(DEFAULT_ADMIN_ROLE) {\n _rollbackDefaultAdminDelay();\n }\n\n /**\n * @dev See {rollbackDefaultAdminDelay}.\n *\n * Internal function without access restriction.\n */\n function _rollbackDefaultAdminDelay() internal virtual {\n _setPendingDelay(0, 0);\n }\n\n /**\n * @dev Returns the amount of seconds to wait after the `newDelay` will\n * become the new {defaultAdminDelay}.\n *\n * The value returned guarantees that if the delay is reduced, it will go into effect\n * after a wait that honors the previously set delay.\n *\n * See {defaultAdminDelayIncreaseWait}.\n */\n function _delayChangeWait(uint48 newDelay) internal view virtual returns (uint48) {\n uint48 currentDelay = defaultAdminDelay();\n\n // When increasing the delay, we schedule the delay change to occur after a period of \"new delay\" has passed, up\n // to a maximum given by defaultAdminDelayIncreaseWait, by default 5 days. For example, if increasing from 1 day\n // to 3 days, the new delay will come into effect after 3 days. If increasing from 1 day to 10 days, the new\n // delay will come into effect after 5 days. The 5 day wait period is intended to be able to fix an error like\n // using milliseconds instead of seconds.\n //\n // When decreasing the delay, we wait the difference between \"current delay\" and \"new delay\". This guarantees\n // that an admin transfer cannot be made faster than \"current delay\" at the time the delay change is scheduled.\n // For example, if decreasing from 10 days to 3 days, the new delay will come into effect after 7 days.\n return\n newDelay > currentDelay\n ? uint48(Math.min(newDelay, defaultAdminDelayIncreaseWait())) // no need to safecast, both inputs are uint48\n : currentDelay - newDelay;\n }\n\n ///\n /// Private setters\n ///\n\n /**\n * @dev Setter of the tuple for pending admin and its schedule.\n *\n * May emit a DefaultAdminTransferCanceled event.\n */\n function _setPendingDefaultAdmin(address newAdmin, uint48 newSchedule) private {\n (, uint48 oldSchedule) = pendingDefaultAdmin();\n\n _pendingDefaultAdmin = newAdmin;\n _pendingDefaultAdminSchedule = newSchedule;\n\n // An `oldSchedule` from `pendingDefaultAdmin()` is only set if it hasn't been accepted.\n if (_isScheduleSet(oldSchedule)) {\n // Emit for implicit cancellations when another default admin was scheduled.\n emit DefaultAdminTransferCanceled();\n }\n }\n\n /**\n * @dev Setter of the tuple for pending delay and its schedule.\n *\n * May emit a DefaultAdminDelayChangeCanceled event.\n */\n function _setPendingDelay(uint48 newDelay, uint48 newSchedule) private {\n uint48 oldSchedule = _pendingDelaySchedule;\n\n if (_isScheduleSet(oldSchedule)) {\n if (_hasSchedulePassed(oldSchedule)) {\n // Materialize a virtual delay\n _currentDelay = _pendingDelay;\n } else {\n // Emit for implicit cancellations when another delay was scheduled.\n emit DefaultAdminDelayChangeCanceled();\n }\n }\n\n _pendingDelay = newDelay;\n _pendingDelaySchedule = newSchedule;\n }\n\n ///\n /// Private helpers\n ///\n\n /**\n * @dev Defines if an `schedule` is considered set. For consistency purposes.\n */\n function _isScheduleSet(uint48 schedule) private pure returns (bool) {\n return schedule != 0;\n }\n\n /**\n * @dev Defines if an `schedule` is considered passed. For consistency purposes.\n */\n function _hasSchedulePassed(uint48 schedule) private view returns (bool) {\n return schedule < block.timestamp;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/access/extensions/IAccessControlDefaultAdminRules.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (access/extensions/IAccessControlDefaultAdminRules.sol)\n\npragma solidity ^0.8.20;\n\nimport {IAccessControl} from \"../IAccessControl.sol\";\n\n/**\n * @dev External interface of AccessControlDefaultAdminRules declared to support ERC-165 detection.\n */\ninterface IAccessControlDefaultAdminRules is IAccessControl {\n /**\n * @dev The new default admin is not a valid default admin.\n */\n error AccessControlInvalidDefaultAdmin(address defaultAdmin);\n\n /**\n * @dev At least one of the following rules was violated:\n *\n * - The `DEFAULT_ADMIN_ROLE` must only be managed by itself.\n * - The `DEFAULT_ADMIN_ROLE` must only be held by one account at the time.\n * - Any `DEFAULT_ADMIN_ROLE` transfer must be in two delayed steps.\n */\n error AccessControlEnforcedDefaultAdminRules();\n\n /**\n * @dev The delay for transferring the default admin delay is enforced and\n * the operation must wait until `schedule`.\n *\n * NOTE: `schedule` can be 0 indicating there's no transfer scheduled.\n */\n error AccessControlEnforcedDefaultAdminDelay(uint48 schedule);\n\n /**\n * @dev Emitted when a {defaultAdmin} transfer is started, setting `newAdmin` as the next\n * address to become the {defaultAdmin} by calling {acceptDefaultAdminTransfer} only after `acceptSchedule`\n * passes.\n */\n event DefaultAdminTransferScheduled(address indexed newAdmin, uint48 acceptSchedule);\n\n /**\n * @dev Emitted when a {pendingDefaultAdmin} is reset if it was never accepted, regardless of its schedule.\n */\n event DefaultAdminTransferCanceled();\n\n /**\n * @dev Emitted when a {defaultAdminDelay} change is started, setting `newDelay` as the next\n * delay to be applied between default admin transfer after `effectSchedule` has passed.\n */\n event DefaultAdminDelayChangeScheduled(uint48 newDelay, uint48 effectSchedule);\n\n /**\n * @dev Emitted when a {pendingDefaultAdminDelay} is reset if its schedule didn't pass.\n */\n event DefaultAdminDelayChangeCanceled();\n\n /**\n * @dev Returns the address of the current `DEFAULT_ADMIN_ROLE` holder.\n */\n function defaultAdmin() external view returns (address);\n\n /**\n * @dev Returns a tuple of a `newAdmin` and an accept schedule.\n *\n * After the `schedule` passes, the `newAdmin` will be able to accept the {defaultAdmin} role\n * by calling {acceptDefaultAdminTransfer}, completing the role transfer.\n *\n * A zero value only in `acceptSchedule` indicates no pending admin transfer.\n *\n * NOTE: A zero address `newAdmin` means that {defaultAdmin} is being renounced.\n */\n function pendingDefaultAdmin() external view returns (address newAdmin, uint48 acceptSchedule);\n\n /**\n * @dev Returns the delay required to schedule the acceptance of a {defaultAdmin} transfer started.\n *\n * This delay will be added to the current timestamp when calling {beginDefaultAdminTransfer} to set\n * the acceptance schedule.\n *\n * NOTE: If a delay change has been scheduled, it will take effect as soon as the schedule passes, making this\n * function returns the new delay. See {changeDefaultAdminDelay}.\n */\n function defaultAdminDelay() external view returns (uint48);\n\n /**\n * @dev Returns a tuple of `newDelay` and an effect schedule.\n *\n * After the `schedule` passes, the `newDelay` will get into effect immediately for every\n * new {defaultAdmin} transfer started with {beginDefaultAdminTransfer}.\n *\n * A zero value only in `effectSchedule` indicates no pending delay change.\n *\n * NOTE: A zero value only for `newDelay` means that the next {defaultAdminDelay}\n * will be zero after the effect schedule.\n */\n function pendingDefaultAdminDelay() external view returns (uint48 newDelay, uint48 effectSchedule);\n\n /**\n * @dev Starts a {defaultAdmin} transfer by setting a {pendingDefaultAdmin} scheduled for acceptance\n * after the current timestamp plus a {defaultAdminDelay}.\n *\n * Requirements:\n *\n * - Only can be called by the current {defaultAdmin}.\n *\n * Emits a DefaultAdminRoleChangeStarted event.\n */\n function beginDefaultAdminTransfer(address newAdmin) external;\n\n /**\n * @dev Cancels a {defaultAdmin} transfer previously started with {beginDefaultAdminTransfer}.\n *\n * A {pendingDefaultAdmin} not yet accepted can also be cancelled with this function.\n *\n * Requirements:\n *\n * - Only can be called by the current {defaultAdmin}.\n *\n * May emit a DefaultAdminTransferCanceled event.\n */\n function cancelDefaultAdminTransfer() external;\n\n /**\n * @dev Completes a {defaultAdmin} transfer previously started with {beginDefaultAdminTransfer}.\n *\n * After calling the function:\n *\n * - `DEFAULT_ADMIN_ROLE` should be granted to the caller.\n * - `DEFAULT_ADMIN_ROLE` should be revoked from the previous holder.\n * - {pendingDefaultAdmin} should be reset to zero values.\n *\n * Requirements:\n *\n * - Only can be called by the {pendingDefaultAdmin}'s `newAdmin`.\n * - The {pendingDefaultAdmin}'s `acceptSchedule` should've passed.\n */\n function acceptDefaultAdminTransfer() external;\n\n /**\n * @dev Initiates a {defaultAdminDelay} update by setting a {pendingDefaultAdminDelay} scheduled for getting\n * into effect after the current timestamp plus a {defaultAdminDelay}.\n *\n * This function guarantees that any call to {beginDefaultAdminTransfer} done between the timestamp this\n * method is called and the {pendingDefaultAdminDelay} effect schedule will use the current {defaultAdminDelay}\n * set before calling.\n *\n * The {pendingDefaultAdminDelay}'s effect schedule is defined in a way that waiting until the schedule and then\n * calling {beginDefaultAdminTransfer} with the new delay will take at least the same as another {defaultAdmin}\n * complete transfer (including acceptance).\n *\n * The schedule is designed for two scenarios:\n *\n * - When the delay is changed for a larger one the schedule is `block.timestamp + newDelay` capped by\n * {defaultAdminDelayIncreaseWait}.\n * - When the delay is changed for a shorter one, the schedule is `block.timestamp + (current delay - new delay)`.\n *\n * A {pendingDefaultAdminDelay} that never got into effect will be canceled in favor of a new scheduled change.\n *\n * Requirements:\n *\n * - Only can be called by the current {defaultAdmin}.\n *\n * Emits a DefaultAdminDelayChangeScheduled event and may emit a DefaultAdminDelayChangeCanceled event.\n */\n function changeDefaultAdminDelay(uint48 newDelay) external;\n\n /**\n * @dev Cancels a scheduled {defaultAdminDelay} change.\n *\n * Requirements:\n *\n * - Only can be called by the current {defaultAdmin}.\n *\n * May emit a DefaultAdminDelayChangeCanceled event.\n */\n function rollbackDefaultAdminDelay() external;\n\n /**\n * @dev Maximum time in seconds for an increase to {defaultAdminDelay} (that is scheduled using {changeDefaultAdminDelay})\n * to take effect. Default to 5 days.\n *\n * When the {defaultAdminDelay} is scheduled to be increased, it goes into effect after the new delay has passed with\n * the purpose of giving enough time for reverting any accidental change (i.e. using milliseconds instead of seconds)\n * that may lock the contract. However, to avoid excessive schedules, the wait is capped by this function and it can\n * be overrode for a custom {defaultAdminDelay} increase scheduling.\n *\n * IMPORTANT: Make sure to add a reasonable amount of time while overriding this value, otherwise,\n * there's a risk of setting a high new delay that goes into effect almost immediately without the\n * possibility of human intervention in the case of an input error (eg. set milliseconds instead of seconds).\n */\n function defaultAdminDelayIncreaseWait() external view returns (uint48);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC5313.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC5313.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface for the Light Contract Ownership Standard.\n *\n * A standardized minimal interface required to identify an account that controls a contract\n */\ninterface IERC5313 {\n /**\n * @dev Gets the address of the owner.\n */\n function owner() external view returns (address);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/draft-IERC6093.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/draft-IERC6093.sol)\npragma solidity ^0.8.20;\n\n/**\n * @dev Standard ERC-20 Errors\n * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-20 tokens.\n */\ninterface IERC20Errors {\n /**\n * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n * @param balance Current balance for the interacting account.\n * @param needed Minimum amount required to perform a transfer.\n */\n error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);\n\n /**\n * @dev Indicates a failure with the token `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n */\n error ERC20InvalidSender(address sender);\n\n /**\n * @dev Indicates a failure with the token `receiver`. Used in transfers.\n * @param receiver Address to which tokens are being transferred.\n */\n error ERC20InvalidReceiver(address receiver);\n\n /**\n * @dev Indicates a failure with the `spender`\u2019s `allowance`. Used in transfers.\n * @param spender Address that may be allowed to operate on tokens without being their owner.\n * @param allowance Amount of tokens a `spender` is allowed to operate with.\n * @param needed Minimum amount required to perform a transfer.\n */\n error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);\n\n /**\n * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.\n * @param approver Address initiating an approval operation.\n */\n error ERC20InvalidApprover(address approver);\n\n /**\n * @dev Indicates a failure with the `spender` to be approved. Used in approvals.\n * @param spender Address that may be allowed to operate on tokens without being their owner.\n */\n error ERC20InvalidSpender(address spender);\n}\n\n/**\n * @dev Standard ERC-721 Errors\n * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-721 tokens.\n */\ninterface IERC721Errors {\n /**\n * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in ERC-20.\n * Used in balance queries.\n * @param owner Address of the current owner of a token.\n */\n error ERC721InvalidOwner(address owner);\n\n /**\n * @dev Indicates a `tokenId` whose `owner` is the zero address.\n * @param tokenId Identifier number of a token.\n */\n error ERC721NonexistentToken(uint256 tokenId);\n\n /**\n * @dev Indicates an error related to the ownership over a particular token. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n * @param tokenId Identifier number of a token.\n * @param owner Address of the current owner of a token.\n */\n error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner);\n\n /**\n * @dev Indicates a failure with the token `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n */\n error ERC721InvalidSender(address sender);\n\n /**\n * @dev Indicates a failure with the token `receiver`. Used in transfers.\n * @param receiver Address to which tokens are being transferred.\n */\n error ERC721InvalidReceiver(address receiver);\n\n /**\n * @dev Indicates a failure with the `operator`\u2019s approval. Used in transfers.\n * @param operator Address that may be allowed to operate on tokens without being their owner.\n * @param tokenId Identifier number of a token.\n */\n error ERC721InsufficientApproval(address operator, uint256 tokenId);\n\n /**\n * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.\n * @param approver Address initiating an approval operation.\n */\n error ERC721InvalidApprover(address approver);\n\n /**\n * @dev Indicates a failure with the `operator` to be approved. Used in approvals.\n * @param operator Address that may be allowed to operate on tokens without being their owner.\n */\n error ERC721InvalidOperator(address operator);\n}\n\n/**\n * @dev Standard ERC-1155 Errors\n * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-1155 tokens.\n */\ninterface IERC1155Errors {\n /**\n * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n * @param balance Current balance for the interacting account.\n * @param needed Minimum amount required to perform a transfer.\n * @param tokenId Identifier number of a token.\n */\n error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId);\n\n /**\n * @dev Indicates a failure with the token `sender`. Used in transfers.\n * @param sender Address whose tokens are being transferred.\n */\n error ERC1155InvalidSender(address sender);\n\n /**\n * @dev Indicates a failure with the token `receiver`. Used in transfers.\n * @param receiver Address to which tokens are being transferred.\n */\n error ERC1155InvalidReceiver(address receiver);\n\n /**\n * @dev Indicates a failure with the `operator`\u2019s approval. Used in transfers.\n * @param operator Address that may be allowed to operate on tokens without being their owner.\n * @param owner Address of the current owner of a token.\n */\n error ERC1155MissingApprovalForAll(address operator, address owner);\n\n /**\n * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.\n * @param approver Address initiating an approval operation.\n */\n error ERC1155InvalidApprover(address approver);\n\n /**\n * @dev Indicates a failure with the `operator` to be approved. Used in approvals.\n * @param operator Address that may be allowed to operate on tokens without being their owner.\n */\n error ERC1155InvalidOperator(address operator);\n\n /**\n * @dev Indicates an array length mismatch between ids and values in a safeBatchTransferFrom operation.\n * Used in batch transfers.\n * @param idsLength Length of the array of token identifiers\n * @param valuesLength Length of the array of token amounts\n */\n error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/ERC20.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"./IERC20.sol\";\nimport {IERC20Metadata} from \"./extensions/IERC20Metadata.sol\";\nimport {Context} from \"../../utils/Context.sol\";\nimport {IERC20Errors} from \"../../interfaces/draft-IERC6093.sol\";\n\n/**\n * @dev Implementation of the {IERC20} interface.\n *\n * This implementation is agnostic to the way tokens are created. This means\n * that a supply mechanism has to be added in a derived contract using {_mint}.\n *\n * TIP: For a detailed writeup see our guide\n * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How\n * to implement supply mechanisms].\n *\n * The default value of {decimals} is 18. To change this, you should override\n * this function so it returns a different value.\n *\n * We have followed general OpenZeppelin Contracts guidelines: functions revert\n * instead returning `false` on failure. This behavior is nonetheless\n * conventional and does not conflict with the expectations of ERC-20\n * applications.\n */\nabstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {\n mapping(address account => uint256) private _balances;\n\n mapping(address account => mapping(address spender => uint256)) private _allowances;\n\n uint256 private _totalSupply;\n\n string private _name;\n string private _symbol;\n\n /**\n * @dev Sets the values for {name} and {symbol}.\n *\n * Both values are immutable: they can only be set once during construction.\n */\n constructor(string memory name_, string memory symbol_) {\n _name = name_;\n _symbol = symbol_;\n }\n\n /**\n * @dev Returns the name of the token.\n */\n function name() public view virtual returns (string memory) {\n return _name;\n }\n\n /**\n * @dev Returns the symbol of the token, usually a shorter version of the\n * name.\n */\n function symbol() public view virtual returns (string memory) {\n return _symbol;\n }\n\n /**\n * @dev Returns the number of decimals used to get its user representation.\n * For example, if `decimals` equals `2`, a balance of `505` tokens should\n * be displayed to a user as `5.05` (`505 / 10 ** 2`).\n *\n * Tokens usually opt for a value of 18, imitating the relationship between\n * Ether and Wei. This is the default value returned by this function, unless\n * it's overridden.\n *\n * NOTE: This information is only used for _display_ purposes: it in\n * no way affects any of the arithmetic of the contract, including\n * {IERC20-balanceOf} and {IERC20-transfer}.\n */\n function decimals() public view virtual returns (uint8) {\n return 18;\n }\n\n /**\n * @dev See {IERC20-totalSupply}.\n */\n function totalSupply() public view virtual returns (uint256) {\n return _totalSupply;\n }\n\n /**\n * @dev See {IERC20-balanceOf}.\n */\n function balanceOf(address account) public view virtual returns (uint256) {\n return _balances[account];\n }\n\n /**\n * @dev See {IERC20-transfer}.\n *\n * Requirements:\n *\n * - `to` cannot be the zero address.\n * - the caller must have a balance of at least `value`.\n */\n function transfer(address to, uint256 value) public virtual returns (bool) {\n address owner = _msgSender();\n _transfer(owner, to, value);\n return true;\n }\n\n /**\n * @dev See {IERC20-allowance}.\n */\n function allowance(address owner, address spender) public view virtual returns (uint256) {\n return _allowances[owner][spender];\n }\n\n /**\n * @dev See {IERC20-approve}.\n *\n * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on\n * `transferFrom`. This is semantically equivalent to an infinite approval.\n *\n * Requirements:\n *\n * - `spender` cannot be the zero address.\n */\n function approve(address spender, uint256 value) public virtual returns (bool) {\n address owner = _msgSender();\n _approve(owner, spender, value);\n return true;\n }\n\n /**\n * @dev See {IERC20-transferFrom}.\n *\n * Skips emitting an {Approval} event indicating an allowance update. This is not\n * required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve].\n *\n * NOTE: Does not update the allowance if the current allowance\n * is the maximum `uint256`.\n *\n * Requirements:\n *\n * - `from` and `to` cannot be the zero address.\n * - `from` must have a balance of at least `value`.\n * - the caller must have allowance for ``from``'s tokens of at least\n * `value`.\n */\n function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {\n address spender = _msgSender();\n _spendAllowance(from, spender, value);\n _transfer(from, to, value);\n return true;\n }\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to`.\n *\n * This internal function is equivalent to {transfer}, and can be used to\n * e.g. implement automatic token fees, slashing mechanisms, etc.\n *\n * Emits a {Transfer} event.\n *\n * NOTE: This function is not virtual, {_update} should be overridden instead.\n */\n function _transfer(address from, address to, uint256 value) internal {\n if (from == address(0)) {\n revert ERC20InvalidSender(address(0));\n }\n if (to == address(0)) {\n revert ERC20InvalidReceiver(address(0));\n }\n _update(from, to, value);\n }\n\n /**\n * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`\n * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding\n * this function.\n *\n * Emits a {Transfer} event.\n */\n function _update(address from, address to, uint256 value) internal virtual {\n if (from == address(0)) {\n // Overflow check required: The rest of the code assumes that totalSupply never overflows\n _totalSupply += value;\n } else {\n uint256 fromBalance = _balances[from];\n if (fromBalance < value) {\n revert ERC20InsufficientBalance(from, fromBalance, value);\n }\n unchecked {\n // Overflow not possible: value <= fromBalance <= totalSupply.\n _balances[from] = fromBalance - value;\n }\n }\n\n if (to == address(0)) {\n unchecked {\n // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.\n _totalSupply -= value;\n }\n } else {\n unchecked {\n // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.\n _balances[to] += value;\n }\n }\n\n emit Transfer(from, to, value);\n }\n\n /**\n * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).\n * Relies on the `_update` mechanism\n *\n * Emits a {Transfer} event with `from` set to the zero address.\n *\n * NOTE: This function is not virtual, {_update} should be overridden instead.\n */\n function _mint(address account, uint256 value) internal {\n if (account == address(0)) {\n revert ERC20InvalidReceiver(address(0));\n }\n _update(address(0), account, value);\n }\n\n /**\n * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.\n * Relies on the `_update` mechanism.\n *\n * Emits a {Transfer} event with `to` set to the zero address.\n *\n * NOTE: This function is not virtual, {_update} should be overridden instead\n */\n function _burn(address account, uint256 value) internal {\n if (account == address(0)) {\n revert ERC20InvalidSender(address(0));\n }\n _update(account, address(0), value);\n }\n\n /**\n * @dev Sets `value` as the allowance of `spender` over the `owner`'s tokens.\n *\n * This internal function is equivalent to `approve`, and can be used to\n * e.g. set automatic allowances for certain subsystems, etc.\n *\n * Emits an {Approval} event.\n *\n * Requirements:\n *\n * - `owner` cannot be the zero address.\n * - `spender` cannot be the zero address.\n *\n * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.\n */\n function _approve(address owner, address spender, uint256 value) internal {\n _approve(owner, spender, value, true);\n }\n\n /**\n * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.\n *\n * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by\n * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any\n * `Approval` event during `transferFrom` operations.\n *\n * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to\n * true using the following override:\n *\n * ```solidity\n * function _approve(address owner, address spender, uint256 value, bool) internal virtual override {\n * super._approve(owner, spender, value, true);\n * }\n * ```\n *\n * Requirements are the same as {_approve}.\n */\n function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {\n if (owner == address(0)) {\n revert ERC20InvalidApprover(address(0));\n }\n if (spender == address(0)) {\n revert ERC20InvalidSpender(address(0));\n }\n _allowances[owner][spender] = value;\n if (emitEvent) {\n emit Approval(owner, spender, value);\n }\n }\n\n /**\n * @dev Updates `owner`'s allowance for `spender` based on spent `value`.\n *\n * Does not update the allowance value in case of infinite allowance.\n * Revert if not enough allowance is available.\n *\n * Does not emit an {Approval} event.\n */\n function _spendAllowance(address owner, address spender, uint256 value) internal virtual {\n uint256 currentAllowance = allowance(owner, spender);\n if (currentAllowance < type(uint256).max) {\n if (currentAllowance < value) {\n revert ERC20InsufficientAllowance(spender, currentAllowance, value);\n }\n unchecked {\n _approve(owner, spender, currentAllowance - value, false);\n }\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/IERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-20 standard as defined in the ERC.\n */\ninterface IERC20 {\n /**\n * @dev Emitted when `value` tokens are moved from one account (`from`) to\n * another (`to`).\n *\n * Note that `value` may be zero.\n */\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n /**\n * @dev Emitted when the allowance of a `spender` for an `owner` is set by\n * a call to {approve}. `value` is the new allowance.\n */\n event Approval(address indexed owner, address indexed spender, uint256 value);\n\n /**\n * @dev Returns the value of tokens in existence.\n */\n function totalSupply() external view returns (uint256);\n\n /**\n * @dev Returns the value of tokens owned by `account`.\n */\n function balanceOf(address account) external view returns (uint256);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transfer(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Returns the remaining number of tokens that `spender` will be\n * allowed to spend on behalf of `owner` through {transferFrom}. This is\n * zero by default.\n *\n * This value changes when {approve} or {transferFrom} are called.\n */\n function allowance(address owner, address spender) external view returns (uint256);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * IMPORTANT: Beware that changing an allowance with this method brings the risk\n * that someone may use both the old and the new allowance by unfortunate\n * transaction ordering. One possible solution to mitigate this race\n * condition is to first reduce the spender's allowance to 0 and set the\n * desired value afterwards:\n * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n *\n * Emits an {Approval} event.\n */\n function approve(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the\n * allowance mechanism. `value` is then deducted from the caller's\n * allowance.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(address from, address to, uint256 value) external returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Metadata.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Metadata.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../IERC20.sol\";\n\n/**\n * @dev Interface for the optional metadata functions from the ERC-20 standard.\n */\ninterface IERC20Metadata is IERC20 {\n /**\n * @dev Returns the name of the token.\n */\n function name() external view returns (string memory);\n\n /**\n * @dev Returns the symbol of the token.\n */\n function symbol() external view returns (string memory);\n\n /**\n * @dev Returns the decimals places of the token.\n */\n function decimals() external view returns (uint8);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Context.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Provides information about the current execution context, including the\n * sender of the transaction and its data. While these are generally available\n * via msg.sender and msg.data, they should not be accessed in such a direct\n * manner, since when dealing with meta-transactions the account sending and\n * paying for execution may not be the actual sender (as far as an application\n * is concerned).\n *\n * This contract is only required for intermediate, library-like contracts.\n */\nabstract contract Context {\n function _msgSender() internal view virtual returns (address) {\n return msg.sender;\n }\n\n function _msgData() internal view virtual returns (bytes calldata) {\n return msg.data;\n }\n\n function _contextSuffixLength() internal view virtual returns (uint256) {\n return 0;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Panic.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/Panic.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Helper library for emitting standardized panic codes.\n *\n * ```solidity\n * contract Example {\n * using Panic for uint256;\n *\n * // Use any of the declared internal constants\n * function foo() { Panic.GENERIC.panic(); }\n *\n * // Alternatively\n * function foo() { Panic.panic(Panic.GENERIC); }\n * }\n * ```\n *\n * Follows the list from https://github.com/ethereum/solidity/blob/v0.8.24/libsolutil/ErrorCodes.h[libsolutil].\n *\n * _Available since v5.1._\n */\n// slither-disable-next-line unused-state\nlibrary Panic {\n /// @dev generic / unspecified error\n uint256 internal constant GENERIC = 0x00;\n /// @dev used by the assert() builtin\n uint256 internal constant ASSERT = 0x01;\n /// @dev arithmetic underflow or overflow\n uint256 internal constant UNDER_OVERFLOW = 0x11;\n /// @dev division or modulo by zero\n uint256 internal constant DIVISION_BY_ZERO = 0x12;\n /// @dev enum conversion error\n uint256 internal constant ENUM_CONVERSION_ERROR = 0x21;\n /// @dev invalid encoding in storage\n uint256 internal constant STORAGE_ENCODING_ERROR = 0x22;\n /// @dev empty array pop\n uint256 internal constant EMPTY_ARRAY_POP = 0x31;\n /// @dev array out of bounds access\n uint256 internal constant ARRAY_OUT_OF_BOUNDS = 0x32;\n /// @dev resource error (too large allocation or too large array)\n uint256 internal constant RESOURCE_ERROR = 0x41;\n /// @dev calling invalid internal function\n uint256 internal constant INVALID_INTERNAL_FUNCTION = 0x51;\n\n /// @dev Reverts with a panic code. Recommended to use with\n /// the internal constants with predefined codes.\n function panic(uint256 code) internal pure {\n assembly (\"memory-safe\") {\n mstore(0x00, 0x4e487b71)\n mstore(0x20, code)\n revert(0x1c, 0x24)\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/introspection/ERC165.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/ERC165.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC165} from \"./IERC165.sol\";\n\n/**\n * @dev Implementation of the {IERC165} interface.\n *\n * Contracts that want to implement ERC-165 should inherit from this contract and override {supportsInterface} to check\n * for the additional interface id that will be supported. For example:\n *\n * ```solidity\n * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {\n * return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);\n * }\n * ```\n */\nabstract contract ERC165 is IERC165 {\n /**\n * @dev See {IERC165-supportsInterface}.\n */\n function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {\n return interfaceId == type(IERC165).interfaceId;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/introspection/IERC165.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/IERC165.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-165 standard, as defined in the\n * https://eips.ethereum.org/EIPS/eip-165[ERC].\n *\n * Implementers can declare support of contract interfaces, which can then be\n * queried by others ({ERC165Checker}).\n *\n * For an implementation, see {ERC165}.\n */\ninterface IERC165 {\n /**\n * @dev Returns true if this contract implements the interface defined by\n * `interfaceId`. See the corresponding\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]\n * to learn more about how these ids are created.\n *\n * This function call must use less than 30 000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/Math.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/math/Math.sol)\n\npragma solidity ^0.8.20;\n\nimport {Panic} from \"../Panic.sol\";\nimport {SafeCast} from \"./SafeCast.sol\";\n\n/**\n * @dev Standard math utilities missing in the Solidity language.\n */\nlibrary Math {\n enum Rounding {\n Floor, // Toward negative infinity\n Ceil, // Toward positive infinity\n Trunc, // Toward zero\n Expand // Away from zero\n }\n\n /**\n * @dev Return the 512-bit addition of two uint256.\n *\n * The result is stored in two 256 variables such that sum = high * 2\u00b2\u2075\u2076 + low.\n */\n function add512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {\n assembly (\"memory-safe\") {\n low := add(a, b)\n high := lt(low, a)\n }\n }\n\n /**\n * @dev Return the 512-bit multiplication of two uint256.\n *\n * The result is stored in two 256 variables such that product = high * 2\u00b2\u2075\u2076 + low.\n */\n function mul512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {\n // 512-bit multiply [high low] = x * y. Compute the product mod 2\u00b2\u2075\u2076 and mod 2\u00b2\u2075\u2076 - 1, then use\n // the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256\n // variables such that product = high * 2\u00b2\u2075\u2076 + low.\n assembly (\"memory-safe\") {\n let mm := mulmod(a, b, not(0))\n low := mul(a, b)\n high := sub(sub(mm, low), lt(mm, low))\n }\n }\n\n /**\n * @dev Returns the addition of two unsigned integers, with a success flag (no overflow).\n */\n function tryAdd(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a + b;\n success = c >= a;\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the subtraction of two unsigned integers, with a success flag (no overflow).\n */\n function trySub(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a - b;\n success = c <= a;\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the multiplication of two unsigned integers, with a success flag (no overflow).\n */\n function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a * b;\n assembly (\"memory-safe\") {\n // Only true when the multiplication doesn't overflow\n // (c / a == b) || (a == 0)\n success := or(eq(div(c, a), b), iszero(a))\n }\n // equivalent to: success ? c : 0\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the division of two unsigned integers, with a success flag (no division by zero).\n */\n function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n success = b > 0;\n assembly (\"memory-safe\") {\n // The `DIV` opcode returns zero when the denominator is 0.\n result := div(a, b)\n }\n }\n }\n\n /**\n * @dev Returns the remainder of dividing two unsigned integers, with a success flag (no division by zero).\n */\n function tryMod(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n success = b > 0;\n assembly (\"memory-safe\") {\n // The `MOD` opcode returns zero when the denominator is 0.\n result := mod(a, b)\n }\n }\n }\n\n /**\n * @dev Unsigned saturating addition, bounds to `2\u00b2\u2075\u2076 - 1` instead of overflowing.\n */\n function saturatingAdd(uint256 a, uint256 b) internal pure returns (uint256) {\n (bool success, uint256 result) = tryAdd(a, b);\n return ternary(success, result, type(uint256).max);\n }\n\n /**\n * @dev Unsigned saturating subtraction, bounds to zero instead of overflowing.\n */\n function saturatingSub(uint256 a, uint256 b) internal pure returns (uint256) {\n (, uint256 result) = trySub(a, b);\n return result;\n }\n\n /**\n * @dev Unsigned saturating multiplication, bounds to `2\u00b2\u2075\u2076 - 1` instead of overflowing.\n */\n function saturatingMul(uint256 a, uint256 b) internal pure returns (uint256) {\n (bool success, uint256 result) = tryMul(a, b);\n return ternary(success, result, type(uint256).max);\n }\n\n /**\n * @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant.\n *\n * IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone.\n * However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute\n * one branch when needed, making this function more expensive.\n */\n function ternary(bool condition, uint256 a, uint256 b) internal pure returns (uint256) {\n unchecked {\n // branchless ternary works because:\n // b ^ (a ^ b) == a\n // b ^ 0 == b\n return b ^ ((a ^ b) * SafeCast.toUint(condition));\n }\n }\n\n /**\n * @dev Returns the largest of two numbers.\n */\n function max(uint256 a, uint256 b) internal pure returns (uint256) {\n return ternary(a > b, a, b);\n }\n\n /**\n * @dev Returns the smallest of two numbers.\n */\n function min(uint256 a, uint256 b) internal pure returns (uint256) {\n return ternary(a < b, a, b);\n }\n\n /**\n * @dev Returns the average of two numbers. The result is rounded towards\n * zero.\n */\n function average(uint256 a, uint256 b) internal pure returns (uint256) {\n // (a + b) / 2 can overflow.\n return (a & b) + (a ^ b) / 2;\n }\n\n /**\n * @dev Returns the ceiling of the division of two numbers.\n *\n * This differs from standard division with `/` in that it rounds towards infinity instead\n * of rounding towards zero.\n */\n function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {\n if (b == 0) {\n // Guarantee the same behavior as in a regular Solidity division.\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n\n // The following calculation ensures accurate ceiling division without overflow.\n // Since a is non-zero, (a - 1) / b will not overflow.\n // The largest possible result occurs when (a - 1) / b is type(uint256).max,\n // but the largest value we can obtain is type(uint256).max - 1, which happens\n // when a = type(uint256).max and b = 1.\n unchecked {\n return SafeCast.toUint(a > 0) * ((a - 1) / b + 1);\n }\n }\n\n /**\n * @dev Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or\n * denominator == 0.\n *\n * Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by\n * Uniswap Labs also under MIT license.\n */\n function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {\n unchecked {\n (uint256 high, uint256 low) = mul512(x, y);\n\n // Handle non-overflow cases, 256 by 256 division.\n if (high == 0) {\n // Solidity will revert if denominator == 0, unlike the div opcode on its own.\n // The surrounding unchecked block does not change this fact.\n // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.\n return low / denominator;\n }\n\n // Make sure the result is less than 2\u00b2\u2075\u2076. Also prevents denominator == 0.\n if (denominator <= high) {\n Panic.panic(ternary(denominator == 0, Panic.DIVISION_BY_ZERO, Panic.UNDER_OVERFLOW));\n }\n\n ///////////////////////////////////////////////\n // 512 by 256 division.\n ///////////////////////////////////////////////\n\n // Make division exact by subtracting the remainder from [high low].\n uint256 remainder;\n assembly (\"memory-safe\") {\n // Compute remainder using mulmod.\n remainder := mulmod(x, y, denominator)\n\n // Subtract 256 bit number from 512 bit number.\n high := sub(high, gt(remainder, low))\n low := sub(low, remainder)\n }\n\n // Factor powers of two out of denominator and compute largest power of two divisor of denominator.\n // Always >= 1. See https://cs.stackexchange.com/q/138556/92363.\n\n uint256 twos = denominator & (0 - denominator);\n assembly (\"memory-safe\") {\n // Divide denominator by twos.\n denominator := div(denominator, twos)\n\n // Divide [high low] by twos.\n low := div(low, twos)\n\n // Flip twos such that it is 2\u00b2\u2075\u2076 / twos. If twos is zero, then it becomes one.\n twos := add(div(sub(0, twos), twos), 1)\n }\n\n // Shift in bits from high into low.\n low |= high * twos;\n\n // Invert denominator mod 2\u00b2\u2075\u2076. Now that denominator is an odd number, it has an inverse modulo 2\u00b2\u2075\u2076 such\n // that denominator * inv \u2261 1 mod 2\u00b2\u2075\u2076. Compute the inverse by starting with a seed that is correct for\n // four bits. That is, denominator * inv \u2261 1 mod 2\u2074.\n uint256 inverse = (3 * denominator) ^ 2;\n\n // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also\n // works in modular arithmetic, doubling the correct bits in each step.\n inverse *= 2 - denominator * inverse; // inverse mod 2\u2078\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b9\u2076\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b3\u00b2\n inverse *= 2 - denominator * inverse; // inverse mod 2\u2076\u2074\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b9\u00b2\u2078\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b2\u2075\u2076\n\n // Because the division is now exact we can divide by multiplying with the modular inverse of denominator.\n // This will give us the correct result modulo 2\u00b2\u2075\u2076. Since the preconditions guarantee that the outcome is\n // less than 2\u00b2\u2075\u2076, this is the final result. We don't need to compute the high bits of the result and high\n // is no longer required.\n result = low * inverse;\n return result;\n }\n }\n\n /**\n * @dev Calculates x * y / denominator with full precision, following the selected rounding direction.\n */\n function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {\n return mulDiv(x, y, denominator) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0);\n }\n\n /**\n * @dev Calculates floor(x * y >> n) with full precision. Throws if result overflows a uint256.\n */\n function mulShr(uint256 x, uint256 y, uint8 n) internal pure returns (uint256 result) {\n unchecked {\n (uint256 high, uint256 low) = mul512(x, y);\n if (high >= 1 << n) {\n Panic.panic(Panic.UNDER_OVERFLOW);\n }\n return (high << (256 - n)) | (low >> n);\n }\n }\n\n /**\n * @dev Calculates x * y >> n with full precision, following the selected rounding direction.\n */\n function mulShr(uint256 x, uint256 y, uint8 n, Rounding rounding) internal pure returns (uint256) {\n return mulShr(x, y, n) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, 1 << n) > 0);\n }\n\n /**\n * @dev Calculate the modular multiplicative inverse of a number in Z/nZ.\n *\n * If n is a prime, then Z/nZ is a field. In that case all elements are inversible, except 0.\n * If n is not a prime, then Z/nZ is not a field, and some elements might not be inversible.\n *\n * If the input value is not inversible, 0 is returned.\n *\n * NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Fermat's little theorem and get the\n * inverse using `Math.modExp(a, n - 2, n)`. See {invModPrime}.\n */\n function invMod(uint256 a, uint256 n) internal pure returns (uint256) {\n unchecked {\n if (n == 0) return 0;\n\n // The inverse modulo is calculated using the Extended Euclidean Algorithm (iterative version)\n // Used to compute integers x and y such that: ax + ny = gcd(a, n).\n // When the gcd is 1, then the inverse of a modulo n exists and it's x.\n // ax + ny = 1\n // ax = 1 + (-y)n\n // ax \u2261 1 (mod n) # x is the inverse of a modulo n\n\n // If the remainder is 0 the gcd is n right away.\n uint256 remainder = a % n;\n uint256 gcd = n;\n\n // Therefore the initial coefficients are:\n // ax + ny = gcd(a, n) = n\n // 0a + 1n = n\n int256 x = 0;\n int256 y = 1;\n\n while (remainder != 0) {\n uint256 quotient = gcd / remainder;\n\n (gcd, remainder) = (\n // The old remainder is the next gcd to try.\n remainder,\n // Compute the next remainder.\n // Can't overflow given that (a % gcd) * (gcd // (a % gcd)) <= gcd\n // where gcd is at most n (capped to type(uint256).max)\n gcd - remainder * quotient\n );\n\n (x, y) = (\n // Increment the coefficient of a.\n y,\n // Decrement the coefficient of n.\n // Can overflow, but the result is casted to uint256 so that the\n // next value of y is \"wrapped around\" to a value between 0 and n - 1.\n x - y * int256(quotient)\n );\n }\n\n if (gcd != 1) return 0; // No inverse exists.\n return ternary(x < 0, n - uint256(-x), uint256(x)); // Wrap the result if it's negative.\n }\n }\n\n /**\n * @dev Variant of {invMod}. More efficient, but only works if `p` is known to be a prime greater than `2`.\n *\n * From https://en.wikipedia.org/wiki/Fermat%27s_little_theorem[Fermat's little theorem], we know that if p is\n * prime, then `a**(p-1) \u2261 1 mod p`. As a consequence, we have `a * a**(p-2) \u2261 1 mod p`, which means that\n * `a**(p-2)` is the modular multiplicative inverse of a in Fp.\n *\n * NOTE: this function does NOT check that `p` is a prime greater than `2`.\n */\n function invModPrime(uint256 a, uint256 p) internal view returns (uint256) {\n unchecked {\n return Math.modExp(a, p - 2, p);\n }\n }\n\n /**\n * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m)\n *\n * Requirements:\n * - modulus can't be zero\n * - underlying staticcall to precompile must succeed\n *\n * IMPORTANT: The result is only valid if the underlying call succeeds. When using this function, make\n * sure the chain you're using it on supports the precompiled contract for modular exponentiation\n * at address 0x05 as specified in https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise,\n * the underlying function will succeed given the lack of a revert, but the result may be incorrectly\n * interpreted as 0.\n */\n function modExp(uint256 b, uint256 e, uint256 m) internal view returns (uint256) {\n (bool success, uint256 result) = tryModExp(b, e, m);\n if (!success) {\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n return result;\n }\n\n /**\n * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m).\n * It includes a success flag indicating if the operation succeeded. Operation will be marked as failed if trying\n * to operate modulo 0 or if the underlying precompile reverted.\n *\n * IMPORTANT: The result is only valid if the success flag is true. When using this function, make sure the chain\n * you're using it on supports the precompiled contract for modular exponentiation at address 0x05 as specified in\n * https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise, the underlying function will succeed given the lack\n * of a revert, but the result may be incorrectly interpreted as 0.\n */\n function tryModExp(uint256 b, uint256 e, uint256 m) internal view returns (bool success, uint256 result) {\n if (m == 0) return (false, 0);\n assembly (\"memory-safe\") {\n let ptr := mload(0x40)\n // | Offset | Content | Content (Hex) |\n // |-----------|------------|--------------------------------------------------------------------|\n // | 0x00:0x1f | size of b | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x20:0x3f | size of e | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x40:0x5f | size of m | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x60:0x7f | value of b | 0x<.............................................................b> |\n // | 0x80:0x9f | value of e | 0x<.............................................................e> |\n // | 0xa0:0xbf | value of m | 0x<.............................................................m> |\n mstore(ptr, 0x20)\n mstore(add(ptr, 0x20), 0x20)\n mstore(add(ptr, 0x40), 0x20)\n mstore(add(ptr, 0x60), b)\n mstore(add(ptr, 0x80), e)\n mstore(add(ptr, 0xa0), m)\n\n // Given the result < m, it's guaranteed to fit in 32 bytes,\n // so we can use the memory scratch space located at offset 0.\n success := staticcall(gas(), 0x05, ptr, 0xc0, 0x00, 0x20)\n result := mload(0x00)\n }\n }\n\n /**\n * @dev Variant of {modExp} that supports inputs of arbitrary length.\n */\n function modExp(bytes memory b, bytes memory e, bytes memory m) internal view returns (bytes memory) {\n (bool success, bytes memory result) = tryModExp(b, e, m);\n if (!success) {\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n return result;\n }\n\n /**\n * @dev Variant of {tryModExp} that supports inputs of arbitrary length.\n */\n function tryModExp(\n bytes memory b,\n bytes memory e,\n bytes memory m\n ) internal view returns (bool success, bytes memory result) {\n if (_zeroBytes(m)) return (false, new bytes(0));\n\n uint256 mLen = m.length;\n\n // Encode call args in result and move the free memory pointer\n result = abi.encodePacked(b.length, e.length, mLen, b, e, m);\n\n assembly (\"memory-safe\") {\n let dataPtr := add(result, 0x20)\n // Write result on top of args to avoid allocating extra memory.\n success := staticcall(gas(), 0x05, dataPtr, mload(result), dataPtr, mLen)\n // Overwrite the length.\n // result.length > returndatasize() is guaranteed because returndatasize() == m.length\n mstore(result, mLen)\n // Set the memory pointer after the returned data.\n mstore(0x40, add(dataPtr, mLen))\n }\n }\n\n /**\n * @dev Returns whether the provided byte array is zero.\n */\n function _zeroBytes(bytes memory byteArray) private pure returns (bool) {\n for (uint256 i = 0; i < byteArray.length; ++i) {\n if (byteArray[i] != 0) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded\n * towards zero.\n *\n * This method is based on Newton's method for computing square roots; the algorithm is restricted to only\n * using integer operations.\n */\n function sqrt(uint256 a) internal pure returns (uint256) {\n unchecked {\n // Take care of easy edge cases when a == 0 or a == 1\n if (a <= 1) {\n return a;\n }\n\n // In this function, we use Newton's method to get a root of `f(x) := x\u00b2 - a`. It involves building a\n // sequence x_n that converges toward sqrt(a). For each iteration x_n, we also define the error between\n // the current value as `\u03b5_n = | x_n - sqrt(a) |`.\n //\n // For our first estimation, we consider `e` the smallest power of 2 which is bigger than the square root\n // of the target. (i.e. `2**(e-1) \u2264 sqrt(a) < 2**e`). We know that `e \u2264 128` because `(2\u00b9\u00b2\u2078)\u00b2 = 2\u00b2\u2075\u2076` is\n // bigger than any uint256.\n //\n // By noticing that\n // `2**(e-1) \u2264 sqrt(a) < 2**e \u2192 (2**(e-1))\u00b2 \u2264 a < (2**e)\u00b2 \u2192 2**(2*e-2) \u2264 a < 2**(2*e)`\n // we can deduce that `e - 1` is `log2(a) / 2`. We can thus compute `x_n = 2**(e-1)` using a method similar\n // to the msb function.\n uint256 aa = a;\n uint256 xn = 1;\n\n if (aa >= (1 << 128)) {\n aa >>= 128;\n xn <<= 64;\n }\n if (aa >= (1 << 64)) {\n aa >>= 64;\n xn <<= 32;\n }\n if (aa >= (1 << 32)) {\n aa >>= 32;\n xn <<= 16;\n }\n if (aa >= (1 << 16)) {\n aa >>= 16;\n xn <<= 8;\n }\n if (aa >= (1 << 8)) {\n aa >>= 8;\n xn <<= 4;\n }\n if (aa >= (1 << 4)) {\n aa >>= 4;\n xn <<= 2;\n }\n if (aa >= (1 << 2)) {\n xn <<= 1;\n }\n\n // We now have x_n such that `x_n = 2**(e-1) \u2264 sqrt(a) < 2**e = 2 * x_n`. This implies \u03b5_n \u2264 2**(e-1).\n //\n // We can refine our estimation by noticing that the middle of that interval minimizes the error.\n // If we move x_n to equal 2**(e-1) + 2**(e-2), then we reduce the error to \u03b5_n \u2264 2**(e-2).\n // This is going to be our x_0 (and \u03b5_0)\n xn = (3 * xn) >> 1; // \u03b5_0 := | x_0 - sqrt(a) | \u2264 2**(e-2)\n\n // From here, Newton's method give us:\n // x_{n+1} = (x_n + a / x_n) / 2\n //\n // One should note that:\n // x_{n+1}\u00b2 - a = ((x_n + a / x_n) / 2)\u00b2 - a\n // = ((x_n\u00b2 + a) / (2 * x_n))\u00b2 - a\n // = (x_n\u2074 + 2 * a * x_n\u00b2 + a\u00b2) / (4 * x_n\u00b2) - a\n // = (x_n\u2074 + 2 * a * x_n\u00b2 + a\u00b2 - 4 * a * x_n\u00b2) / (4 * x_n\u00b2)\n // = (x_n\u2074 - 2 * a * x_n\u00b2 + a\u00b2) / (4 * x_n\u00b2)\n // = (x_n\u00b2 - a)\u00b2 / (2 * x_n)\u00b2\n // = ((x_n\u00b2 - a) / (2 * x_n))\u00b2\n // \u2265 0\n // Which proves that for all n \u2265 1, sqrt(a) \u2264 x_n\n //\n // This gives us the proof of quadratic convergence of the sequence:\n // \u03b5_{n+1} = | x_{n+1} - sqrt(a) |\n // = | (x_n + a / x_n) / 2 - sqrt(a) |\n // = | (x_n\u00b2 + a - 2*x_n*sqrt(a)) / (2 * x_n) |\n // = | (x_n - sqrt(a))\u00b2 / (2 * x_n) |\n // = | \u03b5_n\u00b2 / (2 * x_n) |\n // = \u03b5_n\u00b2 / | (2 * x_n) |\n //\n // For the first iteration, we have a special case where x_0 is known:\n // \u03b5_1 = \u03b5_0\u00b2 / | (2 * x_0) |\n // \u2264 (2**(e-2))\u00b2 / (2 * (2**(e-1) + 2**(e-2)))\n // \u2264 2**(2*e-4) / (3 * 2**(e-1))\n // \u2264 2**(e-3) / 3\n // \u2264 2**(e-3-log2(3))\n // \u2264 2**(e-4.5)\n //\n // For the following iterations, we use the fact that, 2**(e-1) \u2264 sqrt(a) \u2264 x_n:\n // \u03b5_{n+1} = \u03b5_n\u00b2 / | (2 * x_n) |\n // \u2264 (2**(e-k))\u00b2 / (2 * 2**(e-1))\n // \u2264 2**(2*e-2*k) / 2**e\n // \u2264 2**(e-2*k)\n xn = (xn + a / xn) >> 1; // \u03b5_1 := | x_1 - sqrt(a) | \u2264 2**(e-4.5) -- special case, see above\n xn = (xn + a / xn) >> 1; // \u03b5_2 := | x_2 - sqrt(a) | \u2264 2**(e-9) -- general case with k = 4.5\n xn = (xn + a / xn) >> 1; // \u03b5_3 := | x_3 - sqrt(a) | \u2264 2**(e-18) -- general case with k = 9\n xn = (xn + a / xn) >> 1; // \u03b5_4 := | x_4 - sqrt(a) | \u2264 2**(e-36) -- general case with k = 18\n xn = (xn + a / xn) >> 1; // \u03b5_5 := | x_5 - sqrt(a) | \u2264 2**(e-72) -- general case with k = 36\n xn = (xn + a / xn) >> 1; // \u03b5_6 := | x_6 - sqrt(a) | \u2264 2**(e-144) -- general case with k = 72\n\n // Because e \u2264 128 (as discussed during the first estimation phase), we know have reached a precision\n // \u03b5_6 \u2264 2**(e-144) < 1. Given we're operating on integers, then we can ensure that xn is now either\n // sqrt(a) or sqrt(a) + 1.\n return xn - SafeCast.toUint(xn > a / xn);\n }\n }\n\n /**\n * @dev Calculates sqrt(a), following the selected rounding direction.\n */\n function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = sqrt(a);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && result * result < a);\n }\n }\n\n /**\n * @dev Return the log in base 2 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n */\n function log2(uint256 x) internal pure returns (uint256 r) {\n // If value has upper 128 bits set, log2 result is at least 128\n r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7;\n // If upper 64 bits of 128-bit half set, add 64 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6;\n // If upper 32 bits of 64-bit half set, add 32 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5;\n // If upper 16 bits of 32-bit half set, add 16 to result\n r |= SafeCast.toUint((x >> r) > 0xffff) << 4;\n // If upper 8 bits of 16-bit half set, add 8 to result\n r |= SafeCast.toUint((x >> r) > 0xff) << 3;\n // If upper 4 bits of 8-bit half set, add 4 to result\n r |= SafeCast.toUint((x >> r) > 0xf) << 2;\n\n // Shifts value right by the current result and use it as an index into this lookup table:\n //\n // | x (4 bits) | index | table[index] = MSB position |\n // |------------|---------|-----------------------------|\n // | 0000 | 0 | table[0] = 0 |\n // | 0001 | 1 | table[1] = 0 |\n // | 0010 | 2 | table[2] = 1 |\n // | 0011 | 3 | table[3] = 1 |\n // | 0100 | 4 | table[4] = 2 |\n // | 0101 | 5 | table[5] = 2 |\n // | 0110 | 6 | table[6] = 2 |\n // | 0111 | 7 | table[7] = 2 |\n // | 1000 | 8 | table[8] = 3 |\n // | 1001 | 9 | table[9] = 3 |\n // | 1010 | 10 | table[10] = 3 |\n // | 1011 | 11 | table[11] = 3 |\n // | 1100 | 12 | table[12] = 3 |\n // | 1101 | 13 | table[13] = 3 |\n // | 1110 | 14 | table[14] = 3 |\n // | 1111 | 15 | table[15] = 3 |\n //\n // The lookup table is represented as a 32-byte value with the MSB positions for 0-15 in the last 16 bytes.\n assembly (\"memory-safe\") {\n r := or(r, byte(shr(r, x), 0x0000010102020202030303030303030300000000000000000000000000000000))\n }\n }\n\n /**\n * @dev Return the log in base 2, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log2(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << result < value);\n }\n }\n\n /**\n * @dev Return the log in base 10 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n */\n function log10(uint256 value) internal pure returns (uint256) {\n uint256 result = 0;\n unchecked {\n if (value >= 10 ** 64) {\n value /= 10 ** 64;\n result += 64;\n }\n if (value >= 10 ** 32) {\n value /= 10 ** 32;\n result += 32;\n }\n if (value >= 10 ** 16) {\n value /= 10 ** 16;\n result += 16;\n }\n if (value >= 10 ** 8) {\n value /= 10 ** 8;\n result += 8;\n }\n if (value >= 10 ** 4) {\n value /= 10 ** 4;\n result += 4;\n }\n if (value >= 10 ** 2) {\n value /= 10 ** 2;\n result += 2;\n }\n if (value >= 10 ** 1) {\n result += 1;\n }\n }\n return result;\n }\n\n /**\n * @dev Return the log in base 10, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log10(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 10 ** result < value);\n }\n }\n\n /**\n * @dev Return the log in base 256 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n *\n * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.\n */\n function log256(uint256 x) internal pure returns (uint256 r) {\n // If value has upper 128 bits set, log2 result is at least 128\n r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7;\n // If upper 64 bits of 128-bit half set, add 64 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6;\n // If upper 32 bits of 64-bit half set, add 32 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5;\n // If upper 16 bits of 32-bit half set, add 16 to result\n r |= SafeCast.toUint((x >> r) > 0xffff) << 4;\n // Add 1 if upper 8 bits of 16-bit half set, and divide accumulated result by 8\n return (r >> 3) | SafeCast.toUint((x >> r) > 0xff);\n }\n\n /**\n * @dev Return the log in base 256, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log256(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << (result << 3) < value);\n }\n }\n\n /**\n * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.\n */\n function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {\n return uint8(rounding) % 2 == 1;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/SafeCast.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SafeCast.sol)\n// This file was procedurally generated from scripts/generate/templates/SafeCast.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Wrappers over Solidity's uintXX/intXX/bool casting operators with added overflow\n * checks.\n *\n * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can\n * easily result in undesired exploitation or bugs, since developers usually\n * assume that overflows raise errors. `SafeCast` restores this intuition by\n * reverting the transaction when such an operation overflows.\n *\n * Using this library instead of the unchecked operations eliminates an entire\n * class of bugs, so it's recommended to use it always.\n */\nlibrary SafeCast {\n /**\n * @dev Value doesn't fit in an uint of `bits` size.\n */\n error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);\n\n /**\n * @dev An int value doesn't fit in an uint of `bits` size.\n */\n error SafeCastOverflowedIntToUint(int256 value);\n\n /**\n * @dev Value doesn't fit in an int of `bits` size.\n */\n error SafeCastOverflowedIntDowncast(uint8 bits, int256 value);\n\n /**\n * @dev An uint value doesn't fit in an int of `bits` size.\n */\n error SafeCastOverflowedUintToInt(uint256 value);\n\n /**\n * @dev Returns the downcasted uint248 from uint256, reverting on\n * overflow (when the input is greater than largest uint248).\n *\n * Counterpart to Solidity's `uint248` operator.\n *\n * Requirements:\n *\n * - input must fit into 248 bits\n */\n function toUint248(uint256 value) internal pure returns (uint248) {\n if (value > type(uint248).max) {\n revert SafeCastOverflowedUintDowncast(248, value);\n }\n return uint248(value);\n }\n\n /**\n * @dev Returns the downcasted uint240 from uint256, reverting on\n * overflow (when the input is greater than largest uint240).\n *\n * Counterpart to Solidity's `uint240` operator.\n *\n * Requirements:\n *\n * - input must fit into 240 bits\n */\n function toUint240(uint256 value) internal pure returns (uint240) {\n if (value > type(uint240).max) {\n revert SafeCastOverflowedUintDowncast(240, value);\n }\n return uint240(value);\n }\n\n /**\n * @dev Returns the downcasted uint232 from uint256, reverting on\n * overflow (when the input is greater than largest uint232).\n *\n * Counterpart to Solidity's `uint232` operator.\n *\n * Requirements:\n *\n * - input must fit into 232 bits\n */\n function toUint232(uint256 value) internal pure returns (uint232) {\n if (value > type(uint232).max) {\n revert SafeCastOverflowedUintDowncast(232, value);\n }\n return uint232(value);\n }\n\n /**\n * @dev Returns the downcasted uint224 from uint256, reverting on\n * overflow (when the input is greater than largest uint224).\n *\n * Counterpart to Solidity's `uint224` operator.\n *\n * Requirements:\n *\n * - input must fit into 224 bits\n */\n function toUint224(uint256 value) internal pure returns (uint224) {\n if (value > type(uint224).max) {\n revert SafeCastOverflowedUintDowncast(224, value);\n }\n return uint224(value);\n }\n\n /**\n * @dev Returns the downcasted uint216 from uint256, reverting on\n * overflow (when the input is greater than largest uint216).\n *\n * Counterpart to Solidity's `uint216` operator.\n *\n * Requirements:\n *\n * - input must fit into 216 bits\n */\n function toUint216(uint256 value) internal pure returns (uint216) {\n if (value > type(uint216).max) {\n revert SafeCastOverflowedUintDowncast(216, value);\n }\n return uint216(value);\n }\n\n /**\n * @dev Returns the downcasted uint208 from uint256, reverting on\n * overflow (when the input is greater than largest uint208).\n *\n * Counterpart to Solidity's `uint208` operator.\n *\n * Requirements:\n *\n * - input must fit into 208 bits\n */\n function toUint208(uint256 value) internal pure returns (uint208) {\n if (value > type(uint208).max) {\n revert SafeCastOverflowedUintDowncast(208, value);\n }\n return uint208(value);\n }\n\n /**\n * @dev Returns the downcasted uint200 from uint256, reverting on\n * overflow (when the input is greater than largest uint200).\n *\n * Counterpart to Solidity's `uint200` operator.\n *\n * Requirements:\n *\n * - input must fit into 200 bits\n */\n function toUint200(uint256 value) internal pure returns (uint200) {\n if (value > type(uint200).max) {\n revert SafeCastOverflowedUintDowncast(200, value);\n }\n return uint200(value);\n }\n\n /**\n * @dev Returns the downcasted uint192 from uint256, reverting on\n * overflow (when the input is greater than largest uint192).\n *\n * Counterpart to Solidity's `uint192` operator.\n *\n * Requirements:\n *\n * - input must fit into 192 bits\n */\n function toUint192(uint256 value) internal pure returns (uint192) {\n if (value > type(uint192).max) {\n revert SafeCastOverflowedUintDowncast(192, value);\n }\n return uint192(value);\n }\n\n /**\n * @dev Returns the downcasted uint184 from uint256, reverting on\n * overflow (when the input is greater than largest uint184).\n *\n * Counterpart to Solidity's `uint184` operator.\n *\n * Requirements:\n *\n * - input must fit into 184 bits\n */\n function toUint184(uint256 value) internal pure returns (uint184) {\n if (value > type(uint184).max) {\n revert SafeCastOverflowedUintDowncast(184, value);\n }\n return uint184(value);\n }\n\n /**\n * @dev Returns the downcasted uint176 from uint256, reverting on\n * overflow (when the input is greater than largest uint176).\n *\n * Counterpart to Solidity's `uint176` operator.\n *\n * Requirements:\n *\n * - input must fit into 176 bits\n */\n function toUint176(uint256 value) internal pure returns (uint176) {\n if (value > type(uint176).max) {\n revert SafeCastOverflowedUintDowncast(176, value);\n }\n return uint176(value);\n }\n\n /**\n * @dev Returns the downcasted uint168 from uint256, reverting on\n * overflow (when the input is greater than largest uint168).\n *\n * Counterpart to Solidity's `uint168` operator.\n *\n * Requirements:\n *\n * - input must fit into 168 bits\n */\n function toUint168(uint256 value) internal pure returns (uint168) {\n if (value > type(uint168).max) {\n revert SafeCastOverflowedUintDowncast(168, value);\n }\n return uint168(value);\n }\n\n /**\n * @dev Returns the downcasted uint160 from uint256, reverting on\n * overflow (when the input is greater than largest uint160).\n *\n * Counterpart to Solidity's `uint160` operator.\n *\n * Requirements:\n *\n * - input must fit into 160 bits\n */\n function toUint160(uint256 value) internal pure returns (uint160) {\n if (value > type(uint160).max) {\n revert SafeCastOverflowedUintDowncast(160, value);\n }\n return uint160(value);\n }\n\n /**\n * @dev Returns the downcasted uint152 from uint256, reverting on\n * overflow (when the input is greater than largest uint152).\n *\n * Counterpart to Solidity's `uint152` operator.\n *\n * Requirements:\n *\n * - input must fit into 152 bits\n */\n function toUint152(uint256 value) internal pure returns (uint152) {\n if (value > type(uint152).max) {\n revert SafeCastOverflowedUintDowncast(152, value);\n }\n return uint152(value);\n }\n\n /**\n * @dev Returns the downcasted uint144 from uint256, reverting on\n * overflow (when the input is greater than largest uint144).\n *\n * Counterpart to Solidity's `uint144` operator.\n *\n * Requirements:\n *\n * - input must fit into 144 bits\n */\n function toUint144(uint256 value) internal pure returns (uint144) {\n if (value > type(uint144).max) {\n revert SafeCastOverflowedUintDowncast(144, value);\n }\n return uint144(value);\n }\n\n /**\n * @dev Returns the downcasted uint136 from uint256, reverting on\n * overflow (when the input is greater than largest uint136).\n *\n * Counterpart to Solidity's `uint136` operator.\n *\n * Requirements:\n *\n * - input must fit into 136 bits\n */\n function toUint136(uint256 value) internal pure returns (uint136) {\n if (value > type(uint136).max) {\n revert SafeCastOverflowedUintDowncast(136, value);\n }\n return uint136(value);\n }\n\n /**\n * @dev Returns the downcasted uint128 from uint256, reverting on\n * overflow (when the input is greater than largest uint128).\n *\n * Counterpart to Solidity's `uint128` operator.\n *\n * Requirements:\n *\n * - input must fit into 128 bits\n */\n function toUint128(uint256 value) internal pure returns (uint128) {\n if (value > type(uint128).max) {\n revert SafeCastOverflowedUintDowncast(128, value);\n }\n return uint128(value);\n }\n\n /**\n * @dev Returns the downcasted uint120 from uint256, reverting on\n * overflow (when the input is greater than largest uint120).\n *\n * Counterpart to Solidity's `uint120` operator.\n *\n * Requirements:\n *\n * - input must fit into 120 bits\n */\n function toUint120(uint256 value) internal pure returns (uint120) {\n if (value > type(uint120).max) {\n revert SafeCastOverflowedUintDowncast(120, value);\n }\n return uint120(value);\n }\n\n /**\n * @dev Returns the downcasted uint112 from uint256, reverting on\n * overflow (when the input is greater than largest uint112).\n *\n * Counterpart to Solidity's `uint112` operator.\n *\n * Requirements:\n *\n * - input must fit into 112 bits\n */\n function toUint112(uint256 value) internal pure returns (uint112) {\n if (value > type(uint112).max) {\n revert SafeCastOverflowedUintDowncast(112, value);\n }\n return uint112(value);\n }\n\n /**\n * @dev Returns the downcasted uint104 from uint256, reverting on\n * overflow (when the input is greater than largest uint104).\n *\n * Counterpart to Solidity's `uint104` operator.\n *\n * Requirements:\n *\n * - input must fit into 104 bits\n */\n function toUint104(uint256 value) internal pure returns (uint104) {\n if (value > type(uint104).max) {\n revert SafeCastOverflowedUintDowncast(104, value);\n }\n return uint104(value);\n }\n\n /**\n * @dev Returns the downcasted uint96 from uint256, reverting on\n * overflow (when the input is greater than largest uint96).\n *\n * Counterpart to Solidity's `uint96` operator.\n *\n * Requirements:\n *\n * - input must fit into 96 bits\n */\n function toUint96(uint256 value) internal pure returns (uint96) {\n if (value > type(uint96).max) {\n revert SafeCastOverflowedUintDowncast(96, value);\n }\n return uint96(value);\n }\n\n /**\n * @dev Returns the downcasted uint88 from uint256, reverting on\n * overflow (when the input is greater than largest uint88).\n *\n * Counterpart to Solidity's `uint88` operator.\n *\n * Requirements:\n *\n * - input must fit into 88 bits\n */\n function toUint88(uint256 value) internal pure returns (uint88) {\n if (value > type(uint88).max) {\n revert SafeCastOverflowedUintDowncast(88, value);\n }\n return uint88(value);\n }\n\n /**\n * @dev Returns the downcasted uint80 from uint256, reverting on\n * overflow (when the input is greater than largest uint80).\n *\n * Counterpart to Solidity's `uint80` operator.\n *\n * Requirements:\n *\n * - input must fit into 80 bits\n */\n function toUint80(uint256 value) internal pure returns (uint80) {\n if (value > type(uint80).max) {\n revert SafeCastOverflowedUintDowncast(80, value);\n }\n return uint80(value);\n }\n\n /**\n * @dev Returns the downcasted uint72 from uint256, reverting on\n * overflow (when the input is greater than largest uint72).\n *\n * Counterpart to Solidity's `uint72` operator.\n *\n * Requirements:\n *\n * - input must fit into 72 bits\n */\n function toUint72(uint256 value) internal pure returns (uint72) {\n if (value > type(uint72).max) {\n revert SafeCastOverflowedUintDowncast(72, value);\n }\n return uint72(value);\n }\n\n /**\n * @dev Returns the downcasted uint64 from uint256, reverting on\n * overflow (when the input is greater than largest uint64).\n *\n * Counterpart to Solidity's `uint64` operator.\n *\n * Requirements:\n *\n * - input must fit into 64 bits\n */\n function toUint64(uint256 value) internal pure returns (uint64) {\n if (value > type(uint64).max) {\n revert SafeCastOverflowedUintDowncast(64, value);\n }\n return uint64(value);\n }\n\n /**\n * @dev Returns the downcasted uint56 from uint256, reverting on\n * overflow (when the input is greater than largest uint56).\n *\n * Counterpart to Solidity's `uint56` operator.\n *\n * Requirements:\n *\n * - input must fit into 56 bits\n */\n function toUint56(uint256 value) internal pure returns (uint56) {\n if (value > type(uint56).max) {\n revert SafeCastOverflowedUintDowncast(56, value);\n }\n return uint56(value);\n }\n\n /**\n * @dev Returns the downcasted uint48 from uint256, reverting on\n * overflow (when the input is greater than largest uint48).\n *\n * Counterpart to Solidity's `uint48` operator.\n *\n * Requirements:\n *\n * - input must fit into 48 bits\n */\n function toUint48(uint256 value) internal pure returns (uint48) {\n if (value > type(uint48).max) {\n revert SafeCastOverflowedUintDowncast(48, value);\n }\n return uint48(value);\n }\n\n /**\n * @dev Returns the downcasted uint40 from uint256, reverting on\n * overflow (when the input is greater than largest uint40).\n *\n * Counterpart to Solidity's `uint40` operator.\n *\n * Requirements:\n *\n * - input must fit into 40 bits\n */\n function toUint40(uint256 value) internal pure returns (uint40) {\n if (value > type(uint40).max) {\n revert SafeCastOverflowedUintDowncast(40, value);\n }\n return uint40(value);\n }\n\n /**\n * @dev Returns the downcasted uint32 from uint256, reverting on\n * overflow (when the input is greater than largest uint32).\n *\n * Counterpart to Solidity's `uint32` operator.\n *\n * Requirements:\n *\n * - input must fit into 32 bits\n */\n function toUint32(uint256 value) internal pure returns (uint32) {\n if (value > type(uint32).max) {\n revert SafeCastOverflowedUintDowncast(32, value);\n }\n return uint32(value);\n }\n\n /**\n * @dev Returns the downcasted uint24 from uint256, reverting on\n * overflow (when the input is greater than largest uint24).\n *\n * Counterpart to Solidity's `uint24` operator.\n *\n * Requirements:\n *\n * - input must fit into 24 bits\n */\n function toUint24(uint256 value) internal pure returns (uint24) {\n if (value > type(uint24).max) {\n revert SafeCastOverflowedUintDowncast(24, value);\n }\n return uint24(value);\n }\n\n /**\n * @dev Returns the downcasted uint16 from uint256, reverting on\n * overflow (when the input is greater than largest uint16).\n *\n * Counterpart to Solidity's `uint16` operator.\n *\n * Requirements:\n *\n * - input must fit into 16 bits\n */\n function toUint16(uint256 value) internal pure returns (uint16) {\n if (value > type(uint16).max) {\n revert SafeCastOverflowedUintDowncast(16, value);\n }\n return uint16(value);\n }\n\n /**\n * @dev Returns the downcasted uint8 from uint256, reverting on\n * overflow (when the input is greater than largest uint8).\n *\n * Counterpart to Solidity's `uint8` operator.\n *\n * Requirements:\n *\n * - input must fit into 8 bits\n */\n function toUint8(uint256 value) internal pure returns (uint8) {\n if (value > type(uint8).max) {\n revert SafeCastOverflowedUintDowncast(8, value);\n }\n return uint8(value);\n }\n\n /**\n * @dev Converts a signed int256 into an unsigned uint256.\n *\n * Requirements:\n *\n * - input must be greater than or equal to 0.\n */\n function toUint256(int256 value) internal pure returns (uint256) {\n if (value < 0) {\n revert SafeCastOverflowedIntToUint(value);\n }\n return uint256(value);\n }\n\n /**\n * @dev Returns the downcasted int248 from int256, reverting on\n * overflow (when the input is less than smallest int248 or\n * greater than largest int248).\n *\n * Counterpart to Solidity's `int248` operator.\n *\n * Requirements:\n *\n * - input must fit into 248 bits\n */\n function toInt248(int256 value) internal pure returns (int248 downcasted) {\n downcasted = int248(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(248, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int240 from int256, reverting on\n * overflow (when the input is less than smallest int240 or\n * greater than largest int240).\n *\n * Counterpart to Solidity's `int240` operator.\n *\n * Requirements:\n *\n * - input must fit into 240 bits\n */\n function toInt240(int256 value) internal pure returns (int240 downcasted) {\n downcasted = int240(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(240, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int232 from int256, reverting on\n * overflow (when the input is less than smallest int232 or\n * greater than largest int232).\n *\n * Counterpart to Solidity's `int232` operator.\n *\n * Requirements:\n *\n * - input must fit into 232 bits\n */\n function toInt232(int256 value) internal pure returns (int232 downcasted) {\n downcasted = int232(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(232, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int224 from int256, reverting on\n * overflow (when the input is less than smallest int224 or\n * greater than largest int224).\n *\n * Counterpart to Solidity's `int224` operator.\n *\n * Requirements:\n *\n * - input must fit into 224 bits\n */\n function toInt224(int256 value) internal pure returns (int224 downcasted) {\n downcasted = int224(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(224, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int216 from int256, reverting on\n * overflow (when the input is less than smallest int216 or\n * greater than largest int216).\n *\n * Counterpart to Solidity's `int216` operator.\n *\n * Requirements:\n *\n * - input must fit into 216 bits\n */\n function toInt216(int256 value) internal pure returns (int216 downcasted) {\n downcasted = int216(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(216, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int208 from int256, reverting on\n * overflow (when the input is less than smallest int208 or\n * greater than largest int208).\n *\n * Counterpart to Solidity's `int208` operator.\n *\n * Requirements:\n *\n * - input must fit into 208 bits\n */\n function toInt208(int256 value) internal pure returns (int208 downcasted) {\n downcasted = int208(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(208, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int200 from int256, reverting on\n * overflow (when the input is less than smallest int200 or\n * greater than largest int200).\n *\n * Counterpart to Solidity's `int200` operator.\n *\n * Requirements:\n *\n * - input must fit into 200 bits\n */\n function toInt200(int256 value) internal pure returns (int200 downcasted) {\n downcasted = int200(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(200, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int192 from int256, reverting on\n * overflow (when the input is less than smallest int192 or\n * greater than largest int192).\n *\n * Counterpart to Solidity's `int192` operator.\n *\n * Requirements:\n *\n * - input must fit into 192 bits\n */\n function toInt192(int256 value) internal pure returns (int192 downcasted) {\n downcasted = int192(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(192, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int184 from int256, reverting on\n * overflow (when the input is less than smallest int184 or\n * greater than largest int184).\n *\n * Counterpart to Solidity's `int184` operator.\n *\n * Requirements:\n *\n * - input must fit into 184 bits\n */\n function toInt184(int256 value) internal pure returns (int184 downcasted) {\n downcasted = int184(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(184, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int176 from int256, reverting on\n * overflow (when the input is less than smallest int176 or\n * greater than largest int176).\n *\n * Counterpart to Solidity's `int176` operator.\n *\n * Requirements:\n *\n * - input must fit into 176 bits\n */\n function toInt176(int256 value) internal pure returns (int176 downcasted) {\n downcasted = int176(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(176, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int168 from int256, reverting on\n * overflow (when the input is less than smallest int168 or\n * greater than largest int168).\n *\n * Counterpart to Solidity's `int168` operator.\n *\n * Requirements:\n *\n * - input must fit into 168 bits\n */\n function toInt168(int256 value) internal pure returns (int168 downcasted) {\n downcasted = int168(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(168, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int160 from int256, reverting on\n * overflow (when the input is less than smallest int160 or\n * greater than largest int160).\n *\n * Counterpart to Solidity's `int160` operator.\n *\n * Requirements:\n *\n * - input must fit into 160 bits\n */\n function toInt160(int256 value) internal pure returns (int160 downcasted) {\n downcasted = int160(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(160, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int152 from int256, reverting on\n * overflow (when the input is less than smallest int152 or\n * greater than largest int152).\n *\n * Counterpart to Solidity's `int152` operator.\n *\n * Requirements:\n *\n * - input must fit into 152 bits\n */\n function toInt152(int256 value) internal pure returns (int152 downcasted) {\n downcasted = int152(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(152, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int144 from int256, reverting on\n * overflow (when the input is less than smallest int144 or\n * greater than largest int144).\n *\n * Counterpart to Solidity's `int144` operator.\n *\n * Requirements:\n *\n * - input must fit into 144 bits\n */\n function toInt144(int256 value) internal pure returns (int144 downcasted) {\n downcasted = int144(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(144, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int136 from int256, reverting on\n * overflow (when the input is less than smallest int136 or\n * greater than largest int136).\n *\n * Counterpart to Solidity's `int136` operator.\n *\n * Requirements:\n *\n * - input must fit into 136 bits\n */\n function toInt136(int256 value) internal pure returns (int136 downcasted) {\n downcasted = int136(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(136, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int128 from int256, reverting on\n * overflow (when the input is less than smallest int128 or\n * greater than largest int128).\n *\n * Counterpart to Solidity's `int128` operator.\n *\n * Requirements:\n *\n * - input must fit into 128 bits\n */\n function toInt128(int256 value) internal pure returns (int128 downcasted) {\n downcasted = int128(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(128, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int120 from int256, reverting on\n * overflow (when the input is less than smallest int120 or\n * greater than largest int120).\n *\n * Counterpart to Solidity's `int120` operator.\n *\n * Requirements:\n *\n * - input must fit into 120 bits\n */\n function toInt120(int256 value) internal pure returns (int120 downcasted) {\n downcasted = int120(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(120, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int112 from int256, reverting on\n * overflow (when the input is less than smallest int112 or\n * greater than largest int112).\n *\n * Counterpart to Solidity's `int112` operator.\n *\n * Requirements:\n *\n * - input must fit into 112 bits\n */\n function toInt112(int256 value) internal pure returns (int112 downcasted) {\n downcasted = int112(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(112, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int104 from int256, reverting on\n * overflow (when the input is less than smallest int104 or\n * greater than largest int104).\n *\n * Counterpart to Solidity's `int104` operator.\n *\n * Requirements:\n *\n * - input must fit into 104 bits\n */\n function toInt104(int256 value) internal pure returns (int104 downcasted) {\n downcasted = int104(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(104, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int96 from int256, reverting on\n * overflow (when the input is less than smallest int96 or\n * greater than largest int96).\n *\n * Counterpart to Solidity's `int96` operator.\n *\n * Requirements:\n *\n * - input must fit into 96 bits\n */\n function toInt96(int256 value) internal pure returns (int96 downcasted) {\n downcasted = int96(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(96, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int88 from int256, reverting on\n * overflow (when the input is less than smallest int88 or\n * greater than largest int88).\n *\n * Counterpart to Solidity's `int88` operator.\n *\n * Requirements:\n *\n * - input must fit into 88 bits\n */\n function toInt88(int256 value) internal pure returns (int88 downcasted) {\n downcasted = int88(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(88, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int80 from int256, reverting on\n * overflow (when the input is less than smallest int80 or\n * greater than largest int80).\n *\n * Counterpart to Solidity's `int80` operator.\n *\n * Requirements:\n *\n * - input must fit into 80 bits\n */\n function toInt80(int256 value) internal pure returns (int80 downcasted) {\n downcasted = int80(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(80, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int72 from int256, reverting on\n * overflow (when the input is less than smallest int72 or\n * greater than largest int72).\n *\n * Counterpart to Solidity's `int72` operator.\n *\n * Requirements:\n *\n * - input must fit into 72 bits\n */\n function toInt72(int256 value) internal pure returns (int72 downcasted) {\n downcasted = int72(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(72, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int64 from int256, reverting on\n * overflow (when the input is less than smallest int64 or\n * greater than largest int64).\n *\n * Counterpart to Solidity's `int64` operator.\n *\n * Requirements:\n *\n * - input must fit into 64 bits\n */\n function toInt64(int256 value) internal pure returns (int64 downcasted) {\n downcasted = int64(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(64, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int56 from int256, reverting on\n * overflow (when the input is less than smallest int56 or\n * greater than largest int56).\n *\n * Counterpart to Solidity's `int56` operator.\n *\n * Requirements:\n *\n * - input must fit into 56 bits\n */\n function toInt56(int256 value) internal pure returns (int56 downcasted) {\n downcasted = int56(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(56, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int48 from int256, reverting on\n * overflow (when the input is less than smallest int48 or\n * greater than largest int48).\n *\n * Counterpart to Solidity's `int48` operator.\n *\n * Requirements:\n *\n * - input must fit into 48 bits\n */\n function toInt48(int256 value) internal pure returns (int48 downcasted) {\n downcasted = int48(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(48, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int40 from int256, reverting on\n * overflow (when the input is less than smallest int40 or\n * greater than largest int40).\n *\n * Counterpart to Solidity's `int40` operator.\n *\n * Requirements:\n *\n * - input must fit into 40 bits\n */\n function toInt40(int256 value) internal pure returns (int40 downcasted) {\n downcasted = int40(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(40, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int32 from int256, reverting on\n * overflow (when the input is less than smallest int32 or\n * greater than largest int32).\n *\n * Counterpart to Solidity's `int32` operator.\n *\n * Requirements:\n *\n * - input must fit into 32 bits\n */\n function toInt32(int256 value) internal pure returns (int32 downcasted) {\n downcasted = int32(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(32, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int24 from int256, reverting on\n * overflow (when the input is less than smallest int24 or\n * greater than largest int24).\n *\n * Counterpart to Solidity's `int24` operator.\n *\n * Requirements:\n *\n * - input must fit into 24 bits\n */\n function toInt24(int256 value) internal pure returns (int24 downcasted) {\n downcasted = int24(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(24, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int16 from int256, reverting on\n * overflow (when the input is less than smallest int16 or\n * greater than largest int16).\n *\n * Counterpart to Solidity's `int16` operator.\n *\n * Requirements:\n *\n * - input must fit into 16 bits\n */\n function toInt16(int256 value) internal pure returns (int16 downcasted) {\n downcasted = int16(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(16, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int8 from int256, reverting on\n * overflow (when the input is less than smallest int8 or\n * greater than largest int8).\n *\n * Counterpart to Solidity's `int8` operator.\n *\n * Requirements:\n *\n * - input must fit into 8 bits\n */\n function toInt8(int256 value) internal pure returns (int8 downcasted) {\n downcasted = int8(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(8, value);\n }\n }\n\n /**\n * @dev Converts an unsigned uint256 into a signed int256.\n *\n * Requirements:\n *\n * - input must be less than or equal to maxInt256.\n */\n function toInt256(uint256 value) internal pure returns (int256) {\n // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive\n if (value > uint256(type(int256).max)) {\n revert SafeCastOverflowedUintToInt(value);\n }\n return int256(value);\n }\n\n /**\n * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump.\n */\n function toUint(bool b) internal pure returns (uint256 u) {\n assembly (\"memory-safe\") {\n u := iszero(iszero(b))\n }\n }\n}\n" + } + }, + "settings": { + "evmVersion": "paris", + "libraries": {}, + "metadata": { "appendCBOR": true, "bytecodeHash": "none", "useLiteralContent": false }, + "optimizer": { "enabled": true, "runs": 80000 }, + "outputSelection": { + "contracts/interfaces/IBurnMintERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/IGetCCIPAdmin.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/tokens/BaseERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/tokens/CrossChainToken.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/access/AccessControl.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/access/IAccessControl.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/access/extensions/AccessControlDefaultAdminRules.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/access/extensions/IAccessControlDefaultAdminRules.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC5313.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/draft-IERC6093.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/IERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Metadata.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Context.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Panic.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/introspection/ERC165.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/introspection/IERC165.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/Math.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/SafeCast.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + } + }, + "remappings": [ + "forge-std/=node_modules/@chainlink/contracts/src/v0.8/vendor/forge-std/src/", + "@chainlink/policy-management/=node_modules/@chainlink/ace/packages/policy-management/src/", + "@chainlink/contracts/=node_modules/@chainlink/contracts/", + "@openzeppelin/contracts@4.8.3/=node_modules/@openzeppelin/contracts-4.8.3/", + "@openzeppelin/contracts@5.3.0/=node_modules/@openzeppelin/contracts-5.3.0/" + ], + "viaIR": true + } +} diff --git a/ccip-sdk/src/verify/fixtures/ERC20LockBox.abi.json b/ccip-sdk/src/verify/fixtures/ERC20LockBox.abi.json new file mode 100644 index 00000000..bbbef138 --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/ERC20LockBox.abi.json @@ -0,0 +1,378 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "applyAuthorizedCallerUpdates", + "inputs": [ + { + "name": "authorizedCallerArgs", + "type": "tuple", + "internalType": "struct AuthorizedCallers.AuthorizedCallerArgs", + "components": [ + { + "name": "addedCallers", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "removedCallers", + "type": "address[]", + "internalType": "address[]" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "deposit", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getAllAuthorizedCallers", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getToken", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IERC20" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isTokenSupported", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "typeAndVersion", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "withdraw", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "AuthorizedCallerAdded", + "inputs": [ + { + "name": "caller", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AuthorizedCallerRemoved", + "inputs": [ + { + "name": "caller", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Deposit", + "inputs": [ + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "depositor", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferRequested", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Withdrawal", + "inputs": [ + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "recipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "CannotTransferToSelf", + "inputs": [] + }, + { + "type": "error", + "name": "InsufficientBalance", + "inputs": [ + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "MustBeProposedOwner", + "inputs": [] + }, + { + "type": "error", + "name": "OnlyCallableByOwner", + "inputs": [] + }, + { + "type": "error", + "name": "OwnerCannotBeZero", + "inputs": [] + }, + { + "type": "error", + "name": "RecipientCannotBeZeroAddress", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "TokenAmountCannotBeZero", + "inputs": [] + }, + { + "type": "error", + "name": "UnauthorizedCaller", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "UnsupportedToken", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ZeroAddressNotAllowed", + "inputs": [] + } +] diff --git a/ccip-sdk/src/verify/fixtures/ERC20LockBox.standard-input.json b/ccip-sdk/src/verify/fixtures/ERC20LockBox.standard-input.json new file mode 100644 index 00000000..a7d56fdc --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/ERC20LockBox.standard-input.json @@ -0,0 +1,273 @@ +{ + "language": "Solidity", + "sources": { + "contracts/interfaces/ILockBox.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\ninterface ILockBox {\n /// @notice Deposits the token into the lockbox.\n /// @param token The address of the token to deposit.\n /// @param remoteChainSelector The chain selector of the remote chain.\n /// @param amount The amount of tokens to deposit.\n function deposit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) external;\n\n /// @notice Withdraws tokens to a specific recipient.\n /// @param token The address of the token to withdraw.\n /// @param remoteChainSelector The chain selector of the remote chain.\n /// @param amount The amount of tokens to withdraw. If set to max uint256, withdraws the entire balance.\n /// @param recipient The address of the recipient to receive the withdrawn tokens.\n function withdraw(\n address token,\n uint64 remoteChainSelector,\n uint256 amount,\n address recipient\n ) external;\n\n /// @notice Returns whether the lockbox supports a token.\n /// @param token The address of the token.\n /// @return supported True if the token is supported.\n function isTokenSupported(\n address token\n ) external view returns (bool);\n}\n" + }, + "contracts/pools/ERC20LockBox.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.24;\n\nimport {ILockBox} from \"../interfaces/ILockBox.sol\";\nimport {ITypeAndVersion} from \"@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol\";\n\nimport {AuthorizedCallers} from \"@chainlink/contracts/src/v0.8/shared/access/AuthorizedCallers.sol\";\n\nimport {IERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/utils/SafeERC20.sol\";\n\n/// @title ERC20 Lock Box\n/// @notice Per-token lockbox that holds ERC20 liquidity so pools can be upgraded without migrating funds.\n/// @dev This implementation supports only a single token to be deposited in the lockbox. Only the owner can manage the\n/// allowlist; allowed callers can deposit/withdraw.\ncontract ERC20LockBox is ITypeAndVersion, ILockBox, AuthorizedCallers {\n using SafeERC20 for IERC20;\n\n function typeAndVersion() external pure virtual override returns (string memory) {\n return \"ERC20LockBox 2.0.0\";\n }\n\n error InsufficientBalance(uint256 requested, uint256 available);\n error TokenAmountCannotBeZero();\n error RecipientCannotBeZeroAddress();\n error UnsupportedToken(address token);\n\n event Deposit(address indexed token, address indexed depositor, uint256 amount);\n event Withdrawal(address indexed token, address indexed recipient, uint256 amount);\n\n /// @notice The token supported by this lockbox.\n IERC20 internal immutable i_token;\n\n constructor(\n address token\n ) AuthorizedCallers(new address[](0)) {\n if (token == address(0)) {\n revert ZeroAddressNotAllowed();\n }\n\n i_token = IERC20(token);\n }\n\n /// @notice Deposits tokens into this contract. This eases the process of migrating tokens\n /// from a legacy token pool to a new one, since only the allowedCaller needs to be changed. Without it, the tokens\n /// would need to be manually withdrawn and re-deposited into the new token pool from a legacy pool, which is a\n /// time-consuming and error-prone process.\n /// @inheritdoc ILockBox\n function deposit(\n address token,\n uint64, // remoteChainSelector\n uint256 amount\n ) external {\n _validateDepositWithdraw(token, amount);\n\n IERC20(token).safeTransferFrom(msg.sender, address(this), amount);\n\n emit Deposit(token, msg.sender, amount);\n }\n\n /// @inheritdoc ILockBox\n function withdraw(\n address token,\n uint64, // remoteChainSelector\n uint256 amount,\n address recipient\n ) external {\n _validateDepositWithdraw(token, amount);\n\n if (recipient == address(0)) {\n revert RecipientCannotBeZeroAddress();\n }\n\n uint256 balance = IERC20(token).balanceOf(address(this));\n\n // If amount is max uint256, withdraw the entire balance.\n if (amount == type(uint256).max) {\n amount = balance;\n }\n if (amount > balance) {\n revert InsufficientBalance(amount, balance);\n }\n\n IERC20(token).safeTransfer(recipient, amount);\n\n emit Withdrawal(token, recipient, amount);\n }\n\n /// @notice Validates the deposit and withdraw functions.\n /// @param token The token being deposited/withdrawn.\n /// @param amount The amount of tokens to deposit or withdraw.\n function _validateDepositWithdraw(\n address token,\n uint256 amount\n ) internal view {\n if (amount == 0) {\n revert TokenAmountCannotBeZero();\n }\n if (token != address(i_token)) {\n revert UnsupportedToken(token);\n }\n _validateCaller();\n }\n\n /// @notice Returns the token held by this lockbox.\n /// @return token The IERC20 token address.\n function getToken() external view returns (IERC20) {\n return i_token;\n }\n\n /// @notice Returns whether the lockbox supports a token.\n /// @param token The ERC20 token.\n /// @return supported True if the token is supported.\n function isTokenSupported(\n address token\n ) external view returns (bool) {\n return address(i_token) == token;\n }\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/AuthorizedCallers.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.4;\n\nimport {Ownable2StepMsgSender} from \"./Ownable2StepMsgSender.sol\";\nimport {EnumerableSet} from \"@openzeppelin/contracts@4.8.3/utils/structs/EnumerableSet.sol\";\n\n/// @title The AuthorizedCallers contract\n/// @notice A contract that manages multiple authorized callers. Enables restricting access to certain functions to a\n/// set of addresses.\ncontract AuthorizedCallers is Ownable2StepMsgSender {\n using EnumerableSet for EnumerableSet.AddressSet;\n\n event AuthorizedCallerAdded(address caller);\n event AuthorizedCallerRemoved(address caller);\n\n error UnauthorizedCaller(address caller);\n error ZeroAddressNotAllowed();\n\n /// @notice Update args for changing the authorized callers\n struct AuthorizedCallerArgs {\n address[] addedCallers;\n address[] removedCallers;\n }\n\n /// @dev Set of authorized callers\n EnumerableSet.AddressSet internal s_authorizedCallers;\n\n /// @param authorizedCallers the authorized callers to set\n constructor(\n address[] memory authorizedCallers\n ) {\n _applyAuthorizedCallerUpdates(\n AuthorizedCallerArgs({addedCallers: authorizedCallers, removedCallers: new address[](0)})\n );\n }\n\n /// @return authorizedCallers Returns all authorized callers\n function getAllAuthorizedCallers() external view returns (address[] memory) {\n return s_authorizedCallers.values();\n }\n\n /// @notice Updates the list of authorized callers\n /// @param authorizedCallerArgs Callers to add and remove. Removals are performed first.\n function applyAuthorizedCallerUpdates(\n AuthorizedCallerArgs memory authorizedCallerArgs\n ) external onlyOwner {\n _applyAuthorizedCallerUpdates(authorizedCallerArgs);\n }\n\n /// @notice Updates the list of authorized callers\n /// @param authorizedCallerArgs Callers to add and remove. Removals are performed first.\n function _applyAuthorizedCallerUpdates(\n AuthorizedCallerArgs memory authorizedCallerArgs\n ) internal {\n address[] memory removedCallers = authorizedCallerArgs.removedCallers;\n for (uint256 i = 0; i < removedCallers.length; ++i) {\n address caller = removedCallers[i];\n\n if (s_authorizedCallers.remove(caller)) {\n emit AuthorizedCallerRemoved(caller);\n }\n }\n\n address[] memory addedCallers = authorizedCallerArgs.addedCallers;\n for (uint256 i = 0; i < addedCallers.length; ++i) {\n address caller = addedCallers[i];\n\n if (caller == address(0)) {\n revert ZeroAddressNotAllowed();\n }\n\n s_authorizedCallers.add(caller);\n emit AuthorizedCallerAdded(caller);\n }\n }\n\n /// @notice Checks the sender and reverts if it is anyone other than a listed authorized caller.\n function _validateCaller() internal view {\n if (!s_authorizedCallers.contains(msg.sender)) {\n revert UnauthorizedCaller(msg.sender);\n }\n }\n\n /// @notice Checks the sender and reverts if it is anyone other than a listed authorized caller.\n modifier onlyAuthorizedCallers() {\n _validateCaller();\n _;\n }\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2Step.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {IOwnable} from \"../interfaces/IOwnable.sol\";\n\n/// @notice A minimal contract that implements 2-step ownership transfer and nothing more. It's made to be minimal\n/// to reduce the impact of the bytecode size on any contract that inherits from it.\ncontract Ownable2Step is IOwnable {\n /// @notice The pending owner is the address to which ownership may be transferred.\n address private s_pendingOwner;\n /// @notice The owner is the current owner of the contract.\n /// @dev The owner is the second storage variable so any implementing contract could pack other state with it\n /// instead of the much less used s_pendingOwner.\n address private s_owner;\n\n error OwnerCannotBeZero();\n error MustBeProposedOwner();\n error CannotTransferToSelf();\n error OnlyCallableByOwner();\n\n event OwnershipTransferRequested(address indexed from, address indexed to);\n event OwnershipTransferred(address indexed from, address indexed to);\n\n constructor(address newOwner, address pendingOwner) {\n if (newOwner == address(0)) {\n revert OwnerCannotBeZero();\n }\n\n s_owner = newOwner;\n if (pendingOwner != address(0)) {\n _transferOwnership(pendingOwner);\n }\n }\n\n /// @notice Get the current owner\n function owner() public view override returns (address) {\n return s_owner;\n }\n\n /// @notice Allows an owner to begin transferring ownership to a new address. The new owner needs to call\n /// `acceptOwnership` to accept the transfer before any permissions are changed.\n /// @param to The address to which ownership will be transferred.\n function transferOwnership(\n address to\n ) public override onlyOwner {\n _transferOwnership(to);\n }\n\n /// @notice validate, transfer ownership, and emit relevant events\n /// @param to The address to which ownership will be transferred.\n function _transferOwnership(\n address to\n ) private {\n if (to == msg.sender) {\n revert CannotTransferToSelf();\n }\n\n s_pendingOwner = to;\n\n emit OwnershipTransferRequested(s_owner, to);\n }\n\n /// @notice Allows an ownership transfer to be completed by the recipient.\n function acceptOwnership() external override {\n if (msg.sender != s_pendingOwner) {\n revert MustBeProposedOwner();\n }\n\n address oldOwner = s_owner;\n s_owner = msg.sender;\n s_pendingOwner = address(0);\n\n emit OwnershipTransferred(oldOwner, msg.sender);\n }\n\n /// @notice validate access\n function _validateOwnership() internal view {\n if (msg.sender != s_owner) {\n revert OnlyCallableByOwner();\n }\n }\n\n /// @notice Reverts if called by anyone other than the contract owner.\n modifier onlyOwner() {\n _validateOwnership();\n _;\n }\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {Ownable2Step} from \"./Ownable2Step.sol\";\n\n/// @notice Sets the msg.sender to be the owner of the contract and does not set a pending owner.\ncontract Ownable2StepMsgSender is Ownable2Step {\n constructor() Ownable2Step(msg.sender, address(0)) {}\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/IOwnable.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface IOwnable {\n function owner() external returns (address);\n\n function transferOwnership(\n address recipient\n ) external;\n\n function acceptOwnership() external;\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface ITypeAndVersion {\n function typeAndVersion() external pure returns (string memory);\n}\n" + }, + "node_modules/@openzeppelin/contracts-4.8.3/utils/structs/EnumerableSet.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v4.8.0) (utils/structs/EnumerableSet.sol)\n// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.\n\npragma solidity ^0.8.0;\n\n/**\n * @dev Library for managing\n * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive\n * types.\n *\n * Sets have the following properties:\n *\n * - Elements are added, removed, and checked for existence in constant time\n * (O(1)).\n * - Elements are enumerated in O(n). No guarantees are made on the ordering.\n *\n * ```\n * contract Example {\n * // Add the library methods\n * using EnumerableSet for EnumerableSet.AddressSet;\n *\n * // Declare a set state variable\n * EnumerableSet.AddressSet private mySet;\n * }\n * ```\n *\n * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)\n * and `uint256` (`UintSet`) are supported.\n *\n * [WARNING]\n * ====\n * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure\n * unusable.\n * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.\n *\n * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an\n * array of EnumerableSet.\n * ====\n */\nlibrary EnumerableSet {\n // To implement this library for multiple types with as little code\n // repetition as possible, we write it in terms of a generic Set type with\n // bytes32 values.\n // The Set implementation uses private functions, and user-facing\n // implementations (such as AddressSet) are just wrappers around the\n // underlying Set.\n // This means that we can only create new EnumerableSets for types that fit\n // in bytes32.\n\n struct Set {\n // Storage of set values\n bytes32[] _values;\n // Position of the value in the `values` array, plus 1 because index 0\n // means a value is not in the set.\n mapping(bytes32 => uint256) _indexes;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function _add(Set storage set, bytes32 value) private returns (bool) {\n if (!_contains(set, value)) {\n set._values.push(value);\n // The value is stored at length-1, but we add 1 to all indexes\n // and use 0 as a sentinel value\n set._indexes[value] = set._values.length;\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function _remove(Set storage set, bytes32 value) private returns (bool) {\n // We read and store the value's index to prevent multiple reads from the same storage slot\n uint256 valueIndex = set._indexes[value];\n\n if (valueIndex != 0) {\n // Equivalent to contains(set, value)\n // To delete an element from the _values array in O(1), we swap the element to delete with the last one in\n // the array, and then remove the last element (sometimes called as 'swap and pop').\n // This modifies the order of the array, as noted in {at}.\n\n uint256 toDeleteIndex = valueIndex - 1;\n uint256 lastIndex = set._values.length - 1;\n\n if (lastIndex != toDeleteIndex) {\n bytes32 lastValue = set._values[lastIndex];\n\n // Move the last value to the index where the value to delete is\n set._values[toDeleteIndex] = lastValue;\n // Update the index for the moved value\n set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex\n }\n\n // Delete the slot where the moved value was stored\n set._values.pop();\n\n // Delete the index for the deleted slot\n delete set._indexes[value];\n\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function _contains(Set storage set, bytes32 value) private view returns (bool) {\n return set._indexes[value] != 0;\n }\n\n /**\n * @dev Returns the number of values on the set. O(1).\n */\n function _length(Set storage set) private view returns (uint256) {\n return set._values.length;\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function _at(Set storage set, uint256 index) private view returns (bytes32) {\n return set._values[index];\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function _values(Set storage set) private view returns (bytes32[] memory) {\n return set._values;\n }\n\n // Bytes32Set\n\n struct Bytes32Set {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _add(set._inner, value);\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _remove(set._inner, value);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {\n return _contains(set._inner, value);\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(Bytes32Set storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {\n return _at(set._inner, index);\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {\n bytes32[] memory store = _values(set._inner);\n bytes32[] memory result;\n\n /// @solidity memory-safe-assembly\n assembly {\n result := store\n }\n\n return result;\n }\n\n // AddressSet\n\n struct AddressSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(AddressSet storage set, address value) internal returns (bool) {\n return _add(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(AddressSet storage set, address value) internal returns (bool) {\n return _remove(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(AddressSet storage set, address value) internal view returns (bool) {\n return _contains(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(AddressSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(AddressSet storage set, uint256 index) internal view returns (address) {\n return address(uint160(uint256(_at(set._inner, index))));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(AddressSet storage set) internal view returns (address[] memory) {\n bytes32[] memory store = _values(set._inner);\n address[] memory result;\n\n /// @solidity memory-safe-assembly\n assembly {\n result := store\n }\n\n return result;\n }\n\n // UintSet\n\n struct UintSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(UintSet storage set, uint256 value) internal returns (bool) {\n return _add(set._inner, bytes32(value));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(UintSet storage set, uint256 value) internal returns (bool) {\n return _remove(set._inner, bytes32(value));\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(UintSet storage set, uint256 value) internal view returns (bool) {\n return _contains(set._inner, bytes32(value));\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(UintSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(UintSet storage set, uint256 index) internal view returns (uint256) {\n return uint256(_at(set._inner, index));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(UintSet storage set) internal view returns (uint256[] memory) {\n bytes32[] memory store = _values(set._inner);\n uint256[] memory result;\n\n /// @solidity memory-safe-assembly\n assembly {\n result := store\n }\n\n return result;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC1363.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1363.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"./IERC20.sol\";\nimport {IERC165} from \"./IERC165.sol\";\n\n/**\n * @title IERC1363\n * @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].\n *\n * Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract\n * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.\n */\ninterface IERC1363 is IERC20, IERC165 {\n /*\n * Note: the ERC-165 identifier for this interface is 0xb0202a11.\n * 0xb0202a11 ===\n * bytes4(keccak256('transferAndCall(address,uint256)')) ^\n * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^\n * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^\n * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^\n * bytes4(keccak256('approveAndCall(address,uint256)')) ^\n * bytes4(keccak256('approveAndCall(address,uint256,bytes)'))\n */\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferAndCall(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @param data Additional data with no specified format, sent in call to `to`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param from The address which you want to send tokens from.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferFromAndCall(address from, address to, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param from The address which you want to send tokens from.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @param data Additional data with no specified format, sent in call to `to`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.\n * @param spender The address which will spend the funds.\n * @param value The amount of tokens to be spent.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function approveAndCall(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.\n * @param spender The address which will spend the funds.\n * @param value The amount of tokens to be spent.\n * @param data Additional data with no specified format, sent in call to `spender`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC165.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC165.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC165} from \"../utils/introspection/IERC165.sol\";\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC20.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../token/ERC20/IERC20.sol\";\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/IERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-20 standard as defined in the ERC.\n */\ninterface IERC20 {\n /**\n * @dev Emitted when `value` tokens are moved from one account (`from`) to\n * another (`to`).\n *\n * Note that `value` may be zero.\n */\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n /**\n * @dev Emitted when the allowance of a `spender` for an `owner` is set by\n * a call to {approve}. `value` is the new allowance.\n */\n event Approval(address indexed owner, address indexed spender, uint256 value);\n\n /**\n * @dev Returns the value of tokens in existence.\n */\n function totalSupply() external view returns (uint256);\n\n /**\n * @dev Returns the value of tokens owned by `account`.\n */\n function balanceOf(address account) external view returns (uint256);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transfer(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Returns the remaining number of tokens that `spender` will be\n * allowed to spend on behalf of `owner` through {transferFrom}. This is\n * zero by default.\n *\n * This value changes when {approve} or {transferFrom} are called.\n */\n function allowance(address owner, address spender) external view returns (uint256);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * IMPORTANT: Beware that changing an allowance with this method brings the risk\n * that someone may use both the old and the new allowance by unfortunate\n * transaction ordering. One possible solution to mitigate this race\n * condition is to first reduce the spender's allowance to 0 and set the\n * desired value afterwards:\n * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n *\n * Emits an {Approval} event.\n */\n function approve(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the\n * allowance mechanism. `value` is then deducted from the caller's\n * allowance.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(address from, address to, uint256 value) external returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/utils/SafeERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../IERC20.sol\";\nimport {IERC1363} from \"../../../interfaces/IERC1363.sol\";\n\n/**\n * @title SafeERC20\n * @dev Wrappers around ERC-20 operations that throw on failure (when the token\n * contract returns false). Tokens that return no value (and instead revert or\n * throw on failure) are also supported, non-reverting calls are assumed to be\n * successful.\n * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,\n * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.\n */\nlibrary SafeERC20 {\n /**\n * @dev An operation with an ERC-20 token failed.\n */\n error SafeERC20FailedOperation(address token);\n\n /**\n * @dev Indicates a failed `decreaseAllowance` request.\n */\n error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);\n\n /**\n * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,\n * non-reverting calls are assumed to be successful.\n */\n function safeTransfer(IERC20 token, address to, uint256 value) internal {\n _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));\n }\n\n /**\n * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the\n * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.\n */\n function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {\n _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));\n }\n\n /**\n * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.\n */\n function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {\n return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value)));\n }\n\n /**\n * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.\n */\n function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {\n return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value)));\n }\n\n /**\n * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,\n * non-reverting calls are assumed to be successful.\n *\n * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the \"client\"\n * smart contract uses ERC-7674 to set temporary allowances, then the \"client\" smart contract should avoid using\n * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract\n * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.\n */\n function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {\n uint256 oldAllowance = token.allowance(address(this), spender);\n forceApprove(token, spender, oldAllowance + value);\n }\n\n /**\n * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no\n * value, non-reverting calls are assumed to be successful.\n *\n * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the \"client\"\n * smart contract uses ERC-7674 to set temporary allowances, then the \"client\" smart contract should avoid using\n * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract\n * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.\n */\n function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {\n unchecked {\n uint256 currentAllowance = token.allowance(address(this), spender);\n if (currentAllowance < requestedDecrease) {\n revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);\n }\n forceApprove(token, spender, currentAllowance - requestedDecrease);\n }\n }\n\n /**\n * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,\n * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval\n * to be set to zero before setting it to a non-zero value, such as USDT.\n *\n * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function\n * only sets the \"standard\" allowance. Any temporary allowance will remain active, in addition to the value being\n * set here.\n */\n function forceApprove(IERC20 token, address spender, uint256 value) internal {\n bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));\n\n if (!_callOptionalReturnBool(token, approvalCall)) {\n _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));\n _callOptionalReturn(token, approvalCall);\n }\n }\n\n /**\n * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no\n * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * Reverts if the returned value is other than `true`.\n */\n function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {\n if (to.code.length == 0) {\n safeTransfer(token, to, value);\n } else if (!token.transferAndCall(to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target\n * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * Reverts if the returned value is other than `true`.\n */\n function transferFromAndCallRelaxed(\n IERC1363 token,\n address from,\n address to,\n uint256 value,\n bytes memory data\n ) internal {\n if (to.code.length == 0) {\n safeTransferFrom(token, from, to, value);\n } else if (!token.transferFromAndCall(from, to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no\n * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.\n * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}\n * once without retrying, and relies on the returned value to be true.\n *\n * Reverts if the returned value is other than `true`.\n */\n function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {\n if (to.code.length == 0) {\n forceApprove(token, to, value);\n } else if (!token.approveAndCall(to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement\n * on the return value: the return value is optional (but if data is returned, it must not be false).\n * @param token The token targeted by the call.\n * @param data The call data (encoded using abi.encode or one of its variants).\n *\n * This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.\n */\n function _callOptionalReturn(IERC20 token, bytes memory data) private {\n uint256 returnSize;\n uint256 returnValue;\n assembly (\"memory-safe\") {\n let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)\n // bubble errors\n if iszero(success) {\n let ptr := mload(0x40)\n returndatacopy(ptr, 0, returndatasize())\n revert(ptr, returndatasize())\n }\n returnSize := returndatasize()\n returnValue := mload(0)\n }\n\n if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement\n * on the return value: the return value is optional (but if data is returned, it must not be false).\n * @param token The token targeted by the call.\n * @param data The call data (encoded using abi.encode or one of its variants).\n *\n * This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.\n */\n function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {\n bool success;\n uint256 returnSize;\n uint256 returnValue;\n assembly (\"memory-safe\") {\n success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)\n returnSize := returndatasize()\n returnValue := mload(0)\n }\n return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/introspection/IERC165.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/IERC165.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-165 standard, as defined in the\n * https://eips.ethereum.org/EIPS/eip-165[ERC].\n *\n * Implementers can declare support of contract interfaces, which can then be\n * queried by others ({ERC165Checker}).\n *\n * For an implementation, see {ERC165}.\n */\ninterface IERC165 {\n /**\n * @dev Returns true if this contract implements the interface defined by\n * `interfaceId`. See the corresponding\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]\n * to learn more about how these ids are created.\n *\n * This function call must use less than 30 000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n}\n" + } + }, + "settings": { + "evmVersion": "paris", + "libraries": {}, + "metadata": { "appendCBOR": true, "bytecodeHash": "none", "useLiteralContent": false }, + "optimizer": { "enabled": true, "runs": 50000 }, + "outputSelection": { + "contracts/interfaces/ILockBox.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/pools/ERC20LockBox.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/AuthorizedCallers.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2Step.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/IOwnable.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-4.8.3/utils/structs/EnumerableSet.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC1363.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC165.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/IERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/utils/SafeERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/introspection/IERC165.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + } + }, + "remappings": [ + "forge-std/=node_modules/@chainlink/contracts/src/v0.8/vendor/forge-std/src/", + "@chainlink/policy-management/=node_modules/@chainlink/ace/packages/policy-management/src/", + "@chainlink/contracts/=node_modules/@chainlink/contracts/", + "@openzeppelin/contracts@4.8.3/=node_modules/@openzeppelin/contracts-4.8.3/", + "@openzeppelin/contracts@5.3.0/=node_modules/@openzeppelin/contracts-5.3.0/" + ], + "viaIR": true + } +} diff --git a/ccip-sdk/src/verify/fixtures/LockReleaseTokenPool.abi.json b/ccip-sdk/src/verify/fixtures/LockReleaseTokenPool.abi.json new file mode 100644 index 00000000..a6a4f07c --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/LockReleaseTokenPool.abi.json @@ -0,0 +1,2073 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "contract IERC20" + }, + { + "name": "localTokenDecimals", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "advancedPoolHooks", + "type": "address", + "internalType": "address" + }, + { + "name": "rmnProxy", + "type": "address", + "internalType": "address" + }, + { + "name": "router", + "type": "address", + "internalType": "address" + }, + { + "name": "lockBox", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addRemotePool", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "applyChainUpdates", + "inputs": [ + { + "name": "remoteChainSelectorsToRemove", + "type": "uint64[]", + "internalType": "uint64[]" + }, + { + "name": "chainsToAdd", + "type": "tuple[]", + "internalType": "struct TokenPool.ChainUpdate[]", + "components": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddresses", + "type": "bytes[]", + "internalType": "bytes[]" + }, + { + "name": "remoteTokenAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "outboundRateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "applyTokenTransferFeeConfigUpdates", + "inputs": [ + { + "name": "tokenTransferFeeConfigArgs", + "type": "tuple[]", + "internalType": "struct TokenPool.TokenTransferFeeConfigArgs[]", + "components": [ + { + "name": "destChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "tokenTransferFeeConfig", + "type": "tuple", + "internalType": "struct IPoolV2.TokenTransferFeeConfig", + "components": [ + { + "name": "destGasOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destBytesOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "fastFinalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "fastFinalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + } + ] + } + ] + }, + { + "name": "disableTokenTransferFeeConfigs", + "type": "uint64[]", + "internalType": "uint64[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getAdvancedPoolHooks", + "inputs": [], + "outputs": [ + { + "name": "advancedPoolHook", + "type": "address", + "internalType": "contract IAdvancedPoolHooks" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAllowedFinalityConfig", + "inputs": [], + "outputs": [ + { + "name": "allowedFinality", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getCurrentRateLimiterState", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "fastFinality", + "type": "bool", + "internalType": "bool" + } + ], + "outputs": [ + { + "name": "outboundRateLimiterState", + "type": "tuple", + "internalType": "struct RateLimiter.TokenBucket", + "components": [ + { + "name": "tokens", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "lastUpdated", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterState", + "type": "tuple", + "internalType": "struct RateLimiter.TokenBucket", + "components": [ + { + "name": "tokens", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "lastUpdated", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getDynamicConfig", + "inputs": [], + "outputs": [ + { + "name": "router", + "type": "address", + "internalType": "address" + }, + { + "name": "rateLimitAdmin", + "type": "address", + "internalType": "address" + }, + { + "name": "feeAdmin", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getFee", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "destChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "requestedFinalityConfig", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "feeUSDCents", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "destGasOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destBytesOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "tokenFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getLockBox", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRemotePools", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes[]", + "internalType": "bytes[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRemoteToken", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRequiredCCVs", + "inputs": [ + { + "name": "localToken", + "type": "address", + "internalType": "address" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "sourceDenominatedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "requestedFinalityConfig", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "direction", + "type": "uint8", + "internalType": "enum IPoolV2.MessageDirection" + } + ], + "outputs": [ + { + "name": "requiredCCVs", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRmnProxy", + "inputs": [], + "outputs": [ + { + "name": "rmnProxy", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getSupportedChains", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint64[]", + "internalType": "uint64[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getToken", + "inputs": [], + "outputs": [ + { + "name": "token", + "type": "address", + "internalType": "contract IERC20" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTokenDecimals", + "inputs": [], + "outputs": [ + { + "name": "decimals", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTokenTransferFeeConfig", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "destChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "feeConfig", + "type": "tuple", + "internalType": "struct IPoolV2.TokenTransferFeeConfig", + "components": [ + { + "name": "destGasOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destBytesOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "fastFinalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "fastFinalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isRemotePool", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isSupportedChain", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isSupportedToken", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "lockOrBurn", + "inputs": [ + { + "name": "lockOrBurnIn", + "type": "tuple", + "internalType": "struct Pool.LockOrBurnInV1", + "components": [ + { + "name": "receiver", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "originalSender", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + } + ] + } + ], + "outputs": [ + { + "name": "lockOrBurnOutV1", + "type": "tuple", + "internalType": "struct Pool.LockOrBurnOutV1", + "components": [ + { + "name": "destTokenAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "destPoolData", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "lockOrBurn", + "inputs": [ + { + "name": "lockOrBurnIn", + "type": "tuple", + "internalType": "struct Pool.LockOrBurnInV1", + "components": [ + { + "name": "receiver", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "originalSender", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + } + ] + }, + { + "name": "requestedFinalityConfig", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "tokenArgs", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct Pool.LockOrBurnOutV1", + "components": [ + { + "name": "destTokenAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "destPoolData", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "destTokenAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "releaseOrMint", + "inputs": [ + { + "name": "releaseOrMintIn", + "type": "tuple", + "internalType": "struct Pool.ReleaseOrMintInV1", + "components": [ + { + "name": "originalSender", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "sourceDenominatedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + }, + { + "name": "sourcePoolAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "sourcePoolData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "offchainTokenData", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "requestedFinalityConfig", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct Pool.ReleaseOrMintOutV1", + "components": [ + { + "name": "destinationAmount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "releaseOrMint", + "inputs": [ + { + "name": "releaseOrMintIn", + "type": "tuple", + "internalType": "struct Pool.ReleaseOrMintInV1", + "components": [ + { + "name": "originalSender", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "sourceDenominatedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "localToken", + "type": "address", + "internalType": "address" + }, + { + "name": "sourcePoolAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "sourcePoolData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "offchainTokenData", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct Pool.ReleaseOrMintOutV1", + "components": [ + { + "name": "destinationAmount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeRemotePool", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setAllowedFinalityConfig", + "inputs": [ + { + "name": "allowedFinality", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setDynamicConfig", + "inputs": [ + { + "name": "router", + "type": "address", + "internalType": "address" + }, + { + "name": "rateLimitAdmin", + "type": "address", + "internalType": "address" + }, + { + "name": "feeAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setRateLimitConfig", + "inputs": [ + { + "name": "rateLimitConfigArgs", + "type": "tuple[]", + "internalType": "struct TokenPool.RateLimitConfigArgs[]", + "components": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "fastFinality", + "type": "bool", + "internalType": "bool" + }, + { + "name": "outboundRateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "typeAndVersion", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "updateAdvancedPoolHooks", + "inputs": [ + { + "name": "newHook", + "type": "address", + "internalType": "contract IAdvancedPoolHooks" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "withdrawFeeTokens", + "inputs": [ + { + "name": "feeTokens", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "AdvancedPoolHooksUpdated", + "inputs": [ + { + "name": "oldHook", + "type": "address", + "indexed": false, + "internalType": "contract IAdvancedPoolHooks" + }, + { + "name": "newHook", + "type": "address", + "indexed": false, + "internalType": "contract IAdvancedPoolHooks" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ChainAdded", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + }, + { + "name": "remoteToken", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "outboundRateLimiterConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ChainRemoved", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DynamicConfigSet", + "inputs": [ + { + "name": "router", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "rateLimitAdmin", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "feeAdmin", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FastFinalityInboundRateLimitConsumed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FastFinalityOutboundRateLimitConsumed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FeeTokenWithdrawn", + "inputs": [ + { + "name": "receiver", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "feeToken", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FinalityConfigSet", + "inputs": [ + { + "name": "allowedFinality", + "type": "bytes4", + "indexed": false, + "internalType": "bytes4" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "InboundRateLimitConsumed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "LockedOrBurned", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OutboundRateLimitConsumed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferRequested", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RateLimitConfigured", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "fastFinality", + "type": "bool", + "indexed": false, + "internalType": "bool" + }, + { + "name": "outboundRateLimiterConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + }, + { + "name": "inboundRateLimiterConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ReleasedOrMinted", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "recipient", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RemotePoolAdded", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RemotePoolRemoved", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "TokenTransferFeeConfigDeleted", + "inputs": [ + { + "name": "destChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "TokenTransferFeeConfigUpdated", + "inputs": [ + { + "name": "destChainSelector", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "tokenTransferFeeConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct IPoolV2.TokenTransferFeeConfig", + "components": [ + { + "name": "destGasOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destBytesOverhead", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "fastFinalityFeeUSDCents", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "finalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "fastFinalityTransferFeeBps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + } + ] + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "BucketOverfilled", + "inputs": [] + }, + { + "type": "error", + "name": "CallerIsNotARampOnRouter", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "CallerIsNotOwnerOrFeeAdmin", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "CannotTransferToSelf", + "inputs": [] + }, + { + "type": "error", + "name": "ChainAlreadyExists", + "inputs": [ + { + "name": "chainSelector", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "ChainNotAllowed", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "CursedByRMN", + "inputs": [] + }, + { + "type": "error", + "name": "DisabledNonZeroRateLimit", + "inputs": [ + { + "name": "config", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ] + }, + { + "type": "error", + "name": "InvalidDecimalArgs", + "inputs": [ + { + "name": "expected", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "actual", + "type": "uint8", + "internalType": "uint8" + } + ] + }, + { + "type": "error", + "name": "InvalidRateLimitRate", + "inputs": [ + { + "name": "rateLimiterConfig", + "type": "tuple", + "internalType": "struct RateLimiter.Config", + "components": [ + { + "name": "isEnabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "capacity", + "type": "uint128", + "internalType": "uint128" + }, + { + "name": "rate", + "type": "uint128", + "internalType": "uint128" + } + ] + } + ] + }, + { + "type": "error", + "name": "InvalidRemoteChainDecimals", + "inputs": [ + { + "name": "sourcePoolData", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "InvalidRemotePoolForChain", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "InvalidRequestedFinality", + "inputs": [ + { + "name": "requestedFinality", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "allowedFinality", + "type": "bytes4", + "internalType": "bytes4" + } + ] + }, + { + "type": "error", + "name": "InvalidSourcePoolAddress", + "inputs": [ + { + "name": "sourcePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "InvalidToken", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "InvalidTokenTransferFeeConfig", + "inputs": [ + { + "name": "destChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "InvalidTransferFeeBps", + "inputs": [ + { + "name": "bps", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "MustBeProposedOwner", + "inputs": [] + }, + { + "type": "error", + "name": "NonExistentChain", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "OnlyCallableByOwner", + "inputs": [] + }, + { + "type": "error", + "name": "OverflowDetected", + "inputs": [ + { + "name": "remoteDecimals", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "localDecimals", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "remoteAmount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "OwnerCannotBeZero", + "inputs": [] + }, + { + "type": "error", + "name": "PoolAlreadyAdded", + "inputs": [ + { + "name": "remoteChainSelector", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "remotePoolAddress", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "RequestedFinalityCanOnlyHaveOneMode", + "inputs": [ + { + "name": "encodedFinality", + "type": "bytes4", + "internalType": "bytes4" + } + ] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "TokenMaxCapacityExceeded", + "inputs": [ + { + "name": "capacity", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "tokenAddress", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "TokenRateLimitReached", + "inputs": [ + { + "name": "minWaitInSeconds", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "tokenAddress", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "Unauthorized", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ZeroAddressInvalid", + "inputs": [] + }, + { + "type": "error", + "name": "ZeroAddressNotAllowed", + "inputs": [] + } +] diff --git a/ccip-sdk/src/verify/fixtures/LockReleaseTokenPool.standard-input.json b/ccip-sdk/src/verify/fixtures/LockReleaseTokenPool.standard-input.json new file mode 100644 index 00000000..fe726f35 --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/LockReleaseTokenPool.standard-input.json @@ -0,0 +1,615 @@ +{ + "language": "Solidity", + "sources": { + "contracts/interfaces/IAdvancedPoolHooks.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.24;\n\nimport {Pool} from \"../libraries/Pool.sol\";\nimport {IPoolV2} from \"./IPoolV2.sol\";\n\n/// @notice Interface for AdvancedPoolHooks contract. Implementations may contain no-op logic.\ninterface IAdvancedPoolHooks {\n /// @notice Preflight check before lock or burn operation.\n /// @param lockOrBurnIn The lock or burn input parameters.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Additional token arguments.\n /// @param amountPostFee The amount after token pool bps-based fees have been deducted.\n /// @dev This function may revert if the preflight check fails. This means the transaction is rolled back on source.\n function preflightCheck(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs,\n uint256 amountPostFee\n ) external;\n\n /// @notice Postflight check before releasing or minting tokens.\n /// @param releaseOrMintIn The release or mint output parameters.\n /// @param localAmount The local amount to be released or minted.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @dev This function may revert if the postflight check fails. This means the transaction is unexecutable until\n /// the issue is resolved.\n function postflightCheck(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n uint256 localAmount,\n bytes4 requestedFinalityConfig\n ) external;\n\n /// @notice Returns the set of required CCVs for transfers in a specific direction.\n /// @param localToken The address of the local token.\n /// @param remoteChainSelector The remote chain selector for this transfer.\n /// @param amount The amount being transferred.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param extraData Direction-specific payload forwarded by the caller (e.g. token args or source pool data).\n /// @param direction The direction of the transfer (Inbound or Outbound).\n /// @return requiredCCVs Set of required CCV addresses.\n function getRequiredCCVs(\n address localToken,\n uint64 remoteChainSelector,\n uint256 amount,\n bytes4 requestedFinalityConfig,\n bytes calldata extraData,\n IPoolV2.MessageDirection direction\n ) external view returns (address[] memory requiredCCVs);\n}\n" + }, + "contracts/interfaces/ILockBox.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\ninterface ILockBox {\n /// @notice Deposits the token into the lockbox.\n /// @param token The address of the token to deposit.\n /// @param remoteChainSelector The chain selector of the remote chain.\n /// @param amount The amount of tokens to deposit.\n function deposit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) external;\n\n /// @notice Withdraws tokens to a specific recipient.\n /// @param token The address of the token to withdraw.\n /// @param remoteChainSelector The chain selector of the remote chain.\n /// @param amount The amount of tokens to withdraw. If set to max uint256, withdraws the entire balance.\n /// @param recipient The address of the recipient to receive the withdrawn tokens.\n function withdraw(\n address token,\n uint64 remoteChainSelector,\n uint256 amount,\n address recipient\n ) external;\n\n /// @notice Returns whether the lockbox supports a token.\n /// @param token The address of the token.\n /// @return supported True if the token is supported.\n function isTokenSupported(\n address token\n ) external view returns (bool);\n}\n" + }, + "contracts/interfaces/IPool.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport {Pool} from \"../libraries/Pool.sol\";\n\nimport {IERC165} from \"@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol\";\n\n/// @notice Shared public interface for multiple V1 pool types.\n/// Each pool type handles a different child token model e.g. lock/unlock, mint/burn.\ninterface IPoolV1 is IERC165 {\n /// @notice Lock tokens into the pool or burn the tokens.\n /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain.\n /// @return lockOrBurnOut Encoded data fields for the processing of tokens on the destination chain.\n function lockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn\n ) external returns (Pool.LockOrBurnOutV1 memory lockOrBurnOut);\n\n /// @notice Releases or mints tokens to the receiver address.\n /// @param releaseOrMintIn All data required to release or mint tokens.\n /// @return releaseOrMintOut The amount of tokens released or minted on the local chain, denominated\n /// in the local token's decimals.\n /// @dev The offRamp asserts that the balanceOf of the receiver has been incremented by exactly the number\n /// of tokens that is returned in ReleaseOrMintOutV1.destinationAmount. If the amounts do not match, the tx reverts.\n function releaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn\n ) external returns (Pool.ReleaseOrMintOutV1 memory);\n\n /// @notice Checks whether a remote chain is supported in the token pool.\n /// @param remoteChainSelector The selector of the remote chain.\n /// @return true if the given chain is a permissioned remote chain.\n function isSupportedChain(\n uint64 remoteChainSelector\n ) external view returns (bool);\n\n /// @notice Returns if the token pool supports the given token.\n /// @param token The address of the token.\n /// @return true if the token is supported by the pool.\n function isSupportedToken(\n address token\n ) external view returns (bool);\n}\n" + }, + "contracts/interfaces/IPoolV1V2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport {IPoolV1} from \"./IPool.sol\";\nimport {IPoolV2} from \"./IPoolV2.sol\";\n\ninterface IPoolV1V2 is IPoolV1, IPoolV2 {}\n" + }, + "contracts/interfaces/IPoolV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport {Pool} from \"../libraries/Pool.sol\";\n\nimport {IERC165} from \"@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol\";\n\n/// @notice Shared public interface for multiple V2 pool types.\n/// Each pool type handles a different child token model e.g. lock/release, mint/burn.\ninterface IPoolV2 is IERC165 {\n struct TokenTransferFeeConfig {\n uint32 destGasOverhead; // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e Gas charged to execute the token transfer on the destination chain.\n uint32 destBytesOverhead; // \u2502 Data availability bytes.\n uint32 finalityFeeUSDCents; // \u2502 Fee to charge for token transfer with default (wait-for-finality) finality, multiples of 0.01 USD.\n uint32 fastFinalityFeeUSDCents; // \u2502 Fee to charge for token transfer with fast finality (FTF), multiples of 0.01 USD.\n // \u2502 The following two fee is deducted from the transferred asset, not added on top.\n uint16 finalityTransferFeeBps; // \u2502 Fee in basis points for default finality transfers [0-10_000].\n uint16 fastFinalityTransferFeeBps; //\u2502 Fee in basis points for custom finality transfers [0-10_000].\n bool isEnabled; // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f Whether this config is enabled.\n }\n\n enum MessageDirection {\n Outbound,\n Inbound\n }\n\n /// @notice Lock tokens into the pool or burn the tokens.\n /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Additional token arguments.\n /// @return lockOrBurnOut Encoded data fields for the processing of tokens on the destination chain.\n /// @return destTokenAmount The amount of tokens that will be set in TokenTransferV1.amount to be released/mint on destination.\n function lockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n ) external returns (Pool.LockOrBurnOutV1 memory lockOrBurnOut, uint256 destTokenAmount);\n\n /// @notice Releases or mints tokens on the destination chain.\n /// @param releaseOrMintIn Encoded data fields for the processing of tokens on the destination chain.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @return releaseOrMintOut Encoded data fields describing the result of the release or mint.\n function releaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n bytes4 requestedFinalityConfig\n ) external returns (Pool.ReleaseOrMintOutV1 memory releaseOrMintOut);\n\n /// @notice Returns the set of required CCVs for transfers in a given direction.\n /// @param localToken The address of the local token.\n /// @param remoteChainSelector The chain selector of the remote chain.\n /// @param sourceAmount The source-denominated amount of tokens to be transferred.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param extraData Direction-specific payload forwarded by the caller (e.g. token args or source pool data).\n /// @param direction Whether CCVs are required for outbound (source -> remote) or inbound (remote -> destination) transfers.\n /// @return requiredCCVs A set of addresses representing the required CCVs.\n function getRequiredCCVs(\n address localToken,\n uint64 remoteChainSelector,\n uint256 sourceAmount,\n bytes4 requestedFinalityConfig,\n bytes calldata extraData,\n MessageDirection direction\n ) external view returns (address[] memory requiredCCVs);\n\n /// @notice Returns the fee overrides for transferring the pool's token to a destination chain.\n /// @param localToken The address of the local token.\n /// @param destChainSelector The chain selector of the destination chain.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Additional token argument from the CCIP message.\n /// @return feeConfig the fee configuration for transferring the token to the destination chain.\n function getTokenTransferFeeConfig(\n address localToken,\n uint64 destChainSelector,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n ) external view returns (TokenTransferFeeConfig memory feeConfig);\n\n /// @notice Returns the pool fee parameters that will apply to a transfer.\n /// @param localToken The local asset being transferred.\n /// @param destChainSelector The destination lane selector.\n /// @param amount The amount of tokens being bridged on this lane.\n /// @param feeToken The token used to pay feeUSDCents.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param tokenArgs Opaque token arguments supplied by the caller.\n /// @return feeUSDCents Flat fee charged in USD cents (crumbs) for this transfer.\n /// @return destGasOverhead Destination gas charged for accounting in the cost model.\n /// @return destBytesOverhead Destination calldata size attributed to the transfer.\n /// @return tokenFeeBps Bps charged in token units. Value of zero implies no in-token fee.\n /// @return isEnabled Whether the pool's fee config is enabled. If false, OnRamp should use FeeQuoter defaults.\n function getFee(\n address localToken,\n uint64 destChainSelector,\n uint256 amount,\n address feeToken,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n )\n external\n view\n returns (uint256 feeUSDCents, uint32 destGasOverhead, uint32 destBytesOverhead, uint16 tokenFeeBps, bool isEnabled);\n\n /// @notice Gets the token address on the remote chain.\n /// @param remoteChainSelector Remote chain selector.\n /// @dev To support non-evm chains, this value is encoded into bytes.\n function getRemoteToken(\n uint64 remoteChainSelector\n ) external view returns (bytes memory);\n}\n" + }, + "contracts/interfaces/IRMN.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n/// @notice This interface contains the only RMN-related functions that might be used on-chain by other CCIP contracts.\ninterface IRMN {\n /// @notice A Merkle root tagged with the address of the commit store contract it is destined for.\n struct TaggedRoot {\n address commitStore;\n bytes32 root;\n }\n\n /// @notice Callers MUST NOT cache the return value as a blessed tagged root could become unblessed.\n function isBlessed(\n TaggedRoot calldata taggedRoot\n ) external view returns (bool);\n\n /// @notice Iff there is an active global or legacy curse, this function returns true.\n function isCursed() external view returns (bool);\n\n /// @notice Iff there is an active global curse, or an active curse for `subject`, this function returns true.\n /// @param subject To check whether a particular chain is cursed, set to bytes16(uint128(chainSelector)).\n function isCursed(\n bytes16 subject\n ) external view returns (bool);\n}\n" + }, + "contracts/interfaces/IRouter.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport {Client} from \"../libraries/Client.sol\";\n\ninterface IRouter {\n error OnlyOffRamp();\n\n /// @notice Route the message to its intended receiver contract.\n /// @param message Client.Any2EVMMessage struct.\n /// @param gasForCallExactCheck of params for exec.\n /// @param gasLimit set of params for exec.\n /// @param receiver set of params for exec.\n /// @dev if the receiver is a contracts that signals support for CCIP execution through EIP-165.\n /// the contract is called. If not, only tokens are transferred.\n /// @return success A boolean value indicating whether the ccip message was received without errors.\n /// @return retBytes A bytes array containing return data form CCIP receiver.\n /// @return gasUsed the gas used by the external customer call. Does not include any overhead.\n function routeMessage(\n Client.Any2EVMMessage calldata message,\n uint16 gasForCallExactCheck,\n uint256 gasLimit,\n address receiver\n ) external returns (bool success, bytes memory retBytes, uint256 gasUsed);\n\n /// @notice Returns the configured onRamp for a specific destination chain.\n /// @param destChainSelector The destination chain Id to get the onRamp for.\n /// @return onRampAddress The address of the onRamp.\n function getOnRamp(\n uint64 destChainSelector\n ) external view returns (address onRampAddress);\n\n /// @notice Return true if the given offRamp is a configured offRamp for the given source chain.\n /// @param sourceChainSelector The source chain selector to check.\n /// @param offRamp The address of the offRamp to check.\n function isOffRamp(\n uint64 sourceChainSelector,\n address offRamp\n ) external view returns (bool isOffRamp);\n}\n" + }, + "contracts/libraries/Client.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n// End consumer library.\nlibrary Client {\n struct EVMTokenAmount {\n address token; // token address on the local chain.\n uint256 amount; // Amount of tokens.\n }\n\n struct Any2EVMMessage {\n bytes32 messageId; // MessageId corresponding to ccipSend on source.\n uint64 sourceChainSelector; // Source chain selector.\n bytes sender; // abi.encode(address) on EVM source chains; abi.decode(sender, (address)) to recover.\n bytes data; // payload sent in original message.\n EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation.\n }\n\n // If extraArgs is empty bytes, the default is 200k gas limit.\n struct EVM2AnyMessage {\n bytes receiver; // abi.encode(receiver address) for dest EVM chains.\n bytes data; // Data payload.\n EVMTokenAmount[] tokenAmounts; // Token transfers.\n address feeToken; // Address of feeToken. address(0) means you will send msg.value.\n bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV3).\n }\n\n /// @notice Tag to indicate no execution on the destination chain. Execution will need to be done manually.\n /// @dev Preimage for this tag is: keccak256(\"NO_EXECUTION_TAG\")[:4]\n bytes4 public constant NO_EXECUTION_TAG = 0xeba517d2;\n address public constant NO_EXECUTION_ADDRESS = address(bytes20(NO_EXECUTION_TAG));\n\n // ================================================================\n // \u2502 Legacy \u2502\n // ================================================================\n\n // Tag to indicate only a gas limit. Only usable for EVM as destination chain.\n bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9;\n\n struct EVMExtraArgsV1 {\n uint256 gasLimit;\n }\n\n function _argsToBytes(\n EVMExtraArgsV1 memory extraArgs\n ) internal pure returns (bytes memory bts) {\n return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs);\n }\n\n // Tag to indicate a gas limit (or dest chain equivalent processing units) and Out Of Order Execution. This tag is\n // available for multiple chain families. If there is no chain family specific tag, this is the default available\n // for a chain.\n // Note: not available for Solana or Sui VM based chains.\n bytes4 public constant GENERIC_EXTRA_ARGS_V2_TAG = 0x181dcf10;\n\n /// @param gasLimit: gas limit for the callback on the destination chain.\n /// @param allowOutOfOrderExecution: if true, it indicates that the message can be executed in any order relative to\n /// other messages from the same sender. This value's default varies by chain. On some chains, a particular value is\n /// enforced, meaning if the expected value is not set, the message request will revert.\n /// @dev Fully compatible with the previously existing EVMExtraArgsV2.\n struct GenericExtraArgsV2 {\n uint256 gasLimit;\n bool allowOutOfOrderExecution;\n }\n\n // Extra args tag for chains that use the Sui VM.\n bytes4 public constant SUI_EXTRA_ARGS_V1_TAG = 0x21ea4ca9;\n\n // Extra args tag for chains that use the Solana VM.\n bytes4 public constant SVM_EXTRA_ARGS_V1_TAG = 0x1f3b3aba;\n\n struct SVMExtraArgsV1 {\n uint32 computeUnits;\n uint64 accountIsWritableBitmap;\n bool allowOutOfOrderExecution;\n bytes32 tokenReceiver;\n // Additional accounts needed for execution of CCIP receiver. Must be empty if message.receiver is zero.\n // Token transfer related accounts are specified in the token pool lookup table on SVM.\n bytes32[] accounts;\n }\n\n /// @dev The maximum number of accounts that can be passed in SVMExtraArgs.\n uint256 public constant SVM_EXTRA_ARGS_MAX_ACCOUNTS = 64;\n\n /// @dev The expected static payload size of a token transfer when Borsh encoded and submitted to SVM.\n /// TokenPool extra data and offchain data sizes are dynamic, and should be accounted for separately.\n uint256 public constant SVM_TOKEN_TRANSFER_DATA_OVERHEAD = (4 + 32) // source_pool\n + 32 // token_address\n + 4 // gas_amount\n + 4 // extra_data overhead\n + 32 // amount\n + 32 // size of the token lookup table account\n + 32 // token-related accounts in the lookup table, over-estimated to 32, typically between 11 - 13\n + 32 // token account belonging to the token receiver, e.g ATA, not included in the token lookup table\n + 32 // per-chain token pool config, not included in the token lookup table\n + 32 // per-chain token billing config, not always included in the token lookup table\n + 32; // OffRamp pool signer PDA, not included in the token lookup table\n\n /// @dev Number of overhead accounts needed for message execution on SVM.\n /// @dev These are message.receiver, and the OffRamp Signer PDA specific to the receiver.\n uint256 public constant SVM_MESSAGING_ACCOUNTS_OVERHEAD = 2;\n\n /// @dev The size of each SVM account address in bytes.\n uint256 public constant SVM_ACCOUNT_BYTE_SIZE = 32;\n\n struct SuiExtraArgsV1 {\n uint256 gasLimit;\n bool allowOutOfOrderExecution;\n bytes32 tokenReceiver;\n bytes32[] receiverObjectIds;\n }\n\n /// @dev The expected static payload size of a token transfer when BCS encoded and submitted to SUI.\n /// TokenPool extra data and offchain data sizes are dynamic, and should be accounted for separately.\n uint256 public constant SUI_TOKEN_TRANSFER_DATA_OVERHEAD = (4 + 32) // source_pool, 4 bytes for length, 32 bytes for address\n + 32 // dest_token_address\n + 4 // dest_gas_amount\n + 4 // extra_data length, the contents are calculated separately\n + 32; // amount\n\n /// @dev Number of overhead accounts needed for message execution on SUI.\n /// @dev This is the message.receiver.\n uint256 public constant SUI_MESSAGING_ACCOUNTS_OVERHEAD = 1;\n\n /// @dev The maximum number of receiver object ids that can be passed in SuiExtraArgs.\n uint256 public constant SUI_EXTRA_ARGS_MAX_RECEIVER_OBJECT_IDS = 64;\n\n /// @dev The size of each SUI account address in bytes.\n uint256 public constant SUI_ACCOUNT_BYTE_SIZE = 32;\n\n function _argsToBytes(\n GenericExtraArgsV2 memory extraArgs\n ) internal pure returns (bytes memory bts) {\n return abi.encodeWithSelector(GENERIC_EXTRA_ARGS_V2_TAG, extraArgs);\n }\n\n function _svmArgsToBytes(\n SVMExtraArgsV1 memory extraArgs\n ) internal pure returns (bytes memory bts) {\n return abi.encodeWithSelector(SVM_EXTRA_ARGS_V1_TAG, extraArgs);\n }\n\n function _suiArgsToBytes(\n SuiExtraArgsV1 memory extraArgs\n ) internal pure returns (bytes memory bts) {\n return abi.encodeWithSelector(SUI_EXTRA_ARGS_V1_TAG, extraArgs);\n }\n}\n" + }, + "contracts/libraries/FeeTokenHandler.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {IERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/utils/SafeERC20.sol\";\n\nlibrary FeeTokenHandler {\n using SafeERC20 for IERC20;\n\n error ZeroAddressNotAllowed();\n\n event FeeTokenWithdrawn(address indexed receiver, address indexed feeToken, uint256 amount);\n\n /// @notice Withdraws the outstanding fee token balances to the fee aggregator.\n /// @param feeTokens The fee tokens to withdraw.\n /// @param feeAggregator The address to withdraw the fee tokens to, cannot be the zero address.\n function _withdrawFeeTokens(\n address[] calldata feeTokens,\n address feeAggregator\n ) internal {\n if (feeAggregator == address(0)) revert ZeroAddressNotAllowed();\n\n for (uint256 i = 0; i < feeTokens.length; ++i) {\n IERC20 feeToken = IERC20(feeTokens[i]);\n uint256 feeTokenBalance = feeToken.balanceOf(address(this));\n\n if (feeTokenBalance > 0) {\n feeToken.safeTransfer(feeAggregator, feeTokenBalance);\n\n emit FeeTokenWithdrawn(feeAggregator, address(feeToken), feeTokenBalance);\n }\n }\n }\n}\n" + }, + "contracts/libraries/FinalityCodec.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\n/// @notice This library provides encoding and validation for finality parameters used in cross-chain transfers.\n/// @dev this codec supports all the bit flags, even though some might not be assigned any meaning yet. This is\n/// intentional to allow for future flexibility.\n///\n/// @dev Bit layout of the `bytes4` finality value (32 bits, MSB on the left):\n///\n/// Bit: 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 | 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0\n/// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+\n/// | R | R | R | R | R | R | R | R | R | R | R | R | R | R | R | S | block depth (16 bits) |\n/// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+\n/// \\_______________________________ _____________________________/ \\______________________________ _____________________________/\n/// \\/ \\/\n/// flags (16 bits) depth (16 bits)\n/// max = 65535 (0xFFFF)\n///\n/// S (bit 16) = WAIT_FOR_SAFE_FLAG \u2014 wait for the `safe` tag.\n/// R (bits 17-31) = Reserved for future flags (currently unassigned; accepted on the wire).\n/// Reserved bits may be assigned in the future, read the docs for the latest bit definitions.\n///\n/// Special values:\n/// 0x00000000 WAIT_FOR_FINALITY_FLAG \u2014 wait for full finality (safest, default).\n/// 0x00010000 WAIT_FOR_SAFE_FLAG \u2014 wait for the `safe` head (bit 16 set, no depth).\n/// 0x00000001..0x0000FFFF \u2014 wait for N blocks.\nlibrary FinalityCodec {\n error InvalidRequestedFinality(bytes4 requestedFinality, bytes4 allowedFinality);\n /// @notice Requested finality must be exactly one mode: any of the flag bits or a block depth with no upper flag bits.\n /// It cannot combine a flag with a block depth.\n error RequestedFinalityCanOnlyHaveOneMode(bytes4 encodedFinality);\n\n /// @notice The block depth is stored in the lower 16 bits, leaving the upper 16 bits for flags.\n /// For more security, users should wait for finality instead (bytes4(0)).\n uint256 public constant BLOCK_DEPTH_BITS = 16;\n /// @notice The maximum block depth that can be encoded in the finality params.\n uint16 public constant MAX_BLOCK_DEPTH = type(uint16).max;\n /// @notice The block depth mask to extract the block depth from the finality params.\n bytes4 public constant BLOCK_DEPTH_MASK = bytes4(uint32(MAX_BLOCK_DEPTH));\n\n /// @notice The finality flag for waiting for finality is 0, this is the safest option. Any block depth that's deeper\n /// than finality will fall back to finality, meaning a very deep block depth will not be more secure than finality.\n bytes4 public constant WAIT_FOR_FINALITY_FLAG = bytes4(0);\n /// @notice Signals to wait for the `safe` tag.\n bytes4 public constant WAIT_FOR_SAFE_FLAG = bytes4(uint32(1 << BLOCK_DEPTH_BITS));\n\n /// @notice Helper to encode block depth into the finality params. Returns WAIT_FOR_FINALITY_FLAG if the block depth\n /// is zero.\n /// @param blockDepth The block depth to encode into the finality params.\n /// @return The encoded finality params with the block depth.\n function _encodeBlockDepth(\n uint16 blockDepth\n ) internal pure returns (bytes4) {\n return bytes4(uint32(blockDepth));\n }\n\n /// @notice Helper to encode the `safe` tag plus a block depth into the finality params.\n /// NOTE: this format is only allowed for allowed finality, not requested finality, as requested finality can only\n /// contain a single flag or block depth, but allowed finality can contain multiple.\n /// @param blockDepth The block depth to encode into the finality params.\n /// @return The encoded finality params with the `safe` tag and block depth.\n function _encodeBlockDepthAndSafeFlag(\n uint16 blockDepth\n ) internal pure returns (bytes4) {\n return _encodeBlockDepth(blockDepth) | WAIT_FOR_SAFE_FLAG;\n }\n\n /// @notice Validates requested finality: either `bytes4(0)`, exactly one set bit among the upper flag bits, or a pure\n /// block depth (no flag bits, depth in `1..MAX_BLOCK_DEPTH`). Never a flag combined with a non-zero depth. Unknown\n /// flags are accepted here for wire compatibility; pools/CCVs reject modes they do not implement.\n /// @param encodedFinality The encoded finality params to validate.\n function _validateRequestedFinality(\n bytes4 encodedFinality\n ) internal pure {\n // Waiting for finality is always valid.\n if (encodedFinality == WAIT_FOR_FINALITY_FLAG) {\n return;\n }\n bool hasBlockDepth = encodedFinality & BLOCK_DEPTH_MASK != 0;\n uint256 activeModes = hasBlockDepth ? 1 : 0; // If it has depth, it counts as one active mode.\n\n uint32 flags = uint32(encodedFinality) >> BLOCK_DEPTH_BITS;\n if (flags != 0) {\n for (uint256 i = 0; i < 16; ++i) {\n if ((flags & (1 << i)) != 0) {\n activeModes += 1;\n }\n }\n }\n // There must be exactly one active mode: either a block depth or a single flag. Selecting multiple modes is only\n // allowed for `allowedFinality` set by Pools, CCVs, etc., but not for `requestedFinality` set by senders.\n if (activeModes != 1) {\n revert RequestedFinalityCanOnlyHaveOneMode(encodedFinality);\n }\n }\n\n /// @notice Validates that `requestedFinality` is well-formed and permitted by `allowedFinality`.\n /// @param requestedFinality The requested finality params to check.\n /// @param allowedFinality The allowed finality params to check against.\n function _ensureRequestedFinalityAllowed(\n bytes4 requestedFinality,\n bytes4 allowedFinality\n ) internal pure {\n // Finality is always allowed.\n if (requestedFinality == WAIT_FOR_FINALITY_FLAG) {\n return;\n }\n\n // Validate the structural shape of the requested finality, as it is only allowed to signal one mode.\n _validateRequestedFinality(requestedFinality);\n\n // If any of the flags match, the request is allowed only when it has no depth field (flag-only request).\n if (((requestedFinality >> BLOCK_DEPTH_BITS) & (allowedFinality >> BLOCK_DEPTH_BITS)) != 0) {\n return;\n }\n // Otherwise, it must be block-depth based.\n uint32 requestedBlockDepth = uint32(requestedFinality & BLOCK_DEPTH_MASK);\n uint32 allowedBlockDepth = uint32(allowedFinality & BLOCK_DEPTH_MASK);\n if (allowedBlockDepth == 0 || requestedBlockDepth < allowedBlockDepth) {\n revert InvalidRequestedFinality(requestedFinality, allowedFinality);\n }\n }\n}\n" + }, + "contracts/libraries/Pool.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n/// @notice This library contains various token pool functions to aid constructing the return data.\nlibrary Pool {\n // The tag used to signal support for the pool v1 standard.\n // bytes4(keccak256(\"CCIP_POOL_V1\"))\n bytes4 public constant CCIP_POOL_V1 = 0xaff2afbf;\n\n // The number of bytes in the return data for a pool v1 releaseOrMint call.\n // This should match the size of the ReleaseOrMintOutV1 struct.\n uint16 public constant CCIP_POOL_V1_RET_BYTES = 32;\n\n // The default max number of bytes in the return data for a pool v1 lockOrBurn call.\n // This data can be used to send information to the destination chain token pool. Can be overwritten\n // in the TokenTransferFeeConfig.destBytesOverhead if more data is required.\n uint32 public constant CCIP_LOCK_OR_BURN_V1_RET_BYTES = 32;\n\n struct LockOrBurnInV1 {\n bytes receiver; // The recipient of the tokens on the destination chain. For EVM source chains, this is abi-encoded (32 bytes).\n uint64 remoteChainSelector; // \u2500\u256e The chain ID of the destination chain.\n address originalSender; // \u2500\u2500\u2500\u2500\u2500\u256f The original sender of the tx on the source chain.\n uint256 amount; // The amount of tokens to lock or burn, denominated in the source token's decimals.\n address localToken; // The address on this chain of the token to lock or burn.\n }\n\n struct LockOrBurnOutV1 {\n // The address of the destination token, abi encoded in the case of EVM chains.\n // This value is UNTRUSTED as any pool owner can return whatever value they want.\n bytes destTokenAddress;\n // Optional pool data to be transferred to the destination chain. Be default this is capped at\n // CCIP_LOCK_OR_BURN_V1_RET_BYTES bytes. If more data is required, the TokenTransferFeeConfig.destBytesOverhead\n // has to be set for the specific token.\n bytes destPoolData;\n }\n\n struct ReleaseOrMintInV1 {\n bytes originalSender; // The original sender of the tx on the source chain.\n uint64 remoteChainSelector; // \u2500\u2500\u2500\u256e The chain ID of the source chain.\n address receiver; // \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f The recipient of the tokens on the destination chain.\n uint256 sourceDenominatedAmount; // The amount of tokens to release or mint, denominated in the source token's decimals.\n address localToken; // The address on this chain of the token to release or mint.\n /// @dev WARNING: sourcePoolAddress should be checked prior to any processing of funds. Make sure it matches the\n /// expected pool address for the given remoteChainSelector.\n bytes sourcePoolAddress; // The address of the source pool, abi encoded in the case of EVM chains.\n bytes sourcePoolData; // The data received from the source pool to process the release or mint.\n /// @dev WARNING: offchainTokenData is untrusted data.\n bytes offchainTokenData; // The offchain data to process the release or mint.\n }\n\n struct ReleaseOrMintOutV1 {\n // The number of tokens released or minted on the destination chain, denominated in the local token's decimals.\n // This value is expected to be equal to the ReleaseOrMintInV1.amount in the case where the source and destination\n // chain have the same number of decimals.\n uint256 destinationAmount;\n }\n}\n" + }, + "contracts/libraries/RateLimiter.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.4;\n\n/// @notice Implements Token Bucket rate limiting.\n/// @dev uint128 is safe for rate limiter state.\n/// - For USD value rate limiting, it can adequately store USD value in 18 decimals.\n/// - For ERC20 token amount rate limiting, all tokens that will be listed will have at most a supply of uint128.max\n/// tokens, and it will therefore not overflow the bucket. In exceptional scenarios where tokens consumed may be larger\n/// than uint128, e.g. compromised issuer, an enabled RateLimiter will check and revert.\nlibrary RateLimiter {\n error BucketOverfilled();\n error TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress);\n error TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress);\n error InvalidRateLimitRate(Config rateLimiterConfig);\n error DisabledNonZeroRateLimit(Config config);\n\n struct TokenBucket {\n uint128 tokens; // \u2500\u2500\u2500\u2500\u256e Current number of tokens that are in the bucket.\n uint32 lastUpdated; // \u2502 Timestamp in seconds of the last token refill, good for 100+ years.\n bool isEnabled; // \u2500\u2500\u2500\u2500\u256f Indication whether the rate limiting is enabled or not.\n uint128 capacity; // \u2500\u2500\u256e Maximum number of tokens that can be in the bucket.\n uint128 rate; // \u2500\u2500\u2500\u2500\u2500\u2500\u256f Number of tokens per second that the bucket is refilled.\n }\n\n struct Config {\n bool isEnabled; // Indication whether the rate limiting should be enabled.\n uint128 capacity; // \u2500\u2500\u256e Specifies the capacity of the rate limiter.\n uint128 rate; // \u2500\u2500\u2500\u2500\u2500\u256f Specifies the rate of the rate limiter.\n }\n\n /// @notice _consume removes the given tokens from the pool, lowering the rate tokens allowed to be\n /// consumed for subsequent calls.\n /// @param requestTokens The total tokens to be consumed from the bucket.\n /// @param tokenAddress The token to consume capacity for, use 0x0 to indicate aggregate value capacity.\n /// @dev Reverts when requestTokens exceeds bucket capacity or available tokens in the bucket.\n /// @dev emits removal of requestTokens if requestTokens is > 0.\n function _consume(\n TokenBucket storage s_bucket,\n uint256 requestTokens,\n address tokenAddress\n ) internal {\n // If there is no value to remove or rate limiting is turned off, skip this step to reduce gas usage.\n if (!s_bucket.isEnabled || requestTokens == 0) {\n return;\n }\n\n uint256 tokens = s_bucket.tokens;\n uint256 capacity = s_bucket.capacity;\n uint256 timeDiff = block.timestamp - s_bucket.lastUpdated;\n\n if (timeDiff != 0) {\n if (tokens > capacity) revert BucketOverfilled();\n\n // Refill tokens when arriving at a new block time.\n tokens = _calculateRefill(capacity, tokens, timeDiff, s_bucket.rate);\n\n s_bucket.lastUpdated = uint32(block.timestamp);\n }\n\n if (capacity < requestTokens) {\n revert TokenMaxCapacityExceeded(capacity, requestTokens, tokenAddress);\n }\n if (tokens < requestTokens) {\n uint256 rate = s_bucket.rate;\n if (rate == 0) {\n // No tokens will ever be refilled. Check is required to avoid division by zero later.\n revert TokenRateLimitReached(type(uint256).max, tokens, tokenAddress);\n }\n // Wait required until the bucket is refilled enough to accept this value, round up to next higher second.\n // Consume is not guaranteed to succeed after wait time passes if there is competing traffic.\n // This acts as a lower bound of wait time.\n uint256 minWaitInSeconds = ((requestTokens - tokens) + (rate - 1)) / rate;\n\n revert TokenRateLimitReached(minWaitInSeconds, tokens, tokenAddress);\n }\n tokens -= requestTokens;\n\n // Downcast is safe here, as tokens is not larger than capacity.\n s_bucket.tokens = uint128(tokens);\n }\n\n /// @notice Gets the token bucket with its values for the block it was requested at.\n /// @return The token bucket.\n function _currentTokenBucketState(\n TokenBucket memory bucket\n ) internal view returns (TokenBucket memory) {\n // We update the bucket to reflect the status at the exact time of the call. This means we might need to refill a\n // part of the bucket based on the time that has passed since the last update.\n bucket.tokens =\n uint128(_calculateRefill(bucket.capacity, bucket.tokens, block.timestamp - bucket.lastUpdated, bucket.rate));\n bucket.lastUpdated = uint32(block.timestamp);\n return bucket;\n }\n\n /// @notice Sets the rate limited config.\n /// @param s_bucket The token bucket.\n /// @param config The new config.\n function _setTokenBucketConfig(\n TokenBucket storage s_bucket,\n Config memory config\n ) internal {\n if (config.isEnabled) {\n if (config.rate > config.capacity) {\n revert InvalidRateLimitRate(config);\n }\n } else {\n if (config.rate != 0 || config.capacity != 0) {\n revert DisabledNonZeroRateLimit(config);\n }\n }\n\n s_bucket.isEnabled = config.isEnabled;\n s_bucket.tokens = config.capacity;\n s_bucket.capacity = config.capacity;\n s_bucket.rate = config.rate;\n s_bucket.lastUpdated = uint32(block.timestamp);\n }\n\n /// @notice Calculate refilled tokens.\n /// @param capacity bucket capacity.\n /// @param tokens current bucket tokens.\n /// @param timeDiff block time difference since last refill.\n /// @param rate bucket refill rate.\n /// @return the value of tokens after refill.\n function _calculateRefill(\n uint256 capacity,\n uint256 tokens,\n uint256 timeDiff,\n uint256 rate\n ) private pure returns (uint256) {\n return _min(capacity, tokens + timeDiff * rate);\n }\n\n /// @notice Return the smallest of two integers.\n /// @param a first int.\n /// @param b second int.\n /// @return smallest.\n function _min(\n uint256 a,\n uint256 b\n ) internal pure returns (uint256) {\n return a < b ? a : b;\n }\n}\n" + }, + "contracts/pools/LockReleaseTokenPool.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.24;\n\nimport {ILockBox} from \"../interfaces/ILockBox.sol\";\nimport {ITypeAndVersion} from \"@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol\";\n\nimport {TokenPool} from \"./TokenPool.sol\";\n\nimport {IERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/utils/SafeERC20.sol\";\n\n/// @notice Token pool used for tokens on their native chain. This uses a lock and release mechanism.\n/// @dev One token per LockReleaseTokenPool.\ncontract LockReleaseTokenPool is TokenPool, ITypeAndVersion {\n using SafeERC20 for IERC20;\n\n function typeAndVersion() external pure virtual override returns (string memory) {\n return \"LockReleaseTokenPool 2.0.0\";\n }\n\n /// @notice The lock box for the token pool.\n ILockBox internal immutable i_lockBox;\n\n constructor(\n IERC20 token,\n uint8 localTokenDecimals,\n address advancedPoolHooks,\n address rmnProxy,\n address router,\n address lockBox\n ) TokenPool(token, localTokenDecimals, advancedPoolHooks, rmnProxy, router) {\n if (lockBox == address(0)) revert ZeroAddressInvalid();\n\n ILockBox lockBoxContract = ILockBox(lockBox);\n if (!lockBoxContract.isTokenSupported(address(token))) {\n revert InvalidToken(address(token));\n }\n token.forceApprove(lockBox, type(uint256).max);\n i_lockBox = lockBoxContract;\n }\n\n /// @notice Gets the lock box address.\n function getLockBox() external view returns (address) {\n return address(i_lockBox);\n }\n\n /// @inheritdoc TokenPool\n /// @dev The router has already transferred the full amount to this contract before calling lockOrBurn.\n /// For V1 the amount = full amount. For V2 the amount = destTokenAmount (after fees), and fees remain on this contract.\n function _lockOrBurn(\n uint64 remoteChainSelector,\n uint256 amount\n ) internal override {\n i_lockBox.deposit(address(i_token), remoteChainSelector, amount);\n }\n\n /// @inheritdoc TokenPool\n /// @dev Releases tokens from the lock box to the receiver.\n function _releaseOrMint(\n address receiver,\n uint256 amount,\n uint64 remoteChainSelector\n ) internal override {\n i_lockBox.withdraw(address(i_token), remoteChainSelector, amount, receiver);\n }\n}\n" + }, + "contracts/pools/TokenPool.sol": { + "content": "// SPDX-License-Identifier: BUSL-1.1\npragma solidity ^0.8.24;\n\nimport {IAdvancedPoolHooks} from \"../interfaces/IAdvancedPoolHooks.sol\";\nimport {IPoolV1} from \"../interfaces/IPool.sol\";\nimport {IPoolV1V2} from \"../interfaces/IPoolV1V2.sol\";\nimport {IPoolV2} from \"../interfaces/IPoolV2.sol\";\nimport {IRMN} from \"../interfaces/IRMN.sol\";\nimport {IRouter} from \"../interfaces/IRouter.sol\";\n\nimport {FeeTokenHandler} from \"../libraries/FeeTokenHandler.sol\";\nimport {FinalityCodec} from \"../libraries/FinalityCodec.sol\";\nimport {Pool} from \"../libraries/Pool.sol\";\nimport {RateLimiter} from \"../libraries/RateLimiter.sol\";\nimport {Ownable2StepMsgSender} from \"@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol\";\n\nimport {IERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol\";\nimport {IERC20Metadata} from \"@openzeppelin/contracts@5.3.0/token/ERC20/extensions/IERC20Metadata.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts@5.3.0/token/ERC20/utils/SafeERC20.sol\";\nimport {IERC165} from \"@openzeppelin/contracts@5.3.0/utils/introspection/IERC165.sol\";\nimport {EnumerableSet} from \"@openzeppelin/contracts@5.3.0/utils/structs/EnumerableSet.sol\";\n\n/// @notice Base abstract class with common functions for all token pools.\n/// A token pool serves as isolated place for holding tokens and token specific logic\n/// that may execute as tokens move across the bridge.\n/// @dev This pool supports different decimals on different chains but using this feature could impact the total number\n/// of tokens in circulation. Since all of the tokens are locked/burned on the source, and a rounded amount is\n/// minted/released on the destination, the number of tokens minted/released could be less than the number of tokens\n/// burned/locked. This is because the source chain does not know about the destination token decimals. This is not a\n/// problem if the decimals are the same on both chains.\n///\n/// Example:\n/// Assume there is a token with 6 decimals on chain A and 3 decimals on chain B.\n/// - 1.234567 tokens are burned on chain A.\n/// - 1.234 tokens are minted on chain B.\n/// When sending the 1.234 tokens back to chain A, you will receive 1.234000 tokens on chain A, effectively losing\n/// 0.000567 tokens.\n/// In the case of a burnMint pool on chain A, these funds are burned in the pool on chain A.\n/// In the case of a lockRelease pool on chain A, these funds accumulate in the pool on chain A.\nabstract contract TokenPool is IPoolV1V2, Ownable2StepMsgSender {\n using EnumerableSet for EnumerableSet.Bytes32Set;\n using EnumerableSet for EnumerableSet.UintSet;\n using RateLimiter for RateLimiter.TokenBucket;\n using SafeERC20 for IERC20;\n\n error InvalidTransferFeeBps(uint256 bps);\n error InvalidTokenTransferFeeConfig(uint64 destChainSelector);\n error CallerIsNotARampOnRouter(address caller);\n error ZeroAddressInvalid();\n error NonExistentChain(uint64 remoteChainSelector);\n error ChainNotAllowed(uint64 remoteChainSelector);\n error CursedByRMN();\n error ChainAlreadyExists(uint64 chainSelector);\n error InvalidSourcePoolAddress(bytes sourcePoolAddress);\n error InvalidToken(address token);\n error Unauthorized(address caller);\n error PoolAlreadyAdded(uint64 remoteChainSelector, bytes remotePoolAddress);\n error InvalidRemotePoolForChain(uint64 remoteChainSelector, bytes remotePoolAddress);\n error InvalidRemoteChainDecimals(bytes sourcePoolData);\n error OverflowDetected(uint8 remoteDecimals, uint8 localDecimals, uint256 remoteAmount);\n error InvalidDecimalArgs(uint8 expected, uint8 actual);\n error CallerIsNotOwnerOrFeeAdmin(address caller);\n\n event LockedOrBurned(uint64 indexed remoteChainSelector, address token, address sender, uint256 amount);\n event ReleasedOrMinted(\n uint64 indexed remoteChainSelector, address token, address sender, address recipient, uint256 amount\n );\n event ChainAdded(\n uint64 remoteChainSelector,\n bytes remoteToken,\n RateLimiter.Config outboundRateLimiterConfig,\n RateLimiter.Config inboundRateLimiterConfig\n );\n event ChainRemoved(uint64 remoteChainSelector);\n event RemotePoolAdded(uint64 indexed remoteChainSelector, bytes remotePoolAddress);\n event RemotePoolRemoved(uint64 indexed remoteChainSelector, bytes remotePoolAddress);\n event DynamicConfigSet(address router, address rateLimitAdmin, address feeAdmin);\n event OutboundRateLimitConsumed(uint64 indexed remoteChainSelector, address token, uint256 amount);\n event InboundRateLimitConsumed(uint64 indexed remoteChainSelector, address token, uint256 amount);\n event TokenTransferFeeConfigUpdated(uint64 indexed destChainSelector, TokenTransferFeeConfig tokenTransferFeeConfig);\n event TokenTransferFeeConfigDeleted(uint64 indexed destChainSelector);\n event FastFinalityOutboundRateLimitConsumed(uint64 indexed remoteChainSelector, address token, uint256 amount);\n event FastFinalityInboundRateLimitConsumed(uint64 indexed remoteChainSelector, address token, uint256 amount);\n event RateLimitConfigured(\n uint64 indexed remoteChainSelector,\n bool fastFinality,\n RateLimiter.Config outboundRateLimiterConfig,\n RateLimiter.Config inboundRateLimiterConfig\n );\n event FinalityConfigSet(bytes4 allowedFinality);\n event AdvancedPoolHooksUpdated(IAdvancedPoolHooks oldHook, IAdvancedPoolHooks newHook);\n\n struct ChainUpdate {\n uint64 remoteChainSelector; // Remote chain selector.\n bytes[] remotePoolAddresses; // Address of the remote pool, ABI encoded in the case of a remote EVM chain.\n bytes remoteTokenAddress; // Address of the remote token, ABI encoded in the case of a remote EVM chain.\n RateLimiter.Config outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain.\n RateLimiter.Config inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain.\n }\n\n struct RemoteChainConfig {\n RateLimiter.TokenBucket outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain.\n RateLimiter.TokenBucket inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain.\n bytes remoteTokenAddress; // Address of the remote token, ABI encoded in the case of a remote EVM chain.\n EnumerableSet.Bytes32Set remotePools; // Set of remote pool hashes, ABI encoded in the case of a remote EVM chain.\n }\n\n struct RateLimitConfigArgs {\n uint64 remoteChainSelector; // Remote chain selector.\n bool fastFinality; // Whether the rate limit config is for fast finality transfers.\n RateLimiter.Config outboundRateLimiterConfig; // Outbound rate limiter configuration.\n RateLimiter.Config inboundRateLimiterConfig; // Inbound rate limiter configuration.\n }\n\n /// @dev Struct with args for setting the token transfer fee configurations for a destination chain and a set of tokens.\n struct TokenTransferFeeConfigArgs {\n uint64 destChainSelector; // Destination chain selector.\n TokenTransferFeeConfig tokenTransferFeeConfig; // Token transfer fee configuration.\n }\n\n /// @notice The division factor for bps. This also represents the maximum bps fee.\n uint256 internal constant BPS_DIVIDER = 10_000;\n /// @dev The bridgeable token that is managed by this pool. Pools could support multiple tokens at the same time if\n /// required, but this implementation only supports one token.\n IERC20 internal immutable i_token;\n /// @dev The number of decimals of the token managed by this pool.\n uint8 internal immutable i_tokenDecimals;\n /// @dev The address of the RMN proxy.\n address internal immutable i_rmnProxy;\n\n /// @dev The address of the router.\n IRouter internal s_router;\n /// @dev Allowed finality config for fast finality transfers (see `FinalityCodec`).\n /// FinalityCodec.WAIT_FOR_FINALITY_FLAG means wait for finality.\n bytes4 internal s_allowedFinalityConfig;\n /// @dev Optional advanced pool hooks contract for additional features like allowlists and CCV management.\n IAdvancedPoolHooks internal s_advancedPoolHooks;\n /// @dev Separate buckets provide isolated rate limits for fast finality transfers, as their risk\n /// profiles differ from default transfers. When these are not configured, the default buckets are used for all\n /// transfers regardless of the finality requirements.\n mapping(uint64 remoteChainSelector => RateLimiter.TokenBucket tokenBucketOutbound) internal\n s_fastFinalityOutboundRateLimiterConfig;\n mapping(uint64 remoteChainSelector => RateLimiter.TokenBucket tokenBucketInbound) internal\n s_fastFinalityInboundRateLimiterConfig;\n /// @dev A set of allowed chain selectors. We want the allowlist to be enumerable to\n /// be able to quickly determine (without parsing logs) who can access the pool.\n /// @dev The chain selectors are in uint256 format because of the EnumerableSet implementation.\n EnumerableSet.UintSet internal s_remoteChainSelectors;\n mapping(uint64 remoteChainSelector => RemoteChainConfig) internal s_remoteChainConfigs;\n /// @notice A mapping of hashed pool addresses to their unhashed form. This is used to be able to find the actually\n /// configured pools and not just their hashed versions.\n mapping(bytes32 poolAddressHash => bytes poolAddress) internal s_remotePoolAddresses;\n /// @notice The address of the rate limiter admin.\n /// @dev Can be address(0) if none is configured.\n address internal s_rateLimitAdmin;\n /// @dev Optional token-transfer fee overrides keyed by destination chain selector.\n mapping(uint64 destChainSelector => TokenTransferFeeConfig tokenTransferFeeConfig) internal s_tokenTransferFeeConfig;\n /// @notice The address of the fee admin.\n /// @dev Constructor does not set this value so it is opt in only.\n address internal s_feeAdmin;\n\n constructor(\n IERC20 token,\n uint8 localTokenDecimals,\n address advancedPoolHooks,\n address rmnProxy,\n address router\n ) {\n if (address(token) == address(0) || router == address(0) || rmnProxy == address(0)) {\n revert ZeroAddressInvalid();\n }\n i_token = token;\n i_rmnProxy = rmnProxy;\n\n // In the case the token is also the pool, it won't exist yet so we skip this check.\n if (address(token) != address(this)) {\n try IERC20Metadata(address(token)).decimals() returns (uint8 actualTokenDecimals) {\n if (localTokenDecimals != actualTokenDecimals) {\n revert InvalidDecimalArgs(localTokenDecimals, actualTokenDecimals);\n }\n } catch {\n // The decimals function doesn't exist, which is possible since it's optional in the ERC20 spec. We skip the\n // check and assume the supplied token decimals are correct.\n }\n }\n i_tokenDecimals = localTokenDecimals;\n s_advancedPoolHooks = IAdvancedPoolHooks(advancedPoolHooks);\n\n s_router = IRouter(router);\n }\n\n /// @inheritdoc IPoolV1\n /// @param token The token address to check.\n function isSupportedToken(\n address token\n ) public view virtual returns (bool) {\n return token == address(i_token);\n }\n\n /// @notice Gets the IERC20 token that this pool can lock or burn.\n /// @return token The IERC20 token representation.\n function getToken() public view virtual returns (IERC20 token) {\n return i_token;\n }\n\n /// @notice Get RMN proxy address.\n /// @return rmnProxy Address of RMN proxy.\n function getRmnProxy() public view virtual returns (address rmnProxy) {\n return i_rmnProxy;\n }\n\n /// @notice Gets the pools dynamic configuration.\n function getDynamicConfig() public view virtual returns (address router, address rateLimitAdmin, address feeAdmin) {\n return (address(s_router), s_rateLimitAdmin, s_feeAdmin);\n }\n\n /// @notice Gets the finality config as defined in the FinalityCodec library. This value does NOT 1:1 translate to\n /// a block depth. The finality config contains special flags and should only be encoded/decoded using the\n /// FinalityCodec library. Checks must happen by calling `FinalityCodec._ensureRequestedFinalityAllowed`.\n function getAllowedFinalityConfig() public view virtual returns (bytes4 allowedFinality) {\n return s_allowedFinalityConfig;\n }\n\n /// @notice Gets the advanced pool hook contract address used by this pool.\n function getAdvancedPoolHooks() public view virtual returns (IAdvancedPoolHooks advancedPoolHook) {\n return s_advancedPoolHooks;\n }\n\n /// @notice Sets the dynamic configuration for the pool.\n /// @param router The address of the router contract.\n /// @param rateLimitAdmin The address of the rate limiter admin.\n /// @param feeAdmin An additional address that can withdraw fees from this contract.\n /// @dev FeeTokenHandler will revert if feeAdmin is zero when withdrawing fees.\n /// @dev If only the owner can withdraw fees, set feeAdmin to address(0).\n function setDynamicConfig(\n address router,\n address rateLimitAdmin,\n address feeAdmin\n ) public virtual onlyOwner {\n if (router == address(0)) revert ZeroAddressInvalid();\n s_router = IRouter(router);\n s_rateLimitAdmin = rateLimitAdmin;\n s_feeAdmin = feeAdmin;\n\n emit DynamicConfigSet(router, rateLimitAdmin, feeAdmin);\n }\n\n /// @notice Sets the finality config according to the FinalityCodec library encoding.\n /// @param allowedFinality The finality settings allowed in this pool, according to the FinalityCodec encoding.\n function setAllowedFinalityConfig(\n bytes4 allowedFinality\n ) public virtual onlyOwner {\n // Any bytes4 value is accepted as allowedFinality; the FinalityCodec semantics are enforced when requests are\n // checked against this value via FinalityCodec._ensureRequestedFinalityAllowed.\n s_allowedFinalityConfig = allowedFinality;\n\n emit FinalityConfigSet(allowedFinality);\n }\n\n /// @notice Updates the advanced pool hook.\n /// @param newHook The new advanced pool hooks contract.\n function updateAdvancedPoolHooks(\n IAdvancedPoolHooks newHook\n ) public virtual onlyOwner {\n emit AdvancedPoolHooksUpdated(s_advancedPoolHooks, newHook);\n s_advancedPoolHooks = newHook;\n }\n\n /// @notice Signals which version of the pool interface is supported.\n /// @param interfaceId The interface identifier, as specified in ERC-165.\n function supportsInterface(\n bytes4 interfaceId\n ) public view virtual override returns (bool) {\n return interfaceId == Pool.CCIP_POOL_V1 || interfaceId == type(IPoolV2).interfaceId\n || interfaceId == type(IPoolV1).interfaceId || interfaceId == type(IERC165).interfaceId;\n }\n\n // ================================================================\n // \u2502 Lock or Burn \u2502\n // ================================================================\n\n /// @inheritdoc IPoolV2\n /// @dev The _validateLockOrBurn check is an essential security check.\n /// @dev The _getFee function deducts the fee from the amount and returns the amount after fee deduction.\n /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain.\n /// @param requestedFinalityConfig Requested finality config according to the FinalityCodec.\n /// @param tokenArgs Additional token arguments.\n function lockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes calldata tokenArgs\n ) public virtual returns (Pool.LockOrBurnOutV1 memory, uint256 destTokenAmount) {\n uint256 feeAmount = _getFee(lockOrBurnIn, requestedFinalityConfig);\n _validateLockOrBurn(lockOrBurnIn, requestedFinalityConfig, tokenArgs, feeAmount);\n destTokenAmount = lockOrBurnIn.amount - feeAmount;\n _lockOrBurn(lockOrBurnIn.remoteChainSelector, destTokenAmount);\n\n emit LockedOrBurned({\n remoteChainSelector: lockOrBurnIn.remoteChainSelector,\n token: lockOrBurnIn.localToken,\n sender: msg.sender,\n amount: destTokenAmount\n });\n\n return (\n Pool.LockOrBurnOutV1({\n destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: _encodeLocalDecimals()\n }),\n destTokenAmount\n );\n }\n\n /// @inheritdoc IPoolV1\n /// @dev The _validateLockOrBurn check is an essential security check.\n /// @dev _getFee is not called in this legacy method, so the full amount is locked or burned.\n /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain.\n function lockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn\n ) public virtual returns (Pool.LockOrBurnOutV1 memory lockOrBurnOutV1) {\n _validateLockOrBurn(lockOrBurnIn, FinalityCodec.WAIT_FOR_FINALITY_FLAG, \"\", 0); // feeAmount is zero\n _lockOrBurn(lockOrBurnIn.remoteChainSelector, lockOrBurnIn.amount);\n\n emit LockedOrBurned({\n remoteChainSelector: lockOrBurnIn.remoteChainSelector,\n token: lockOrBurnIn.localToken,\n sender: msg.sender,\n amount: lockOrBurnIn.amount\n });\n\n return Pool.LockOrBurnOutV1({\n destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: _encodeLocalDecimals()\n });\n }\n\n /// @notice Contains the specific lock or burn token logic for a pool.\n /// @dev overriding this method allows us to create pools with different lock/burn signatures\n /// without duplicating the underlying logic.\n /// @param remoteChainSelector The selector of the remote chain.\n /// @param amount The amount of tokens to lock or burn.\n function _lockOrBurn(\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {}\n\n // ================================================================\n // \u2502 Release or Mint \u2502\n // ================================================================\n\n /// @inheritdoc IPoolV2\n /// @dev The _validateReleaseOrMint check is an essential security check.\n /// @param releaseOrMintIn Encoded data fields for the processing of tokens on the destination chain.\n /// @param requestedFinalityConfig Requested finality config according to the FinalityCodec.\n function releaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n bytes4 requestedFinalityConfig\n ) public virtual override(IPoolV2) returns (Pool.ReleaseOrMintOutV1 memory) {\n uint256 localAmount = _calculateLocalAmount(\n releaseOrMintIn.sourceDenominatedAmount, _parseRemoteDecimals(releaseOrMintIn.sourcePoolData)\n );\n\n _validateReleaseOrMint(releaseOrMintIn, localAmount, requestedFinalityConfig);\n\n _releaseOrMint(releaseOrMintIn.receiver, localAmount, releaseOrMintIn.remoteChainSelector);\n\n emit ReleasedOrMinted({\n remoteChainSelector: releaseOrMintIn.remoteChainSelector,\n token: releaseOrMintIn.localToken,\n sender: msg.sender,\n recipient: releaseOrMintIn.receiver,\n amount: localAmount\n });\n\n return Pool.ReleaseOrMintOutV1({destinationAmount: localAmount});\n }\n\n /// @inheritdoc IPoolV1\n /// @dev calls IPoolV2.releaseOrMint with default finality.\n /// @param releaseOrMintIn Encoded data fields for the processing of tokens on the destination chain.\n function releaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn\n ) public virtual override returns (Pool.ReleaseOrMintOutV1 memory) {\n return releaseOrMint(releaseOrMintIn, FinalityCodec.WAIT_FOR_FINALITY_FLAG);\n }\n\n /// @notice Contains the specific release or mint token logic for a pool.\n /// @dev overriding this method allows us to create pools with different release/mint signatures\n /// without duplicating the underlying logic.\n /// @param receiver The address to receive the tokens.\n /// @param amount The amount of tokens to release or mint.\n /// @param remoteChainSelector The selector of the remote chain.\n function _releaseOrMint(\n address receiver,\n uint256 amount,\n uint64 remoteChainSelector\n ) internal virtual {}\n\n // ================================================================\n // \u2502 Validation \u2502\n // ================================================================\n\n /// @notice Validates the lock or burn input for correctness on\n /// - token to be locked or burned\n /// - RMN curse status\n /// - if the sender is a valid onRamp\n /// - rate limiting for either default or FTF transfer messages.\n /// - preflight checks hooks (if enabled)\n /// @param lockOrBurnIn The input to validate.\n /// @param requestedFinality The requested finality speed according to the FinalityCodec encoding.\n /// @param tokenArgs Additional token arguments passed in by the sender of the message.\n /// @param feeAmount The fee amount deducted from the transfer amount.\n /// @dev This function should always be called before executing a lock or burn. Not doing so would allow\n /// for various exploits.\n function _validateLockOrBurn(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinality,\n bytes memory tokenArgs,\n uint256 feeAmount\n ) internal virtual {\n if (!isSupportedToken(lockOrBurnIn.localToken)) {\n revert InvalidToken(lockOrBurnIn.localToken);\n }\n if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(lockOrBurnIn.remoteChainSelector)))) revert CursedByRMN();\n\n _onlyOnRamp(lockOrBurnIn.remoteChainSelector);\n\n uint256 amount = lockOrBurnIn.amount - feeAmount;\n\n // If FTF is requested, validate against the allowed and apply the custom rate limit.\n if (requestedFinality != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n // Use the codec to validate that the requested finality is allowed by the pool's configuration. This will revert\n // if the requested finality is not allowed.\n FinalityCodec._ensureRequestedFinalityAllowed(requestedFinality, s_allowedFinalityConfig);\n _consumeFastFinalityOutboundRateLimit(lockOrBurnIn.localToken, lockOrBurnIn.remoteChainSelector, amount);\n } else {\n _consumeOutboundRateLimit(lockOrBurnIn.localToken, lockOrBurnIn.remoteChainSelector, amount);\n }\n\n _preflightCheck(lockOrBurnIn, requestedFinality, tokenArgs, amount);\n }\n\n /// @notice Hook for pre-flight checks on lock or burn.\n /// @dev These hooks are optional but take up a lot of space in the contracts bytecode. To avoid this overhead when\n /// not needed, you can override this function in the derived contract with an empty implementation. This will result\n /// in the compiler removing the function and all related code, saving close to 1KB.\n /// @param lockOrBurnIn The input to validate.\n /// @param requestedFinalityConfig The requested finality config according to the FinalityCodec encoding.\n /// @param tokenArgs Additional token arguments passed in by the sender of the message.\n /// @param amountPostFee The amount after token pool bps-based fees have been deducted.\n function _preflightCheck(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig,\n bytes memory tokenArgs,\n uint256 amountPostFee\n ) internal virtual {\n if (address(s_advancedPoolHooks) != address(0)) {\n s_advancedPoolHooks.preflightCheck(lockOrBurnIn, requestedFinalityConfig, tokenArgs, amountPostFee);\n }\n }\n\n /// @notice Validates the release or mint input for correctness on\n /// - token to be released or minted\n /// - RMN curse status\n /// - if the sender is a valid offRamp\n /// - if the source pool is configured for the remote chain\n /// - rate limiting for either default or FTF transfer messages.\n /// @param releaseOrMintIn The input to validate.\n /// @param localAmount The local amount to be released or minted.\n /// @param requestedFinalityConfig The requested finality encoding (see `FinalityCodec`).\n /// @dev This function should always be called before executing a release or mint. Not doing so would allow\n /// for various exploits.\n function _validateReleaseOrMint(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n uint256 localAmount,\n bytes4 requestedFinalityConfig\n ) internal virtual {\n if (!isSupportedToken(releaseOrMintIn.localToken)) {\n revert InvalidToken(releaseOrMintIn.localToken);\n }\n if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(releaseOrMintIn.remoteChainSelector)))) revert CursedByRMN();\n _onlyOffRamp(releaseOrMintIn.remoteChainSelector);\n\n // Validates that the source pool address is configured on this pool.\n if (!isRemotePool(releaseOrMintIn.remoteChainSelector, releaseOrMintIn.sourcePoolAddress)) {\n revert InvalidSourcePoolAddress(releaseOrMintIn.sourcePoolAddress);\n }\n if (requestedFinalityConfig != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n _consumeFastFinalityInboundRateLimit(releaseOrMintIn.localToken, releaseOrMintIn.remoteChainSelector, localAmount);\n } else {\n _consumeInboundRateLimit(releaseOrMintIn.localToken, releaseOrMintIn.remoteChainSelector, localAmount);\n }\n\n _postflightCheck(releaseOrMintIn, localAmount, requestedFinalityConfig);\n }\n\n /// @notice Hook for post-flight checks on release or mint.\n /// @dev These hooks are optional but take up a lot of space in the contracts bytecode. To avoid this overhead when\n /// not needed, you can override this function in the derived contract with an empty implementation. This will result\n /// in the compiler removing the function and all related code, saving close to 1KB.\n /// @param releaseOrMintIn The input to validate.\n /// @param localAmount The local amount to be released or minted.\n /// @param requestedFinalityConfig The requested finality encoding (see `FinalityCodec`).\n function _postflightCheck(\n Pool.ReleaseOrMintInV1 calldata releaseOrMintIn,\n uint256 localAmount,\n bytes4 requestedFinalityConfig\n ) internal virtual {\n if (address(s_advancedPoolHooks) != address(0)) {\n s_advancedPoolHooks.postflightCheck(releaseOrMintIn, localAmount, requestedFinalityConfig);\n }\n }\n\n // ================================================================\n // \u2502 Token decimals \u2502\n // ================================================================\n\n /// @notice Gets the IERC20 token decimals on the local chain.\n function getTokenDecimals() public view virtual returns (uint8 decimals) {\n return i_tokenDecimals;\n }\n\n function _encodeLocalDecimals() internal view virtual returns (bytes memory) {\n return abi.encode(i_tokenDecimals);\n }\n\n function _parseRemoteDecimals(\n bytes memory sourcePoolData\n ) internal view virtual returns (uint8) {\n // Fallback to the local token decimals if the source pool data is empty. This allows for backwards compatibility.\n if (sourcePoolData.length == 0) {\n return i_tokenDecimals;\n }\n if (sourcePoolData.length != 32) {\n revert InvalidRemoteChainDecimals(sourcePoolData);\n }\n uint256 remoteDecimals = abi.decode(sourcePoolData, (uint256));\n if (remoteDecimals > type(uint8).max) {\n revert InvalidRemoteChainDecimals(sourcePoolData);\n }\n return uint8(remoteDecimals);\n }\n\n /// @notice Calculates the local amount based on the remote amount and decimals.\n /// @param remoteAmount The amount on the remote chain.\n /// @param remoteDecimals The decimals of the token on the remote chain.\n /// @return The local amount.\n /// @dev This function protects against overflows. If there is a transaction that hits the overflow check, it is\n /// probably incorrect as that means the amount cannot be represented on this chain. If the local decimals have been\n /// wrongly configured, the token issuer could redeploy the pool with the correct decimals and manually re-execute the\n /// CCIP tx to fix the issue.\n function _calculateLocalAmount(\n uint256 remoteAmount,\n uint8 remoteDecimals\n ) internal view virtual returns (uint256) {\n if (remoteDecimals == i_tokenDecimals) {\n return remoteAmount;\n }\n if (remoteDecimals > i_tokenDecimals) {\n uint8 decimalsDiff = remoteDecimals - i_tokenDecimals;\n if (decimalsDiff > 77) {\n // This is a safety check to prevent overflow in the next calculation.\n revert OverflowDetected(remoteDecimals, i_tokenDecimals, remoteAmount);\n }\n // Solidity rounds down so there is no risk of minting more tokens than the remote chain sent.\n return remoteAmount / (10 ** decimalsDiff);\n }\n\n // This is a safety check to prevent overflow in the next calculation.\n // More than 77 would never fit in a uint256 and would cause an overflow. We also check if the resulting amount\n // would overflow.\n uint8 diffDecimals = i_tokenDecimals - remoteDecimals;\n if (diffDecimals > 77 || remoteAmount > type(uint256).max / (10 ** diffDecimals)) {\n revert OverflowDetected(remoteDecimals, i_tokenDecimals, remoteAmount);\n }\n\n return remoteAmount * (10 ** diffDecimals);\n }\n\n // ================================================================\n // \u2502 Chain permissions \u2502\n // ================================================================\n\n /// @notice Gets the pool address on the remote chain.\n /// @param remoteChainSelector Remote chain selector.\n /// @dev To support non-evm chains, this value is encoded into bytes\n function getRemotePools(\n uint64 remoteChainSelector\n ) public view virtual returns (bytes[] memory) {\n bytes32[] memory remotePoolHashes = s_remoteChainConfigs[remoteChainSelector].remotePools.values();\n\n bytes[] memory remotePools = new bytes[](remotePoolHashes.length);\n for (uint256 i = 0; i < remotePoolHashes.length; ++i) {\n remotePools[i] = s_remotePoolAddresses[remotePoolHashes[i]];\n }\n\n return remotePools;\n }\n\n /// @notice Checks if the pool address is configured on the remote chain.\n /// @param remoteChainSelector Remote chain selector.\n /// @param remotePoolAddress The address of the remote pool.\n function isRemotePool(\n uint64 remoteChainSelector,\n bytes memory remotePoolAddress\n ) public view virtual returns (bool) {\n return s_remoteChainConfigs[remoteChainSelector].remotePools.contains(keccak256(remotePoolAddress));\n }\n\n /// @inheritdoc IPoolV2\n /// @param remoteChainSelector Remote chain selector.\n function getRemoteToken(\n uint64 remoteChainSelector\n ) public view virtual returns (bytes memory) {\n return s_remoteChainConfigs[remoteChainSelector].remoteTokenAddress;\n }\n\n /// @notice Adds a remote pool for a given chain selector. This could be due to a pool being upgraded on the remote\n /// chain. We don't simply want to replace the old pool as there could still be valid inflight messages from the old\n /// pool. This function allows for multiple pools to be added for a single chain selector.\n /// @param remoteChainSelector The remote chain selector for which the remote pool address is being added.\n /// @param remotePoolAddress The address of the new remote pool.\n function addRemotePool(\n uint64 remoteChainSelector,\n bytes calldata remotePoolAddress\n ) external virtual onlyOwner {\n if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector);\n\n _setRemotePool(remoteChainSelector, remotePoolAddress);\n }\n\n /// @notice Removes the remote pool address for a given chain selector.\n /// @dev All inflight txs from the remote pool will be rejected after it is removed. To ensure no loss of funds, there\n /// should be no inflight txs from the given pool.\n /// @param remoteChainSelector The remote chain selector.\n /// @param remotePoolAddress The remote pool address to remove.\n function removeRemotePool(\n uint64 remoteChainSelector,\n bytes calldata remotePoolAddress\n ) external virtual onlyOwner {\n if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector);\n\n if (!s_remoteChainConfigs[remoteChainSelector].remotePools.remove(keccak256(remotePoolAddress))) {\n revert InvalidRemotePoolForChain(remoteChainSelector, remotePoolAddress);\n }\n\n emit RemotePoolRemoved(remoteChainSelector, remotePoolAddress);\n }\n\n /// @inheritdoc IPoolV1\n /// @param remoteChainSelector The remote chain selector to check.\n function isSupportedChain(\n uint64 remoteChainSelector\n ) public view virtual returns (bool) {\n return s_remoteChainSelectors.contains(remoteChainSelector);\n }\n\n /// @notice Get list of allowed chains\n /// @return list of chains.\n function getSupportedChains() public view virtual returns (uint64[] memory) {\n uint256[] memory uint256ChainSelectors = s_remoteChainSelectors.values();\n uint64[] memory chainSelectors = new uint64[](uint256ChainSelectors.length);\n for (uint256 i = 0; i < uint256ChainSelectors.length; ++i) {\n chainSelectors[i] = uint64(uint256ChainSelectors[i]);\n }\n\n return chainSelectors;\n }\n\n /// @notice Sets the permissions for a list of chains selectors. Actual senders for these chains\n /// need to be allowed on the Router to interact with this pool.\n /// @param remoteChainSelectorsToRemove A list of chain selectors to remove.\n /// @param chainsToAdd A list of chains and their new permission status & rate limits. Rate limits\n /// are only used when the chain is being added through `allowed` being true.\n /// @dev Only callable by the owner\n function applyChainUpdates(\n uint64[] calldata remoteChainSelectorsToRemove,\n ChainUpdate[] calldata chainsToAdd\n ) external virtual onlyOwner {\n for (uint256 i = 0; i < remoteChainSelectorsToRemove.length; ++i) {\n uint64 remoteChainSelectorToRemove = remoteChainSelectorsToRemove[i];\n // If the chain doesn't exist, revert.\n if (!s_remoteChainSelectors.remove(remoteChainSelectorToRemove)) {\n revert NonExistentChain(remoteChainSelectorToRemove);\n }\n\n // Remove all remote pool hashes for the chain.\n bytes32[] memory remotePools = s_remoteChainConfigs[remoteChainSelectorToRemove].remotePools.values();\n for (uint256 j = 0; j < remotePools.length; ++j) {\n s_remoteChainConfigs[remoteChainSelectorToRemove].remotePools.remove(remotePools[j]);\n }\n\n delete s_remoteChainConfigs[remoteChainSelectorToRemove];\n delete s_fastFinalityOutboundRateLimiterConfig[remoteChainSelectorToRemove];\n delete s_fastFinalityInboundRateLimiterConfig[remoteChainSelectorToRemove];\n\n emit ChainRemoved(remoteChainSelectorToRemove);\n }\n\n for (uint256 i = 0; i < chainsToAdd.length; ++i) {\n ChainUpdate memory newChain = chainsToAdd[i];\n if (newChain.remoteTokenAddress.length == 0) {\n revert ZeroAddressInvalid();\n }\n\n // If the chain already exists, revert\n if (!s_remoteChainSelectors.add(newChain.remoteChainSelector)) {\n revert ChainAlreadyExists(newChain.remoteChainSelector);\n }\n\n RemoteChainConfig storage remoteChainConfig = s_remoteChainConfigs[newChain.remoteChainSelector];\n remoteChainConfig.outboundRateLimiterConfig._setTokenBucketConfig(newChain.outboundRateLimiterConfig);\n remoteChainConfig.inboundRateLimiterConfig._setTokenBucketConfig(newChain.inboundRateLimiterConfig);\n\n remoteChainConfig.remoteTokenAddress = newChain.remoteTokenAddress;\n\n for (uint256 j = 0; j < newChain.remotePoolAddresses.length; ++j) {\n _setRemotePool(newChain.remoteChainSelector, newChain.remotePoolAddresses[j]);\n }\n\n emit ChainAdded(\n newChain.remoteChainSelector,\n newChain.remoteTokenAddress,\n newChain.outboundRateLimiterConfig,\n newChain.inboundRateLimiterConfig\n );\n }\n }\n\n /// @notice Adds a pool address to the allowed remote token pools for a particular chain.\n /// @param remoteChainSelector The remote chain selector for which the remote pool address is being added.\n /// @param remotePoolAddress The address of the new remote pool.\n function _setRemotePool(\n uint64 remoteChainSelector,\n bytes memory remotePoolAddress\n ) internal virtual {\n if (remotePoolAddress.length == 0) {\n revert ZeroAddressInvalid();\n }\n\n bytes32 poolHash = keccak256(remotePoolAddress);\n\n // Check if the pool already exists.\n if (!s_remoteChainConfigs[remoteChainSelector].remotePools.add(poolHash)) {\n revert PoolAlreadyAdded(remoteChainSelector, remotePoolAddress);\n }\n\n // Add the pool to the mapping to be able to un-hash it later.\n s_remotePoolAddresses[poolHash] = remotePoolAddress;\n\n emit RemotePoolAdded(remoteChainSelector, remotePoolAddress);\n }\n\n // ================================================================\n // \u2502 Rate limiting \u2502\n // ================================================================\n\n /// @dev The inbound rate limits should be slightly higher than the outbound rate limits. This is because many chains\n /// finalize blocks in batches. CCIP also commits messages in batches: the commit plugin bundles multiple messages in\n /// a single merkle root.\n /// Imagine the following scenario.\n /// - Chain A has an inbound and outbound rate limit of 100 tokens capacity and 1 token per second refill rate.\n /// - Chain B has an inbound and outbound rate limit of 100 tokens capacity and 1 token per second refill rate.\n ///\n /// At time 0:\n /// - Chain A sends 100 tokens to Chain B.\n /// At time 5:\n /// - Chain A sends 5 tokens to Chain B.\n /// At time 6:\n /// The epoch that contains blocks [0-5] is finalized.\n /// Both transactions will be included in the same merkle root and become executable at the same time. This means\n /// the token pool on chain B requires a capacity of 105 to successfully execute both messages at the same time.\n /// The exact additional capacity required depends on the refill rate and the size of the source chain epochs and the\n /// CCIP round time. For simplicity, a 5-10% buffer should be sufficient in most cases.\n\n /// @notice Consumes outbound rate limiting capacity in this pool.\n /// @param remoteChainSelector The remote chain selector.\n /// @param amount The amount of tokens consumed.\n function _consumeOutboundRateLimit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {\n s_remoteChainConfigs[remoteChainSelector].outboundRateLimiterConfig._consume(amount, token);\n\n emit OutboundRateLimitConsumed({token: token, remoteChainSelector: remoteChainSelector, amount: amount});\n }\n\n /// @notice Consumes inbound rate limiting capacity in this pool.\n /// @param remoteChainSelector The remote chain selector.\n /// @param amount The amount of tokens consumed.\n function _consumeInboundRateLimit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {\n s_remoteChainConfigs[remoteChainSelector].inboundRateLimiterConfig._consume(amount, token);\n\n emit InboundRateLimitConsumed({token: token, remoteChainSelector: remoteChainSelector, amount: amount});\n }\n\n /// @notice Consumes fast finality outbound rate limiting capacity in this pool.\n /// @dev If fast finality rate limiter is not enabled for the chain, it will fallback to the default\n /// rate limiter.\n /// @param remoteChainSelector The remote chain selector.\n /// @param amount The amount of tokens consumed.\n function _consumeFastFinalityOutboundRateLimit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {\n if (!s_fastFinalityOutboundRateLimiterConfig[remoteChainSelector].isEnabled) {\n _consumeOutboundRateLimit(token, remoteChainSelector, amount);\n return;\n }\n\n s_fastFinalityOutboundRateLimiterConfig[remoteChainSelector]._consume(amount, token);\n\n emit FastFinalityOutboundRateLimitConsumed({token: token, remoteChainSelector: remoteChainSelector, amount: amount});\n }\n\n /// @notice Consumes fast finality inbound rate limiting capacity in this pool.\n /// @dev If fast finality rate limiter is not enabled for the chain, it will fallback to the default\n /// rate limiter.\n /// @param remoteChainSelector The remote chain selector.\n /// @param amount The amount of tokens consumed.\n function _consumeFastFinalityInboundRateLimit(\n address token,\n uint64 remoteChainSelector,\n uint256 amount\n ) internal virtual {\n if (!s_fastFinalityInboundRateLimiterConfig[remoteChainSelector].isEnabled) {\n _consumeInboundRateLimit(token, remoteChainSelector, amount);\n return;\n }\n\n s_fastFinalityInboundRateLimiterConfig[remoteChainSelector]._consume(amount, token);\n\n emit FastFinalityInboundRateLimitConsumed({token: token, remoteChainSelector: remoteChainSelector, amount: amount});\n }\n\n /// @notice Returns the outbound and inbound rate limiter state for the given remote chain at the time of the call.\n /// @param remoteChainSelector The remote chain selector.\n /// @param fastFinality Whether to get the fast finality rate limiter state.\n /// @return outboundRateLimiterState The outbound token bucket.\n /// @return inboundRateLimiterState The inbound token bucket.\n function getCurrentRateLimiterState(\n uint64 remoteChainSelector,\n bool fastFinality\n )\n external\n view\n virtual\n returns (\n RateLimiter.TokenBucket memory outboundRateLimiterState,\n RateLimiter.TokenBucket memory inboundRateLimiterState\n )\n {\n if (fastFinality) {\n return (\n s_fastFinalityOutboundRateLimiterConfig[remoteChainSelector]._currentTokenBucketState(),\n s_fastFinalityInboundRateLimiterConfig[remoteChainSelector]._currentTokenBucketState()\n );\n }\n RemoteChainConfig storage config = s_remoteChainConfigs[remoteChainSelector];\n return (\n config.outboundRateLimiterConfig._currentTokenBucketState(),\n config.inboundRateLimiterConfig._currentTokenBucketState()\n );\n }\n\n /// @notice Sets the rate limit configurations for specified remote chains.\n /// @param rateLimitConfigArgs Array of structs containing remote chain selectors and their rate limiter configs.\n function setRateLimitConfig(\n RateLimitConfigArgs[] calldata rateLimitConfigArgs\n ) external virtual {\n _onlyOwnerOrRateLimitAdmin();\n\n for (uint256 i = 0; i < rateLimitConfigArgs.length; ++i) {\n RateLimitConfigArgs calldata configArgs = rateLimitConfigArgs[i];\n\n uint64 remoteChainSelector = configArgs.remoteChainSelector;\n if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector);\n\n if (configArgs.fastFinality) {\n s_fastFinalityOutboundRateLimiterConfig[remoteChainSelector]._setTokenBucketConfig(\n configArgs.outboundRateLimiterConfig\n );\n s_fastFinalityInboundRateLimiterConfig[remoteChainSelector]._setTokenBucketConfig(\n configArgs.inboundRateLimiterConfig\n );\n } else {\n s_remoteChainConfigs[remoteChainSelector].outboundRateLimiterConfig\n ._setTokenBucketConfig(configArgs.outboundRateLimiterConfig);\n s_remoteChainConfigs[remoteChainSelector].inboundRateLimiterConfig\n ._setTokenBucketConfig(configArgs.inboundRateLimiterConfig);\n }\n\n emit RateLimitConfigured(\n remoteChainSelector,\n configArgs.fastFinality,\n configArgs.outboundRateLimiterConfig,\n configArgs.inboundRateLimiterConfig\n );\n }\n }\n\n // ================================================================\n // \u2502 Access \u2502\n // ================================================================\n\n /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender\n /// is a permissioned onRamp for the given chain on the Router.\n /// @dev This function is marked virtual as other token pools may inherit from this contract, but do\n /// not receive calls from the ramps directly, instead receiving them from a proxy contract. In that\n /// situation this function must be overridden and the ramp-check removed and replaced with a different\n /// access-control scheme.\n /// @param remoteChainSelector The remote chain selector.\n function _onlyOnRamp(\n uint64 remoteChainSelector\n ) internal view virtual {\n if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector);\n if (!(msg.sender == s_router.getOnRamp(remoteChainSelector))) revert CallerIsNotARampOnRouter(msg.sender);\n }\n\n /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender\n /// is a permissioned offRamp for the given chain on the Router.\n /// @dev This function is marked virtual as other token pools may inherit from this contract, but do\n /// not receive calls from the ramps directly, instead receiving them from a proxy contract. In that\n /// situation this function must be overridden and the ramp-check removed and replaced with a different\n /// access-control scheme.\n /// @param remoteChainSelector The remote chain selector.\n function _onlyOffRamp(\n uint64 remoteChainSelector\n ) internal view virtual {\n if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector);\n if (!s_router.isOffRamp(remoteChainSelector, msg.sender)) revert CallerIsNotARampOnRouter(msg.sender);\n }\n\n /// @notice Checks whether the msg.sender is either the owner or the rate limit admin.\n function _onlyOwnerOrRateLimitAdmin() internal view virtual {\n if (msg.sender != s_rateLimitAdmin && msg.sender != owner()) {\n revert Unauthorized(msg.sender);\n }\n }\n\n /// @notice Returns the set of required CCVs for transfers in a specific direction.\n /// @dev This function delegates to AdvancedPoolHooks if configured, otherwise returns an empty array.\n /// @param localToken The address of the local token.\n /// @param remoteChainSelector The remote chain selector for this transfer.\n /// @param sourceDenominatedAmount The amount being transferred, source denominated.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n /// @param extraData Direction-specific payload forwarded by the caller (e.g. token args or source pool data).\n /// @param direction The direction of the transfer (Inbound or Outbound).\n /// @return requiredCCVs Set of required CCV addresses.\n function getRequiredCCVs(\n address localToken,\n uint64 remoteChainSelector,\n uint256 sourceDenominatedAmount,\n bytes4 requestedFinalityConfig,\n bytes calldata extraData,\n IPoolV2.MessageDirection direction\n ) public view virtual returns (address[] memory requiredCCVs) {\n if (address(s_advancedPoolHooks) == address(0)) {\n return new address[](0);\n }\n\n // By default, the amount is equal to the source denominated amount.\n uint256 amount = sourceDenominatedAmount;\n\n // The source fee amount is not classified as transferred value, meaning we have to subtract it from the amount\n // before passing it into the hook. The inbound amount is already post-fee so we only need to do this for outbound\n // transfers.\n if (direction == IPoolV2.MessageDirection.Outbound) {\n TokenTransferFeeConfig memory feeConfig = s_tokenTransferFeeConfig[remoteChainSelector];\n if (feeConfig.isEnabled) {\n if (requestedFinalityConfig != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n amount =\n sourceDenominatedAmount - (sourceDenominatedAmount * feeConfig.fastFinalityTransferFeeBps) / BPS_DIVIDER;\n } else {\n amount = sourceDenominatedAmount - (sourceDenominatedAmount * feeConfig.finalityTransferFeeBps) / BPS_DIVIDER;\n }\n }\n } else {\n // For inbound transfers, the amount is already post-fee so we don't need to do any additional calculations to get\n // the amount that will be received by the user. However, we still need to convert it to the local amount based on\n // decimals for the hooks.\n\n // extraData is sourcePoolData for inbound transfers, which contains the remote decimals.\n amount = _calculateLocalAmount(sourceDenominatedAmount, _parseRemoteDecimals(extraData));\n }\n\n return s_advancedPoolHooks.getRequiredCCVs(\n localToken, remoteChainSelector, amount, requestedFinalityConfig, extraData, direction\n );\n }\n\n // ================================================================\n // \u2502 Fee \u2502\n // ================================================================\n\n /// @notice Updates the token transfer fee configurations for specified destination chains.\n /// @param tokenTransferFeeConfigArgs Array of structs containing destination chain selectors and their fee configs.\n /// @param disableTokenTransferFeeConfigs Array of destination chain selectors to disable custom fee configs for.\n function applyTokenTransferFeeConfigUpdates(\n TokenTransferFeeConfigArgs[] calldata tokenTransferFeeConfigArgs,\n uint64[] calldata disableTokenTransferFeeConfigs\n ) external virtual onlyOwner {\n for (uint256 i = 0; i < tokenTransferFeeConfigArgs.length; ++i) {\n uint64 destChainSelector = tokenTransferFeeConfigArgs[i].destChainSelector;\n if (!isSupportedChain(destChainSelector)) revert NonExistentChain(destChainSelector);\n\n TokenTransferFeeConfig calldata tokenTransferFeeConfig = tokenTransferFeeConfigArgs[i].tokenTransferFeeConfig;\n\n // Reject configs with isEnabled: false - use disableTokenTransferFeeConfigs parameter instead.\n if (!tokenTransferFeeConfig.isEnabled) {\n revert InvalidTokenTransferFeeConfig(destChainSelector);\n }\n\n if (tokenTransferFeeConfig.finalityTransferFeeBps >= BPS_DIVIDER) {\n revert InvalidTransferFeeBps(tokenTransferFeeConfig.finalityTransferFeeBps);\n }\n if (tokenTransferFeeConfig.fastFinalityTransferFeeBps >= BPS_DIVIDER) {\n revert InvalidTransferFeeBps(tokenTransferFeeConfig.fastFinalityTransferFeeBps);\n }\n // Gas overhead must be non-zero for proper fee accounting.\n if (tokenTransferFeeConfig.destGasOverhead == 0) {\n revert InvalidTokenTransferFeeConfig(destChainSelector);\n }\n\n s_tokenTransferFeeConfig[destChainSelector] = tokenTransferFeeConfig;\n emit TokenTransferFeeConfigUpdated(destChainSelector, tokenTransferFeeConfig);\n }\n\n for (uint256 i = 0; i < disableTokenTransferFeeConfigs.length; ++i) {\n uint64 destChainSelector = disableTokenTransferFeeConfigs[i];\n delete s_tokenTransferFeeConfig[destChainSelector];\n emit TokenTransferFeeConfigDeleted(destChainSelector);\n }\n }\n\n /// @notice Returns the token transfer fee override for a destination chain.\n /// @param destChainSelector The destination chain selector used for lookup.\n /// @return feeConfig The enabled fee configuration for the lane.\n function getTokenTransferFeeConfig(\n address, // localToken\n uint64 destChainSelector,\n bytes4, // requestedFinalityConfig\n bytes calldata // tokenArgs\n ) external view virtual returns (TokenTransferFeeConfig memory feeConfig) {\n return s_tokenTransferFeeConfig[destChainSelector];\n }\n\n /// @inheritdoc IPoolV2\n /// @notice Returns the pool fee parameters that will apply to a transfer.\n /// @param destChainSelector The destination lane selector.\n /// @param requestedFinalityConfig Requested finality encoding (see `FinalityCodec`).\n function getFee(\n address, // localToken\n uint64 destChainSelector,\n uint256, // amount\n address, // feeToken\n bytes4 requestedFinalityConfig,\n bytes calldata // tokenArgs\n )\n external\n view\n virtual\n returns (uint256 feeUSDCents, uint32 destGasOverhead, uint32 destBytesOverhead, uint16 tokenFeeBps, bool isEnabled)\n {\n FinalityCodec._ensureRequestedFinalityAllowed(requestedFinalityConfig, s_allowedFinalityConfig);\n\n TokenTransferFeeConfig memory feeConfig = s_tokenTransferFeeConfig[destChainSelector];\n\n // If config is disabled, return zeros with isEnabled=false to signal OnRamp to use FeeQuoter defaults.\n if (!feeConfig.isEnabled) {\n return (0, 0, 0, 0, false);\n }\n\n if (requestedFinalityConfig != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n return (\n feeConfig.fastFinalityFeeUSDCents,\n feeConfig.destGasOverhead,\n feeConfig.destBytesOverhead,\n feeConfig.fastFinalityTransferFeeBps,\n true\n );\n }\n return (\n feeConfig.finalityFeeUSDCents,\n feeConfig.destGasOverhead,\n feeConfig.destBytesOverhead,\n feeConfig.finalityTransferFeeBps,\n true\n );\n }\n\n /// @dev Calculates the fee based on the transferred amount, and the configured basis points.\n /// @param lockOrBurnIn The original lock or burn request.\n /// @param requestedFinalityConfig The requested finality encoding (see `FinalityCodec`).\n /// A value of zero (FinalityCodec.WAIT_FOR_FINALITY_FLAG) applies default finality fees.\n /// Returns the fee amount.\n function _getFee(\n Pool.LockOrBurnInV1 calldata lockOrBurnIn,\n bytes4 requestedFinalityConfig\n ) internal view virtual returns (uint256) {\n TokenTransferFeeConfig storage feeConfig = s_tokenTransferFeeConfig[lockOrBurnIn.remoteChainSelector];\n\n // Determine which fee basis points to apply based on finality type.\n if (requestedFinalityConfig != FinalityCodec.WAIT_FOR_FINALITY_FLAG) {\n return (lockOrBurnIn.amount * feeConfig.fastFinalityTransferFeeBps) / BPS_DIVIDER;\n } else {\n return (lockOrBurnIn.amount * feeConfig.finalityTransferFeeBps) / BPS_DIVIDER;\n }\n }\n\n /// @notice Withdraws accrued fee token balances to the provided `recipient`.\n /// @dev Only callable by the owner or the fee admin.\n /// @dev FeeTokenHandler will revert if `recipient` is zero address.\n /// @dev Pools accrue fees directly on this contract. Lock/release pools send bridge liquidity to their ERC20 lockbox\n /// during the lock flow, which means any balance left on this contract represents fees that have accrued to the pool.\n /// Because user liquidity never resides on `address(this)` for lock/release pools, transferring the full contract\n /// balance is safe and clears only accrued fees.\n /// @param feeTokens The token addresses to withdraw, including the pool token when applicable.\n /// @param recipient The address to withdraw the fee tokens to.\n function withdrawFeeTokens(\n address[] calldata feeTokens,\n address recipient\n ) external virtual {\n if (msg.sender != owner() && msg.sender != s_feeAdmin) {\n revert CallerIsNotOwnerOrFeeAdmin(msg.sender);\n }\n FeeTokenHandler._withdrawFeeTokens(feeTokens, recipient);\n }\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2Step.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {IOwnable} from \"../interfaces/IOwnable.sol\";\n\n/// @notice A minimal contract that implements 2-step ownership transfer and nothing more. It's made to be minimal\n/// to reduce the impact of the bytecode size on any contract that inherits from it.\ncontract Ownable2Step is IOwnable {\n /// @notice The pending owner is the address to which ownership may be transferred.\n address private s_pendingOwner;\n /// @notice The owner is the current owner of the contract.\n /// @dev The owner is the second storage variable so any implementing contract could pack other state with it\n /// instead of the much less used s_pendingOwner.\n address private s_owner;\n\n error OwnerCannotBeZero();\n error MustBeProposedOwner();\n error CannotTransferToSelf();\n error OnlyCallableByOwner();\n\n event OwnershipTransferRequested(address indexed from, address indexed to);\n event OwnershipTransferred(address indexed from, address indexed to);\n\n constructor(address newOwner, address pendingOwner) {\n if (newOwner == address(0)) {\n revert OwnerCannotBeZero();\n }\n\n s_owner = newOwner;\n if (pendingOwner != address(0)) {\n _transferOwnership(pendingOwner);\n }\n }\n\n /// @notice Get the current owner\n function owner() public view override returns (address) {\n return s_owner;\n }\n\n /// @notice Allows an owner to begin transferring ownership to a new address. The new owner needs to call\n /// `acceptOwnership` to accept the transfer before any permissions are changed.\n /// @param to The address to which ownership will be transferred.\n function transferOwnership(\n address to\n ) public override onlyOwner {\n _transferOwnership(to);\n }\n\n /// @notice validate, transfer ownership, and emit relevant events\n /// @param to The address to which ownership will be transferred.\n function _transferOwnership(\n address to\n ) private {\n if (to == msg.sender) {\n revert CannotTransferToSelf();\n }\n\n s_pendingOwner = to;\n\n emit OwnershipTransferRequested(s_owner, to);\n }\n\n /// @notice Allows an ownership transfer to be completed by the recipient.\n function acceptOwnership() external override {\n if (msg.sender != s_pendingOwner) {\n revert MustBeProposedOwner();\n }\n\n address oldOwner = s_owner;\n s_owner = msg.sender;\n s_pendingOwner = address(0);\n\n emit OwnershipTransferred(oldOwner, msg.sender);\n }\n\n /// @notice validate access\n function _validateOwnership() internal view {\n if (msg.sender != s_owner) {\n revert OnlyCallableByOwner();\n }\n }\n\n /// @notice Reverts if called by anyone other than the contract owner.\n modifier onlyOwner() {\n _validateOwnership();\n _;\n }\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport {Ownable2Step} from \"./Ownable2Step.sol\";\n\n/// @notice Sets the msg.sender to be the owner of the contract and does not set a pending owner.\ncontract Ownable2StepMsgSender is Ownable2Step {\n constructor() Ownable2Step(msg.sender, address(0)) {}\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/IOwnable.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface IOwnable {\n function owner() external returns (address);\n\n function transferOwnership(\n address recipient\n ) external;\n\n function acceptOwnership() external;\n}\n" + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface ITypeAndVersion {\n function typeAndVersion() external pure returns (string memory);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC1363.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1363.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"./IERC20.sol\";\nimport {IERC165} from \"./IERC165.sol\";\n\n/**\n * @title IERC1363\n * @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].\n *\n * Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract\n * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.\n */\ninterface IERC1363 is IERC20, IERC165 {\n /*\n * Note: the ERC-165 identifier for this interface is 0xb0202a11.\n * 0xb0202a11 ===\n * bytes4(keccak256('transferAndCall(address,uint256)')) ^\n * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^\n * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^\n * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^\n * bytes4(keccak256('approveAndCall(address,uint256)')) ^\n * bytes4(keccak256('approveAndCall(address,uint256,bytes)'))\n */\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferAndCall(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @param data Additional data with no specified format, sent in call to `to`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param from The address which you want to send tokens from.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferFromAndCall(address from, address to, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism\n * and then calls {IERC1363Receiver-onTransferReceived} on `to`.\n * @param from The address which you want to send tokens from.\n * @param to The address which you want to transfer to.\n * @param value The amount of tokens to be transferred.\n * @param data Additional data with no specified format, sent in call to `to`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.\n * @param spender The address which will spend the funds.\n * @param value The amount of tokens to be spent.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function approveAndCall(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.\n * @param spender The address which will spend the funds.\n * @param value The amount of tokens to be spent.\n * @param data Additional data with no specified format, sent in call to `spender`.\n * @return A boolean value indicating whether the operation succeeded unless throwing.\n */\n function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC165.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC165.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC165} from \"../utils/introspection/IERC165.sol\";\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC20.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../token/ERC20/IERC20.sol\";\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/IERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-20 standard as defined in the ERC.\n */\ninterface IERC20 {\n /**\n * @dev Emitted when `value` tokens are moved from one account (`from`) to\n * another (`to`).\n *\n * Note that `value` may be zero.\n */\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n /**\n * @dev Emitted when the allowance of a `spender` for an `owner` is set by\n * a call to {approve}. `value` is the new allowance.\n */\n event Approval(address indexed owner, address indexed spender, uint256 value);\n\n /**\n * @dev Returns the value of tokens in existence.\n */\n function totalSupply() external view returns (uint256);\n\n /**\n * @dev Returns the value of tokens owned by `account`.\n */\n function balanceOf(address account) external view returns (uint256);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transfer(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Returns the remaining number of tokens that `spender` will be\n * allowed to spend on behalf of `owner` through {transferFrom}. This is\n * zero by default.\n *\n * This value changes when {approve} or {transferFrom} are called.\n */\n function allowance(address owner, address spender) external view returns (uint256);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * IMPORTANT: Beware that changing an allowance with this method brings the risk\n * that someone may use both the old and the new allowance by unfortunate\n * transaction ordering. One possible solution to mitigate this race\n * condition is to first reduce the spender's allowance to 0 and set the\n * desired value afterwards:\n * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n *\n * Emits an {Approval} event.\n */\n function approve(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the\n * allowance mechanism. `value` is then deducted from the caller's\n * allowance.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(address from, address to, uint256 value) external returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Metadata.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Metadata.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../IERC20.sol\";\n\n/**\n * @dev Interface for the optional metadata functions from the ERC-20 standard.\n */\ninterface IERC20Metadata is IERC20 {\n /**\n * @dev Returns the name of the token.\n */\n function name() external view returns (string memory);\n\n /**\n * @dev Returns the symbol of the token.\n */\n function symbol() external view returns (string memory);\n\n /**\n * @dev Returns the decimals places of the token.\n */\n function decimals() external view returns (uint8);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/utils/SafeERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)\n\npragma solidity ^0.8.20;\n\nimport {IERC20} from \"../IERC20.sol\";\nimport {IERC1363} from \"../../../interfaces/IERC1363.sol\";\n\n/**\n * @title SafeERC20\n * @dev Wrappers around ERC-20 operations that throw on failure (when the token\n * contract returns false). Tokens that return no value (and instead revert or\n * throw on failure) are also supported, non-reverting calls are assumed to be\n * successful.\n * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,\n * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.\n */\nlibrary SafeERC20 {\n /**\n * @dev An operation with an ERC-20 token failed.\n */\n error SafeERC20FailedOperation(address token);\n\n /**\n * @dev Indicates a failed `decreaseAllowance` request.\n */\n error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);\n\n /**\n * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,\n * non-reverting calls are assumed to be successful.\n */\n function safeTransfer(IERC20 token, address to, uint256 value) internal {\n _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));\n }\n\n /**\n * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the\n * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.\n */\n function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {\n _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));\n }\n\n /**\n * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.\n */\n function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {\n return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value)));\n }\n\n /**\n * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.\n */\n function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {\n return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value)));\n }\n\n /**\n * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,\n * non-reverting calls are assumed to be successful.\n *\n * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the \"client\"\n * smart contract uses ERC-7674 to set temporary allowances, then the \"client\" smart contract should avoid using\n * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract\n * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.\n */\n function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {\n uint256 oldAllowance = token.allowance(address(this), spender);\n forceApprove(token, spender, oldAllowance + value);\n }\n\n /**\n * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no\n * value, non-reverting calls are assumed to be successful.\n *\n * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the \"client\"\n * smart contract uses ERC-7674 to set temporary allowances, then the \"client\" smart contract should avoid using\n * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract\n * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.\n */\n function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {\n unchecked {\n uint256 currentAllowance = token.allowance(address(this), spender);\n if (currentAllowance < requestedDecrease) {\n revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);\n }\n forceApprove(token, spender, currentAllowance - requestedDecrease);\n }\n }\n\n /**\n * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,\n * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval\n * to be set to zero before setting it to a non-zero value, such as USDT.\n *\n * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function\n * only sets the \"standard\" allowance. Any temporary allowance will remain active, in addition to the value being\n * set here.\n */\n function forceApprove(IERC20 token, address spender, uint256 value) internal {\n bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));\n\n if (!_callOptionalReturnBool(token, approvalCall)) {\n _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));\n _callOptionalReturn(token, approvalCall);\n }\n }\n\n /**\n * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no\n * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * Reverts if the returned value is other than `true`.\n */\n function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {\n if (to.code.length == 0) {\n safeTransfer(token, to, value);\n } else if (!token.transferAndCall(to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target\n * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * Reverts if the returned value is other than `true`.\n */\n function transferFromAndCallRelaxed(\n IERC1363 token,\n address from,\n address to,\n uint256 value,\n bytes memory data\n ) internal {\n if (to.code.length == 0) {\n safeTransferFrom(token, from, to, value);\n } else if (!token.transferFromAndCall(from, to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no\n * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when\n * targeting contracts.\n *\n * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.\n * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}\n * once without retrying, and relies on the returned value to be true.\n *\n * Reverts if the returned value is other than `true`.\n */\n function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {\n if (to.code.length == 0) {\n forceApprove(token, to, value);\n } else if (!token.approveAndCall(to, value, data)) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement\n * on the return value: the return value is optional (but if data is returned, it must not be false).\n * @param token The token targeted by the call.\n * @param data The call data (encoded using abi.encode or one of its variants).\n *\n * This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.\n */\n function _callOptionalReturn(IERC20 token, bytes memory data) private {\n uint256 returnSize;\n uint256 returnValue;\n assembly (\"memory-safe\") {\n let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)\n // bubble errors\n if iszero(success) {\n let ptr := mload(0x40)\n returndatacopy(ptr, 0, returndatasize())\n revert(ptr, returndatasize())\n }\n returnSize := returndatasize()\n returnValue := mload(0)\n }\n\n if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {\n revert SafeERC20FailedOperation(address(token));\n }\n }\n\n /**\n * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement\n * on the return value: the return value is optional (but if data is returned, it must not be false).\n * @param token The token targeted by the call.\n * @param data The call data (encoded using abi.encode or one of its variants).\n *\n * This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.\n */\n function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {\n bool success;\n uint256 returnSize;\n uint256 returnValue;\n assembly (\"memory-safe\") {\n success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)\n returnSize := returndatasize()\n returnValue := mload(0)\n }\n return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Arrays.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/Arrays.sol)\n// This file was procedurally generated from scripts/generate/templates/Arrays.js.\n\npragma solidity ^0.8.20;\n\nimport {Comparators} from \"./Comparators.sol\";\nimport {SlotDerivation} from \"./SlotDerivation.sol\";\nimport {StorageSlot} from \"./StorageSlot.sol\";\nimport {Math} from \"./math/Math.sol\";\n\n/**\n * @dev Collection of functions related to array types.\n */\nlibrary Arrays {\n using SlotDerivation for bytes32;\n using StorageSlot for bytes32;\n\n /**\n * @dev Sort an array of uint256 (in memory) following the provided comparator function.\n *\n * This function does the sorting \"in place\", meaning that it overrides the input. The object is returned for\n * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.\n *\n * NOTE: this function's cost is `O(n \u00b7 log(n))` in average and `O(n\u00b2)` in the worst case, with n the length of the\n * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful\n * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may\n * consume more gas than is available in a block, leading to potential DoS.\n *\n * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.\n */\n function sort(\n uint256[] memory array,\n function(uint256, uint256) pure returns (bool) comp\n ) internal pure returns (uint256[] memory) {\n _quickSort(_begin(array), _end(array), comp);\n return array;\n }\n\n /**\n * @dev Variant of {sort} that sorts an array of uint256 in increasing order.\n */\n function sort(uint256[] memory array) internal pure returns (uint256[] memory) {\n sort(array, Comparators.lt);\n return array;\n }\n\n /**\n * @dev Sort an array of address (in memory) following the provided comparator function.\n *\n * This function does the sorting \"in place\", meaning that it overrides the input. The object is returned for\n * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.\n *\n * NOTE: this function's cost is `O(n \u00b7 log(n))` in average and `O(n\u00b2)` in the worst case, with n the length of the\n * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful\n * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may\n * consume more gas than is available in a block, leading to potential DoS.\n *\n * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.\n */\n function sort(\n address[] memory array,\n function(address, address) pure returns (bool) comp\n ) internal pure returns (address[] memory) {\n sort(_castToUint256Array(array), _castToUint256Comp(comp));\n return array;\n }\n\n /**\n * @dev Variant of {sort} that sorts an array of address in increasing order.\n */\n function sort(address[] memory array) internal pure returns (address[] memory) {\n sort(_castToUint256Array(array), Comparators.lt);\n return array;\n }\n\n /**\n * @dev Sort an array of bytes32 (in memory) following the provided comparator function.\n *\n * This function does the sorting \"in place\", meaning that it overrides the input. The object is returned for\n * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.\n *\n * NOTE: this function's cost is `O(n \u00b7 log(n))` in average and `O(n\u00b2)` in the worst case, with n the length of the\n * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful\n * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may\n * consume more gas than is available in a block, leading to potential DoS.\n *\n * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.\n */\n function sort(\n bytes32[] memory array,\n function(bytes32, bytes32) pure returns (bool) comp\n ) internal pure returns (bytes32[] memory) {\n sort(_castToUint256Array(array), _castToUint256Comp(comp));\n return array;\n }\n\n /**\n * @dev Variant of {sort} that sorts an array of bytes32 in increasing order.\n */\n function sort(bytes32[] memory array) internal pure returns (bytes32[] memory) {\n sort(_castToUint256Array(array), Comparators.lt);\n return array;\n }\n\n /**\n * @dev Performs a quick sort of a segment of memory. The segment sorted starts at `begin` (inclusive), and stops\n * at end (exclusive). Sorting follows the `comp` comparator.\n *\n * Invariant: `begin <= end`. This is the case when initially called by {sort} and is preserved in subcalls.\n *\n * IMPORTANT: Memory locations between `begin` and `end` are not validated/zeroed. This function should\n * be used only if the limits are within a memory array.\n */\n function _quickSort(uint256 begin, uint256 end, function(uint256, uint256) pure returns (bool) comp) private pure {\n unchecked {\n if (end - begin < 0x40) return;\n\n // Use first element as pivot\n uint256 pivot = _mload(begin);\n // Position where the pivot should be at the end of the loop\n uint256 pos = begin;\n\n for (uint256 it = begin + 0x20; it < end; it += 0x20) {\n if (comp(_mload(it), pivot)) {\n // If the value stored at the iterator's position comes before the pivot, we increment the\n // position of the pivot and move the value there.\n pos += 0x20;\n _swap(pos, it);\n }\n }\n\n _swap(begin, pos); // Swap pivot into place\n _quickSort(begin, pos, comp); // Sort the left side of the pivot\n _quickSort(pos + 0x20, end, comp); // Sort the right side of the pivot\n }\n }\n\n /**\n * @dev Pointer to the memory location of the first element of `array`.\n */\n function _begin(uint256[] memory array) private pure returns (uint256 ptr) {\n assembly (\"memory-safe\") {\n ptr := add(array, 0x20)\n }\n }\n\n /**\n * @dev Pointer to the memory location of the first memory word (32bytes) after `array`. This is the memory word\n * that comes just after the last element of the array.\n */\n function _end(uint256[] memory array) private pure returns (uint256 ptr) {\n unchecked {\n return _begin(array) + array.length * 0x20;\n }\n }\n\n /**\n * @dev Load memory word (as a uint256) at location `ptr`.\n */\n function _mload(uint256 ptr) private pure returns (uint256 value) {\n assembly {\n value := mload(ptr)\n }\n }\n\n /**\n * @dev Swaps the elements memory location `ptr1` and `ptr2`.\n */\n function _swap(uint256 ptr1, uint256 ptr2) private pure {\n assembly {\n let value1 := mload(ptr1)\n let value2 := mload(ptr2)\n mstore(ptr1, value2)\n mstore(ptr2, value1)\n }\n }\n\n /// @dev Helper: low level cast address memory array to uint256 memory array\n function _castToUint256Array(address[] memory input) private pure returns (uint256[] memory output) {\n assembly {\n output := input\n }\n }\n\n /// @dev Helper: low level cast bytes32 memory array to uint256 memory array\n function _castToUint256Array(bytes32[] memory input) private pure returns (uint256[] memory output) {\n assembly {\n output := input\n }\n }\n\n /// @dev Helper: low level cast address comp function to uint256 comp function\n function _castToUint256Comp(\n function(address, address) pure returns (bool) input\n ) private pure returns (function(uint256, uint256) pure returns (bool) output) {\n assembly {\n output := input\n }\n }\n\n /// @dev Helper: low level cast bytes32 comp function to uint256 comp function\n function _castToUint256Comp(\n function(bytes32, bytes32) pure returns (bool) input\n ) private pure returns (function(uint256, uint256) pure returns (bool) output) {\n assembly {\n output := input\n }\n }\n\n /**\n * @dev Searches a sorted `array` and returns the first index that contains\n * a value greater or equal to `element`. If no such index exists (i.e. all\n * values in the array are strictly less than `element`), the array length is\n * returned. Time complexity O(log n).\n *\n * NOTE: The `array` is expected to be sorted in ascending order, and to\n * contain no repeated elements.\n *\n * IMPORTANT: Deprecated. This implementation behaves as {lowerBound} but lacks\n * support for repeated elements in the array. The {lowerBound} function should\n * be used instead.\n */\n function findUpperBound(uint256[] storage array, uint256 element) internal view returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeAccess(array, mid).value > element) {\n high = mid;\n } else {\n low = mid + 1;\n }\n }\n\n // At this point `low` is the exclusive upper bound. We will return the inclusive upper bound.\n if (low > 0 && unsafeAccess(array, low - 1).value == element) {\n return low - 1;\n } else {\n return low;\n }\n }\n\n /**\n * @dev Searches an `array` sorted in ascending order and returns the first\n * index that contains a value greater or equal than `element`. If no such index\n * exists (i.e. all values in the array are strictly less than `element`), the array\n * length is returned. Time complexity O(log n).\n *\n * See C++'s https://en.cppreference.com/w/cpp/algorithm/lower_bound[lower_bound].\n */\n function lowerBound(uint256[] storage array, uint256 element) internal view returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeAccess(array, mid).value < element) {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n } else {\n high = mid;\n }\n }\n\n return low;\n }\n\n /**\n * @dev Searches an `array` sorted in ascending order and returns the first\n * index that contains a value strictly greater than `element`. If no such index\n * exists (i.e. all values in the array are strictly less than `element`), the array\n * length is returned. Time complexity O(log n).\n *\n * See C++'s https://en.cppreference.com/w/cpp/algorithm/upper_bound[upper_bound].\n */\n function upperBound(uint256[] storage array, uint256 element) internal view returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeAccess(array, mid).value > element) {\n high = mid;\n } else {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n }\n }\n\n return low;\n }\n\n /**\n * @dev Same as {lowerBound}, but with an array in memory.\n */\n function lowerBoundMemory(uint256[] memory array, uint256 element) internal pure returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeMemoryAccess(array, mid) < element) {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n } else {\n high = mid;\n }\n }\n\n return low;\n }\n\n /**\n * @dev Same as {upperBound}, but with an array in memory.\n */\n function upperBoundMemory(uint256[] memory array, uint256 element) internal pure returns (uint256) {\n uint256 low = 0;\n uint256 high = array.length;\n\n if (high == 0) {\n return 0;\n }\n\n while (low < high) {\n uint256 mid = Math.average(low, high);\n\n // Note that mid will always be strictly less than high (i.e. it will be a valid array index)\n // because Math.average rounds towards zero (it does integer division with truncation).\n if (unsafeMemoryAccess(array, mid) > element) {\n high = mid;\n } else {\n // this cannot overflow because mid < high\n unchecked {\n low = mid + 1;\n }\n }\n }\n\n return low;\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeAccess(address[] storage arr, uint256 pos) internal pure returns (StorageSlot.AddressSlot storage) {\n bytes32 slot;\n assembly (\"memory-safe\") {\n slot := arr.slot\n }\n return slot.deriveArray().offset(pos).getAddressSlot();\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeAccess(bytes32[] storage arr, uint256 pos) internal pure returns (StorageSlot.Bytes32Slot storage) {\n bytes32 slot;\n assembly (\"memory-safe\") {\n slot := arr.slot\n }\n return slot.deriveArray().offset(pos).getBytes32Slot();\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeAccess(uint256[] storage arr, uint256 pos) internal pure returns (StorageSlot.Uint256Slot storage) {\n bytes32 slot;\n assembly (\"memory-safe\") {\n slot := arr.slot\n }\n return slot.deriveArray().offset(pos).getUint256Slot();\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeMemoryAccess(address[] memory arr, uint256 pos) internal pure returns (address res) {\n assembly {\n res := mload(add(add(arr, 0x20), mul(pos, 0x20)))\n }\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeMemoryAccess(bytes32[] memory arr, uint256 pos) internal pure returns (bytes32 res) {\n assembly {\n res := mload(add(add(arr, 0x20), mul(pos, 0x20)))\n }\n }\n\n /**\n * @dev Access an array in an \"unsafe\" way. Skips solidity \"index-out-of-range\" check.\n *\n * WARNING: Only use if you are certain `pos` is lower than the array length.\n */\n function unsafeMemoryAccess(uint256[] memory arr, uint256 pos) internal pure returns (uint256 res) {\n assembly {\n res := mload(add(add(arr, 0x20), mul(pos, 0x20)))\n }\n }\n\n /**\n * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden.\n *\n * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.\n */\n function unsafeSetLength(address[] storage array, uint256 len) internal {\n assembly (\"memory-safe\") {\n sstore(array.slot, len)\n }\n }\n\n /**\n * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden.\n *\n * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.\n */\n function unsafeSetLength(bytes32[] storage array, uint256 len) internal {\n assembly (\"memory-safe\") {\n sstore(array.slot, len)\n }\n }\n\n /**\n * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden.\n *\n * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.\n */\n function unsafeSetLength(uint256[] storage array, uint256 len) internal {\n assembly (\"memory-safe\") {\n sstore(array.slot, len)\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Comparators.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/Comparators.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Provides a set of functions to compare values.\n *\n * _Available since v5.1._\n */\nlibrary Comparators {\n function lt(uint256 a, uint256 b) internal pure returns (bool) {\n return a < b;\n }\n\n function gt(uint256 a, uint256 b) internal pure returns (bool) {\n return a > b;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Panic.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/Panic.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Helper library for emitting standardized panic codes.\n *\n * ```solidity\n * contract Example {\n * using Panic for uint256;\n *\n * // Use any of the declared internal constants\n * function foo() { Panic.GENERIC.panic(); }\n *\n * // Alternatively\n * function foo() { Panic.panic(Panic.GENERIC); }\n * }\n * ```\n *\n * Follows the list from https://github.com/ethereum/solidity/blob/v0.8.24/libsolutil/ErrorCodes.h[libsolutil].\n *\n * _Available since v5.1._\n */\n// slither-disable-next-line unused-state\nlibrary Panic {\n /// @dev generic / unspecified error\n uint256 internal constant GENERIC = 0x00;\n /// @dev used by the assert() builtin\n uint256 internal constant ASSERT = 0x01;\n /// @dev arithmetic underflow or overflow\n uint256 internal constant UNDER_OVERFLOW = 0x11;\n /// @dev division or modulo by zero\n uint256 internal constant DIVISION_BY_ZERO = 0x12;\n /// @dev enum conversion error\n uint256 internal constant ENUM_CONVERSION_ERROR = 0x21;\n /// @dev invalid encoding in storage\n uint256 internal constant STORAGE_ENCODING_ERROR = 0x22;\n /// @dev empty array pop\n uint256 internal constant EMPTY_ARRAY_POP = 0x31;\n /// @dev array out of bounds access\n uint256 internal constant ARRAY_OUT_OF_BOUNDS = 0x32;\n /// @dev resource error (too large allocation or too large array)\n uint256 internal constant RESOURCE_ERROR = 0x41;\n /// @dev calling invalid internal function\n uint256 internal constant INVALID_INTERNAL_FUNCTION = 0x51;\n\n /// @dev Reverts with a panic code. Recommended to use with\n /// the internal constants with predefined codes.\n function panic(uint256 code) internal pure {\n assembly (\"memory-safe\") {\n mstore(0x00, 0x4e487b71)\n mstore(0x20, code)\n revert(0x1c, 0x24)\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/SlotDerivation.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/SlotDerivation.sol)\n// This file was procedurally generated from scripts/generate/templates/SlotDerivation.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Library for computing storage (and transient storage) locations from namespaces and deriving slots\n * corresponding to standard patterns. The derivation method for array and mapping matches the storage layout used by\n * the solidity language / compiler.\n *\n * See https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays[Solidity docs for mappings and dynamic arrays.].\n *\n * Example usage:\n * ```solidity\n * contract Example {\n * // Add the library methods\n * using StorageSlot for bytes32;\n * using SlotDerivation for bytes32;\n *\n * // Declare a namespace\n * string private constant _NAMESPACE = \"\"; // eg. OpenZeppelin.Slot\n *\n * function setValueInNamespace(uint256 key, address newValue) internal {\n * _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value = newValue;\n * }\n *\n * function getValueInNamespace(uint256 key) internal view returns (address) {\n * return _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value;\n * }\n * }\n * ```\n *\n * TIP: Consider using this library along with {StorageSlot}.\n *\n * NOTE: This library provides a way to manipulate storage locations in a non-standard way. Tooling for checking\n * upgrade safety will ignore the slots accessed through this library.\n *\n * _Available since v5.1._\n */\nlibrary SlotDerivation {\n /**\n * @dev Derive an ERC-7201 slot from a string (namespace).\n */\n function erc7201Slot(string memory namespace) internal pure returns (bytes32 slot) {\n assembly (\"memory-safe\") {\n mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1))\n slot := and(keccak256(0x00, 0x20), not(0xff))\n }\n }\n\n /**\n * @dev Add an offset to a slot to get the n-th element of a structure or an array.\n */\n function offset(bytes32 slot, uint256 pos) internal pure returns (bytes32 result) {\n unchecked {\n return bytes32(uint256(slot) + pos);\n }\n }\n\n /**\n * @dev Derive the location of the first element in an array from the slot where the length is stored.\n */\n function deriveArray(bytes32 slot) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, slot)\n result := keccak256(0x00, 0x20)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, address key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, and(key, shr(96, not(0))))\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, bool key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, iszero(iszero(key)))\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, bytes32 key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, key)\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, uint256 key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, key)\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, int256 key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n mstore(0x00, key)\n mstore(0x20, slot)\n result := keccak256(0x00, 0x40)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, string memory key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n let length := mload(key)\n let begin := add(key, 0x20)\n let end := add(begin, length)\n let cache := mload(end)\n mstore(end, slot)\n result := keccak256(begin, add(length, 0x20))\n mstore(end, cache)\n }\n }\n\n /**\n * @dev Derive the location of a mapping element from the key.\n */\n function deriveMapping(bytes32 slot, bytes memory key) internal pure returns (bytes32 result) {\n assembly (\"memory-safe\") {\n let length := mload(key)\n let begin := add(key, 0x20)\n let end := add(begin, length)\n let cache := mload(end)\n mstore(end, slot)\n result := keccak256(begin, add(length, 0x20))\n mstore(end, cache)\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/StorageSlot.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/StorageSlot.sol)\n// This file was procedurally generated from scripts/generate/templates/StorageSlot.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Library for reading and writing primitive types to specific storage slots.\n *\n * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts.\n * This library helps with reading and writing to such slots without the need for inline assembly.\n *\n * The functions in this library return Slot structs that contain a `value` member that can be used to read or write.\n *\n * Example usage to set ERC-1967 implementation slot:\n * ```solidity\n * contract ERC1967 {\n * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot.\n * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;\n *\n * function _getImplementation() internal view returns (address) {\n * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;\n * }\n *\n * function _setImplementation(address newImplementation) internal {\n * require(newImplementation.code.length > 0);\n * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;\n * }\n * }\n * ```\n *\n * TIP: Consider using this library along with {SlotDerivation}.\n */\nlibrary StorageSlot {\n struct AddressSlot {\n address value;\n }\n\n struct BooleanSlot {\n bool value;\n }\n\n struct Bytes32Slot {\n bytes32 value;\n }\n\n struct Uint256Slot {\n uint256 value;\n }\n\n struct Int256Slot {\n int256 value;\n }\n\n struct StringSlot {\n string value;\n }\n\n struct BytesSlot {\n bytes value;\n }\n\n /**\n * @dev Returns an `AddressSlot` with member `value` located at `slot`.\n */\n function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `BooleanSlot` with member `value` located at `slot`.\n */\n function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Bytes32Slot` with member `value` located at `slot`.\n */\n function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Uint256Slot` with member `value` located at `slot`.\n */\n function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `Int256Slot` with member `value` located at `slot`.\n */\n function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns a `StringSlot` with member `value` located at `slot`.\n */\n function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns an `StringSlot` representation of the string storage pointer `store`.\n */\n function getStringSlot(string storage store) internal pure returns (StringSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := store.slot\n }\n }\n\n /**\n * @dev Returns a `BytesSlot` with member `value` located at `slot`.\n */\n function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := slot\n }\n }\n\n /**\n * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`.\n */\n function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) {\n assembly (\"memory-safe\") {\n r.slot := store.slot\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/introspection/IERC165.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/IERC165.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Interface of the ERC-165 standard, as defined in the\n * https://eips.ethereum.org/EIPS/eip-165[ERC].\n *\n * Implementers can declare support of contract interfaces, which can then be\n * queried by others ({ERC165Checker}).\n *\n * For an implementation, see {ERC165}.\n */\ninterface IERC165 {\n /**\n * @dev Returns true if this contract implements the interface defined by\n * `interfaceId`. See the corresponding\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]\n * to learn more about how these ids are created.\n *\n * This function call must use less than 30 000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/Math.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/math/Math.sol)\n\npragma solidity ^0.8.20;\n\nimport {Panic} from \"../Panic.sol\";\nimport {SafeCast} from \"./SafeCast.sol\";\n\n/**\n * @dev Standard math utilities missing in the Solidity language.\n */\nlibrary Math {\n enum Rounding {\n Floor, // Toward negative infinity\n Ceil, // Toward positive infinity\n Trunc, // Toward zero\n Expand // Away from zero\n }\n\n /**\n * @dev Return the 512-bit addition of two uint256.\n *\n * The result is stored in two 256 variables such that sum = high * 2\u00b2\u2075\u2076 + low.\n */\n function add512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {\n assembly (\"memory-safe\") {\n low := add(a, b)\n high := lt(low, a)\n }\n }\n\n /**\n * @dev Return the 512-bit multiplication of two uint256.\n *\n * The result is stored in two 256 variables such that product = high * 2\u00b2\u2075\u2076 + low.\n */\n function mul512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {\n // 512-bit multiply [high low] = x * y. Compute the product mod 2\u00b2\u2075\u2076 and mod 2\u00b2\u2075\u2076 - 1, then use\n // the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256\n // variables such that product = high * 2\u00b2\u2075\u2076 + low.\n assembly (\"memory-safe\") {\n let mm := mulmod(a, b, not(0))\n low := mul(a, b)\n high := sub(sub(mm, low), lt(mm, low))\n }\n }\n\n /**\n * @dev Returns the addition of two unsigned integers, with a success flag (no overflow).\n */\n function tryAdd(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a + b;\n success = c >= a;\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the subtraction of two unsigned integers, with a success flag (no overflow).\n */\n function trySub(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a - b;\n success = c <= a;\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the multiplication of two unsigned integers, with a success flag (no overflow).\n */\n function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n uint256 c = a * b;\n assembly (\"memory-safe\") {\n // Only true when the multiplication doesn't overflow\n // (c / a == b) || (a == 0)\n success := or(eq(div(c, a), b), iszero(a))\n }\n // equivalent to: success ? c : 0\n result = c * SafeCast.toUint(success);\n }\n }\n\n /**\n * @dev Returns the division of two unsigned integers, with a success flag (no division by zero).\n */\n function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n success = b > 0;\n assembly (\"memory-safe\") {\n // The `DIV` opcode returns zero when the denominator is 0.\n result := div(a, b)\n }\n }\n }\n\n /**\n * @dev Returns the remainder of dividing two unsigned integers, with a success flag (no division by zero).\n */\n function tryMod(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {\n unchecked {\n success = b > 0;\n assembly (\"memory-safe\") {\n // The `MOD` opcode returns zero when the denominator is 0.\n result := mod(a, b)\n }\n }\n }\n\n /**\n * @dev Unsigned saturating addition, bounds to `2\u00b2\u2075\u2076 - 1` instead of overflowing.\n */\n function saturatingAdd(uint256 a, uint256 b) internal pure returns (uint256) {\n (bool success, uint256 result) = tryAdd(a, b);\n return ternary(success, result, type(uint256).max);\n }\n\n /**\n * @dev Unsigned saturating subtraction, bounds to zero instead of overflowing.\n */\n function saturatingSub(uint256 a, uint256 b) internal pure returns (uint256) {\n (, uint256 result) = trySub(a, b);\n return result;\n }\n\n /**\n * @dev Unsigned saturating multiplication, bounds to `2\u00b2\u2075\u2076 - 1` instead of overflowing.\n */\n function saturatingMul(uint256 a, uint256 b) internal pure returns (uint256) {\n (bool success, uint256 result) = tryMul(a, b);\n return ternary(success, result, type(uint256).max);\n }\n\n /**\n * @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant.\n *\n * IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone.\n * However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute\n * one branch when needed, making this function more expensive.\n */\n function ternary(bool condition, uint256 a, uint256 b) internal pure returns (uint256) {\n unchecked {\n // branchless ternary works because:\n // b ^ (a ^ b) == a\n // b ^ 0 == b\n return b ^ ((a ^ b) * SafeCast.toUint(condition));\n }\n }\n\n /**\n * @dev Returns the largest of two numbers.\n */\n function max(uint256 a, uint256 b) internal pure returns (uint256) {\n return ternary(a > b, a, b);\n }\n\n /**\n * @dev Returns the smallest of two numbers.\n */\n function min(uint256 a, uint256 b) internal pure returns (uint256) {\n return ternary(a < b, a, b);\n }\n\n /**\n * @dev Returns the average of two numbers. The result is rounded towards\n * zero.\n */\n function average(uint256 a, uint256 b) internal pure returns (uint256) {\n // (a + b) / 2 can overflow.\n return (a & b) + (a ^ b) / 2;\n }\n\n /**\n * @dev Returns the ceiling of the division of two numbers.\n *\n * This differs from standard division with `/` in that it rounds towards infinity instead\n * of rounding towards zero.\n */\n function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {\n if (b == 0) {\n // Guarantee the same behavior as in a regular Solidity division.\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n\n // The following calculation ensures accurate ceiling division without overflow.\n // Since a is non-zero, (a - 1) / b will not overflow.\n // The largest possible result occurs when (a - 1) / b is type(uint256).max,\n // but the largest value we can obtain is type(uint256).max - 1, which happens\n // when a = type(uint256).max and b = 1.\n unchecked {\n return SafeCast.toUint(a > 0) * ((a - 1) / b + 1);\n }\n }\n\n /**\n * @dev Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or\n * denominator == 0.\n *\n * Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by\n * Uniswap Labs also under MIT license.\n */\n function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {\n unchecked {\n (uint256 high, uint256 low) = mul512(x, y);\n\n // Handle non-overflow cases, 256 by 256 division.\n if (high == 0) {\n // Solidity will revert if denominator == 0, unlike the div opcode on its own.\n // The surrounding unchecked block does not change this fact.\n // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.\n return low / denominator;\n }\n\n // Make sure the result is less than 2\u00b2\u2075\u2076. Also prevents denominator == 0.\n if (denominator <= high) {\n Panic.panic(ternary(denominator == 0, Panic.DIVISION_BY_ZERO, Panic.UNDER_OVERFLOW));\n }\n\n ///////////////////////////////////////////////\n // 512 by 256 division.\n ///////////////////////////////////////////////\n\n // Make division exact by subtracting the remainder from [high low].\n uint256 remainder;\n assembly (\"memory-safe\") {\n // Compute remainder using mulmod.\n remainder := mulmod(x, y, denominator)\n\n // Subtract 256 bit number from 512 bit number.\n high := sub(high, gt(remainder, low))\n low := sub(low, remainder)\n }\n\n // Factor powers of two out of denominator and compute largest power of two divisor of denominator.\n // Always >= 1. See https://cs.stackexchange.com/q/138556/92363.\n\n uint256 twos = denominator & (0 - denominator);\n assembly (\"memory-safe\") {\n // Divide denominator by twos.\n denominator := div(denominator, twos)\n\n // Divide [high low] by twos.\n low := div(low, twos)\n\n // Flip twos such that it is 2\u00b2\u2075\u2076 / twos. If twos is zero, then it becomes one.\n twos := add(div(sub(0, twos), twos), 1)\n }\n\n // Shift in bits from high into low.\n low |= high * twos;\n\n // Invert denominator mod 2\u00b2\u2075\u2076. Now that denominator is an odd number, it has an inverse modulo 2\u00b2\u2075\u2076 such\n // that denominator * inv \u2261 1 mod 2\u00b2\u2075\u2076. Compute the inverse by starting with a seed that is correct for\n // four bits. That is, denominator * inv \u2261 1 mod 2\u2074.\n uint256 inverse = (3 * denominator) ^ 2;\n\n // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also\n // works in modular arithmetic, doubling the correct bits in each step.\n inverse *= 2 - denominator * inverse; // inverse mod 2\u2078\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b9\u2076\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b3\u00b2\n inverse *= 2 - denominator * inverse; // inverse mod 2\u2076\u2074\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b9\u00b2\u2078\n inverse *= 2 - denominator * inverse; // inverse mod 2\u00b2\u2075\u2076\n\n // Because the division is now exact we can divide by multiplying with the modular inverse of denominator.\n // This will give us the correct result modulo 2\u00b2\u2075\u2076. Since the preconditions guarantee that the outcome is\n // less than 2\u00b2\u2075\u2076, this is the final result. We don't need to compute the high bits of the result and high\n // is no longer required.\n result = low * inverse;\n return result;\n }\n }\n\n /**\n * @dev Calculates x * y / denominator with full precision, following the selected rounding direction.\n */\n function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {\n return mulDiv(x, y, denominator) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0);\n }\n\n /**\n * @dev Calculates floor(x * y >> n) with full precision. Throws if result overflows a uint256.\n */\n function mulShr(uint256 x, uint256 y, uint8 n) internal pure returns (uint256 result) {\n unchecked {\n (uint256 high, uint256 low) = mul512(x, y);\n if (high >= 1 << n) {\n Panic.panic(Panic.UNDER_OVERFLOW);\n }\n return (high << (256 - n)) | (low >> n);\n }\n }\n\n /**\n * @dev Calculates x * y >> n with full precision, following the selected rounding direction.\n */\n function mulShr(uint256 x, uint256 y, uint8 n, Rounding rounding) internal pure returns (uint256) {\n return mulShr(x, y, n) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, 1 << n) > 0);\n }\n\n /**\n * @dev Calculate the modular multiplicative inverse of a number in Z/nZ.\n *\n * If n is a prime, then Z/nZ is a field. In that case all elements are inversible, except 0.\n * If n is not a prime, then Z/nZ is not a field, and some elements might not be inversible.\n *\n * If the input value is not inversible, 0 is returned.\n *\n * NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Fermat's little theorem and get the\n * inverse using `Math.modExp(a, n - 2, n)`. See {invModPrime}.\n */\n function invMod(uint256 a, uint256 n) internal pure returns (uint256) {\n unchecked {\n if (n == 0) return 0;\n\n // The inverse modulo is calculated using the Extended Euclidean Algorithm (iterative version)\n // Used to compute integers x and y such that: ax + ny = gcd(a, n).\n // When the gcd is 1, then the inverse of a modulo n exists and it's x.\n // ax + ny = 1\n // ax = 1 + (-y)n\n // ax \u2261 1 (mod n) # x is the inverse of a modulo n\n\n // If the remainder is 0 the gcd is n right away.\n uint256 remainder = a % n;\n uint256 gcd = n;\n\n // Therefore the initial coefficients are:\n // ax + ny = gcd(a, n) = n\n // 0a + 1n = n\n int256 x = 0;\n int256 y = 1;\n\n while (remainder != 0) {\n uint256 quotient = gcd / remainder;\n\n (gcd, remainder) = (\n // The old remainder is the next gcd to try.\n remainder,\n // Compute the next remainder.\n // Can't overflow given that (a % gcd) * (gcd // (a % gcd)) <= gcd\n // where gcd is at most n (capped to type(uint256).max)\n gcd - remainder * quotient\n );\n\n (x, y) = (\n // Increment the coefficient of a.\n y,\n // Decrement the coefficient of n.\n // Can overflow, but the result is casted to uint256 so that the\n // next value of y is \"wrapped around\" to a value between 0 and n - 1.\n x - y * int256(quotient)\n );\n }\n\n if (gcd != 1) return 0; // No inverse exists.\n return ternary(x < 0, n - uint256(-x), uint256(x)); // Wrap the result if it's negative.\n }\n }\n\n /**\n * @dev Variant of {invMod}. More efficient, but only works if `p` is known to be a prime greater than `2`.\n *\n * From https://en.wikipedia.org/wiki/Fermat%27s_little_theorem[Fermat's little theorem], we know that if p is\n * prime, then `a**(p-1) \u2261 1 mod p`. As a consequence, we have `a * a**(p-2) \u2261 1 mod p`, which means that\n * `a**(p-2)` is the modular multiplicative inverse of a in Fp.\n *\n * NOTE: this function does NOT check that `p` is a prime greater than `2`.\n */\n function invModPrime(uint256 a, uint256 p) internal view returns (uint256) {\n unchecked {\n return Math.modExp(a, p - 2, p);\n }\n }\n\n /**\n * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m)\n *\n * Requirements:\n * - modulus can't be zero\n * - underlying staticcall to precompile must succeed\n *\n * IMPORTANT: The result is only valid if the underlying call succeeds. When using this function, make\n * sure the chain you're using it on supports the precompiled contract for modular exponentiation\n * at address 0x05 as specified in https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise,\n * the underlying function will succeed given the lack of a revert, but the result may be incorrectly\n * interpreted as 0.\n */\n function modExp(uint256 b, uint256 e, uint256 m) internal view returns (uint256) {\n (bool success, uint256 result) = tryModExp(b, e, m);\n if (!success) {\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n return result;\n }\n\n /**\n * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m).\n * It includes a success flag indicating if the operation succeeded. Operation will be marked as failed if trying\n * to operate modulo 0 or if the underlying precompile reverted.\n *\n * IMPORTANT: The result is only valid if the success flag is true. When using this function, make sure the chain\n * you're using it on supports the precompiled contract for modular exponentiation at address 0x05 as specified in\n * https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise, the underlying function will succeed given the lack\n * of a revert, but the result may be incorrectly interpreted as 0.\n */\n function tryModExp(uint256 b, uint256 e, uint256 m) internal view returns (bool success, uint256 result) {\n if (m == 0) return (false, 0);\n assembly (\"memory-safe\") {\n let ptr := mload(0x40)\n // | Offset | Content | Content (Hex) |\n // |-----------|------------|--------------------------------------------------------------------|\n // | 0x00:0x1f | size of b | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x20:0x3f | size of e | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x40:0x5f | size of m | 0x0000000000000000000000000000000000000000000000000000000000000020 |\n // | 0x60:0x7f | value of b | 0x<.............................................................b> |\n // | 0x80:0x9f | value of e | 0x<.............................................................e> |\n // | 0xa0:0xbf | value of m | 0x<.............................................................m> |\n mstore(ptr, 0x20)\n mstore(add(ptr, 0x20), 0x20)\n mstore(add(ptr, 0x40), 0x20)\n mstore(add(ptr, 0x60), b)\n mstore(add(ptr, 0x80), e)\n mstore(add(ptr, 0xa0), m)\n\n // Given the result < m, it's guaranteed to fit in 32 bytes,\n // so we can use the memory scratch space located at offset 0.\n success := staticcall(gas(), 0x05, ptr, 0xc0, 0x00, 0x20)\n result := mload(0x00)\n }\n }\n\n /**\n * @dev Variant of {modExp} that supports inputs of arbitrary length.\n */\n function modExp(bytes memory b, bytes memory e, bytes memory m) internal view returns (bytes memory) {\n (bool success, bytes memory result) = tryModExp(b, e, m);\n if (!success) {\n Panic.panic(Panic.DIVISION_BY_ZERO);\n }\n return result;\n }\n\n /**\n * @dev Variant of {tryModExp} that supports inputs of arbitrary length.\n */\n function tryModExp(\n bytes memory b,\n bytes memory e,\n bytes memory m\n ) internal view returns (bool success, bytes memory result) {\n if (_zeroBytes(m)) return (false, new bytes(0));\n\n uint256 mLen = m.length;\n\n // Encode call args in result and move the free memory pointer\n result = abi.encodePacked(b.length, e.length, mLen, b, e, m);\n\n assembly (\"memory-safe\") {\n let dataPtr := add(result, 0x20)\n // Write result on top of args to avoid allocating extra memory.\n success := staticcall(gas(), 0x05, dataPtr, mload(result), dataPtr, mLen)\n // Overwrite the length.\n // result.length > returndatasize() is guaranteed because returndatasize() == m.length\n mstore(result, mLen)\n // Set the memory pointer after the returned data.\n mstore(0x40, add(dataPtr, mLen))\n }\n }\n\n /**\n * @dev Returns whether the provided byte array is zero.\n */\n function _zeroBytes(bytes memory byteArray) private pure returns (bool) {\n for (uint256 i = 0; i < byteArray.length; ++i) {\n if (byteArray[i] != 0) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded\n * towards zero.\n *\n * This method is based on Newton's method for computing square roots; the algorithm is restricted to only\n * using integer operations.\n */\n function sqrt(uint256 a) internal pure returns (uint256) {\n unchecked {\n // Take care of easy edge cases when a == 0 or a == 1\n if (a <= 1) {\n return a;\n }\n\n // In this function, we use Newton's method to get a root of `f(x) := x\u00b2 - a`. It involves building a\n // sequence x_n that converges toward sqrt(a). For each iteration x_n, we also define the error between\n // the current value as `\u03b5_n = | x_n - sqrt(a) |`.\n //\n // For our first estimation, we consider `e` the smallest power of 2 which is bigger than the square root\n // of the target. (i.e. `2**(e-1) \u2264 sqrt(a) < 2**e`). We know that `e \u2264 128` because `(2\u00b9\u00b2\u2078)\u00b2 = 2\u00b2\u2075\u2076` is\n // bigger than any uint256.\n //\n // By noticing that\n // `2**(e-1) \u2264 sqrt(a) < 2**e \u2192 (2**(e-1))\u00b2 \u2264 a < (2**e)\u00b2 \u2192 2**(2*e-2) \u2264 a < 2**(2*e)`\n // we can deduce that `e - 1` is `log2(a) / 2`. We can thus compute `x_n = 2**(e-1)` using a method similar\n // to the msb function.\n uint256 aa = a;\n uint256 xn = 1;\n\n if (aa >= (1 << 128)) {\n aa >>= 128;\n xn <<= 64;\n }\n if (aa >= (1 << 64)) {\n aa >>= 64;\n xn <<= 32;\n }\n if (aa >= (1 << 32)) {\n aa >>= 32;\n xn <<= 16;\n }\n if (aa >= (1 << 16)) {\n aa >>= 16;\n xn <<= 8;\n }\n if (aa >= (1 << 8)) {\n aa >>= 8;\n xn <<= 4;\n }\n if (aa >= (1 << 4)) {\n aa >>= 4;\n xn <<= 2;\n }\n if (aa >= (1 << 2)) {\n xn <<= 1;\n }\n\n // We now have x_n such that `x_n = 2**(e-1) \u2264 sqrt(a) < 2**e = 2 * x_n`. This implies \u03b5_n \u2264 2**(e-1).\n //\n // We can refine our estimation by noticing that the middle of that interval minimizes the error.\n // If we move x_n to equal 2**(e-1) + 2**(e-2), then we reduce the error to \u03b5_n \u2264 2**(e-2).\n // This is going to be our x_0 (and \u03b5_0)\n xn = (3 * xn) >> 1; // \u03b5_0 := | x_0 - sqrt(a) | \u2264 2**(e-2)\n\n // From here, Newton's method give us:\n // x_{n+1} = (x_n + a / x_n) / 2\n //\n // One should note that:\n // x_{n+1}\u00b2 - a = ((x_n + a / x_n) / 2)\u00b2 - a\n // = ((x_n\u00b2 + a) / (2 * x_n))\u00b2 - a\n // = (x_n\u2074 + 2 * a * x_n\u00b2 + a\u00b2) / (4 * x_n\u00b2) - a\n // = (x_n\u2074 + 2 * a * x_n\u00b2 + a\u00b2 - 4 * a * x_n\u00b2) / (4 * x_n\u00b2)\n // = (x_n\u2074 - 2 * a * x_n\u00b2 + a\u00b2) / (4 * x_n\u00b2)\n // = (x_n\u00b2 - a)\u00b2 / (2 * x_n)\u00b2\n // = ((x_n\u00b2 - a) / (2 * x_n))\u00b2\n // \u2265 0\n // Which proves that for all n \u2265 1, sqrt(a) \u2264 x_n\n //\n // This gives us the proof of quadratic convergence of the sequence:\n // \u03b5_{n+1} = | x_{n+1} - sqrt(a) |\n // = | (x_n + a / x_n) / 2 - sqrt(a) |\n // = | (x_n\u00b2 + a - 2*x_n*sqrt(a)) / (2 * x_n) |\n // = | (x_n - sqrt(a))\u00b2 / (2 * x_n) |\n // = | \u03b5_n\u00b2 / (2 * x_n) |\n // = \u03b5_n\u00b2 / | (2 * x_n) |\n //\n // For the first iteration, we have a special case where x_0 is known:\n // \u03b5_1 = \u03b5_0\u00b2 / | (2 * x_0) |\n // \u2264 (2**(e-2))\u00b2 / (2 * (2**(e-1) + 2**(e-2)))\n // \u2264 2**(2*e-4) / (3 * 2**(e-1))\n // \u2264 2**(e-3) / 3\n // \u2264 2**(e-3-log2(3))\n // \u2264 2**(e-4.5)\n //\n // For the following iterations, we use the fact that, 2**(e-1) \u2264 sqrt(a) \u2264 x_n:\n // \u03b5_{n+1} = \u03b5_n\u00b2 / | (2 * x_n) |\n // \u2264 (2**(e-k))\u00b2 / (2 * 2**(e-1))\n // \u2264 2**(2*e-2*k) / 2**e\n // \u2264 2**(e-2*k)\n xn = (xn + a / xn) >> 1; // \u03b5_1 := | x_1 - sqrt(a) | \u2264 2**(e-4.5) -- special case, see above\n xn = (xn + a / xn) >> 1; // \u03b5_2 := | x_2 - sqrt(a) | \u2264 2**(e-9) -- general case with k = 4.5\n xn = (xn + a / xn) >> 1; // \u03b5_3 := | x_3 - sqrt(a) | \u2264 2**(e-18) -- general case with k = 9\n xn = (xn + a / xn) >> 1; // \u03b5_4 := | x_4 - sqrt(a) | \u2264 2**(e-36) -- general case with k = 18\n xn = (xn + a / xn) >> 1; // \u03b5_5 := | x_5 - sqrt(a) | \u2264 2**(e-72) -- general case with k = 36\n xn = (xn + a / xn) >> 1; // \u03b5_6 := | x_6 - sqrt(a) | \u2264 2**(e-144) -- general case with k = 72\n\n // Because e \u2264 128 (as discussed during the first estimation phase), we know have reached a precision\n // \u03b5_6 \u2264 2**(e-144) < 1. Given we're operating on integers, then we can ensure that xn is now either\n // sqrt(a) or sqrt(a) + 1.\n return xn - SafeCast.toUint(xn > a / xn);\n }\n }\n\n /**\n * @dev Calculates sqrt(a), following the selected rounding direction.\n */\n function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = sqrt(a);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && result * result < a);\n }\n }\n\n /**\n * @dev Return the log in base 2 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n */\n function log2(uint256 x) internal pure returns (uint256 r) {\n // If value has upper 128 bits set, log2 result is at least 128\n r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7;\n // If upper 64 bits of 128-bit half set, add 64 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6;\n // If upper 32 bits of 64-bit half set, add 32 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5;\n // If upper 16 bits of 32-bit half set, add 16 to result\n r |= SafeCast.toUint((x >> r) > 0xffff) << 4;\n // If upper 8 bits of 16-bit half set, add 8 to result\n r |= SafeCast.toUint((x >> r) > 0xff) << 3;\n // If upper 4 bits of 8-bit half set, add 4 to result\n r |= SafeCast.toUint((x >> r) > 0xf) << 2;\n\n // Shifts value right by the current result and use it as an index into this lookup table:\n //\n // | x (4 bits) | index | table[index] = MSB position |\n // |------------|---------|-----------------------------|\n // | 0000 | 0 | table[0] = 0 |\n // | 0001 | 1 | table[1] = 0 |\n // | 0010 | 2 | table[2] = 1 |\n // | 0011 | 3 | table[3] = 1 |\n // | 0100 | 4 | table[4] = 2 |\n // | 0101 | 5 | table[5] = 2 |\n // | 0110 | 6 | table[6] = 2 |\n // | 0111 | 7 | table[7] = 2 |\n // | 1000 | 8 | table[8] = 3 |\n // | 1001 | 9 | table[9] = 3 |\n // | 1010 | 10 | table[10] = 3 |\n // | 1011 | 11 | table[11] = 3 |\n // | 1100 | 12 | table[12] = 3 |\n // | 1101 | 13 | table[13] = 3 |\n // | 1110 | 14 | table[14] = 3 |\n // | 1111 | 15 | table[15] = 3 |\n //\n // The lookup table is represented as a 32-byte value with the MSB positions for 0-15 in the last 16 bytes.\n assembly (\"memory-safe\") {\n r := or(r, byte(shr(r, x), 0x0000010102020202030303030303030300000000000000000000000000000000))\n }\n }\n\n /**\n * @dev Return the log in base 2, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log2(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << result < value);\n }\n }\n\n /**\n * @dev Return the log in base 10 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n */\n function log10(uint256 value) internal pure returns (uint256) {\n uint256 result = 0;\n unchecked {\n if (value >= 10 ** 64) {\n value /= 10 ** 64;\n result += 64;\n }\n if (value >= 10 ** 32) {\n value /= 10 ** 32;\n result += 32;\n }\n if (value >= 10 ** 16) {\n value /= 10 ** 16;\n result += 16;\n }\n if (value >= 10 ** 8) {\n value /= 10 ** 8;\n result += 8;\n }\n if (value >= 10 ** 4) {\n value /= 10 ** 4;\n result += 4;\n }\n if (value >= 10 ** 2) {\n value /= 10 ** 2;\n result += 2;\n }\n if (value >= 10 ** 1) {\n result += 1;\n }\n }\n return result;\n }\n\n /**\n * @dev Return the log in base 10, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log10(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 10 ** result < value);\n }\n }\n\n /**\n * @dev Return the log in base 256 of a positive value rounded towards zero.\n * Returns 0 if given 0.\n *\n * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.\n */\n function log256(uint256 x) internal pure returns (uint256 r) {\n // If value has upper 128 bits set, log2 result is at least 128\n r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7;\n // If upper 64 bits of 128-bit half set, add 64 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6;\n // If upper 32 bits of 64-bit half set, add 32 to result\n r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5;\n // If upper 16 bits of 32-bit half set, add 16 to result\n r |= SafeCast.toUint((x >> r) > 0xffff) << 4;\n // Add 1 if upper 8 bits of 16-bit half set, and divide accumulated result by 8\n return (r >> 3) | SafeCast.toUint((x >> r) > 0xff);\n }\n\n /**\n * @dev Return the log in base 256, following the selected rounding direction, of a positive value.\n * Returns 0 if given 0.\n */\n function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {\n unchecked {\n uint256 result = log256(value);\n return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << (result << 3) < value);\n }\n }\n\n /**\n * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.\n */\n function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {\n return uint8(rounding) % 2 == 1;\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/SafeCast.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SafeCast.sol)\n// This file was procedurally generated from scripts/generate/templates/SafeCast.js.\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Wrappers over Solidity's uintXX/intXX/bool casting operators with added overflow\n * checks.\n *\n * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can\n * easily result in undesired exploitation or bugs, since developers usually\n * assume that overflows raise errors. `SafeCast` restores this intuition by\n * reverting the transaction when such an operation overflows.\n *\n * Using this library instead of the unchecked operations eliminates an entire\n * class of bugs, so it's recommended to use it always.\n */\nlibrary SafeCast {\n /**\n * @dev Value doesn't fit in an uint of `bits` size.\n */\n error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);\n\n /**\n * @dev An int value doesn't fit in an uint of `bits` size.\n */\n error SafeCastOverflowedIntToUint(int256 value);\n\n /**\n * @dev Value doesn't fit in an int of `bits` size.\n */\n error SafeCastOverflowedIntDowncast(uint8 bits, int256 value);\n\n /**\n * @dev An uint value doesn't fit in an int of `bits` size.\n */\n error SafeCastOverflowedUintToInt(uint256 value);\n\n /**\n * @dev Returns the downcasted uint248 from uint256, reverting on\n * overflow (when the input is greater than largest uint248).\n *\n * Counterpart to Solidity's `uint248` operator.\n *\n * Requirements:\n *\n * - input must fit into 248 bits\n */\n function toUint248(uint256 value) internal pure returns (uint248) {\n if (value > type(uint248).max) {\n revert SafeCastOverflowedUintDowncast(248, value);\n }\n return uint248(value);\n }\n\n /**\n * @dev Returns the downcasted uint240 from uint256, reverting on\n * overflow (when the input is greater than largest uint240).\n *\n * Counterpart to Solidity's `uint240` operator.\n *\n * Requirements:\n *\n * - input must fit into 240 bits\n */\n function toUint240(uint256 value) internal pure returns (uint240) {\n if (value > type(uint240).max) {\n revert SafeCastOverflowedUintDowncast(240, value);\n }\n return uint240(value);\n }\n\n /**\n * @dev Returns the downcasted uint232 from uint256, reverting on\n * overflow (when the input is greater than largest uint232).\n *\n * Counterpart to Solidity's `uint232` operator.\n *\n * Requirements:\n *\n * - input must fit into 232 bits\n */\n function toUint232(uint256 value) internal pure returns (uint232) {\n if (value > type(uint232).max) {\n revert SafeCastOverflowedUintDowncast(232, value);\n }\n return uint232(value);\n }\n\n /**\n * @dev Returns the downcasted uint224 from uint256, reverting on\n * overflow (when the input is greater than largest uint224).\n *\n * Counterpart to Solidity's `uint224` operator.\n *\n * Requirements:\n *\n * - input must fit into 224 bits\n */\n function toUint224(uint256 value) internal pure returns (uint224) {\n if (value > type(uint224).max) {\n revert SafeCastOverflowedUintDowncast(224, value);\n }\n return uint224(value);\n }\n\n /**\n * @dev Returns the downcasted uint216 from uint256, reverting on\n * overflow (when the input is greater than largest uint216).\n *\n * Counterpart to Solidity's `uint216` operator.\n *\n * Requirements:\n *\n * - input must fit into 216 bits\n */\n function toUint216(uint256 value) internal pure returns (uint216) {\n if (value > type(uint216).max) {\n revert SafeCastOverflowedUintDowncast(216, value);\n }\n return uint216(value);\n }\n\n /**\n * @dev Returns the downcasted uint208 from uint256, reverting on\n * overflow (when the input is greater than largest uint208).\n *\n * Counterpart to Solidity's `uint208` operator.\n *\n * Requirements:\n *\n * - input must fit into 208 bits\n */\n function toUint208(uint256 value) internal pure returns (uint208) {\n if (value > type(uint208).max) {\n revert SafeCastOverflowedUintDowncast(208, value);\n }\n return uint208(value);\n }\n\n /**\n * @dev Returns the downcasted uint200 from uint256, reverting on\n * overflow (when the input is greater than largest uint200).\n *\n * Counterpart to Solidity's `uint200` operator.\n *\n * Requirements:\n *\n * - input must fit into 200 bits\n */\n function toUint200(uint256 value) internal pure returns (uint200) {\n if (value > type(uint200).max) {\n revert SafeCastOverflowedUintDowncast(200, value);\n }\n return uint200(value);\n }\n\n /**\n * @dev Returns the downcasted uint192 from uint256, reverting on\n * overflow (when the input is greater than largest uint192).\n *\n * Counterpart to Solidity's `uint192` operator.\n *\n * Requirements:\n *\n * - input must fit into 192 bits\n */\n function toUint192(uint256 value) internal pure returns (uint192) {\n if (value > type(uint192).max) {\n revert SafeCastOverflowedUintDowncast(192, value);\n }\n return uint192(value);\n }\n\n /**\n * @dev Returns the downcasted uint184 from uint256, reverting on\n * overflow (when the input is greater than largest uint184).\n *\n * Counterpart to Solidity's `uint184` operator.\n *\n * Requirements:\n *\n * - input must fit into 184 bits\n */\n function toUint184(uint256 value) internal pure returns (uint184) {\n if (value > type(uint184).max) {\n revert SafeCastOverflowedUintDowncast(184, value);\n }\n return uint184(value);\n }\n\n /**\n * @dev Returns the downcasted uint176 from uint256, reverting on\n * overflow (when the input is greater than largest uint176).\n *\n * Counterpart to Solidity's `uint176` operator.\n *\n * Requirements:\n *\n * - input must fit into 176 bits\n */\n function toUint176(uint256 value) internal pure returns (uint176) {\n if (value > type(uint176).max) {\n revert SafeCastOverflowedUintDowncast(176, value);\n }\n return uint176(value);\n }\n\n /**\n * @dev Returns the downcasted uint168 from uint256, reverting on\n * overflow (when the input is greater than largest uint168).\n *\n * Counterpart to Solidity's `uint168` operator.\n *\n * Requirements:\n *\n * - input must fit into 168 bits\n */\n function toUint168(uint256 value) internal pure returns (uint168) {\n if (value > type(uint168).max) {\n revert SafeCastOverflowedUintDowncast(168, value);\n }\n return uint168(value);\n }\n\n /**\n * @dev Returns the downcasted uint160 from uint256, reverting on\n * overflow (when the input is greater than largest uint160).\n *\n * Counterpart to Solidity's `uint160` operator.\n *\n * Requirements:\n *\n * - input must fit into 160 bits\n */\n function toUint160(uint256 value) internal pure returns (uint160) {\n if (value > type(uint160).max) {\n revert SafeCastOverflowedUintDowncast(160, value);\n }\n return uint160(value);\n }\n\n /**\n * @dev Returns the downcasted uint152 from uint256, reverting on\n * overflow (when the input is greater than largest uint152).\n *\n * Counterpart to Solidity's `uint152` operator.\n *\n * Requirements:\n *\n * - input must fit into 152 bits\n */\n function toUint152(uint256 value) internal pure returns (uint152) {\n if (value > type(uint152).max) {\n revert SafeCastOverflowedUintDowncast(152, value);\n }\n return uint152(value);\n }\n\n /**\n * @dev Returns the downcasted uint144 from uint256, reverting on\n * overflow (when the input is greater than largest uint144).\n *\n * Counterpart to Solidity's `uint144` operator.\n *\n * Requirements:\n *\n * - input must fit into 144 bits\n */\n function toUint144(uint256 value) internal pure returns (uint144) {\n if (value > type(uint144).max) {\n revert SafeCastOverflowedUintDowncast(144, value);\n }\n return uint144(value);\n }\n\n /**\n * @dev Returns the downcasted uint136 from uint256, reverting on\n * overflow (when the input is greater than largest uint136).\n *\n * Counterpart to Solidity's `uint136` operator.\n *\n * Requirements:\n *\n * - input must fit into 136 bits\n */\n function toUint136(uint256 value) internal pure returns (uint136) {\n if (value > type(uint136).max) {\n revert SafeCastOverflowedUintDowncast(136, value);\n }\n return uint136(value);\n }\n\n /**\n * @dev Returns the downcasted uint128 from uint256, reverting on\n * overflow (when the input is greater than largest uint128).\n *\n * Counterpart to Solidity's `uint128` operator.\n *\n * Requirements:\n *\n * - input must fit into 128 bits\n */\n function toUint128(uint256 value) internal pure returns (uint128) {\n if (value > type(uint128).max) {\n revert SafeCastOverflowedUintDowncast(128, value);\n }\n return uint128(value);\n }\n\n /**\n * @dev Returns the downcasted uint120 from uint256, reverting on\n * overflow (when the input is greater than largest uint120).\n *\n * Counterpart to Solidity's `uint120` operator.\n *\n * Requirements:\n *\n * - input must fit into 120 bits\n */\n function toUint120(uint256 value) internal pure returns (uint120) {\n if (value > type(uint120).max) {\n revert SafeCastOverflowedUintDowncast(120, value);\n }\n return uint120(value);\n }\n\n /**\n * @dev Returns the downcasted uint112 from uint256, reverting on\n * overflow (when the input is greater than largest uint112).\n *\n * Counterpart to Solidity's `uint112` operator.\n *\n * Requirements:\n *\n * - input must fit into 112 bits\n */\n function toUint112(uint256 value) internal pure returns (uint112) {\n if (value > type(uint112).max) {\n revert SafeCastOverflowedUintDowncast(112, value);\n }\n return uint112(value);\n }\n\n /**\n * @dev Returns the downcasted uint104 from uint256, reverting on\n * overflow (when the input is greater than largest uint104).\n *\n * Counterpart to Solidity's `uint104` operator.\n *\n * Requirements:\n *\n * - input must fit into 104 bits\n */\n function toUint104(uint256 value) internal pure returns (uint104) {\n if (value > type(uint104).max) {\n revert SafeCastOverflowedUintDowncast(104, value);\n }\n return uint104(value);\n }\n\n /**\n * @dev Returns the downcasted uint96 from uint256, reverting on\n * overflow (when the input is greater than largest uint96).\n *\n * Counterpart to Solidity's `uint96` operator.\n *\n * Requirements:\n *\n * - input must fit into 96 bits\n */\n function toUint96(uint256 value) internal pure returns (uint96) {\n if (value > type(uint96).max) {\n revert SafeCastOverflowedUintDowncast(96, value);\n }\n return uint96(value);\n }\n\n /**\n * @dev Returns the downcasted uint88 from uint256, reverting on\n * overflow (when the input is greater than largest uint88).\n *\n * Counterpart to Solidity's `uint88` operator.\n *\n * Requirements:\n *\n * - input must fit into 88 bits\n */\n function toUint88(uint256 value) internal pure returns (uint88) {\n if (value > type(uint88).max) {\n revert SafeCastOverflowedUintDowncast(88, value);\n }\n return uint88(value);\n }\n\n /**\n * @dev Returns the downcasted uint80 from uint256, reverting on\n * overflow (when the input is greater than largest uint80).\n *\n * Counterpart to Solidity's `uint80` operator.\n *\n * Requirements:\n *\n * - input must fit into 80 bits\n */\n function toUint80(uint256 value) internal pure returns (uint80) {\n if (value > type(uint80).max) {\n revert SafeCastOverflowedUintDowncast(80, value);\n }\n return uint80(value);\n }\n\n /**\n * @dev Returns the downcasted uint72 from uint256, reverting on\n * overflow (when the input is greater than largest uint72).\n *\n * Counterpart to Solidity's `uint72` operator.\n *\n * Requirements:\n *\n * - input must fit into 72 bits\n */\n function toUint72(uint256 value) internal pure returns (uint72) {\n if (value > type(uint72).max) {\n revert SafeCastOverflowedUintDowncast(72, value);\n }\n return uint72(value);\n }\n\n /**\n * @dev Returns the downcasted uint64 from uint256, reverting on\n * overflow (when the input is greater than largest uint64).\n *\n * Counterpart to Solidity's `uint64` operator.\n *\n * Requirements:\n *\n * - input must fit into 64 bits\n */\n function toUint64(uint256 value) internal pure returns (uint64) {\n if (value > type(uint64).max) {\n revert SafeCastOverflowedUintDowncast(64, value);\n }\n return uint64(value);\n }\n\n /**\n * @dev Returns the downcasted uint56 from uint256, reverting on\n * overflow (when the input is greater than largest uint56).\n *\n * Counterpart to Solidity's `uint56` operator.\n *\n * Requirements:\n *\n * - input must fit into 56 bits\n */\n function toUint56(uint256 value) internal pure returns (uint56) {\n if (value > type(uint56).max) {\n revert SafeCastOverflowedUintDowncast(56, value);\n }\n return uint56(value);\n }\n\n /**\n * @dev Returns the downcasted uint48 from uint256, reverting on\n * overflow (when the input is greater than largest uint48).\n *\n * Counterpart to Solidity's `uint48` operator.\n *\n * Requirements:\n *\n * - input must fit into 48 bits\n */\n function toUint48(uint256 value) internal pure returns (uint48) {\n if (value > type(uint48).max) {\n revert SafeCastOverflowedUintDowncast(48, value);\n }\n return uint48(value);\n }\n\n /**\n * @dev Returns the downcasted uint40 from uint256, reverting on\n * overflow (when the input is greater than largest uint40).\n *\n * Counterpart to Solidity's `uint40` operator.\n *\n * Requirements:\n *\n * - input must fit into 40 bits\n */\n function toUint40(uint256 value) internal pure returns (uint40) {\n if (value > type(uint40).max) {\n revert SafeCastOverflowedUintDowncast(40, value);\n }\n return uint40(value);\n }\n\n /**\n * @dev Returns the downcasted uint32 from uint256, reverting on\n * overflow (when the input is greater than largest uint32).\n *\n * Counterpart to Solidity's `uint32` operator.\n *\n * Requirements:\n *\n * - input must fit into 32 bits\n */\n function toUint32(uint256 value) internal pure returns (uint32) {\n if (value > type(uint32).max) {\n revert SafeCastOverflowedUintDowncast(32, value);\n }\n return uint32(value);\n }\n\n /**\n * @dev Returns the downcasted uint24 from uint256, reverting on\n * overflow (when the input is greater than largest uint24).\n *\n * Counterpart to Solidity's `uint24` operator.\n *\n * Requirements:\n *\n * - input must fit into 24 bits\n */\n function toUint24(uint256 value) internal pure returns (uint24) {\n if (value > type(uint24).max) {\n revert SafeCastOverflowedUintDowncast(24, value);\n }\n return uint24(value);\n }\n\n /**\n * @dev Returns the downcasted uint16 from uint256, reverting on\n * overflow (when the input is greater than largest uint16).\n *\n * Counterpart to Solidity's `uint16` operator.\n *\n * Requirements:\n *\n * - input must fit into 16 bits\n */\n function toUint16(uint256 value) internal pure returns (uint16) {\n if (value > type(uint16).max) {\n revert SafeCastOverflowedUintDowncast(16, value);\n }\n return uint16(value);\n }\n\n /**\n * @dev Returns the downcasted uint8 from uint256, reverting on\n * overflow (when the input is greater than largest uint8).\n *\n * Counterpart to Solidity's `uint8` operator.\n *\n * Requirements:\n *\n * - input must fit into 8 bits\n */\n function toUint8(uint256 value) internal pure returns (uint8) {\n if (value > type(uint8).max) {\n revert SafeCastOverflowedUintDowncast(8, value);\n }\n return uint8(value);\n }\n\n /**\n * @dev Converts a signed int256 into an unsigned uint256.\n *\n * Requirements:\n *\n * - input must be greater than or equal to 0.\n */\n function toUint256(int256 value) internal pure returns (uint256) {\n if (value < 0) {\n revert SafeCastOverflowedIntToUint(value);\n }\n return uint256(value);\n }\n\n /**\n * @dev Returns the downcasted int248 from int256, reverting on\n * overflow (when the input is less than smallest int248 or\n * greater than largest int248).\n *\n * Counterpart to Solidity's `int248` operator.\n *\n * Requirements:\n *\n * - input must fit into 248 bits\n */\n function toInt248(int256 value) internal pure returns (int248 downcasted) {\n downcasted = int248(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(248, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int240 from int256, reverting on\n * overflow (when the input is less than smallest int240 or\n * greater than largest int240).\n *\n * Counterpart to Solidity's `int240` operator.\n *\n * Requirements:\n *\n * - input must fit into 240 bits\n */\n function toInt240(int256 value) internal pure returns (int240 downcasted) {\n downcasted = int240(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(240, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int232 from int256, reverting on\n * overflow (when the input is less than smallest int232 or\n * greater than largest int232).\n *\n * Counterpart to Solidity's `int232` operator.\n *\n * Requirements:\n *\n * - input must fit into 232 bits\n */\n function toInt232(int256 value) internal pure returns (int232 downcasted) {\n downcasted = int232(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(232, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int224 from int256, reverting on\n * overflow (when the input is less than smallest int224 or\n * greater than largest int224).\n *\n * Counterpart to Solidity's `int224` operator.\n *\n * Requirements:\n *\n * - input must fit into 224 bits\n */\n function toInt224(int256 value) internal pure returns (int224 downcasted) {\n downcasted = int224(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(224, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int216 from int256, reverting on\n * overflow (when the input is less than smallest int216 or\n * greater than largest int216).\n *\n * Counterpart to Solidity's `int216` operator.\n *\n * Requirements:\n *\n * - input must fit into 216 bits\n */\n function toInt216(int256 value) internal pure returns (int216 downcasted) {\n downcasted = int216(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(216, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int208 from int256, reverting on\n * overflow (when the input is less than smallest int208 or\n * greater than largest int208).\n *\n * Counterpart to Solidity's `int208` operator.\n *\n * Requirements:\n *\n * - input must fit into 208 bits\n */\n function toInt208(int256 value) internal pure returns (int208 downcasted) {\n downcasted = int208(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(208, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int200 from int256, reverting on\n * overflow (when the input is less than smallest int200 or\n * greater than largest int200).\n *\n * Counterpart to Solidity's `int200` operator.\n *\n * Requirements:\n *\n * - input must fit into 200 bits\n */\n function toInt200(int256 value) internal pure returns (int200 downcasted) {\n downcasted = int200(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(200, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int192 from int256, reverting on\n * overflow (when the input is less than smallest int192 or\n * greater than largest int192).\n *\n * Counterpart to Solidity's `int192` operator.\n *\n * Requirements:\n *\n * - input must fit into 192 bits\n */\n function toInt192(int256 value) internal pure returns (int192 downcasted) {\n downcasted = int192(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(192, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int184 from int256, reverting on\n * overflow (when the input is less than smallest int184 or\n * greater than largest int184).\n *\n * Counterpart to Solidity's `int184` operator.\n *\n * Requirements:\n *\n * - input must fit into 184 bits\n */\n function toInt184(int256 value) internal pure returns (int184 downcasted) {\n downcasted = int184(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(184, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int176 from int256, reverting on\n * overflow (when the input is less than smallest int176 or\n * greater than largest int176).\n *\n * Counterpart to Solidity's `int176` operator.\n *\n * Requirements:\n *\n * - input must fit into 176 bits\n */\n function toInt176(int256 value) internal pure returns (int176 downcasted) {\n downcasted = int176(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(176, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int168 from int256, reverting on\n * overflow (when the input is less than smallest int168 or\n * greater than largest int168).\n *\n * Counterpart to Solidity's `int168` operator.\n *\n * Requirements:\n *\n * - input must fit into 168 bits\n */\n function toInt168(int256 value) internal pure returns (int168 downcasted) {\n downcasted = int168(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(168, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int160 from int256, reverting on\n * overflow (when the input is less than smallest int160 or\n * greater than largest int160).\n *\n * Counterpart to Solidity's `int160` operator.\n *\n * Requirements:\n *\n * - input must fit into 160 bits\n */\n function toInt160(int256 value) internal pure returns (int160 downcasted) {\n downcasted = int160(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(160, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int152 from int256, reverting on\n * overflow (when the input is less than smallest int152 or\n * greater than largest int152).\n *\n * Counterpart to Solidity's `int152` operator.\n *\n * Requirements:\n *\n * - input must fit into 152 bits\n */\n function toInt152(int256 value) internal pure returns (int152 downcasted) {\n downcasted = int152(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(152, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int144 from int256, reverting on\n * overflow (when the input is less than smallest int144 or\n * greater than largest int144).\n *\n * Counterpart to Solidity's `int144` operator.\n *\n * Requirements:\n *\n * - input must fit into 144 bits\n */\n function toInt144(int256 value) internal pure returns (int144 downcasted) {\n downcasted = int144(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(144, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int136 from int256, reverting on\n * overflow (when the input is less than smallest int136 or\n * greater than largest int136).\n *\n * Counterpart to Solidity's `int136` operator.\n *\n * Requirements:\n *\n * - input must fit into 136 bits\n */\n function toInt136(int256 value) internal pure returns (int136 downcasted) {\n downcasted = int136(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(136, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int128 from int256, reverting on\n * overflow (when the input is less than smallest int128 or\n * greater than largest int128).\n *\n * Counterpart to Solidity's `int128` operator.\n *\n * Requirements:\n *\n * - input must fit into 128 bits\n */\n function toInt128(int256 value) internal pure returns (int128 downcasted) {\n downcasted = int128(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(128, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int120 from int256, reverting on\n * overflow (when the input is less than smallest int120 or\n * greater than largest int120).\n *\n * Counterpart to Solidity's `int120` operator.\n *\n * Requirements:\n *\n * - input must fit into 120 bits\n */\n function toInt120(int256 value) internal pure returns (int120 downcasted) {\n downcasted = int120(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(120, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int112 from int256, reverting on\n * overflow (when the input is less than smallest int112 or\n * greater than largest int112).\n *\n * Counterpart to Solidity's `int112` operator.\n *\n * Requirements:\n *\n * - input must fit into 112 bits\n */\n function toInt112(int256 value) internal pure returns (int112 downcasted) {\n downcasted = int112(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(112, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int104 from int256, reverting on\n * overflow (when the input is less than smallest int104 or\n * greater than largest int104).\n *\n * Counterpart to Solidity's `int104` operator.\n *\n * Requirements:\n *\n * - input must fit into 104 bits\n */\n function toInt104(int256 value) internal pure returns (int104 downcasted) {\n downcasted = int104(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(104, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int96 from int256, reverting on\n * overflow (when the input is less than smallest int96 or\n * greater than largest int96).\n *\n * Counterpart to Solidity's `int96` operator.\n *\n * Requirements:\n *\n * - input must fit into 96 bits\n */\n function toInt96(int256 value) internal pure returns (int96 downcasted) {\n downcasted = int96(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(96, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int88 from int256, reverting on\n * overflow (when the input is less than smallest int88 or\n * greater than largest int88).\n *\n * Counterpart to Solidity's `int88` operator.\n *\n * Requirements:\n *\n * - input must fit into 88 bits\n */\n function toInt88(int256 value) internal pure returns (int88 downcasted) {\n downcasted = int88(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(88, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int80 from int256, reverting on\n * overflow (when the input is less than smallest int80 or\n * greater than largest int80).\n *\n * Counterpart to Solidity's `int80` operator.\n *\n * Requirements:\n *\n * - input must fit into 80 bits\n */\n function toInt80(int256 value) internal pure returns (int80 downcasted) {\n downcasted = int80(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(80, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int72 from int256, reverting on\n * overflow (when the input is less than smallest int72 or\n * greater than largest int72).\n *\n * Counterpart to Solidity's `int72` operator.\n *\n * Requirements:\n *\n * - input must fit into 72 bits\n */\n function toInt72(int256 value) internal pure returns (int72 downcasted) {\n downcasted = int72(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(72, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int64 from int256, reverting on\n * overflow (when the input is less than smallest int64 or\n * greater than largest int64).\n *\n * Counterpart to Solidity's `int64` operator.\n *\n * Requirements:\n *\n * - input must fit into 64 bits\n */\n function toInt64(int256 value) internal pure returns (int64 downcasted) {\n downcasted = int64(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(64, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int56 from int256, reverting on\n * overflow (when the input is less than smallest int56 or\n * greater than largest int56).\n *\n * Counterpart to Solidity's `int56` operator.\n *\n * Requirements:\n *\n * - input must fit into 56 bits\n */\n function toInt56(int256 value) internal pure returns (int56 downcasted) {\n downcasted = int56(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(56, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int48 from int256, reverting on\n * overflow (when the input is less than smallest int48 or\n * greater than largest int48).\n *\n * Counterpart to Solidity's `int48` operator.\n *\n * Requirements:\n *\n * - input must fit into 48 bits\n */\n function toInt48(int256 value) internal pure returns (int48 downcasted) {\n downcasted = int48(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(48, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int40 from int256, reverting on\n * overflow (when the input is less than smallest int40 or\n * greater than largest int40).\n *\n * Counterpart to Solidity's `int40` operator.\n *\n * Requirements:\n *\n * - input must fit into 40 bits\n */\n function toInt40(int256 value) internal pure returns (int40 downcasted) {\n downcasted = int40(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(40, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int32 from int256, reverting on\n * overflow (when the input is less than smallest int32 or\n * greater than largest int32).\n *\n * Counterpart to Solidity's `int32` operator.\n *\n * Requirements:\n *\n * - input must fit into 32 bits\n */\n function toInt32(int256 value) internal pure returns (int32 downcasted) {\n downcasted = int32(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(32, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int24 from int256, reverting on\n * overflow (when the input is less than smallest int24 or\n * greater than largest int24).\n *\n * Counterpart to Solidity's `int24` operator.\n *\n * Requirements:\n *\n * - input must fit into 24 bits\n */\n function toInt24(int256 value) internal pure returns (int24 downcasted) {\n downcasted = int24(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(24, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int16 from int256, reverting on\n * overflow (when the input is less than smallest int16 or\n * greater than largest int16).\n *\n * Counterpart to Solidity's `int16` operator.\n *\n * Requirements:\n *\n * - input must fit into 16 bits\n */\n function toInt16(int256 value) internal pure returns (int16 downcasted) {\n downcasted = int16(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(16, value);\n }\n }\n\n /**\n * @dev Returns the downcasted int8 from int256, reverting on\n * overflow (when the input is less than smallest int8 or\n * greater than largest int8).\n *\n * Counterpart to Solidity's `int8` operator.\n *\n * Requirements:\n *\n * - input must fit into 8 bits\n */\n function toInt8(int256 value) internal pure returns (int8 downcasted) {\n downcasted = int8(value);\n if (downcasted != value) {\n revert SafeCastOverflowedIntDowncast(8, value);\n }\n }\n\n /**\n * @dev Converts an unsigned uint256 into a signed int256.\n *\n * Requirements:\n *\n * - input must be less than or equal to maxInt256.\n */\n function toInt256(uint256 value) internal pure returns (int256) {\n // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive\n if (value > uint256(type(int256).max)) {\n revert SafeCastOverflowedUintToInt(value);\n }\n return int256(value);\n }\n\n /**\n * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump.\n */\n function toUint(bool b) internal pure returns (uint256 u) {\n assembly (\"memory-safe\") {\n u := iszero(iszero(b))\n }\n }\n}\n" + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/structs/EnumerableSet.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/structs/EnumerableSet.sol)\n// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.\n\npragma solidity ^0.8.20;\n\nimport {Arrays} from \"../Arrays.sol\";\n\n/**\n * @dev Library for managing\n * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive\n * types.\n *\n * Sets have the following properties:\n *\n * - Elements are added, removed, and checked for existence in constant time\n * (O(1)).\n * - Elements are enumerated in O(n). No guarantees are made on the ordering.\n * - Set can be cleared (all elements removed) in O(n).\n *\n * ```solidity\n * contract Example {\n * // Add the library methods\n * using EnumerableSet for EnumerableSet.AddressSet;\n *\n * // Declare a set state variable\n * EnumerableSet.AddressSet private mySet;\n * }\n * ```\n *\n * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)\n * and `uint256` (`UintSet`) are supported.\n *\n * [WARNING]\n * ====\n * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure\n * unusable.\n * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.\n *\n * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an\n * array of EnumerableSet.\n * ====\n */\nlibrary EnumerableSet {\n // To implement this library for multiple types with as little code\n // repetition as possible, we write it in terms of a generic Set type with\n // bytes32 values.\n // The Set implementation uses private functions, and user-facing\n // implementations (such as AddressSet) are just wrappers around the\n // underlying Set.\n // This means that we can only create new EnumerableSets for types that fit\n // in bytes32.\n\n struct Set {\n // Storage of set values\n bytes32[] _values;\n // Position is the index of the value in the `values` array plus 1.\n // Position 0 is used to mean a value is not in the set.\n mapping(bytes32 value => uint256) _positions;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function _add(Set storage set, bytes32 value) private returns (bool) {\n if (!_contains(set, value)) {\n set._values.push(value);\n // The value is stored at length-1, but we add 1 to all indexes\n // and use 0 as a sentinel value\n set._positions[value] = set._values.length;\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function _remove(Set storage set, bytes32 value) private returns (bool) {\n // We cache the value's position to prevent multiple reads from the same storage slot\n uint256 position = set._positions[value];\n\n if (position != 0) {\n // Equivalent to contains(set, value)\n // To delete an element from the _values array in O(1), we swap the element to delete with the last one in\n // the array, and then remove the last element (sometimes called as 'swap and pop').\n // This modifies the order of the array, as noted in {at}.\n\n uint256 valueIndex = position - 1;\n uint256 lastIndex = set._values.length - 1;\n\n if (valueIndex != lastIndex) {\n bytes32 lastValue = set._values[lastIndex];\n\n // Move the lastValue to the index where the value to delete is\n set._values[valueIndex] = lastValue;\n // Update the tracked position of the lastValue (that was just moved)\n set._positions[lastValue] = position;\n }\n\n // Delete the slot where the moved value was stored\n set._values.pop();\n\n // Delete the tracked position for the deleted slot\n delete set._positions[value];\n\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function _clear(Set storage set) private {\n uint256 len = _length(set);\n for (uint256 i = 0; i < len; ++i) {\n delete set._positions[set._values[i]];\n }\n Arrays.unsafeSetLength(set._values, 0);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function _contains(Set storage set, bytes32 value) private view returns (bool) {\n return set._positions[value] != 0;\n }\n\n /**\n * @dev Returns the number of values on the set. O(1).\n */\n function _length(Set storage set) private view returns (uint256) {\n return set._values.length;\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function _at(Set storage set, uint256 index) private view returns (bytes32) {\n return set._values[index];\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function _values(Set storage set) private view returns (bytes32[] memory) {\n return set._values;\n }\n\n // Bytes32Set\n\n struct Bytes32Set {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _add(set._inner, value);\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _remove(set._inner, value);\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function clear(Bytes32Set storage set) internal {\n _clear(set._inner);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {\n return _contains(set._inner, value);\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(Bytes32Set storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {\n return _at(set._inner, index);\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {\n bytes32[] memory store = _values(set._inner);\n bytes32[] memory result;\n\n assembly (\"memory-safe\") {\n result := store\n }\n\n return result;\n }\n\n // AddressSet\n\n struct AddressSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(AddressSet storage set, address value) internal returns (bool) {\n return _add(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(AddressSet storage set, address value) internal returns (bool) {\n return _remove(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function clear(AddressSet storage set) internal {\n _clear(set._inner);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(AddressSet storage set, address value) internal view returns (bool) {\n return _contains(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(AddressSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(AddressSet storage set, uint256 index) internal view returns (address) {\n return address(uint160(uint256(_at(set._inner, index))));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(AddressSet storage set) internal view returns (address[] memory) {\n bytes32[] memory store = _values(set._inner);\n address[] memory result;\n\n assembly (\"memory-safe\") {\n result := store\n }\n\n return result;\n }\n\n // UintSet\n\n struct UintSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(UintSet storage set, uint256 value) internal returns (bool) {\n return _add(set._inner, bytes32(value));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(UintSet storage set, uint256 value) internal returns (bool) {\n return _remove(set._inner, bytes32(value));\n }\n\n /**\n * @dev Removes all the values from a set. O(n).\n *\n * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the\n * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.\n */\n function clear(UintSet storage set) internal {\n _clear(set._inner);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(UintSet storage set, uint256 value) internal view returns (bool) {\n return _contains(set._inner, bytes32(value));\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(UintSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(UintSet storage set, uint256 index) internal view returns (uint256) {\n return uint256(_at(set._inner, index));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(UintSet storage set) internal view returns (uint256[] memory) {\n bytes32[] memory store = _values(set._inner);\n uint256[] memory result;\n\n assembly (\"memory-safe\") {\n result := store\n }\n\n return result;\n }\n}\n" + } + }, + "settings": { + "evmVersion": "paris", + "libraries": {}, + "metadata": { "appendCBOR": true, "bytecodeHash": "none", "useLiteralContent": false }, + "optimizer": { "enabled": true, "runs": 50000 }, + "outputSelection": { + "contracts/interfaces/IAdvancedPoolHooks.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/ILockBox.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/IPool.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/IPoolV1V2.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/IPoolV2.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/IRMN.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/interfaces/IRouter.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/libraries/Client.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/libraries/FeeTokenHandler.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/libraries/FinalityCodec.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/libraries/Pool.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/libraries/RateLimiter.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/pools/LockReleaseTokenPool.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "contracts/pools/TokenPool.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2Step.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/access/Ownable2StepMsgSender.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/IOwnable.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC1363.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC165.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/interfaces/IERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/IERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Metadata.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/token/ERC20/utils/SafeERC20.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Arrays.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Comparators.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/Panic.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/SlotDerivation.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/StorageSlot.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/introspection/IERC165.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/Math.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/math/SafeCast.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + }, + "node_modules/@openzeppelin/contracts-5.3.0/utils/structs/EnumerableSet.sol": { + "": ["ast"], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.bytecode.linkReferences", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "evm.deployedBytecode.linkReferences", + "evm.deployedBytecode.immutableReferences", + "evm.methodIdentifiers", + "metadata" + ] + } + }, + "remappings": [ + "forge-std/=node_modules/@chainlink/contracts/src/v0.8/vendor/forge-std/src/", + "@chainlink/policy-management/=node_modules/@chainlink/ace/packages/policy-management/src/", + "@chainlink/contracts/=node_modules/@chainlink/contracts/", + "@openzeppelin/contracts@4.8.3/=node_modules/@openzeppelin/contracts-4.8.3/", + "@openzeppelin/contracts@5.3.0/=node_modules/@openzeppelin/contracts-5.3.0/" + ], + "viaIR": true + } +} diff --git a/ccip-sdk/src/verify/fixtures/manifest.json b/ccip-sdk/src/verify/fixtures/manifest.json new file mode 100644 index 00000000..4311a25c --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/manifest.json @@ -0,0 +1,44 @@ +{ + "CrossChainToken": { + "contractName": "contracts/tokens/CrossChainToken.sol:CrossChainToken", + "compilerVersion": "v0.8.26+commit.8a97fa7a", + "standardInput": "CrossChainToken.standard-input.json", + "abi": "CrossChainToken.abi.json", + "initCode": "CrossChainToken.bin" + }, + "ERC20LockBox": { + "contractName": "contracts/pools/ERC20LockBox.sol:ERC20LockBox", + "compilerVersion": "v0.8.26+commit.8a97fa7a", + "standardInput": "ERC20LockBox.standard-input.json", + "abi": "ERC20LockBox.abi.json", + "initCode": "ERC20LockBox.bin" + }, + "LockReleaseTokenPool": { + "contractName": "contracts/pools/LockReleaseTokenPool.sol:LockReleaseTokenPool", + "compilerVersion": "v0.8.26+commit.8a97fa7a", + "standardInput": "LockReleaseTokenPool.standard-input.json", + "abi": "LockReleaseTokenPool.abi.json", + "initCode": "LockReleaseTokenPool.bin" + }, + "BurnMintTokenPool": { + "contractName": "contracts/pools/BurnMintTokenPool.sol:BurnMintTokenPool", + "compilerVersion": "v0.8.26+commit.8a97fa7a", + "standardInput": "BurnMintTokenPool.standard-input.json", + "abi": "BurnMintTokenPool.abi.json", + "initCode": "BurnMintTokenPool.bin" + }, + "CrossChainPoolToken": { + "contractName": "contracts/pools/CrossChainPoolToken.sol:CrossChainPoolToken", + "compilerVersion": "v0.8.26+commit.8a97fa7a", + "standardInput": "CrossChainPoolToken.standard-input.json", + "abi": "CrossChainPoolToken.abi.json", + "initCode": "CrossChainPoolToken.bin" + }, + "AdvancedPoolHooks": { + "contractName": "contracts/pools/AdvancedPoolHooks.sol:AdvancedPoolHooks", + "compilerVersion": "v0.8.26+commit.8a97fa7a", + "standardInput": "AdvancedPoolHooks.standard-input.json", + "abi": "AdvancedPoolHooks.abi.json", + "initCode": "AdvancedPoolHooks.bin" + } +} diff --git a/ccip-sdk/src/verify/fixtures/verifiers.json b/ccip-sdk/src/verify/fixtures/verifiers.json new file mode 100644 index 00000000..348e837b --- /dev/null +++ b/ccip-sdk/src/verify/fixtures/verifiers.json @@ -0,0 +1,500 @@ +{ + "31": { + "key": "bitcoin-testnet-rootstock", + "chainId": 31, + "explorer": "https://explorer.testnet.rsk.co", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://explorer.testnet.rsk.co)." + }, + "51": { + "key": "xdc-testnet", + "chainId": 51, + "explorer": "https://testnet.xdcscan.com", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "81": { + "key": "polkadot-testnet-astar-shibuya", + "chainId": 81, + "explorer": "https://shibuya.blockscout.com", + "provider": "blockscout", + "apiUrl": "https://shibuya.blockscout.com/api", + "note": "Also on Sourcify." + }, + "97": { + "key": "bsc-testnet", + "chainId": 97, + "explorer": "https://testnet.bscscan.com", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "133": { + "key": "ethereum-testnet-sepolia-hashkey-1", + "chainId": 133, + "explorer": "https://hashkeychain-testnet-explorer.alt.technology", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://hashkeychain-testnet-explorer.alt.technology)." + }, + "157": { + "key": "shibarium-testnet-puppynet", + "chainId": 157, + "explorer": "https://puppyscan.shib.io", + "provider": "etherscan-standalone", + "apiUrl": "https://api-puppyscan.shib.io/api", + "needsApiKey": true, + "note": "Standalone Etherscan-family explorer; needs its own API base + key (not the v2 endpoint)." + }, + "240": { + "key": "cronos-zkevm-testnet-sepolia", + "chainId": 240, + "explorer": "https://explorer.zkevm.cronos.org/testnet", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://explorer.zkevm.cronos.org/testnet)." + }, + "296": { + "key": "hedera-testnet", + "chainId": 296, + "explorer": "https://hashscan.io/testnet", + "provider": "etherscan-standalone", + "apiUrl": "https://api.hashscan.io/api", + "needsApiKey": true, + "note": "Standalone Etherscan-family explorer; needs its own API base + key (not the v2 endpoint). Also on Sourcify." + }, + "300": { + "key": "ethereum-testnet-sepolia-zksync-1", + "chainId": 300, + "explorer": "https://sepolia.explorer.zksync.io", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://sepolia.explorer.zksync.io)." + }, + "338": { + "key": "cronos-testnet", + "chainId": 338, + "explorer": "https://explorer.cronos.org/testnet", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://explorer.cronos.org/testnet)." + }, + "679": { + "key": "janction-testnet-sepolia", + "chainId": 679, + "explorer": "https://janction-testnet-explorer.alt.technology", + "provider": "unknown", + "note": "Custom explorer (https://janction-testnet-explorer.alt.technology); not on Etherscan v2, Blockscout, or Sourcify — needs manual config." + }, + "919": { + "key": "ethereum-testnet-sepolia-mode-1", + "chainId": 919, + "explorer": "https://sepolia.explorer.mode.network", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://sepolia.explorer.mode.network)." + }, + "998": { + "key": "hyperliquid-testnet", + "chainId": 998, + "explorer": "https://explore-testnet.hyperpc.app", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://explore-testnet.hyperpc.app)." + }, + "1001": { + "key": "kaia-testnet-kairos", + "chainId": 1001, + "explorer": "https://kairos.kaiascan.io", + "provider": "etherscan-standalone", + "apiUrl": "https://api-kairos.kaiascan.io/api", + "needsApiKey": true, + "note": "Standalone Etherscan-family explorer; needs its own API base + key (not the v2 endpoint). Also on Sourcify." + }, + "1112": { + "key": "wemix-testnet", + "chainId": 1112, + "explorer": "https://scan.wemix.com/wemixTestnet", + "provider": "etherscan-standalone", + "apiUrl": "https://api-scan.wemix.com/api", + "needsApiKey": true, + "note": "Standalone Etherscan-family explorer; needs its own API base + key (not the v2 endpoint). Also on Sourcify." + }, + "1114": { + "key": "core-testnet", + "chainId": 1114, + "explorer": "https://scan.test2.btcs.network", + "provider": "etherscan-standalone", + "apiUrl": "https://api-scan.test2.btcs.network/api", + "needsApiKey": true, + "note": "Standalone Etherscan-family explorer; needs its own API base + key (not the v2 endpoint). Also on Sourcify." + }, + "1123": { + "key": "bitcoin-testnet-bsquared-1", + "chainId": 1123, + "explorer": "https://testnet-explorer.bsquared.network", + "provider": "unknown", + "note": "Custom explorer (https://testnet-explorer.bsquared.network); not on Etherscan v2, Blockscout, or Sourcify — needs manual config." + }, + "1301": { + "key": "ethereum-testnet-sepolia-unichain-1", + "chainId": 1301, + "explorer": "https://sepolia.uniscan.xyz", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "1328": { + "key": "sei-testnet-atlantic", + "chainId": 1328, + "explorer": "https://seitrace.com", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "1687": { + "key": "mint-testnet", + "chainId": 1687, + "explorer": "https://sepolia-testnet-explorer.mintchain.io", + "provider": "unknown", + "note": "Custom explorer (https://sepolia-testnet-explorer.mintchain.io); not on Etherscan v2, Blockscout, or Sourcify — needs manual config." + }, + "1740": { + "key": "metal-testnet", + "chainId": 1740, + "explorer": "https://testnet.explorer.metall2.com", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://testnet.explorer.metall2.com)." + }, + "1946": { + "key": "ethereum-testnet-sepolia-soneium-1", + "chainId": 1946, + "explorer": "https://soneium-minato.blockscout.com", + "provider": "blockscout", + "apiUrl": "https://soneium-minato.blockscout.com/api", + "note": "Also on Sourcify." + }, + "2391": { + "key": "tac-testnet", + "chainId": 2391, + "explorer": "https://spb.explorer.tac.build", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://spb.explorer.tac.build)." + }, + "2910": { + "key": "ethereum-testnet-hoodi-morph", + "chainId": 2910, + "explorer": "https://explorer-hoodi.morphl2.io", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://explorer-hoodi.morphl2.io)." + }, + "4202": { + "key": "ethereum-testnet-sepolia-lisk-1", + "chainId": 4202, + "explorer": "https://sepolia-blockscout.lisk.com", + "provider": "blockscout", + "apiUrl": "https://sepolia-blockscout.lisk.com/api", + "note": "Also on Sourcify." + }, + "4801": { + "key": "ethereum-testnet-sepolia-worldchain-1", + "chainId": 4801, + "explorer": "https://sepolia.worldscan.org", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "5003": { + "key": "ethereum-testnet-sepolia-mantle-1", + "chainId": 5003, + "explorer": "https://sepolia.mantlescan.xyz", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "5611": { + "key": "binance-smart-chain-testnet-opbnb-1", + "chainId": 5611, + "explorer": "https://opbnb-testnet.bscscan.com", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "6343": { + "key": "megaeth-testnet-2", + "chainId": 6343, + "explorer": "https://megaeth-testnet-v2.blockscout.com", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "9746": { + "key": "plasma-testnet", + "chainId": 9746, + "explorer": "https://testnet.plasmascan.to", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "10143": { + "key": "monad-testnet", + "chainId": 10143, + "explorer": "https://testnet.monadexplorer.com", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "10200": { + "key": "xdai-testnet-chiado", + "chainId": 10200, + "explorer": "https://gnosis-chiado.blockscout.com", + "provider": "blockscout", + "apiUrl": "https://gnosis-chiado.blockscout.com/api", + "note": "Also on Sourcify." + }, + "11124": { + "key": "abstract-testnet", + "chainId": 11124, + "explorer": "https://sepolia.abscan.org", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "14601": { + "key": "sonic-testnet", + "chainId": 14601, + "explorer": "https://explorer.testnet.soniclabs.com", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "16602": { + "key": "0g-testnet-galileo-1", + "chainId": 16602, + "explorer": "https://chainscan-galileo.0g.ai", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://chainscan-galileo.0g.ai)." + }, + "33111": { + "key": "apechain-testnet-curtis", + "chainId": 33111, + "explorer": "https://explorer.curtis.apechain.com", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "33431": { + "key": "edge-testnet", + "chainId": 33431, + "explorer": "https://edge-testnet.explorer.alchemy.com", + "provider": "unknown", + "note": "Custom explorer (https://edge-testnet.explorer.alchemy.com); not on Etherscan v2, Blockscout, or Sourcify — needs manual config." + }, + "37111": { + "key": "ethereum-testnet-sepolia-lens-1", + "chainId": 37111, + "explorer": "https://explorer.testnet.lens.xyz", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://explorer.testnet.lens.xyz)." + }, + "42431": { + "key": "tempo-testnet-moderato", + "chainId": 42431, + "explorer": "https://explore.moderato.tempo.xyz", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://explore.moderato.tempo.xyz)." + }, + "43113": { + "key": "avalanche-fuji-testnet", + "chainId": 43113, + "explorer": "https://testnet.snowtrace.io", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "46630": { + "key": "robinhood-testnet", + "chainId": 46630, + "explorer": "https://explorer.testnet.chain.robinhood.com", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://explorer.testnet.chain.robinhood.com)." + }, + "53302": { + "key": "superseed-testnet", + "chainId": 53302, + "explorer": "https://sepolia-explorer.superseed.xyz", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://sepolia-explorer.superseed.xyz)." + }, + "59141": { + "key": "ethereum-testnet-sepolia-linea-1", + "chainId": 59141, + "explorer": "https://sepolia.lineascan.build", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "59902": { + "key": "ethereum-testnet-sepolia-andromeda-1", + "chainId": 59902, + "explorer": "https://sepolia-explorer.metisdevops.link", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://sepolia-explorer.metisdevops.link)." + }, + "80002": { + "key": "polygon-testnet-amoy", + "chainId": 80002, + "explorer": "https://amoy.polygonscan.com", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "84532": { + "key": "ethereum-testnet-sepolia-base-1", + "chainId": 84532, + "explorer": "https://sepolia.basescan.org", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "98867": { + "key": "plume-testnet-sepolia", + "chainId": 98867, + "explorer": "https://testnet-explorer.plumenetwork.xyz", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://testnet-explorer.plumenetwork.xyz)." + }, + "99999": { + "key": "adi-testnet", + "chainId": 99999, + "explorer": "https://explorer.ab.testnet.adifoundation.ai", + "provider": "unknown", + "note": "Custom explorer (https://explorer.ab.testnet.adifoundation.ai); not on Etherscan v2, Blockscout, or Sourcify — needs manual config." + }, + "192940": { + "key": "mind-testnet", + "chainId": 192940, + "explorer": "https://explorer-testnet.mindnetwork.xyz", + "provider": "unknown", + "note": "Custom explorer (https://explorer-testnet.mindnetwork.xyz); not on Etherscan v2, Blockscout, or Sourcify — needs manual config." + }, + "200810": { + "key": "bitcoin-testnet-bitlayer-1", + "chainId": 200810, + "explorer": "https://testnet.btrscan.com", + "provider": "etherscan-standalone", + "apiUrl": "https://api-testnet.btrscan.com/api", + "needsApiKey": true, + "note": "Standalone Etherscan-family explorer; needs its own API base + key (not the v2 endpoint). Also on Sourcify." + }, + "202601": { + "key": "ethereum-testnet-sepolia-ronin-1", + "chainId": 202601, + "explorer": "https://saigon-explorer.roninchain.com", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://saigon-explorer.roninchain.com)." + }, + "421614": { + "key": "ethereum-testnet-sepolia-arbitrum-1", + "chainId": 421614, + "explorer": "https://sepolia.arbiscan.io", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "534351": { + "key": "ethereum-testnet-sepolia-scroll-1", + "chainId": 534351, + "explorer": "https://sepolia.scrollscan.dev", + "provider": "etherscan-standalone", + "apiUrl": "https://api-sepolia.scrollscan.dev/api", + "needsApiKey": true, + "note": "Standalone Etherscan-family explorer; needs its own API base + key (not the v2 endpoint). Also on Sourcify." + }, + "560048": { + "key": "ethereum-testnet-hoodi", + "chainId": 560048, + "explorer": "https://hoodi.etherscan.io", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "686868": { + "key": "bitcoin-testnet-merlin", + "chainId": 686868, + "explorer": "https://testnet-scan.merlinchain.io", + "provider": "etherscan-standalone", + "apiUrl": "https://api-testnet-scan.merlinchain.io/api", + "needsApiKey": true, + "note": "Standalone Etherscan-family explorer; needs its own API base + key (not the v2 endpoint)." + }, + "688689": { + "key": "pharos-atlantic-testnet", + "chainId": 688689, + "explorer": "https://atlantic.pharosscan.xyz", + "provider": "etherscan-standalone", + "apiUrl": "https://api-atlantic.pharosscan.xyz/api", + "needsApiKey": true, + "note": "Standalone Etherscan-family explorer; needs its own API base + key (not the v2 endpoint)." + }, + "743111": { + "key": "hemi-testnet-sepolia", + "chainId": 743111, + "explorer": "https://testnet.explorer.hemi.xyz", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://testnet.explorer.hemi.xyz)." + }, + "763373": { + "key": "ink-testnet-sepolia", + "chainId": 763373, + "explorer": "https://explorer-sepolia.inkonchain.com", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://explorer-sepolia.inkonchain.com)." + }, + "808813": { + "key": "bitcoin-testnet-sepolia-bob-1", + "chainId": 808813, + "explorer": "https://bob-sepolia.explorer.gobob.xyz", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://bob-sepolia.explorer.gobob.xyz)." + }, + "2019775": { + "key": "jovay-testnet", + "chainId": 2019775, + "explorer": "https://sepolia-explorer.jovay.io/l2", + "provider": "unknown", + "note": "Custom explorer (https://sepolia-explorer.jovay.io/l2); not on Etherscan v2, Blockscout, or Sourcify — needs manual config." + }, + "5042002": { + "key": "arc-testnet", + "chainId": 5042002, + "explorer": "https://testnet.arcscan.app", + "provider": "etherscan-standalone", + "apiUrl": "https://api-testnet.arcscan.app/api", + "needsApiKey": true, + "note": "Standalone Etherscan-family explorer; needs its own API base + key (not the v2 endpoint). Also on Sourcify." + }, + "6281971": { + "key": "dogeos-testnet-chikyu", + "chainId": 6281971, + "explorer": "https://blockscout.testnet.dogeos.com", + "provider": "blockscout", + "apiUrl": "https://blockscout.testnet.dogeos.com/api", + "note": "Also on Sourcify." + }, + "11155111": { + "key": "ethereum-testnet-sepolia", + "chainId": 11155111, + "explorer": "https://sepolia.etherscan.io", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "11155420": { + "key": "ethereum-testnet-sepolia-optimism-1", + "chainId": 11155420, + "explorer": "https://sepolia-optimism.etherscan.io", + "provider": "etherscan-v2", + "note": "Also on Sourcify." + }, + "12227332": { + "key": "neox-testnet-t4", + "chainId": 12227332, + "explorer": "https://xt4scan.ngd.network", + "provider": "etherscan-standalone", + "apiUrl": "https://api-xt4scan.ngd.network/api", + "needsApiKey": true, + "note": "Standalone Etherscan-family explorer; needs its own API base + key (not the v2 endpoint). Also on Sourcify." + }, + "21000001": { + "key": "ethereum-testnet-sepolia-corn-1", + "chainId": 21000001, + "explorer": "https://testnet.cornscan.io", + "provider": "etherscan-standalone", + "apiUrl": "https://api-testnet.cornscan.io/api", + "needsApiKey": true, + "note": "Standalone Etherscan-family explorer; needs its own API base + key (not the v2 endpoint)." + }, + "999999999": { + "key": "zora-testnet", + "chainId": 999999999, + "explorer": "https://sepolia.explorer.zora.energy", + "provider": "sourcify", + "note": "No Etherscan/Blockscout API; verified via Sourcify (https://sepolia.explorer.zora.energy)." + } +} diff --git a/ccip-sdk/src/verify/index.ts b/ccip-sdk/src/verify/index.ts new file mode 100644 index 00000000..0b3e2814 --- /dev/null +++ b/ccip-sdk/src/verify/index.ts @@ -0,0 +1,36 @@ +/** + * The set of CCIP contracts the SDK ships pre-built verification artifacts for. + * Used as the `contract` discriminator in {@link verifyDeployedContract} and the + * `name` argument to {@link getVerificationArtifact}. + */ +export type DeployableContract = + | 'CrossChainToken' + | 'ERC20LockBox' + | 'LockReleaseTokenPool' + | 'BurnMintTokenPool' + | 'CrossChainPoolToken' + | 'AdvancedPoolHooks' + +export { verifyContract } from './verify.ts' +export { ETHERSCAN_V2_API_URL, EtherscanV2Client } from './etherscan.ts' +export { SOURCIFY_API_URL, SourcifyClient } from './sourcify.ts' +export { encodeConstructorArgs, encodeConstructorArgsFromTypes } from './constructor-args.ts' +export { resolveLongCompilerVersion } from './solc-version.ts' +export { + getVerificationArtifact, + listDeployableContracts, + resolveVerifier, + verifyDeployedContract, +} from './registry.ts' +export type { + ManifestEntry, + VerificationArtifact, + VerifierEntry, + VerifierProvider, +} from './registry.ts' +export type { + ConstructorArgs, + StandardJsonInput, + VerifyContractInput, + VerifyResult, +} from './types.ts' diff --git a/ccip-sdk/src/verify/registry.ts b/ccip-sdk/src/verify/registry.ts new file mode 100644 index 00000000..f1c757de --- /dev/null +++ b/ccip-sdk/src/verify/registry.ts @@ -0,0 +1,191 @@ +/* + * Runtime registry over the BUNDLED verification artifacts. + * + * Option 1 architecture: an offline bundler (`scripts/build-fixtures.ts`) runs once per CCIP + * release and emits, into the published SDK package: + * - manifest.json (name to { contractName, compilerVersion, files }) + * - Name.standard-input.json (sources + settings; produced WITHOUT forge/hardhat) + * - Name.abi.json (for ABI-encoding constructor args) + * - Name.bin (init/creation bytecode the SDK already has, for deploy) + * + * At runtime the SDK does NO generation — it loads the manifest and reads the pinned files. + * `verifyDeployedContract()` below is the full "deploy-then-verify" call the CLI would expose. + */ +/* eslint-disable import-x/no-nodejs-modules -- Node.js-only: reads pre-built fixtures shipped with the package */ +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +/* eslint-enable import-x/no-nodejs-modules */ + +import type { DeployableContract } from './index.ts' +import type { ConstructorArgs, StandardJsonInput, VerifyResult } from './types.ts' +import { verifyContract } from './verify.ts' +import { + CCIPContractVerificationFailedError, + CCIPUnknownVerificationContractError, +} from '../errors/index.ts' + +/** One row of the bundled `manifest.json`: how to locate a contract's verification artifacts. */ +export interface ManifestEntry { + /** Fully-qualified Solidity name `path/File.sol:Name`. */ + contractName: string + /** Long, commit-qualified solc version, e.g. `v0.8.26+commit.8a97fa7a`. */ + compilerVersion: string + /** Filename of the bundled standard-json input, relative to the fixtures dir. */ + standardInput: string + /** Filename of the bundled ABI json, relative to the fixtures dir. */ + abi: string + /** Optional filename of the bundled init/creation bytecode. */ + initCode?: string +} + +// Bundled with the package; resolved relative to this module, not the cwd. +const FIXTURES_DIR = fileURLToPath(new URL('./fixtures', import.meta.url)) + +const manifest = JSON.parse( + readFileSync(path.join(FIXTURES_DIR, 'manifest.json'), 'utf8'), +) as Record + +/** The kind of verification API a chain exposes, as recorded in the bundled `verifiers.json`. */ +export type VerifierProvider = + | 'etherscan-v2' + | 'blockscout' + | 'etherscan-standalone' + | 'sourcify' + | 'unknown' + +/** One row of the bundled `verifiers.json`: how to verify on a given CCIP chain. */ +export interface VerifierEntry { + /** Human-readable chain key, e.g. `ethereum-testnet-sepolia`. */ + key: string + /** EVM chain id. */ + chainId: number + /** Explorer base URL, or `null` if the chain has no known explorer. */ + explorer: string | null + /** Which verification API family this chain uses. */ + provider: VerifierProvider + /** Optional explorer API base URL (for Blockscout / standalone Etherscan instances). */ + apiUrl?: string + /** Whether the explorer requires an API key. */ + needsApiKey?: boolean + /** Optional free-form note. */ + note?: string +} + +const verifiers = JSON.parse( + readFileSync(path.join(FIXTURES_DIR, 'verifiers.json'), 'utf8'), +) as Record + +/** How to verify on a given CCIP testnet chainId (or undefined if not a known CCIP testnet). */ +export function resolveVerifier(chainId: number): VerifierEntry | undefined { + return verifiers[String(chainId)] +} + +/** The manifest keys of every contract the SDK ships verification artifacts for. */ +export function listDeployableContracts(): string[] { + return Object.keys(manifest) +} + +/** A fully-loaded verification artifact: the sources/settings and ABI for one contract. */ +export interface VerificationArtifact { + /** Fully-qualified Solidity name `path/File.sol:Name`. */ + contractName: string + /** Long, commit-qualified solc version. */ + compilerVersion: string + /** The standard JSON input (sources + settings) that produced the deployed bytecode. */ + standardJsonInput: StandardJsonInput + /** The contract ABI, used to encode constructor arguments. */ + abi: unknown[] +} + +/** Load the bundled artifact for a known contract (e.g. "CrossChainToken"). */ +export function getVerificationArtifact( + name: DeployableContract | (string & {}), +): VerificationArtifact { + const entry = manifest[name] + if (!entry) { + throw new CCIPUnknownVerificationContractError(name, listDeployableContracts()) + } + return { + contractName: entry.contractName, + compilerVersion: entry.compilerVersion, + standardJsonInput: JSON.parse( + readFileSync(path.join(FIXTURES_DIR, entry.standardInput), 'utf8'), + ) as StandardJsonInput, + abi: JSON.parse(readFileSync(path.join(FIXTURES_DIR, entry.abi), 'utf8')) as unknown[], + } +} + +/** + * The high-level SDK/CLI entry point. The caller only supplies what's truly per-deployment: + * which contract, where it landed, the chain, the API key, and the constructor params they + * already passed to deploy. Everything else (sources, settings, compiler version, FQN) comes + * from the bundle. + */ +export async function verifyDeployedContract( + params: { + // a key from the manifest, e.g. "CrossChainToken" + + contract: DeployableContract | (string & {}) + chainId: number + contractAddress: string + apiKey: string + /** constructor values (encoded against the bundled ABI) or pre-encoded hex. */ + constructorValues?: ReadonlyArray + constructorArgs?: ConstructorArgs + apiUrl?: string + /** Force a specific verifier; otherwise auto-resolved from the bundled verifier map. */ + verifier?: + | { provider: 'etherscan' | 'blockscout'; apiUrl: string; apiKey?: string } + | { provider: 'sourcify'; apiUrl?: string } + /** If true (default), chains with no known Etherscan/Blockscout API fall back to Sourcify. */ + fallbackToSourcify?: boolean + }, + deps: Parameters[1] = {}, +): Promise { + const art = getVerificationArtifact(params.contract) + + const constructorArgs: ConstructorArgs = + params.constructorArgs ?? + (params.constructorValues + ? { kind: 'values', abi: art.abi, values: params.constructorValues } + : { kind: 'none' }) + + // Auto-route to the right verifier for this chain unless the caller forced one. + const fallbackToSourcify = params.fallbackToSourcify ?? true + let verifier = params.verifier + if (!verifier && !params.apiUrl) { + const v = resolveVerifier(params.chainId) + if (v?.provider === 'blockscout' && v.apiUrl) { + verifier = { provider: 'blockscout', apiUrl: v.apiUrl } + } else if (v?.provider === 'sourcify') { + verifier = { provider: 'sourcify' } // key-less, no per-chain URL + } else if (v?.provider === 'etherscan-standalone' && v.apiUrl) { + // standalone explorer: same Etherscan protocol but its own base + its own API key + verifier = { provider: 'etherscan', apiUrl: v.apiUrl, apiKey: params.apiKey } + } else if (v && v.provider === 'unknown') { + // No Etherscan/Blockscout API — Sourcify needs neither a key nor a per-chain URL. + if (fallbackToSourcify) verifier = { provider: 'sourcify' } + else + throw new CCIPContractVerificationFailedError( + `no known verification API for chain ${params.chainId} (${v.key}, ${v.explorer ?? 'no explorer'}). Pass an explicit { verifier }.`, + ) + } + // etherscan-v2 (or unknown chain) -> fall through to the default v2 endpoint + } + + return verifyContract( + { + chainId: params.chainId, + contractAddress: params.contractAddress, + contractName: art.contractName, + standardJsonInput: art.standardJsonInput, + compilerVersion: art.compilerVersion, + constructorArgs, + apiKey: params.apiKey, + apiUrl: params.apiUrl, + verifier, + }, + deps, + ) +} diff --git a/ccip-sdk/src/verify/solc-version.ts b/ccip-sdk/src/verify/solc-version.ts new file mode 100644 index 00000000..353ed892 --- /dev/null +++ b/ccip-sdk/src/verify/solc-version.ts @@ -0,0 +1,72 @@ +/* + * Resolve a short solc version ("0.8.26") to the long, commit-qualified form that + * Etherscan requires: "v0.8.26+commit.8a97fa7a". + * + * WHY this matters: + * - Etherscan keys its compiler dropdown on the EXACT build, including the commit hash. + * - Foundry does this by looking the commit up in the official solc release list when the + * locally-known version has no build metadata (foundry-src crates/verify/src/etherscan/mod.rs + * ensure_solc_build_metadata / lookup_compiler_version). + * - Hardhat sidesteps the lookup because solc already recorded solcLongVersion in its + * build-info; but a standalone SDK has no build-info, so we resolve it like foundry does. + * + * The list lives at https://binaries.soliditylang.org/bin/list.json — a map of + * releases (short to filename "soljson-v0.8.26+commit.8a97fa7a.js") plus a builds array + * with explicit longVersion. We use releases (smallest, authoritative). + */ + +import { CCIPContractVerificationError } from '../errors/index.ts' + +const SOLC_LIST_URL = 'https://binaries.soliditylang.org/bin/list.json' + +/* + * Tiny offline fallback so the SDK resolves the bundled contracts' compiler version without a + * network round-trip to the solc CDN. In the real SDK you'd cache list.json (it changes rarely) + * or ship a pinned map for the compiler versions CCIP token contracts are built with. + */ +const PINNED_LONG_VERSIONS: Record = { + '0.8.26': 'v0.8.26+commit.8a97fa7a', + '0.8.24': 'v0.8.24+commit.e11b9ed9', + '0.8.19': 'v0.8.19+commit.7dd6d404', +} + +/** Resolve a short solc version to the long commit-qualified form Etherscan requires. */ +export async function resolveLongCompilerVersion( + shortVersion: string, + opts: { fetchImpl?: typeof fetch; allowNetwork?: boolean } = {}, +): Promise { + // Already long? (contains a build hash) -> just ensure the leading "v". + if (shortVersion.includes('+commit.')) { + return shortVersion.startsWith('v') ? shortVersion : `v${shortVersion}` + } + + const bare = shortVersion.replace(/^v/, '') + + const pinned = PINNED_LONG_VERSIONS[bare] + if (pinned) return pinned + + if (opts.allowNetwork === false) { + throw new CCIPContractVerificationError( + `Unknown solc version "${bare}" and network lookup disabled. Add it to PINNED_LONG_VERSIONS or enable network.`, + ) + } + + const fetchImpl = opts.fetchImpl ?? ((...args: Parameters) => fetch(...args)) + const res = await fetchImpl(SOLC_LIST_URL) + if (!res.ok) + throw new CCIPContractVerificationError( + `Failed to fetch solc list.json: ${res.status} ${res.statusText}`, + ) + const list = (await res.json()) as { releases?: Record } + + const filename = list.releases?.[bare] + if (!filename) + throw new CCIPContractVerificationError(`solc version "${bare}" not found in releases list`) + + // filename looks like "soljson-v0.8.26+commit.8a97fa7a.js" + const match = /^soljson-(v\d+\.\d+\.\d+\+commit\.[0-9a-f]+)\.js$/.exec(filename) + const longVersion = match?.[1] + if (!longVersion) + throw new CCIPContractVerificationError(`Unexpected solc release filename: ${filename}`) + return longVersion +} diff --git a/ccip-sdk/src/verify/sourcify.ts b/ccip-sdk/src/verify/sourcify.ts new file mode 100644 index 00000000..5aeca02d --- /dev/null +++ b/ccip-sdk/src/verify/sourcify.ts @@ -0,0 +1,119 @@ +/* + * Sourcify verification provider — the key-less, registry-less universal verifier. + * + * Unlike Etherscan/Blockscout (form-encoded verifysourcecode actions), Sourcify uses a JSON + * REST API and matches the on-chain bytecode against a recompile of the standard-json. It needs + * NO API key and NO per-chain explorer URL — just the chainId + the standard-json we already + * bundle. This is what foundry (crates/verify/src/sourcify.rs) and hardhat + * (packages/hardhat-verify/src/internal/sourcify.ts) use to cover chains outside Etherscan v2. + * + * Sourcify v2 endpoints (base/chainId/address are path params): + * verify : POST base/v2/verify/chainId/address (JSON) -> { verificationId } + * poll : GET base/v2/verify/verificationId -> { isJobCompleted, contract, error } + * lookup : GET base/v2/contract/chainId/address -> { match, ... } or 404 + * + * Constructor args are NOT sent: Sourcify recompiles and compares bytecode; the (optional) + * creationTransactionHash only helps it do a creation-bytecode match. + */ +import { defaultFetch } from './etherscan.ts' +import type { StandardJsonInput } from './types.ts' +import { CCIPContractVerificationError } from '../errors/index.ts' + +/** Default Sourcify server base URL (key-less, multi-chain). */ +export const SOURCIFY_API_URL = 'https://sourcify.dev/server' + +/** Arguments for a Sourcify verification submission. */ +export interface SourcifyVerifyArgs { + /** EVM chain id. */ + chainId: number + /** The deployed contract address. */ + address: string + /** The standard JSON input (sources + settings) to recompile and match. */ + stdJsonInput: StandardJsonInput + /** "sourceName:ContractName" — same FQN as Etherscan's contractname. */ + contractIdentifier: string + /** Full solc version, NO leading "v": e.g. "0.8.26+commit.8a97fa7a". */ + compilerVersion: string + /** Optional: lets Sourcify also attempt a creation-bytecode match. */ + creationTransactionHash?: string +} + +/** The outcome of polling a Sourcify verification job. */ +export interface SourcifyJobResult { + /** Whether the job has finished. */ + done: boolean + /** "match" | "exact_match" | null — Sourcify's match grade once done. */ + match: string | null + /** Sourcify error/custom code when the job failed (e.g. "already_verified"). */ + errorCode?: string +} + +/** Key-less JSON client for the Sourcify v2 verification API. */ +export class SourcifyClient { + private readonly apiUrl: string + private readonly fetchImpl: typeof fetch + + /** Builds a Sourcify client with an optional server base URL and fetch impl. */ + constructor(apiUrl: string = SOURCIFY_API_URL, fetchImpl: typeof fetch = defaultFetch) { + this.apiUrl = apiUrl + this.fetchImpl = fetchImpl + } + + /** The server base URL with any trailing slash removed. */ + private base(): string { + return this.apiUrl.replace(/\/$/, '') + } + + /** Already has a verified match on Sourcify? */ + async isVerified(chainId: number, address: string): Promise { + const res = await this.fetchImpl(`${this.base()}/v2/contract/${chainId}/${address}`) + if (res.status === 404) return false + if (!res.ok) return false + const body = (await res.json()) as { match?: string | null } + return body.match != null + } + + /** Submit a verification job; returns the verificationId (job guid) or 'already-verified'. */ + async verify( + args: SourcifyVerifyArgs, + ): Promise<{ verificationId?: string; alreadyVerified?: boolean }> { + const body: Record = { + stdJsonInput: args.stdJsonInput, // NOTE: the object itself, not JSON.stringify'd + contractIdentifier: args.contractIdentifier, + compilerVersion: args.compilerVersion, + } + if (args.creationTransactionHash) body.creationTransactionHash = args.creationTransactionHash + + const res = await this.fetchImpl(`${this.base()}/v2/verify/${args.chainId}/${args.address}`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }) + + if (res.status === 409) return { alreadyVerified: true } // already verified + if (res.status !== 202 && res.status !== 200) { + const text = await res.text().catch(() => '') + throw new CCIPContractVerificationError( + `Sourcify verify failed: HTTP ${res.status} ${text.slice(0, 300)}`, + ) + } + const json = (await res.json()) as { verificationId?: string } + return { verificationId: json.verificationId } + } + + /** Poll a verification job. */ + async checkStatus(verificationId: string): Promise { + const res = await this.fetchImpl(`${this.base()}/v2/verify/${verificationId}`) + if (!res.ok) throw new CCIPContractVerificationError(`Sourcify status HTTP ${res.status}`) + const j = (await res.json()) as { + isJobCompleted?: boolean + contract?: { match?: string | null } + error?: { customCode?: string; message?: string } + } + return { + done: Boolean(j.isJobCompleted), + match: j.contract?.match ?? null, + errorCode: j.error?.customCode, + } + } +} diff --git a/ccip-sdk/src/verify/types.ts b/ccip-sdk/src/verify/types.ts new file mode 100644 index 00000000..2bc87fdb --- /dev/null +++ b/ccip-sdk/src/verify/types.ts @@ -0,0 +1,106 @@ +/* + * Shared types for the CCIP contract-verification module. + * + * The model mirrors how forge verify-contract and @nomicfoundation/hardhat-verify + * talk to the Etherscan V2 API. + */ + +/** + * The canonical Solidity "Standard JSON Input" — exactly what solc consumes and + * what Etherscan expects for the standard-json `codeformat`. + * + * This object is the compilation input the SDK already holds (sources + settings); the same + * inputs that produced the deployed bytecode must be the ones submitted for verification. + */ +export interface StandardJsonInput { + /** Source language. */ + language: 'Solidity' | 'Vyper' + /** Map of source path to its file content. */ + sources: Record + /** Compiler settings that produced the deployed bytecode. */ + settings: { + /** Optimizer configuration. */ + optimizer?: { enabled: boolean; runs: number } + /** Target EVM version. */ + evmVersion?: string + /** Whether compilation used the IR pipeline. */ + viaIR?: boolean + /** Metadata settings (bytecode hash mode, literal content, CBOR append). */ + metadata?: { + bytecodeHash?: 'ipfs' | 'none' | 'bzzr1' + useLiteralContent?: boolean + appendCBOR?: boolean + } + /** Import remappings. */ + remappings?: string[] + /** Per-file library link map (file to LibName to address); set when libraries are used. */ + libraries?: Record> + /** solc output selection. */ + outputSelection?: Record> + } +} + +/** Constructor arguments may be supplied either as decoded values or as already-encoded hex. */ +export type ConstructorArgs = + | { kind: 'values'; abi: ReadonlyArray; values: ReadonlyArray } + /** Pre-ABI-encoded calldata; with or without 0x, with or without the (irrelevant) selector stripped. */ + | { kind: 'encoded'; hex: string } + | { kind: 'none' } + +/** Everything `verifyContract` needs to verify one deployed contract. */ +export interface VerifyContractInput { + /** EVM chain id; selects the explorer via the Etherscan V2 single endpoint. */ + chainId: number + /** The already-deployed contract address. */ + contractAddress: string + /** Fully-qualified name as it appears in the standard-json `sources` keys: `path/File.sol:Name`. */ + contractName: string + /** The bundled standard JSON input (sources + settings that produced the init code). */ + standardJsonInput: StandardJsonInput + /** Short solc version, e.g. "0.8.26". Resolved to the long `v0.8.26+commit.HASH` form. */ + compilerVersion: string + /** Constructor arguments — see {@link ConstructorArgs}. */ + constructorArgs: ConstructorArgs + /** User-provided Etherscan **V2** API key (one key works across all supported chains). */ + apiKey: string + + /** Optional: override the explorer base (e.g. a Blockscout/V1 instance). Defaults to V2. */ + apiUrl?: string + /** + * Optional explorer-provider override for chains NOT on Etherscan v2. When set, this takes + * precedence over chainId/apiKey/apiUrl. Use for Blockscout instances and standalone + * Etherscan-family explorers (Scrollscan, etc.) that need their own base URL / key. + */ + verifier?: + | { + provider: 'etherscan' | 'blockscout' + /** Full API base, e.g. "https://base-sepolia.blockscout.com/api" or a standalone etherscan API. */ + apiUrl: string + /** Optional API key (Blockscout usually needs none; standalone etherscan instances do). */ + apiKey?: string + } + | { + provider: 'sourcify' + /** Sourcify server base; defaults to https://sourcify.dev/server. No key. */ + apiUrl?: string + } + /** Optional creation-tx hash; lets Sourcify also attempt a creation-bytecode match. */ + creationTransactionHash?: string + /** Optional: SPDX licenseType code (1..14). Cosmetic on Etherscan; omitted by default. */ + licenseType?: number + /** Optional polling tuning. `confirmAttempts` = extra getsourcecode re-checks after a poll timeout + * (for slow explorers like Routescan whose checkverifystatus lags). */ + polling?: { intervalMs?: number; timeoutMs?: number; confirmAttempts?: number } +} + +/** The outcome of a verification attempt. */ +export interface VerifyResult { + /** Terminal verification status. */ + status: 'verified' | 'already-verified' | 'failed' + /** The explorer GUID of the submission, when one was issued. */ + guid?: string + /** Human-readable result message. */ + message: string + /** Best-effort link to the verified contract page. */ + explorerUrl?: string +} diff --git a/ccip-sdk/src/verify/verify.test.ts b/ccip-sdk/src/verify/verify.test.ts new file mode 100644 index 00000000..115bb165 --- /dev/null +++ b/ccip-sdk/src/verify/verify.test.ts @@ -0,0 +1,110 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { + getVerificationArtifact, + listDeployableContracts, + verifyDeployedContract, +} from './index.ts' +import { CCIPUnknownVerificationContractError } from '../errors/index.ts' + +/** Records the form fields POSTed to `verifysourcecode` so tests can assert on them. */ +interface SubmittedForm { + codeformat?: string + compilerversion?: string + contractname?: string + sourceCode?: string +} + +/** + * Builds a fully-offline Etherscan-style `fetch` mock that drives one contract through + * the submit, pending, then verified sequence, capturing the submitted verify form. + */ +function makeFetchMock(captured: SubmittedForm): typeof fetch { + let statusChecks = 0 + + const envelope = (status: string, message: string, result: string): Response => + new Response(JSON.stringify({ status, message, result }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }) + + const impl: typeof fetch = (input, init) => { + const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url + const action = new URL(url).searchParams.get('action') + + if (action === 'getsourcecode') { + // Not-yet-verified array so the already-verified short-circuit does NOT trigger. + return Promise.resolve(envelope('1', 'OK', '[{"SourceCode":""}]')) + } + if (action === 'verifysourcecode') { + const body = typeof init?.body === 'string' ? init.body : '' + const form = new URLSearchParams(body) + captured.codeformat = form.get('codeformat') ?? undefined + captured.compilerversion = form.get('compilerversion') ?? undefined + captured.contractname = form.get('contractname') ?? undefined + captured.sourceCode = form.get('sourceCode') ?? undefined + return Promise.resolve(envelope('1', 'OK', 'guid-12345')) + } + if (action === 'checkverifystatus') { + statusChecks += 1 + // First poll: pending; subsequent polls: verified. + return statusChecks === 1 + ? Promise.resolve(envelope('1', 'OK', 'Pending in queue')) + : Promise.resolve(envelope('1', 'OK', 'Pass - Verified')) + } + throw new Error(`unexpected action: ${action ?? 'null'}`) + } + + return impl +} + +void describe('verifyDeployedContract', () => { + void it('drives the submit, pending, then verified sequence with the expected form', async () => { + const captured: SubmittedForm = {} + const fetchImpl = makeFetchMock(captured) + + const result = await verifyDeployedContract( + { + contract: 'CrossChainToken', + chainId: 11155111, + contractAddress: '0x0000000000000000000000000000000000000001', + apiKey: 'x', + constructorArgs: { kind: 'none' }, + }, + { fetchImpl, sleep: () => Promise.resolve() }, + ) + + assert.equal(result.status, 'verified') + assert.equal(captured.codeformat, 'solidity-standard-json-input') + assert.equal(captured.compilerversion, 'v0.8.26+commit.8a97fa7a') + assert.equal(captured.contractname, 'contracts/tokens/CrossChainToken.sol:CrossChainToken') + assert.ok(captured.sourceCode && captured.sourceCode.length > 0) + }) +}) + +void describe('listDeployableContracts', () => { + void it('returns the 6 bundled contract keys', () => { + const keys = listDeployableContracts() + assert.deepEqual([...keys].sort(), [ + 'AdvancedPoolHooks', + 'BurnMintTokenPool', + 'CrossChainPoolToken', + 'CrossChainToken', + 'ERC20LockBox', + 'LockReleaseTokenPool', + ]) + }) +}) + +void describe('getVerificationArtifact', () => { + void it('loads a non-empty standard-json input for a known contract', () => { + const art = getVerificationArtifact('BurnMintTokenPool') + assert.equal(art.contractName, 'contracts/pools/BurnMintTokenPool.sol:BurnMintTokenPool') + assert.ok(Object.keys(art.standardJsonInput.sources).length > 0) + }) + + void it('throws CCIPUnknownVerificationContractError for an unknown contract', () => { + assert.throws(() => getVerificationArtifact('Nope'), CCIPUnknownVerificationContractError) + }) +}) diff --git a/ccip-sdk/src/verify/verify.ts b/ccip-sdk/src/verify/verify.ts new file mode 100644 index 00000000..f49f013e --- /dev/null +++ b/ccip-sdk/src/verify/verify.ts @@ -0,0 +1,202 @@ +/* + * High-level verifyContract() — the function the CCIP SDK would expose. + * + * Flow (mirrors forge verify-contract and hardhat-verify orchestration): + * 1. Resolve the long, commit-qualified compiler version. + * 2. ABI-encode the constructor arguments (no 0x, no selector). + * 3. (optional) Short-circuit if already verified. + * 4. POST verifysourcecode and receive a GUID. + * 5. Poll checkverifystatus until Pass / Fail / Already-Verified or timeout. + * + * The SDK already holds the two heavy inputs: + * - the standard JSON input (sources + settings used to produce the init code), and + * - the constructor params the user supplied at deploy time. + * So at the call site the user only adds: deployed address, chainId, and their API key. + */ + +import { encodeConstructorArgs } from './constructor-args.ts' +import { EtherscanV2Client, defaultFetch } from './etherscan.ts' +import { resolveLongCompilerVersion } from './solc-version.ts' +import { SourcifyClient } from './sourcify.ts' +import type { VerifyContractInput, VerifyResult } from './types.ts' +import { CCIPContractVerificationError } from '../errors/index.ts' + +// Status markers returned by checkverifystatus (see Etherscan v2 docs + both clients). +const PENDING = 'Pending in queue' +const SUCCESS = 'Pass - Verified' +const FAIL_PREFIX = 'Fail - Unable to verify' +const ALREADY_VERIFIED_MARKERS = ['Contract source code already verified', 'Already Verified'] + +/** Verify an already-deployed contract on Etherscan/Blockscout/Sourcify and await the outcome. */ +export async function verifyContract( + input: VerifyContractInput, + deps: { + fetchImpl?: typeof fetch + allowNetworkForSolcList?: boolean + sleep?: (ms: number) => Promise + } = {}, +): Promise { + const fetchImpl = deps.fetchImpl ?? defaultFetch + const sleep = deps.sleep ?? ((ms: number) => new Promise((r) => setTimeout(r, ms))) + + // Sourcify is a structurally different API (JSON, bytecode-match, no key) — handle it separately. + if (input.verifier?.provider === 'sourcify') { + return verifyOnSourcify(input, fetchImpl, sleep) + } + + // A `verifier` override (Blockscout / standalone explorer) wins over the default v2 setup. + const client = input.verifier + ? new EtherscanV2Client( + input.chainId, + input.verifier.apiKey ?? '', + input.verifier.apiUrl, + fetchImpl, + input.verifier.provider, + ) + : new EtherscanV2Client(input.chainId, input.apiKey, input.apiUrl, fetchImpl, 'etherscan') + + // 1. compiler version: "0.8.26" -> "v0.8.26+commit.8a97fa7a" + const compilerversion = await resolveLongCompilerVersion(input.compilerVersion, { + fetchImpl, + allowNetwork: deps.allowNetworkForSolcList, + }) + + // 2. constructor args -> hex (no 0x, no selector) + const constructorArguments = encodeConstructorArgs(input.constructorArgs) + + // 3. (optional) skip if already verified + if (await client.isVerified(input.contractAddress)) { + return { + status: 'already-verified', + message: 'Contract source code already verified', + explorerUrl: undefined, + } + } + + // 4. submit + let guid: string + try { + guid = await client.verifySourceCode({ + codeformat: 'solidity-standard-json-input', + sourceCode: JSON.stringify(input.standardJsonInput), + contractaddress: input.contractAddress, + contractname: input.contractName, + compilerversion, + constructorArguments: constructorArguments || undefined, + licenseType: input.licenseType, + }) + } catch (err) { + if (err instanceof CCIPContractVerificationError && isAlreadyVerified(err.message)) { + return { status: 'already-verified', message: err.message } + } + throw err + } + + // 5. poll + const intervalMs = input.polling?.intervalMs ?? 3_000 + const timeoutMs = input.polling?.timeoutMs ?? 120_000 + const deadline = Date.now() + timeoutMs + + // Etherscan needs a moment before the GUID is queryable (hardhat sleeps ~0.5s first). + await sleep(Math.min(1_000, intervalMs)) + + for (;;) { + const res = await client.checkVerifyStatus(guid) + const result = res.result + + if (result === PENDING) { + if (Date.now() > deadline) { + // Some explorers (e.g. Routescan/Snowtrace) finish verifying but lag on checkverifystatus, + // sometimes by minutes. Confirm the real outcome via getsourcecode, retrying a few times. + for (let i = 0; i < (input.polling?.confirmAttempts ?? 4); i++) { + if (await client.isVerified(input.contractAddress)) { + return { + status: 'verified', + guid, + message: 'Verified (confirmed via getsourcecode after status lag)', + } + } + await sleep(intervalMs) + } + return { + status: 'failed', + guid, + message: `Timed out after ${timeoutMs}ms while pending (explorer may still finish; re-check getsourcecode)`, + } + } + await sleep(intervalMs) + continue + } + if (result === SUCCESS) { + return { status: 'verified', guid, message: result } + } + if (isAlreadyVerified(result)) { + return { status: 'already-verified', guid, message: result } + } + if (result.startsWith(FAIL_PREFIX)) { + return { status: 'failed', guid, message: result } + } + // status "0" with some other message => hard failure. + if (res.status === '0') { + return { status: 'failed', guid, message: result || res.message } + } + // Unknown but ok-ish; treat as terminal success-ish to avoid infinite loops. + return { status: 'verified', guid, message: result } + } +} + +function isAlreadyVerified(msg: string): boolean { + return ALREADY_VERIFIED_MARKERS.some((m) => msg.startsWith(m)) +} + +/* Sourcify flow: JSON submit, then poll by verificationId until matched. No key, no chainid. */ +async function verifyOnSourcify( + input: VerifyContractInput, + fetchImpl: typeof fetch, + sleep: (ms: number) => Promise, +): Promise { + const apiUrl = input.verifier?.provider === 'sourcify' ? input.verifier.apiUrl : undefined + const client = new SourcifyClient(apiUrl, fetchImpl) + + if (await client.isVerified(input.chainId, input.contractAddress)) { + return { status: 'already-verified', message: 'Already verified on Sourcify' } + } + + // Sourcify wants the bare solc version (no leading "v") and the standard-json object itself. + const compilerVersion = (await resolveLongCompilerVersion(input.compilerVersion)).replace( + /^v/, + '', + ) + + const submit = await client.verify({ + chainId: input.chainId, + address: input.contractAddress, + stdJsonInput: input.standardJsonInput, + contractIdentifier: input.contractName, + compilerVersion, + creationTransactionHash: input.creationTransactionHash, + }) + if (submit.alreadyVerified) + return { status: 'already-verified', message: 'Already verified on Sourcify' } + const guid = submit.verificationId + if (!guid) return { status: 'failed', message: 'Sourcify returned no verificationId' } + + const intervalMs = input.polling?.intervalMs ?? 3_000 + const timeoutMs = input.polling?.timeoutMs ?? 120_000 + const deadline = Date.now() + timeoutMs + await sleep(Math.min(1_000, intervalMs)) + + for (;;) { + const st = await client.checkStatus(guid) + if (!st.done) { + if (Date.now() > deadline) + return { status: 'failed', guid, message: `Sourcify timed out after ${timeoutMs}ms` } + await sleep(intervalMs) + continue + } + if (st.errorCode === 'already_verified') + return { status: 'already-verified', guid, message: 'Already verified on Sourcify' } + if (st.match) return { status: 'verified', guid, message: `Sourcify: ${st.match}` } // "match" | "exact_match" + return { status: 'failed', guid, message: st.errorCode ?? 'Sourcify: no match' } + } +} diff --git a/docs/cct-poc.md b/docs/cct-poc.md new file mode 100644 index 00000000..84b3b629 --- /dev/null +++ b/docs/cct-poc.md @@ -0,0 +1,1421 @@ +# Cross-chain token (CCT) proof of concept: deploy and transfer across EVM, Solana, and Aptos + +> Audience: a developer deploying a cross-chain token (CCT) with `ccip-cli` across EVM, Solana, and +> Aptos testnets. Assumes you can run a shell, fund testnet wallets, and read a block explorer; does +> not assume prior CCIP contract knowledge. + +> Owner: ccip-tools-ts maintainers. Last reviewed: 2026-06-26. Applies to: `ccip-cli` and +> `@chainlink/ccip-sdk` 1.7.1, CCT v2.0 on EVM, testnets Sepolia / Base Sepolia / Solana Devnet / +> Aptos Testnet. + +This guide deploys cross-chain tokens and pools with `ccip-cli`, wires a 3-chain mesh (EVM, Solana, +Aptos), and sends cross-chain transfers across it. The commands are taken from real testnet runs on +Sepolia, Solana Devnet, and Aptos Testnet. + +> EVM deploys now produce the canonical CCT v2.0 contracts: `CrossChainToken 2.0.0`, +> `BurnMintTokenPool 2.0.0`, `LockReleaseTokenPool 2.0.0` (which auto-deploys an `ERC20LockBox 2.0.0`), +> and the combined `CrossChainPoolToken 2.0.0`, a single contract that is both token and pool, via +> `ccip-cli pool deploy-combined`. The legacy `BurnMintERC20` / `FactoryBurnMintERC20` and v1.6.1 +> pools are no longer deployed. EVM CLI changes: `token deploy` drops `--token-type` and adds +> `--ccip-admin`, `--burn-mint-role-admin`, and `--pre-mint-recipient`; `pool deploy` drops +> `--allowlist` and adds `--advanced-pool-hooks` and `--lock-box`. Verified live on Sepolia v2-staging. + +Token and pool stack used in this guide: + +| Chain | Token type | Pool type | Decimals | +| --------------- | --------------------- | ------------------------------------------------------------------------------------------ | -------- | +| EVM (Sepolia) | CrossChainToken 2.0.0 | BurnMintTokenPool / LockReleaseTokenPool 2.0.0 (lock-release auto-deploys an ERC20LockBox) | 18 | +| Solana (Devnet) | Token-2022 (SPL) | BurnMint | 9 | +| Aptos (Testnet) | Managed Token | Managed Token Pool | 8 | + +> EVM deploy alternatives. The separate token-then-pool path below is the default. On EVM you can also +> deploy a single `CrossChainPoolToken 2.0.0` that is both token and pool with +> `ccip-cli pool deploy-combined`, or deploy token + pool (or a pool for an existing token) through a +> `TokenPoolFactory 2.0.0` with `ccip-cli pool deploy-via-factory` (CREATE2). Every EVM deploy command +> takes `--verify` to verify the contracts it created on the source-chain explorer. See +> [Verify the EVM contracts on Etherscan](#5-verify-the-evm-contracts-on-etherscan). + +> Prerequisite: run all commands from the `ccip-cli/` directory. + +--- + +## Table of contents + +1. [Prerequisites and wallet setup](#1-prerequisites-and-wallet-setup) +2. [Phase 1: deploy tokens](#2-phase-1-deploy-tokens) +3. [Phase 2: mint tokens](#3-phase-2-mint-tokens) +4. [Phase 3: deploy pools](#4-phase-3-deploy-pools) +5. [Verify the EVM contracts on Etherscan](#5-verify-the-evm-contracts-on-etherscan) +6. [Phase 4: register as token admin](#6-phase-4-register-as-token-admin) +7. [Phase 5: grant mint/burn access to the pool](#7-phase-5-grant-mintburn-access-to-the-pool) +8. [Phase 6: create the token ALT (Solana only)](#8-phase-6-create-the-token-alt-solana-only) +9. [Phase 7: apply chain updates (mesh configuration)](#9-phase-7-apply-chain-updates-mesh-configuration) +10. [Phase 8: set the pool in the TokenAdminRegistry](#10-phase-8-set-the-pool-in-the-tokenadminregistry) +11. [Phase 9: cross-chain transfers](#11-phase-9-cross-chain-transfers) +12. [Manage EVM CCT v2 pools: liquidity and config](#12-manage-evm-cct-v2-pools-liquidity-and-config) +13. [Additional operations](#13-additional-operations) +14. [Known issues and gotchas](#14-known-issues-and-gotchas) + +--- + +## 1. Prerequisites and wallet setup + +### Tools required + +- Node.js 20+ +- `spl-token` CLI, for Solana token minting +- `cast` (from [Foundry](https://book.getfoundry.sh/)), for EVM direct contract calls (minting) +- `aptos` CLI, for Aptos token minting and (optionally) contract deployment + +### Build the project + +Clone the repo and build both the SDK and CLI before running any commands: + +```bash +git clone && cd ccip-tools-ts + +# Install dependencies +npm install + +# Build SDK + CLI (must be done from the repo root) +npm run build +``` + +After building, the CLI is available at `ccip-cli/dist/index.js`. Run every command in this guide from +the `ccip-cli/` directory: + +```bash +cd ccip-cli +node dist/index.js --help +``` + +### Accounts and funding + +You need accounts on all 3 chains with enough native tokens to cover transaction fees: + +| Chain | Account | How to fund | Estimated cost | +| --------------- | -------------------------------------------------- | ---------------------------------------------------- | -------------- | +| EVM (Sepolia) | Generate with any Ethereum wallet (MetaMask, etc.) | [Sepolia faucet](https://faucets.chain.link/sepolia) | ~0.1 ETH | +| Solana (Devnet) | `solana-keygen new -o ~/.config/solana/id.json` | `solana airdrop 5 --url devnet` | ~5 SOL | +| Aptos (Testnet) | Derive from same private key (Ed25519) | [Aptos faucet](https://aptos.dev/en/network/faucet) | ~2 APT | + +> The same 32-byte hex private key works for both EVM and Aptos; they derive different addresses from +> it. Solana needs a separate keypair file (`~/.config/solana/id.json`). + +### `.env` file setup + +Create a `.env` file in the `ccip-cli/` directory with your RPC endpoints and private key. The CLI +reads it by default (`--rpcs-file ./.env`): + +```bash +# RPC endpoints (one per chain) +RPC_ETHEREUM_SEPOLIA=https://1rpc.io/sepolia +RPC_SOLANA_DEVNET=https://api.devnet.solana.com +RPC_APTOS_TESTNET=https://fullnode.testnet.aptoslabs.com/v1 + +# EVM/Aptos private key (32-byte hex, no 0x prefix) +# Used automatically when --wallet is omitted for EVM/Aptos commands +PRIVATE_KEY= + +# Optional: Etherscan V2 multichain API key, for contract verification +# Used by --verify on EVM deploy commands and by `ccip-cli verify` +ETHERSCAN_API_KEY= +``` + +### Wallet configuration + +| Chain | How to pass wallet | Wallet location | Notes | +| ------ | ------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| EVM | `-w ` or `PRIVATE_KEY` in `.env` | `.env` file | 32-byte hex, auto-loaded from `.env` if `--wallet` omitted | +| Solana | `-w ~/.config/solana/id.json` | `~/.config/solana/id.json` | JSON keypair file (64-byte array). Always pass `-w` explicitly; `PRIVATE_KEY` from `.env` does not work for Solana | +| Aptos | `-w ` or `PRIVATE_KEY` in `.env` | `.env` file | Same 32-byte hex as EVM (Ed25519 seed), derives a different address | + +### CCIP contract addresses (testnet) + +Fetch from `https://docs.chain.link/api/ccip/v1/chains?environment=testnet`. + +| Chain | Router | Registry module (EVM) | +| --------------- | -------------------------------------------------------------------- | -------------------------------------------- | +| EVM (Sepolia) | `0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59` | `0xa3c796d480638d7476792230da1E2ADa86e031b0` | +| Solana (Devnet) | `Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C` | — | +| Aptos (Testnet) | `0xc748085bd02022a9696dfa2058774f92a07401208bbd34cfd0c6d0ac0287ee45` | — | + +Solana pool program IDs: + +- BurnMint: `41FGToCmdaWa1dgZLKFAjvmx6e6AjVTX7SVRibvsMGVB` +- LockRelease: `8eqh8wppT9c5rw4ERqNCffvU6cNFJWff9WmkcYtmGiqC` + +Aptos MCMS address: `0xbdf1b9aacb4e21bf6f255105831df0172e911d4748e488196fde10d2e2a4e32d` + +--- + +## 2. Phase 1: deploy tokens + +Deploy a token on each chain. All chains use the same token name and symbol for consistency. + +### EVM (Sepolia) — CrossChainToken 2.0.0, 18 decimals + +`token deploy` on an EVM network deploys a canonical CrossChainToken 2.0.0 (no `--token-type`). +CrossChainToken uses OpenZeppelin AccessControl roles (`MINTER_ROLE` / `BURNER_ROLE`). The deployer +becomes owner, CCIP admin, and burn-mint-role admin by default, but is not a minter unless you +pre-mint or grant the role explicitly (see Phase 2). + +EVM-only flags (verified against `token/deploy.ts`): + +| Flag | Meaning | +| ---------------------------------- | ------------------------------------------------------------------------------- | +| `--max-supply` | Cap in whole units (omit for unlimited) | +| `--initial-supply` | Pre-mint amount in whole units (minted in the constructor) | +| `--pre-mint-recipient` | Who receives the pre-mint (defaults to owner) | +| `--ccip-admin` | Address returned by `getCCIPAdmin()` (defaults to owner/signer) | +| `--burn-mint-role-admin` | Address allowed to grant/revoke `MINTER_ROLE`/`BURNER_ROLE` (defaults to owner) | +| `--owner` | Owner for 2-step admin (defaults to signer) | +| `--verify` / `--etherscan-api-key` | Verify the deployed token on the explorer | + +```bash +ccip-cli token deploy \ + -n ethereum-testnet-sepolia \ + --name "CCT Test Token" \ + --symbol CCTEST \ + --decimals 18 \ + --initial-supply 1000000 \ + -f json +``` + +Output: `tokenAddress` and `txHash`. + +To verify the contract on Etherscan in the same step, add `--verify` (needs `ETHERSCAN_API_KEY` in the +env or `--etherscan-api-key`): + +```bash +ccip-cli token deploy \ + -n ethereum-testnet-sepolia \ + --name "CCT Test Token" \ + --symbol CCTEST \ + --decimals 18 \ + --initial-supply 1000000 \ + --verify \ + -f json +``` + +Verification is covered in full in +[Verify the EVM contracts on Etherscan](#5-verify-the-evm-contracts-on-etherscan), including the +standalone `ccip-cli verify` command for contracts you already deployed. + +#### EVM alternative A — combined token + pool (`pool deploy-combined`) + +Deploys a single CrossChainPoolToken 2.0.0, one contract that is both the ERC20 token and its own CCIP +token pool, so you skip the separate Phase 3 pool deploy. Flags from `pool/deploy-combined.ts`: +`--name` / `--symbol` / `--decimals` / `--router-address` (required), plus optional `--max-supply`, +`--initial-supply` (pre-mint), `--advanced-pool-hooks`, `--ccip-admin`, `--pre-mint-recipient`, and +`--verify` / `--etherscan-api-key`. + +```bash +ccip-cli pool deploy-combined \ + -n ethereum-testnet-sepolia \ + --name "CCT Test Token" \ + --symbol CCTEST \ + --decimals 18 \ + --router-address 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 \ + --initial-supply 1000000 \ + -f json +``` + +Output: `address` (the contract; both `tokenAddress` and `poolAddress` equal it) and `txHash`. + +#### EVM alternative B — deploy via TokenPoolFactory (`pool deploy-via-factory`) + +Deploys token + pool (or just a pool for an existing token via `--token-address`) through a +TokenPoolFactory 2.0.0 using CREATE2, for either `--pool-type burn-mint` or `--pool-type lock-release` +(lock-release auto-deploys an ERC20LockBox 2.0.0, returned as `lockBoxAddress`). Flags from +`pool/deploy-via-factory.ts`: `--factory` and `--pool-type` (required), plus `--decimals` (default +18), `--token-address` (existing-token mode), `--name` / `--symbol` / `--max-supply` / `--pre-mint` / +`--pre-mint-recipient` (new-token mode, smallest units), `--lock-box`, `--salt`, `--future-owner`, and +`--verify` / `--etherscan-api-key`. With `--verify`, every contract the factory created (token, pool, +and the auto-deployed lockbox) is verified. + +Testnet factory addresses (live-verified 2026-06-26): + +| Chain | TokenPoolFactory 2.0.0 | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Ethereum Sepolia | [`0x93c57146d11a6Ca73fc53e5902aCB6900E553858`](https://sepolia.etherscan.io/address/0x93c57146d11a6Ca73fc53e5902aCB6900E553858#code) | +| Base Sepolia | [`0x90e449aE080F480B1FaDA508E7B85FD85D0c1E1F`](https://sepolia.basescan.org/address/0x90e449aE080F480B1FaDA508E7B85FD85D0c1E1F#code) | + +```bash +# New token + burn-mint pool, both verified +ccip-cli pool deploy-via-factory \ + -n ethereum-testnet-sepolia \ + --factory 0x93c57146d11a6Ca73fc53e5902aCB6900E553858 \ + --pool-type burn-mint \ + --name "CCT Test Token" \ + --symbol CCTEST \ + --decimals 18 \ + --verify \ + -f json +``` + +Output: `tokenAddress`, `poolAddress`, `txHash` (and `lockBoxAddress` for lock-release). + +### Solana (Devnet) — Token-2022, 9 decimals + +Token-2022 (SPL Token Extensions) with Metaplex metadata: + +```bash +ccip-cli token deploy \ + -n solana-devnet \ + --wallet ~/.config/solana/id.json \ + --name "CCT Test Token" \ + --symbol CCTEST \ + --decimals 9 \ + --token-program token-2022 \ + --metadata-uri "https://cyan-pleasant-anteater-613.mypinata.cloud/ipfs/bafkreieirlwjqbtzniqsgcjebzexlcspcmvd4woh3ajvf2p4fuivkenw6i" \ + --initial-supply 1000000 \ + -f json +``` + +Output: `tokenAddress`, `txHash`, `metadataAddress`. + +### Aptos (Testnet) — Managed Token, 8 decimals + +> Warning: the Aptos `token deploy` command will likely be removed from the SDK. It requires the Aptos +> CLI installed locally to compile Move contracts, which makes it impractical to bundle in the SDK. +> Recommendation: deploy Aptos tokens directly from the +> [`chainlink-aptos`](https://github.com/smartcontractkit/chainlink-aptos) repo using +> `aptos move deploy-object`. See the [Aptos CLI docs](https://aptos.dev/tools/aptos-cli/) for +> installation. + +Managed tokens use allowlist-based access control. The deployer is the owner and can add or remove +minters and burners. + +```bash +ccip-cli token deploy \ + -n aptos-testnet \ + -w \ + --name "CCT Test Token" \ + --symbol CCTEST \ + --decimals 8 \ + --initial-supply 1000000 \ + -f json +``` + +Output: `tokenAddress`, `txHash`, `codeObjectAddress`. + +### Record your addresses + +Save the token addresses from each chain. You need them throughout the remaining steps. + +```bash +EVM_TOKEN=0x... +SOLANA_TOKEN= +APTOS_TOKEN=0x... +# Also save the Aptos code object address for minting later +APTOS_CODE_OBJECT=0x... +``` + +--- + +## 3. Phase 2: mint tokens + +Mint tokens to your wallet for transfer testing. + +### EVM (Sepolia) — CrossChainToken + +CrossChainToken uses OpenZeppelin AccessControl roles, not dedicated `grantMintRole`/`mint` helpers. +The simplest path is to pre-mint at deploy time via `--initial-supply` (Phase 1): the constructor +mints to `--pre-mint-recipient` (default: owner) without needing any role. If you did that, your +wallet already holds the tokens and you can skip the rest of this step. + +To mint more later, the caller must hold `MINTER_ROLE`. The deployer (owner / burn-mint-role admin) +can grant it. The convenient way is the CLI, which grants both roles at once and auto-detects the v2 +token (no `--token-type`): + +```bash +# Grant MINTER_ROLE + BURNER_ROLE to your wallet (or to the pool — see Phase 5) +ccip-cli token grant-mint-burn-access \ + -n ethereum-testnet-sepolia \ + --token-address $EVM_TOKEN \ + --authority \ + -f json +``` + +Then mint with `cast`: + +```bash +# Mint 1,000,000 tokens (1000000 * 10^18) +cast send $EVM_TOKEN \ + "mint(address,uint256)" \ + \ + 1000000000000000000000000 \ + --private-key \ + --rpc-url https://1rpc.io/sepolia +``` + +> For raw `cast`, the role grant is `grantRole(MINTER_ROLE, addr)` where +> `MINTER_ROLE = keccak256("MINTER_ROLE")`. The CLI's `grant-mint-burn-access` (or the token's +> `grantMintAndBurnRoles(addr)` convenience setter) is simpler. + +### Solana (Devnet) — Token-2022 + +The deployer is the mint authority. No extra permission needed: + +```bash +spl-token mint $SOLANA_TOKEN 1000000 --url devnet +``` + +### Aptos (Testnet) + +Uses the code object address from the token deploy output: + +```bash +aptos move run \ + --function-id "$APTOS_CODE_OBJECT::managed_token::mint" \ + --args "address:" "u64:100000000000000" \ + --url https://fullnode.testnet.aptoslabs.com/v1 \ + --private-key \ + --assume-yes +``` + +> The `u64` amount is in raw units (1,000,000 tokens \* 10^8 decimals = 100000000000000). + +--- + +## 4. Phase 3: deploy pools + +Deploy a burn-mint token pool on each chain. + +### EVM (Sepolia) — burn-mint pool + +Deploys a BurnMintTokenPool 2.0.0. `pool deploy` no longer takes `--allowlist`; the EVM-only options +are `--advanced-pool-hooks` (optional, defaults to the zero address) and `--lock-box` (lock-release +only: supply an existing `ERC20LockBox`, otherwise one is auto-deployed). Add `--verify` (with +`ETHERSCAN_API_KEY` / `--etherscan-api-key`) to verify the pool, and the auto-deployed lockbox for +lock-release, on the explorer. + +```bash +ccip-cli pool deploy \ + -n ethereum-testnet-sepolia \ + --pool-type burn-mint \ + --token-address $EVM_TOKEN \ + --local-token-decimals 18 \ + --router-address 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 \ + -f json +``` + +> Lock-release. With `--pool-type lock-release`, the signed deploy deploys a LockReleaseTokenPool +> 2.0.0 and auto-deploys an ERC20LockBox 2.0.0, returning both `poolAddress` and `lockBoxAddress` in +> the output. Pass `--lock-box ` to reuse an existing lockbox instead. Lock-release pools need +> liquidity before they can release on the destination; see +> [Manage EVM CCT v2 pools: liquidity and config](#12-manage-evm-cct-v2-pools-liquidity-and-config). + +### Solana (Devnet) — burn-mint pool + +```bash +ccip-cli pool deploy \ + -n solana-devnet \ + --wallet ~/.config/solana/id.json \ + --pool-type burn-mint \ + --token-address $SOLANA_TOKEN \ + --local-token-decimals 9 \ + --pool-program-id 41FGToCmdaWa1dgZLKFAjvmx6e6AjVTX7SVRibvsMGVB \ + -f json +``` + +### Aptos (Testnet) — managed token pool + +> Warning: the Aptos `pool deploy` command will likely be removed from the SDK. It requires the Aptos +> CLI installed locally to compile Move contracts, which makes it impractical to bundle in the SDK. +> Recommendation: deploy Aptos pools directly from the +> [`chainlink-aptos`](https://github.com/smartcontractkit/chainlink-aptos) repo using +> `aptos move deploy-object`. See the [Aptos CLI docs](https://aptos.dev/tools/aptos-cli/) for +> installation. + +```bash +ccip-cli pool deploy \ + -n aptos-testnet \ + -w \ + --pool-type burn-mint \ + --token-address $APTOS_TOKEN \ + --local-token-decimals 8 \ + --router-address 0xc748085bd02022a9696dfa2058774f92a07401208bbd34cfd0c6d0ac0287ee45 \ + --mcms-address 0xbdf1b9aacb4e21bf6f255105831df0172e911d4748e488196fde10d2e2a4e32d \ + -f json +``` + +The Aptos pool deploy runs as two internal steps: publish the CCIPTokenPool shared dependency, then +publish the managed_token_pool module. The SDK handles both automatically. + +### Record pool addresses + +```bash +EVM_POOL=0x... +SOLANA_POOL= +APTOS_POOL=0x... +``` + +--- + +## 5. Verify the EVM contracts on Etherscan + +This section is EVM-only and optional. Skip ahead to [Phase 4](#6-phase-4-register-as-token-admin) if +you are not on EVM, or come back here later. + +Every CCT v2 contract you deploy on EVM can be verified on the source-chain explorer, including +contracts the `TokenPoolFactory` created with CREATE2. Verification uses the Etherscan V2 multichain +API. Provide the key with `ETHERSCAN_API_KEY` in the environment, or `--etherscan-api-key` on the +command. There are two paths: the `--verify` flag on a deploy command, and the standalone +`ccip-cli verify` command for a contract you already deployed. + +### Verify at deploy time with `--verify` + +Add `--verify` to any EVM deploy command and it verifies what it just deployed: + +- `ccip-cli token deploy --verify` verifies the `CrossChainToken`. +- `ccip-cli pool deploy --verify` verifies the pool, and the auto-deployed `ERC20LockBox` for + lock-release. +- `ccip-cli pool deploy-combined --verify` verifies the `CrossChainPoolToken`. +- `ccip-cli pool deploy-via-factory --verify` verifies every contract the factory created (token, + pool, and the auto-deployed lockbox). The factory contracts are born in internal CREATE2 calls, so + the constructor args are carried through from the deploy rather than recovered later. + +### Verify an existing contract with `ccip-cli verify` + +`ccip-cli verify` verifies a contract you already deployed. Given just `--contract` and `--address`, +it derives the constructor args from the contract's on-chain creation code (stripping the known SDK +bytecode), including factory CREATE2 deploys via transaction tracing. `--contract` accepts +`CrossChainToken`, `BurnMintTokenPool`, `LockReleaseTokenPool`, `CrossChainPoolToken`, `ERC20LockBox`, +or `AdvancedPoolHooks`. + +```bash +# Constructor args auto-derived from on-chain creation code +ccip-cli verify \ + -n ethereum-testnet-sepolia \ + --contract CrossChainToken \ + --address $EVM_TOKEN +``` + +Auto-derivation needs an API key (to fetch the creation code). Two escape hatches let you skip it: +pass `--constructor-args 0x...` (ABI-encoded) to skip derivation entirely, or `--creation-tx ` +to derive from a known creation transaction without an explorer lookup. + +### Live-verified examples (2026-06-26) + +These Sepolia contracts were deployed with `ccip-cli` and verified live on 2026-06-26. Open each link +to see the verified source on Etherscan; this is what a successful verify looks like. + +| Contract | How it was deployed | Address (Sepolia, verified) | +| ---------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| TokenPoolFactory 2.0.0 | bundled fixture | [`0x93c57146d11a6Ca73fc53e5902aCB6900E553858`](https://sepolia.etherscan.io/address/0x93c57146d11a6Ca73fc53e5902aCB6900E553858#code) | +| CrossChainToken (factory) | `pool deploy-via-factory` lock-release | [`0x94b968AFeDf10015eE676B6b87c64EC6B60EbcF2`](https://sepolia.etherscan.io/address/0x94b968AFeDf10015eE676B6b87c64EC6B60EbcF2#code) | +| LockReleaseTokenPool (factory) | `pool deploy-via-factory` lock-release | [`0x85eea37C663354B1141b08239d1fBCfdF8aD1594`](https://sepolia.etherscan.io/address/0x85eea37C663354B1141b08239d1fBCfdF8aD1594#code) | +| ERC20LockBox (factory auto-deploy) | `pool deploy-via-factory` lock-release | [`0x8851E5c07fB48ad33affEf86E30476771B51f143`](https://sepolia.etherscan.io/address/0x8851E5c07fB48ad33affEf86E30476771B51f143#code) | +| CrossChainToken (direct) | `token deploy` | [`0x099D317FdF4DEd2BeAD645196A6385690D4a1dF6`](https://sepolia.etherscan.io/address/0x099D317FdF4DEd2BeAD645196A6385690D4a1dF6#code) | + +The factory lock-release row is one `pool deploy-via-factory --pool-type lock-release --verify` run: +the token, the pool, and the auto-deployed lockbox were all verified from a single CREATE2 deploy. The +direct row is a plain `token deploy` whose `CrossChainToken` was verified afterward with +`ccip-cli verify`. + +--- + +## 6. Phase 4: register as token admin + +This is a 2-step process: propose, then accept. You must be the token owner/admin to propose. + +### Step 1: propose admin + +#### EVM (Sepolia) + +CrossChainToken implements `getCCIPAdmin()` (set via `--ccip-admin` at deploy, defaults to the +owner/signer), so use `--registration-method get-ccip-admin`: + +```bash +ccip-cli token-admin propose-admin \ + -n ethereum-testnet-sepolia \ + --token-address $EVM_TOKEN \ + --registry-module-address 0xa3c796d480638d7476792230da1E2ADa86e031b0 \ + --registration-method get-ccip-admin \ + -f json +``` + +#### Solana (Devnet) + +```bash +ccip-cli token-admin propose-admin \ + -n solana-devnet \ + --wallet ~/.config/solana/id.json \ + --token-address $SOLANA_TOKEN \ + --administrator \ + --router-address Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C \ + -f json +``` + +#### Aptos (Testnet) + +```bash +ccip-cli token-admin propose-admin \ + -n aptos-testnet \ + -w \ + --token-address $APTOS_TOKEN \ + --administrator \ + --router-address 0xc748085bd02022a9696dfa2058774f92a07401208bbd34cfd0c6d0ac0287ee45 \ + -f json +``` + +### Verify with `get-config` + +After proposing, confirm that `pendingAdministrator` is set: + +```bash +ccip-cli token-admin get-config \ + -n ethereum-testnet-sepolia \ + --token-address $EVM_TOKEN \ + --router-address 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 \ + -f json +``` + +Expected: `pendingAdministrator` = your wallet address, `administrator` = zero address. + +### Step 2: accept admin + +#### EVM + +```bash +ccip-cli token-admin accept-admin \ + -n ethereum-testnet-sepolia \ + --token-address $EVM_TOKEN \ + --router-address 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 \ + -f json +``` + +#### Solana + +```bash +ccip-cli token-admin accept-admin \ + -n solana-devnet \ + --wallet ~/.config/solana/id.json \ + --token-address $SOLANA_TOKEN \ + --router-address Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C \ + -f json +``` + +#### Aptos + +```bash +ccip-cli token-admin accept-admin \ + -n aptos-testnet \ + -w \ + --token-address $APTOS_TOKEN \ + --router-address 0xc748085bd02022a9696dfa2058774f92a07401208bbd34cfd0c6d0ac0287ee45 \ + -f json +``` + +After accepting, `get-config` shows `administrator` = your wallet, `pendingAdministrator` = zero. + +--- + +## 7. Phase 5: grant mint/burn access to the pool + +The pool needs mint and burn permissions on the token to process cross-chain transfers. + +### EVM (Sepolia) — CrossChainToken + +The SDK auto-detects the v2 CrossChainToken; no `--token-type` needed. This grants the pool the +AccessControl `MINTER_ROLE`/`BURNER_ROLE` (via the token's `grantMintAndBurnRoles` convenience +setter): + +```bash +ccip-cli token grant-mint-burn-access \ + -n ethereum-testnet-sepolia \ + -w \ + --token-address $EVM_TOKEN \ + --authority $EVM_POOL \ + --rpc https://1rpc.io/sepolia \ + -f json +``` + +The default `--role mintAndBurn` grants both. Use `--role mint` or `--role burn` for granular control. + +### Solana (Devnet) + +Solana uses the SPL Token mint authority. This transfers mint authority to the specified address; your +wallet loses direct minting ability. + +For CCIP, the recommended flow is: + +1. Create an SPL Multisig (with `create-multisig`) containing the pool's signer PDA plus your wallet. +2. Transfer mint authority to the multisig. + +```bash +# Step 1: Create multisig (1-of-2: pool signer PDA + your wallet) +# --token-address is an alias for --mint (standard Solana terminology) +ccip-cli token create-multisig \ + -n solana-devnet \ + --wallet ~/.config/solana/id.json \ + --token-address $SOLANA_TOKEN \ + --pool-program-id 41FGToCmdaWa1dgZLKFAjvmx6e6AjVTX7SVRibvsMGVB \ + --additional-signers \ + --threshold 1 \ + --rpcs https://api.devnet.solana.com \ + -f json +``` + +Save the `multisigAddress` from the output: + +```bash +SOLANA_MULTISIG= +``` + +```bash +# Step 2: Transfer mint authority to multisig +ccip-cli token grant-mint-burn-access \ + -n solana-devnet \ + -w ~/.config/solana/id.json \ + --token-address $SOLANA_TOKEN \ + --authority $SOLANA_MULTISIG \ + --rpc https://api.devnet.solana.com \ + -f json +``` + +### Aptos (Testnet) + +For Managed tokens, this calls `apply_allowed_minter_updates` plus `apply_allowed_burner_updates` (2 +txs). The owner keeps minting ability; this is additive, not a transfer. + +Pass the pool address as `--authority`. The SDK resolves the pool's store address (resource signer +PDA) internally via `get_store_address` and grants mint/burn to that address. + +```bash +ccip-cli token grant-mint-burn-access \ + -n aptos-testnet \ + -w \ + --token-address $APTOS_TOKEN \ + --authority $APTOS_POOL \ + --rpc https://fullnode.testnet.aptoslabs.com/v1 \ + -f json +``` + +### Verify with `get-mint-burn-info` + +```bash +# EVM — shows minters[] and burners[] arrays +ccip-cli token get-mint-burn-info \ + -n ethereum-testnet-sepolia \ + --token-address $EVM_TOKEN \ + --rpc https://1rpc.io/sepolia \ + -f json + +# Solana — shows mintAuthority, isMultisig, multisigThreshold, multisigMembers +ccip-cli token get-mint-burn-info \ + -n solana-devnet \ + --token-address $SOLANA_TOKEN \ + --rpc https://api.devnet.solana.com \ + -f json + +# Aptos — shows owner, allowedMinters[], allowedBurners[] +ccip-cli token get-mint-burn-info \ + -n aptos-testnet \ + --token-address $APTOS_TOKEN \ + --rpc https://fullnode.testnet.aptoslabs.com/v1 \ + -f json +``` + +> On EVM, `get-mint-burn-info` lists the `MINTER_ROLE`/`BURNER_ROLE` holders of the CrossChainToken +> (via `AccessControlEnumerable`, falling back to scanning `RoleGranted` events for non-enumerable +> tokens). + +--- + +## 8. Phase 6: create the token ALT (Solana only) + +Solana needs an Address Lookup Table (ALT) holding 10 base CCIP addresses for the token's pool. This +is a prerequisite for `set-pool`. + +```bash +ccip-cli token-admin create-token-alt \ + -n solana-devnet \ + --wallet ~/.config/solana/id.json \ + --token-address $SOLANA_TOKEN \ + --pool-address $SOLANA_POOL \ + --router-address Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C \ + --additional-addresses $SOLANA_MULTISIG \ + --rpcs https://api.devnet.solana.com +``` + +> Include the SPL Multisig via `--additional-addresses` so the router can reference it in +> `releaseOrMintTokens` transactions. The ALT then holds 11 entries (10 base CCIP + 1 multisig). + +Save the ALT address: + +```bash +SOLANA_ALT= +``` + +--- + +## 9. Phase 7: apply chain updates (mesh configuration) + +Configure each pool to know about the remote chains, their tokens, pools, and rate limiters. This +builds a mesh where each pool knows how to reach every other pool. + +### Configuration file format + +Create a JSON config file for each chain. Each file lists the other 2 chains as remotes. + +> Rate limiter values are in the local token's smallest unit. `capacity` is the maximum tokens in the +> bucket; `rate` is tokens per second refill. Scale these values by the local token's decimals: +> +> - EVM (18 decimals): 10,000 tokens = `10000 × 10^18` = `10000000000000000000000` +> - Solana (9 decimals): 10,000 tokens = `10000 × 10^9` = `10000000000000` +> - Aptos (8 decimals): 10,000 tokens = `10000 × 10^8` = `1000000000000` + +#### EVM config (evm-config.json) + +```json +{ + "chainsToRemove": [], + "chainsToAdd": [ + { + "remoteChainSelector": "solana-devnet", + "remotePoolAddresses": [""], + "remoteTokenAddress": "", + "remoteTokenDecimals": 9, + "outboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "10000000000000000000000", + "rate": "1000000000000000000000" + }, + "inboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "10000000000000000000000", + "rate": "1000000000000000000000" + } + }, + { + "remoteChainSelector": "aptos-testnet", + "remotePoolAddresses": [""], + "remoteTokenAddress": "", + "remoteTokenDecimals": 8, + "outboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "10000000000000000000000", + "rate": "1000000000000000000000" + }, + "inboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "10000000000000000000000", + "rate": "1000000000000000000000" + } + } + ] +} +``` + +#### Solana config (solana-config.json) + +```json +{ + "chainsToRemove": [], + "chainsToAdd": [ + { + "remoteChainSelector": "ethereum-testnet-sepolia", + "remotePoolAddresses": [""], + "remoteTokenAddress": "", + "remoteTokenDecimals": 18, + "outboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "10000000000000", + "rate": "1000000000000" + }, + "inboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "10000000000000", + "rate": "1000000000000" + } + }, + { + "remoteChainSelector": "aptos-testnet", + "remotePoolAddresses": [""], + "remoteTokenAddress": "", + "remoteTokenDecimals": 8, + "outboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "10000000000000", + "rate": "1000000000000" + }, + "inboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "10000000000000", + "rate": "1000000000000" + } + } + ] +} +``` + +#### Aptos config (aptos-config.json) + +```json +{ + "chainsToRemove": [], + "chainsToAdd": [ + { + "remoteChainSelector": "ethereum-testnet-sepolia", + "remotePoolAddresses": [""], + "remoteTokenAddress": "", + "remoteTokenDecimals": 18, + "outboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "1000000000000", + "rate": "100000000000" + }, + "inboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "1000000000000", + "rate": "100000000000" + } + }, + { + "remoteChainSelector": "solana-devnet", + "remotePoolAddresses": [""], + "remoteTokenAddress": "", + "remoteTokenDecimals": 9, + "outboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "1000000000000", + "rate": "100000000000" + }, + "inboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "1000000000000", + "rate": "100000000000" + } + } + ] +} +``` + +### Apply on each chain + +#### EVM + +```bash +ccip-cli pool apply-chain-updates \ + -n ethereum-testnet-sepolia \ + --pool-address $EVM_POOL \ + --config /path/to/evm-config.json \ + -f json +``` + +#### Solana + +```bash +ccip-cli pool apply-chain-updates \ + -n solana-devnet \ + --wallet ~/.config/solana/id.json \ + --pool-address $SOLANA_POOL \ + --config /path/to/solana-config.json \ + --rpcs https://api.devnet.solana.com \ + -f json +``` + +#### Aptos + +```bash +ccip-cli pool apply-chain-updates \ + -n aptos-testnet \ + -w \ + --pool-address $APTOS_POOL \ + --config /path/to/aptos-config.json \ + --rpc https://fullnode.testnet.aptoslabs.com/v1 \ + -f json +``` + +### Verify with `pool get-config` + +Check each pool to confirm remote chains, pool addresses, token addresses, and rate limiters are set +correctly: + +```bash +# EVM — check Solana remote +ccip-cli pool get-config \ + -n ethereum-testnet-sepolia \ + --pool-address $EVM_POOL \ + --remote-chain solana-devnet \ + -f json + +# Solana — check EVM remote +ccip-cli pool get-config \ + -n solana-devnet \ + --pool-address $SOLANA_POOL \ + --remote-chain ethereum-testnet-sepolia \ + -f json + +# Aptos — check EVM remote +ccip-cli pool get-config \ + -n aptos-testnet \ + --pool-address $APTOS_POOL \ + --remote-chain ethereum-testnet-sepolia \ + -f json +``` + +Each should show `remotePools`, `remoteToken`, and +`outboundRateLimiterState`/`inboundRateLimiterState` with the values from your config files. + +### Solana pool token ATA (existing pools only) + +If you are configuring an existing pool (not deployed via this tutorial), the Pool Signer PDA's +Associated Token Account (ATA) must exist before inbound transfers. For pools deployed via +`ccip-cli pool deploy`, this is created automatically. + +```bash +# Only needed for existing pools, NOT for fresh deploys from this tutorial +spl-token create-account $SOLANA_TOKEN \ + --owner \ + --fee-payer ~/.config/solana/id.json \ + --url devnet +``` + +--- + +## 10. Phase 8: set the pool in the TokenAdminRegistry + +Register the pool in the TokenAdminRegistry, linking token to pool so the CCIP Router can route +cross-chain messages through it. + +### EVM (Sepolia) + +```bash +ccip-cli token-admin set-pool \ + -n ethereum-testnet-sepolia \ + --token-address $EVM_TOKEN \ + --pool-address $EVM_POOL \ + --router-address 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 +``` + +### Solana (Devnet) + +Requires `--pool-lookup-table` (the ALT from Phase 6): + +```bash +ccip-cli token-admin set-pool \ + -n solana-devnet \ + --wallet ~/.config/solana/id.json \ + --token-address $SOLANA_TOKEN \ + --pool-address $SOLANA_POOL \ + --router-address Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C \ + --pool-lookup-table $SOLANA_ALT \ + --rpcs https://api.devnet.solana.com +``` + +### Aptos (Testnet) + +```bash +ccip-cli token-admin set-pool \ + -n aptos-testnet \ + --token-address $APTOS_TOKEN \ + --pool-address $APTOS_POOL \ + --router-address 0xc748085bd02022a9696dfa2058774f92a07401208bbd34cfd0c6d0ac0287ee45 +``` + +### Verify with `get-config` + +```bash +ccip-cli token-admin get-config \ + -n ethereum-testnet-sepolia \ + --token-address $EVM_TOKEN \ + --router-address 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 \ + -f json +``` + +The `tokenPool` field should now match your pool address. On Solana, the output also shows +`poolLookupTable` and `poolLookupTableEntries`. + +--- + +## 11. Phase 9: cross-chain transfers + +With the mesh configured, send tokens between chains. + +### EVM to Solana + +```bash +ccip-cli send \ + -s ethereum-testnet-sepolia \ + -d solana-devnet \ + -r 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 \ + --to \ + -t $EVM_TOKEN=1.0 \ + --ooo -L 0 -f log +``` + +### EVM to Aptos + +```bash +ccip-cli send \ + -s ethereum-testnet-sepolia \ + -d aptos-testnet \ + -r 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 \ + --to \ + -t $EVM_TOKEN=1.0 \ + --ooo -L 0 -f log +``` + +### Solana to EVM + +```bash +ccip-cli send \ + -s solana-devnet \ + -d ethereum-testnet-sepolia \ + -r Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C \ + --to \ + --wallet ~/.config/solana/id.json \ + -t $SOLANA_TOKEN=0.5 \ + --rpcs https://api.devnet.solana.com \ + --ooo -L 0 -f log +``` + +### Aptos to EVM + +```bash +ccip-cli send \ + -s aptos-testnet \ + -d ethereum-testnet-sepolia \ + -r 0xc748085bd02022a9696dfa2058774f92a07401208bbd34cfd0c6d0ac0287ee45 \ + --to \ + -w \ + -t $APTOS_TOKEN=1.0 \ + --rpc https://fullnode.testnet.aptoslabs.com/v1 \ + --ooo -L 0 -f log +``` + +### Track message status + +```bash +ccip-cli show \ + --rpcs \ + --rpcs \ + -f json +``` + +You can also track on the CCIP Explorer: `https://ccip.chain.link/msg/`. + +### Transfer flags + +| Flag | Description | +| ------- | ------------------------------------------------ | +| `-s` | Source chain name | +| `-d` | Destination chain name | +| `-r` | Router address on source chain | +| `--to` | Recipient address on destination chain | +| `-t` | Token and amount (`=`) | +| `--ooo` | Out-of-order execution (recommended for testing) | +| `-L 0` | Gas limit 0 (no receiver contract execution) | + +### Aptos and Solana direct lanes + +Direct Aptos-to-Solana lanes may not be configured at the router level on testnet. This is a Chainlink +infrastructure limitation, not a code issue. If you get `E_UNSUPPORTED_DESTINATION_CHAIN`, the lane +does not exist yet. Use EVM as a hub. + +--- + +## 12. Manage EVM CCT v2 pools: liquidity and config + +These operations are EVM-only and apply to the CCT v2.0 stack (`CrossChainToken`, `BurnMintTokenPool`, +`LockReleaseTokenPool`, `CrossChainPoolToken`, `ERC20LockBox`). To verify any of these contracts on +the explorer, see [Verify the EVM contracts on Etherscan](#5-verify-the-evm-contracts-on-etherscan). + +### Provide liquidity (lock-release only) + +A lock-release pool can only release tokens on the destination if it holds liquidity. Fund it with +`pool provide-liquidity` (version-aware: works for both v2.0 and v1.x lock-release pools). `--amount` +is in whole token units; the CLI resolves the pool's token decimals and scales it for you. + +```bash +ccip-cli pool provide-liquidity \ + -n ethereum-testnet-sepolia \ + --pool-address $EVM_POOL \ + --amount 1000 \ + -f json +``` + +### v2 pool config setters + +CCT v2.0 pools expose on-chain fee and finality configuration. These require the pool owner (or fee +admin where noted). + +Token-transfer fee config sets per-destination bps and flat fees. Generate a template, edit it, then +apply (you can also pipe the JSON via stdin): + +```bash +# Generate a template +ccip-cli pool set-fee-config --generate-config > fee-config.json + +# Apply (must be pool owner or fee admin) +ccip-cli pool set-fee-config \ + -n ethereum-testnet-sepolia \ + --pool-address $EVM_POOL \ + --config fee-config.json \ + -f json +``` + +Each entry in `feeConfigs[]` carries `remoteChainSelector` (name or numeric selector), +`destGasOverhead`, `destBytesOverhead`, `finalityFeeUSDCents`, `fastFinalityFeeUSDCents`, +`finalityTransferFeeBps`, `fastFinalityTransferFeeBps`, and `isEnabled`. List selectors under +`disable[]` to turn a destination's config off. + +Allowed-finality config sets `--finality` to `finalized`, `safe`, or a block-depth integer (`0`–`65535`) +for Faster-Than-Finality: + +```bash +ccip-cli pool set-finality-config \ + -n ethereum-testnet-sepolia \ + --pool-address $EVM_POOL \ + --finality finalized \ + -f json +``` + +The fee admin setter delegates fee-config rights to another address: + +```bash +ccip-cli pool set-fee-admin \ + -n ethereum-testnet-sepolia \ + --pool-address $EVM_POOL \ + --fee-admin \ + -f json +``` + +--- + +## 13. Additional operations + +### Append remote pool addresses + +Add remote pool addresses to an existing chain config (for example, when a new pool is deployed on a +remote chain): + +```bash +ccip-cli pool append-remote-pool-addresses \ + -n ethereum-testnet-sepolia \ + --pool-address $EVM_POOL \ + --remote-chain solana-devnet \ + --remote-pool-addresses \ + -f json +``` + +### Remove remote pool addresses + +```bash +ccip-cli pool remove-remote-pool-addresses \ + -n ethereum-testnet-sepolia \ + --pool-address $EVM_POOL \ + --remote-chain solana-devnet \ + --remote-pool-addresses \ + -f json +``` + +### Delete chain config + +Remove an entire remote chain configuration from a pool: + +```bash +ccip-cli pool delete-chain-config \ + -n ethereum-testnet-sepolia \ + --pool-address $EVM_POOL \ + --remote-chain solana-devnet \ + -f json +``` + +### Set rate limiter config + +Uses a JSON config file (generate a template with `--generate-config`): + +```bash +# Generate template +ccip-cli pool set-rate-limiter-config --generate-config > rate-limiter-config.json + +# Apply (edit the template first with your values) +ccip-cli pool set-rate-limiter-config \ + -n ethereum-testnet-sepolia \ + --pool-address $EVM_POOL \ + --config rate-limiter-config.json \ + -f json +``` + +Example `rate-limiter-config.json` (values in local token's smallest unit): + +```json +{ + "chainConfigs": [ + { + "remoteChainSelector": "solana-devnet", + "outboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "10000000000000000000000", + "rate": "1000000000000000000000" + }, + "inboundRateLimiterConfig": { + "isEnabled": true, + "capacity": "10000000000000000000000", + "rate": "1000000000000000000000" + } + } + ] +} +``` + +### Revoke mint/burn access + +Revoke mint or burn permissions individually: + +```bash +ccip-cli token revoke-mint-burn-access \ + -n ethereum-testnet-sepolia \ + -w \ + --token-address $EVM_TOKEN \ + --authority $EVM_POOL \ + --role mint \ + --rpc https://1rpc.io/sepolia \ + -f json +``` + +### Transfer admin + +Transfer the TokenAdminRegistry admin role to another address (2-step: transfer then accept): + +```bash +# Current admin initiates transfer +ccip-cli token-admin transfer-admin \ + -n ethereum-testnet-sepolia \ + --token-address $EVM_TOKEN \ + --new-admin \ + --router-address 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 \ + -f json + +# New admin accepts +ccip-cli token-admin accept-admin \ + -n ethereum-testnet-sepolia \ + -w \ + --token-address $EVM_TOKEN \ + --router-address 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 \ + -f json +``` + +### Pool transfer ownership + +EVM and Solana use a 2-step process: + +```bash +# Owner proposes new owner +ccip-cli pool transfer-ownership \ + -n ethereum-testnet-sepolia \ + --pool-address $EVM_POOL \ + --new-owner + +# New owner accepts +ccip-cli pool accept-ownership \ + -n ethereum-testnet-sepolia \ + -w \ + --pool-address $EVM_POOL +``` + +Aptos uses a 3-step process: + +```bash +# Step 1: Current owner proposes +ccip-cli pool transfer-ownership \ + -n aptos-testnet \ + --pool-address $APTOS_POOL \ + --new-owner + +# Step 2: New owner signals acceptance +ccip-cli pool accept-ownership \ + -n aptos-testnet \ + -w \ + --pool-address $APTOS_POOL + +# Step 3: Current owner finalizes (AptosFramework object::transfer) +ccip-cli pool execute-ownership-transfer \ + -n aptos-testnet \ + -w \ + --pool-address $APTOS_POOL \ + --new-owner +``` + +--- + +## 14. Known issues and gotchas + +### CrossChainToken uses AccessControl roles + +The EVM CrossChainToken uses OpenZeppelin AccessControl (`MINTER_ROLE` / `BURNER_ROLE`), not dedicated +`grantMintRole`/`grantBurnRole` helpers. Use `grant-mint-burn-access` (no `--token-type`) or the +token's `grantMintAndBurnRoles(addr)` convenience setter; raw `cast` needs +`grantRole(MINTER_ROLE, addr)`. The deployer is owner, CCIP admin, and burn-mint-role admin by +default, but is not a minter unless pre-minting or granting the role. + +### Pool address encoding (fixed) + +Remote pool addresses in `applyChainUpdates` must preserve their original byte length: + +- Token addresses: left-padded to 32 bytes (correct for all chains). +- Pool addresses: raw bytes at original length (20 bytes for EVM addresses). + +The Solana on-chain program compares incoming `sourcePoolAddress` (20 raw bytes for EVM) against +stored pool addresses. If stored as 32 bytes (left-padded), the comparison fails with +`InvalidSourcePoolAddress`. + +### Solana mint authority transfer + +`grant-mint-burn-access` on Solana transfers mint authority; your wallet loses direct minting ability. +Create a multisig first (via `create-multisig`) to retain access alongside the pool. + +### Aptos 3-step ownership transfer + +EVM and Solana use 2 steps (propose, accept). Aptos uses 3: propose, accept, execute. The current +owner must call `execute-ownership-transfer` after the new owner accepts. + +### Aptos and Solana direct lanes + +Direct lanes between Aptos Testnet and Solana Devnet may not exist at the router level. This is a +Chainlink infrastructure limitation. Use EVM as a hub for Aptos-to-Solana transfers. + +### `show` command crash on SVM destinations + +The `show` command crashes when viewing SVM-destination messages because `looksUsdcData()` expects hex +`BytesLike`, but the CCIP API returns `extraData` as base64 for SVM destinations. + +--- + +## Quick reference: complete flow checklist + +``` +For each chain: + [ ] 1. Deploy token (EVM: CrossChainToken 2.0.0, Solana: Token-2022, Aptos: Managed) + - EVM alternatives: pool deploy-combined (CrossChainPoolToken = token+pool, skip step 3) + or pool deploy-via-factory (TokenPoolFactory 2.0.0, CREATE2) + - EVM: add --verify (+ ETHERSCAN_API_KEY) to verify on the explorer + [ ] 2. Mint tokens to wallet (EVM: pre-mint via --initial-supply, or grant MINTER_ROLE then cast mint) + [ ] 3. Deploy pool (EVM: BurnMintTokenPool / LockReleaseTokenPool 2.0.0 — lock-release auto-deploys ERC20LockBox; + Solana: BurnMint; Aptos: Managed). EVM: optional --verify + [ ] -. Verify the EVM contracts (--verify at deploy, or ccip-cli verify afterward) + [ ] 4. Propose admin (token-admin propose-admin) + [ ] 5. Accept admin (token-admin accept-admin) + [ ] 6. Grant mint/burn access to pool + - EVM: grant-mint-burn-access (auto-detects v2 CrossChainToken; no --token-type) + - Solana: create-multisig first, then grant to multisig + - Aptos: pass pool address as --authority (SDK auto-resolves store address; additive, owner keeps access) + [ ] 7. (Solana only) Create Token ALT (include multisig in --additional-addresses) + +Cross-chain mesh: + [ ] 8. Apply chain updates on EACH pool (pointing to all remote chains) + [ ] 9. Set pool on EACH chain (token-admin set-pool) + +EVM CCT v2 extras (optional): + [ ] provide liquidity (lock-release pools), set fee / finality / fee-admin config + (pool set-fee-config / set-finality-config / set-fee-admin) + +Testing: + [ ] 10. Send cross-chain transfer (EVM<->Solana, EVM<->Aptos) + [ ] 11. Track message with `show` or CCIP Explorer +``` diff --git a/eslint.config.mjs b/eslint.config.mjs index e1f30f09..75af6ce6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -150,6 +150,7 @@ export default defineConfig( }, { // Ban cli imports from @chainlink/ccip-sdk modules other than /src/index.ts + // and /src/cct/*/index.ts (heavy deps kept out of the main barrel). files: ['ccip-cli/src/**/*.ts'], rules: { 'no-restricted-imports': [ @@ -157,8 +158,10 @@ export default defineConfig( { patterns: [ { - regex: '^(?!@chainlink/ccip-sdk/src/index\\.ts$).*\\/ccip-sdk\\b', - message: 'Import from @chainlink/ccip-sdk/src/index.ts instead of other modules.', + regex: + '^(?!@chainlink/ccip-sdk/src/index\\.ts$|@chainlink/ccip-sdk/src/cct/[^/]+/index\\.ts$).*\\/ccip-sdk\\b', + message: + 'Import from @chainlink/ccip-sdk/src/index.ts or @chainlink/ccip-sdk/src/cct/*/index.ts instead of other modules.', }, ], }, diff --git a/package-lock.json b/package-lock.json index 4f8683e6..1af401a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -206,6 +206,10 @@ "dependencies": { "@aptos-labs/ts-sdk": "^6.3.1", "@coral-xyz/anchor": "^0.29.0", + "@metaplex-foundation/mpl-token-metadata": "^3.4.0", + "@metaplex-foundation/umi": "^1.5.1", + "@metaplex-foundation/umi-bundle-defaults": "^1.5.1", + "@metaplex-foundation/umi-web3js-adapters": "^1.5.1", "@mysten/bcs": "^2.1.0", "@mysten/sui": "^2.19.0", "@noble/hashes": "^2.2.0", @@ -5151,20 +5155,20 @@ } }, "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", "license": "MIT", "optional": true, "dependencies": { @@ -5172,9 +5176,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "license": "MIT", "optional": true, "dependencies": { @@ -7604,6 +7608,212 @@ "@chevrotain/types": "~11.1.2" } }, + "node_modules/@metaplex-foundation/mpl-token-metadata": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-3.4.0.tgz", + "integrity": "sha512-AxBAYCK73JWxY3g9//z/C9krkR0t1orXZDknUPS4+GjwGH2vgPfsk04yfZ31Htka2AdS9YE/3wH7sMUBHKn9Rg==", + "license": "Apache-2.0", + "dependencies": { + "@metaplex-foundation/mpl-toolbox": "^0.10.0" + }, + "peerDependencies": { + "@metaplex-foundation/umi": ">= 0.8.2 <= 1" + } + }, + "node_modules/@metaplex-foundation/mpl-toolbox": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/mpl-toolbox/-/mpl-toolbox-0.10.0.tgz", + "integrity": "sha512-84KD1L5cFyw5xnntHwL4uPwfcrkKSiwuDeypiVr92qCUFuF3ZENa2zlFVPu+pQcjTlod2LmEX3MhBmNjRMpdKg==", + "license": "Apache-2.0", + "peerDependencies": { + "@metaplex-foundation/umi": ">= 0.8.2 <= 1" + } + }, + "node_modules/@metaplex-foundation/umi": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi/-/umi-1.5.1.tgz", + "integrity": "sha512-ONRv5a0kv+23AMlR8oyFBHnjVg3o3N8pUfFcV4gzbg6OgZf87zHsPWBfED3OTJqx267v1bEn6d6DABXNFq9Z3A==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-options": "^1.5.1", + "@metaplex-foundation/umi-public-keys": "^1.5.1", + "@metaplex-foundation/umi-serializers": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-bundle-defaults": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-bundle-defaults/-/umi-bundle-defaults-1.5.1.tgz", + "integrity": "sha512-7qoXenAkQbcj468HGAeLZDyg3eEhcS9rWAnGqjnKgWOlL1czL2Qwho0FEtqOv57IHwAJSTpbHbcvABmdpTjjdw==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-downloader-http": "^1.5.1", + "@metaplex-foundation/umi-eddsa-web3js": "^1.5.1", + "@metaplex-foundation/umi-http-fetch": "^1.5.1", + "@metaplex-foundation/umi-program-repository": "^1.5.1", + "@metaplex-foundation/umi-rpc-chunk-get-accounts": "^1.5.1", + "@metaplex-foundation/umi-rpc-web3js": "^1.5.1", + "@metaplex-foundation/umi-serializer-data-view": "^1.5.1", + "@metaplex-foundation/umi-transaction-factory-web3js": "^1.5.1" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1", + "@solana/web3.js": "^1.72.0" + } + }, + "node_modules/@metaplex-foundation/umi-downloader-http": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-downloader-http/-/umi-downloader-http-1.5.1.tgz", + "integrity": "sha512-1s9gSTaDtwELyxBRE6Wmdr3xWeb4Z1uU04dj3Hg8VU+TN6/3wchh93+rIGZT5D3zzdh4+yPxdYV+4ZEr3T5glQ==", + "license": "MIT", + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-eddsa-web3js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-eddsa-web3js/-/umi-eddsa-web3js-1.5.1.tgz", + "integrity": "sha512-ZlzmXXAa1Ujk00G5TmqXM81J25+k/8sqt0zxBUlLTUSOxzlhxhlUKdErIhpHazbKq+eGck+Onm17oAwVKdKAcw==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-web3js-adapters": "^1.5.1", + "@noble/curves": "^1.0.0", + "yaml": "^2.7.0" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1", + "@solana/web3.js": "^1.72.0" + } + }, + "node_modules/@metaplex-foundation/umi-http-fetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-http-fetch/-/umi-http-fetch-1.5.1.tgz", + "integrity": "sha512-AOjZJo3Ua4a2FvgA85x5f0TkMSb+13Ao3uLIQ9FbScV42kqZnDox8KjJ7tKm1ZtYDlCYD0pSFMKPOC9NPDnHDg==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.7" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-options": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-options/-/umi-options-1.5.1.tgz", + "integrity": "sha512-ZE6uXgFA3rElFq4gJxZM2diAqZdFqL65bOnAggwdnnei5XXRzFyNF16wYSqlHnPLvG6ohRHWiXww8d2Mb83xFg==", + "license": "MIT" + }, + "node_modules/@metaplex-foundation/umi-program-repository": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-program-repository/-/umi-program-repository-1.5.1.tgz", + "integrity": "sha512-E5W0IjwFgDGuBTshISbbEh/s8deqxcOzzEjOOlYdMXnevVsfNLwBBIAY4NPJg3v5vpFlKODwUGB5BxCUVthzJg==", + "license": "MIT", + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-public-keys": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-public-keys/-/umi-public-keys-1.5.1.tgz", + "integrity": "sha512-joTnI1mRtYRfIaTo98uaYRjBPszsdyHuq0vvd6QbSX+MPvu3enkWi+UicuykEc3VXd5tcGdNMiGSx4jgXG6pkw==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-serializers-encodings": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-rpc-chunk-get-accounts": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-rpc-chunk-get-accounts/-/umi-rpc-chunk-get-accounts-1.5.1.tgz", + "integrity": "sha512-3dnGobT1Xwul7fXzQr8660UHSnFOCWEed4T449oNekrVsHp2o00fdOqjXwo11DYhS1rjm+gbzRSazRKb62uF2Q==", + "license": "MIT", + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-rpc-web3js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-rpc-web3js/-/umi-rpc-web3js-1.5.1.tgz", + "integrity": "sha512-CxHyruh2gW2b/ZOwHFFtooOgtu9hBrOJTd3HUMtD/jpaturApa3itsL/zNt4K34tELzVIUL7N78LDjNpzbu9Kw==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-web3js-adapters": "^1.5.1" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1", + "@solana/web3.js": "^1.72.0" + } + }, + "node_modules/@metaplex-foundation/umi-serializer-data-view": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializer-data-view/-/umi-serializer-data-view-1.5.1.tgz", + "integrity": "sha512-9Wxqk3bGVJ0xNmHhHrOUhdu/90Q1IT3FZRZN4eGckb0sf7Bgls7kBTkFfgXFmUh2VBnE0GnnncXeHKtop5RSFA==", + "license": "MIT", + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-serializers": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers/-/umi-serializers-1.5.1.tgz", + "integrity": "sha512-scXciBylbJ4iwfxOF1Xx2XiBzoYUD8fSKWTsMal5Rj1hMRDe6b2XZcsBOjio61iAr8aTtFPmKpqxeBdLwmQ0ZQ==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-options": "^1.5.1", + "@metaplex-foundation/umi-public-keys": "^1.5.1", + "@metaplex-foundation/umi-serializers-core": "^1.5.1", + "@metaplex-foundation/umi-serializers-encodings": "^1.5.1", + "@metaplex-foundation/umi-serializers-numbers": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-serializers-core": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-core/-/umi-serializers-core-1.5.1.tgz", + "integrity": "sha512-6nYsbTCLq421x7JT1B3/iNgPpSARj/wL9naoKbOreHrk2ip/4R7vQstVRMl0Gx+Hv2tHnEIbFo3JBtWyC377Qw==", + "license": "MIT" + }, + "node_modules/@metaplex-foundation/umi-serializers-encodings": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-encodings/-/umi-serializers-encodings-1.5.1.tgz", + "integrity": "sha512-cVvwWmREE/Pmvjvsd50F18P53HDT0vzZECD6uYWIVzxgwpOiRDFu6r/vGbweomHoWzfTvuU6hiKuKv2KsOoXQA==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-serializers-core": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-serializers-numbers": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-numbers/-/umi-serializers-numbers-1.5.1.tgz", + "integrity": "sha512-7DVF1VJIdT44Pe6qWKaqGu4YVgE10OeLMYpm7C16SujSBgQGB/I2bh8NBifyH2R3oHhoyfE9qgIKB3dgRazN6A==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-serializers-core": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-transaction-factory-web3js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-transaction-factory-web3js/-/umi-transaction-factory-web3js-1.5.1.tgz", + "integrity": "sha512-g4NfvtnmXtH1Q/Y9LdCsFtDRHQZmZWW7uKz+N9a+IVsJTTvpWFALMHm66dFDQGa0ExAYxAj7j6uZH2qDn0zarA==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-web3js-adapters": "^1.5.1" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1", + "@solana/web3.js": "^1.72.0" + } + }, + "node_modules/@metaplex-foundation/umi-web3js-adapters": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-web3js-adapters/-/umi-web3js-adapters-1.5.1.tgz", + "integrity": "sha512-6W3JElD0B0EbgHofVKqk4PbP/JDrUHIKWciM7tEuXTDXbuXbSECDe7qlTU0JZXmVZNfYufI6FHnkCfPys2ZnIQ==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1", + "@solana/web3.js": "^1.72.0" + } + }, "node_modules/@microsoft/tsdoc": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", @@ -7938,9 +8148,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7957,9 +8164,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7976,9 +8180,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7995,9 +8196,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8241,9 +8439,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8264,9 +8459,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8287,9 +8479,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8310,9 +8499,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8333,9 +8519,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8356,9 +8539,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10840,9 +11020,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10857,9 +11034,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10874,9 +11048,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10891,9 +11062,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10908,9 +11076,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10925,9 +11090,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10942,9 +11104,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10959,9 +11118,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10976,9 +11132,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10993,9 +11146,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11035,6 +11185,40 @@ "node": ">=14.0.0" } }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz",