diff --git a/multichain-testing/package.json b/multichain-testing/package.json index f73af9bbeb5..264df591262 100644 --- a/multichain-testing/package.json +++ b/multichain-testing/package.json @@ -8,7 +8,7 @@ "lint-fix": "yarn lint:eslint --fix", "test": "echo 'Run specific test suites:\nyarn test:main (needs `make start`)\nyarn test:fast-usdc (needs `make start FILE=config.fusdc.yaml`)'", "test:main": "ava --config ava.main.config.js", - "test:fast-usdc": "FILE=config.fusdc.yaml ava --config ava.fusdc.config.js", + "test:fast-usdc": "ava --config ava.fusdc.config.js", "starship:setup": "make setup-deps setup-kind", "starship:install": "make install", "starship:port-forward": "make port-forward", diff --git a/multichain-testing/scripts/fast-usdc-tool.ts b/multichain-testing/scripts/fast-usdc-tool.ts new file mode 100755 index 00000000000..09cfc72f6a5 --- /dev/null +++ b/multichain-testing/scripts/fast-usdc-tool.ts @@ -0,0 +1,277 @@ +#!/usr/bin/env -S node --import ts-blank-space/register +/** + * @file tools for local integration testing for FastUSDC. See USAGE. + */ +import '@endo/init'; +import { parseArgs } from 'node:util'; +import type { ExecutionContext } from 'ava'; +import { encodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js'; +import { AmountMath, type Brand } from '@agoric/ertp'; +import type { USDCProposalShapes } from '@agoric/fast-usdc/src/pool-share-math.js'; +import type { PoolMetrics } from '@agoric/fast-usdc/src/types.js'; +import { divideBy } from '@agoric/zoe/src/contractSupport/ratio.js'; +import { makeDenomTools } from '../tools/asset-info.js'; +import { makeDoOffer } from '../tools/e2e-tools.js'; +import { commonSetup } from '../test/support.js'; +import { + makeFeedPolicyPartial, + oracleMnemonics, +} from '../test/fast-usdc/config.js'; + +const USAGE = ` +Usage: + No arguments - start the contract and fund the liquidity pool + start - only start the contract + fund-pool - fund the FUSDC Liquidity Pool + provision-wallet - provision a smart wallet (requires --mnemonic) + fund-faucet - fund the faucet account with bridged USDC + register-forwarding - register forwarding account on noble (requires --eud) + --oracle - Comma-separated list of oracle addresses + + +Examples: + ./fast-usdc-tool.ts + ./fast-usdc-tool.ts --oracle oracle1:addr1,oracle2:addr2 + ./fast-usdc-tool.ts start + ./fast-usdc-tool.ts start --oracle oracle1:addr1,oracle2:addr2 + ./fast-usdc-tool.ts fund-pool + ./fast-usdc-tool.ts fund-faucet + ./fast-usdc-tool.ts register-forwarding --eud osmo123 +`; + +const contractName = 'fastUsdc'; +const contractBuilder = + '../packages/builders/scripts/fast-usdc/init-fast-usdc.js'; + +/** ava test context partial, to appease dependencies expecting this */ +const runT = { + log: console.log, + is: (actual: unknown, expected: unknown, message: string) => { + if (actual !== expected) { + throw new Error( + `Condition: ${message} failed. Expected ${expected} got ${actual}.`, + ); + } + }, +} as ExecutionContext; + +// from ../test/fast-usdc/fast-usdc.test.ts +type VStorageClient = Awaited>['vstorageClient']; +const agoricNamesQ = (vsc: VStorageClient) => + harden({ + brands: (_assetKind: K) => + vsc + .queryData('published.agoricNames.brand') + .then(pairs => Object.fromEntries(pairs) as Record>), + }); + +const fastLPQ = (vsc: VStorageClient) => + harden({ + metrics: () => + vsc.queryData(`published.fastUsdc.poolMetrics`) as Promise, + info: () => + vsc.queryData(`published.${contractName}`) as Promise<{ + poolAccount: string; + settlementAccount: string; + }>, + }); + +const parseCommandLine = () => { + const { values, positionals } = parseArgs({ + options: { + eud: { + type: 'string', + }, + oracle: { + type: 'string', + }, + mnemonic: { + type: 'string', + }, + help: { + type: 'boolean', + }, + }, + allowPositionals: true, + }); + + if (values.help) { + console.log(USAGE); + return undefined; + } + + const command = positionals[0]; + const mnemonic = values.mnemonic; + const suppliedOracles = values.oracle?.split(','); + const oracles = suppliedOracles || [ + 'oracle1:agoric1yupasge4528pgkszg9v328x4faxtkldsnygwjl', + 'oracle2:agoric1dh04lnl7epr7l4cpvqqprxvam7ewdswj7yv6ep', + 'oracle3:agoric1ujmk0492mauq2f2vrcn7ylq3w3x55k0ap9mt2p', + ]; + const eud = values.eud; + + return { command, eud, mnemonic, oracles, provisionOracles: true }; +}; + +const main = async () => { + const job = parseCommandLine(); + if (!job) return undefined; + const { command, eud, mnemonic, oracles, provisionOracles } = job; + const { + chainInfo, + commonBuilderOpts, + deleteTestKeys, + faucetTools, + nobleTools, + provisionSmartWallet, + setupTestKeys, + startContract, + vstorageClient, + } = await commonSetup(runT, { config: '../config.fusdc.yaml' }); + + const assertProvisioned = async address => { + try { + await vstorageClient.queryData(`published.wallet.${address}.current`); + } catch { + throw new Error(`${address} is not provisioned`); + } + }; + + const provisionWallet = async (mnemonic: string) => { + // provision-one must be called by the owner, so we need to add the key to the test keyring + const keyname = 'temp'; + const address = (await setupTestKeys([keyname], [mnemonic]))[keyname]; + try { + await provisionSmartWallet(address, { + BLD: 100n, + IST: 100n, + }); + } finally { + await deleteTestKeys([keyname]); + } + }; + + const start = async () => { + const { getTransferChannelId, toDenomHash } = makeDenomTools(chainInfo); + const usdcDenom = toDenomHash('uusdc', 'noblelocal', 'agoric'); + const nobleAgoricChannelId = getTransferChannelId('agoriclocal', 'noble'); + if (!nobleAgoricChannelId) + throw new Error('nobleAgoricChannelId not found'); + console.log('nobleAgoricChannelId', nobleAgoricChannelId); + console.log('usdcDenom', usdcDenom); + + for (const oracle of oracles) { + if (provisionOracles) { + await provisionWallet(oracleMnemonics[oracle.split(':')[0]]); + } else { + console.log(`Confirming ${oracle} smart wallet provisioned...`); + // oracles must be provisioned before the contract starts + await assertProvisioned(oracle.split(':')[1]); + } + } + + await startContract(contractName, contractBuilder, { + oracle: oracles, + usdcDenom, + feedPolicy: JSON.stringify(makeFeedPolicyPartial(nobleAgoricChannelId)), + ...commonBuilderOpts, + }); + }; + + const fundFaucet = async () => faucetTools.fundFaucet([['noble', 'uusdc']]); + + const fundLiquidityPool = async () => { + await fundFaucet(); + const accounts = ['lp']; + await deleteTestKeys(accounts).catch(); + const wallets = await setupTestKeys(accounts); + const lpUser = await provisionSmartWallet(wallets['lp'], { + USDC: 8_000n, + BLD: 100n, + }); + const lpDoOffer = makeDoOffer(lpUser); + const { USDC } = await agoricNamesQ(vstorageClient).brands('nat'); + const { shareWorth } = await fastLPQ(vstorageClient).metrics(); + + const LP_DEPOSIT_AMOUNT = 8_000n * 10n ** 6n; + const give = { USDC: AmountMath.make(USDC as Brand, LP_DEPOSIT_AMOUNT) }; + const want = { PoolShare: divideBy(give.USDC, shareWorth) }; + const proposal: USDCProposalShapes['deposit'] = harden({ give, want }); + + await lpDoOffer({ + id: `lp-deposit-${Date.now()}`, + invitationSpec: { + source: 'agoricContract', + instancePath: [contractName], + callPipe: [['makeDepositInvitation']], + }, + // @ts-expect-error 'NatAmount' vs 'AnyAmount' + proposal, + }); + }; + + const registerForwardingAccount = async (EUD: string) => { + console.log('eud', EUD); + const { settlementAccount } = await vstorageClient.queryData( + `published.${contractName}`, + ); + console.log('settlementAccount:', settlementAccount); + + const recipientAddress = encodeAddressHook(settlementAccount, { + EUD, + }); + console.log('recipientAddress:', recipientAddress); + + const { getTransferChannelId } = makeDenomTools(chainInfo); + const nobleAgoricChannelId = getTransferChannelId('agoriclocal', 'noble'); + if (!nobleAgoricChannelId) + throw new Error('nobleAgoricChannelId not found'); + + const txRes = nobleTools.registerForwardingAcct( + nobleAgoricChannelId, + recipientAddress, + ); + runT.is(txRes?.code, 0, 'registered forwarding account'); + + const { address } = nobleTools.queryForwardingAddress( + nobleAgoricChannelId, + recipientAddress, + ); + console.log('forwardingAddress:', address); + return address; + }; + + // Execute commands based on input + switch (command) { + case 'start': + await start(); + break; + case 'fund-pool': + await fundLiquidityPool(); + break; + case 'fund-faucet': + await fundFaucet(); + break; + case 'provision-wallet': + if (!mnemonic) { + throw new Error('--mnemonic is required for provision-wallet command'); + } + await provisionWallet(mnemonic); + break; + case 'register-forwarding': + if (!eud) { + throw new Error('--eud is required for register-forwarding command'); + } + await registerForwardingAccount(eud); + break; + default: + // No command provided - run both start and fundLiquidityPool + await start(); + await fundLiquidityPool(); + } +}; + +main().catch(error => { + console.error('An error occurred:', error); + process.exit(1); +}); diff --git a/multichain-testing/test/fast-usdc/config.ts b/multichain-testing/test/fast-usdc/config.ts index c7b1833ec6d..bebd15c642f 100644 --- a/multichain-testing/test/fast-usdc/config.ts +++ b/multichain-testing/test/fast-usdc/config.ts @@ -1,4 +1,5 @@ import type { IBCChannelID } from '@agoric/vats'; +import type { FeedPolicy } from '@agoric/fast-usdc/src/types.js'; export const oracleMnemonics = { oracle1: @@ -10,19 +11,13 @@ export const oracleMnemonics = { }; harden(oracleMnemonics); -export const makeFeedPolicy = (nobleAgoricChannelId: IBCChannelID) => { +export const makeFeedPolicyPartial = ( + nobleAgoricChannelId: IBCChannelID, +): Omit => { + // XXX consider using toExternalConfig to marshal bigints and send ChainPolicies return { nobleAgoricChannelId, nobleDomainId: 4, - chainPolicies: { - Arbitrum: { - attenuatedCttpBridgeAddress: - '0xe298b93ffB5eA1FB628e0C0D55A43aeaC268e347', - cctpTokenMessengerAddress: '0x19330d10D9Cc8751218eaf51E8885D058642E08A', - chainId: 42161, - confirmations: 2, - }, - }, }; }; -harden(makeFeedPolicy); +harden(makeFeedPolicyPartial); diff --git a/multichain-testing/test/fast-usdc/fast-usdc.test.ts b/multichain-testing/test/fast-usdc/fast-usdc.test.ts index 1b0958a3441..587989ff82b 100644 --- a/multichain-testing/test/fast-usdc/fast-usdc.test.ts +++ b/multichain-testing/test/fast-usdc/fast-usdc.test.ts @@ -11,7 +11,7 @@ import { makeDenomTools } from '../../tools/asset-info.js'; import { createWallet } from '../../tools/wallet.js'; import { makeQueryClient } from '../../tools/query.js'; import { commonSetup, type SetupContextWithWallets } from '../support.js'; -import { makeFeedPolicy, oracleMnemonics } from './config.js'; +import { makeFeedPolicyPartial, oracleMnemonics } from './config.js'; import { makeRandomDigits } from '../../tools/random.js'; import { makeTracer } from '@agoric/internal'; import type { @@ -48,7 +48,9 @@ const contractBuilder = const LP_DEPOSIT_AMOUNT = 8_000n * 10n ** 6n; test.before(async t => { - const { setupTestKeys, ...common } = await commonSetup(t); + const { setupTestKeys, ...common } = await commonSetup(t, { + config: '../config.fusdc.yaml', + }); const { chainInfo, commonBuilderOpts, @@ -81,7 +83,7 @@ test.before(async t => { await startContract(contractName, contractBuilder, { oracle: keys(oracleMnemonics).map(n => `${n}:${wallets[n]}`), usdcDenom, - feedPolicy: JSON.stringify(makeFeedPolicy(nobleAgoricChannelId)), + feedPolicy: JSON.stringify(makeFeedPolicyPartial(nobleAgoricChannelId)), ...commonBuilderOpts, }); diff --git a/multichain-testing/test/fast-usdc/noble-forwarding.test.ts b/multichain-testing/test/fast-usdc/noble-forwarding.test.ts index 84ce4f3ae95..3f797deb8e4 100644 --- a/multichain-testing/test/fast-usdc/noble-forwarding.test.ts +++ b/multichain-testing/test/fast-usdc/noble-forwarding.test.ts @@ -9,7 +9,7 @@ const test = anyTest as TestFn; test('noble forwarding', async t => { const { nobleTools, retryUntilCondition, useChain, vstorageClient } = - await commonSetup(t); + await commonSetup(t, { config: '../config.fusdc.yaml' }); const agoricWallet = await createWallet('agoric'); const agoricAddr = (await agoricWallet.getAccounts())[0].address; diff --git a/multichain-testing/test/support.ts b/multichain-testing/test/support.ts index f71e2c112cc..0f3dd42c14f 100644 --- a/multichain-testing/test/support.ts +++ b/multichain-testing/test/support.ts @@ -68,11 +68,14 @@ const makeKeyring = async ( return { setupTestKeys, deleteTestKeys }; }; -export const commonSetup = async (t: ExecutionContext) => { +export const commonSetup = async ( + t: ExecutionContext, + { config = '../config.yaml' } = {}, +) => { let useChain: MultichainRegistry['useChain']; try { const registry = await setupRegistry({ - config: `../${process.env.FILE || 'config.yaml'}`, + config, }); useChain = registry.useChain; } catch (e) { diff --git a/multichain-testing/tools/noble-tools.ts b/multichain-testing/tools/noble-tools.ts index cd72a332857..2a1d63fc39d 100644 --- a/multichain-testing/tools/noble-tools.ts +++ b/multichain-testing/tools/noble-tools.ts @@ -33,17 +33,10 @@ export const makeNobleTools = ( opts = { encoding: 'utf-8' as const, stdio: ['ignore', 'pipe', 'ignore'] }, ) => execFileSync(kubectlBinary, [...makeKubeArgs(), ...args], opts); - const checkEnv = () => { - if (process.env.FILE !== 'config.fusdc.yaml') { - console.error('Warning: Noble chain must be running for this to work'); - } - }; - const registerForwardingAcct = ( channelId: IBCChannelID, address: ChainAddress['value'], ): { txhash: string; code: number; data: string; height: string } => { - checkEnv(); log('creating forwarding address', address, channelId); return JSON.parse( exec([ @@ -61,7 +54,6 @@ export const makeNobleTools = ( }; const mockCctpMint = (amount: bigint, destination: ChainAddress['value']) => { - checkEnv(); const denomAmount = `${Number(amount)}uusdc`; log('mock cctp mint', destination, denomAmount); return JSON.parse( @@ -84,7 +76,6 @@ export const makeNobleTools = ( channelId: IBCChannelID, address: ChainAddress['value'], ): { address: NobleAddress; exists: boolean } => { - checkEnv(); log('querying forwarding address', address, channelId); return JSON.parse( exec([ diff --git a/packages/boot/test/fast-usdc/fast-usdc.test.ts b/packages/boot/test/fast-usdc/fast-usdc.test.ts index f0692aba0fd..015dded9ea5 100644 --- a/packages/boot/test/fast-usdc/fast-usdc.test.ts +++ b/packages/boot/test/fast-usdc/fast-usdc.test.ts @@ -158,7 +158,7 @@ test.serial('writes feed policy to vstorage', async t => { const opts = { node: 'fastUsdc.feedPolicy', owner: 'the general and chain-specific policies for the Fast USDC feed', - showValue: JSON.parse, + showValue: defaultSerializer.parse, }; await documentStorageSchema(t, storage, opts); }); diff --git a/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.md b/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.md index f12f507d7ea..68ea14d4de8 100644 --- a/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.md +++ b/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.md @@ -17,34 +17,69 @@ Generated by [AVA](https://avajs.dev). { chainPolicies: { Arbitrum: { - attenuatedCttpBridgeAddress: '0xe298b93ffB5eA1FB628e0C0D55A43aeaC268e347', + attenuatedCttpBridgeAddresses: [ + '0xe298b93ffB5eA1FB628e0C0D55A43aeaC268e347', + ], cctpTokenMessengerAddress: '0x19330d10D9Cc8751218eaf51E8885D058642E08A', chainId: 42161, confirmations: 2, + rateLimits: { + blockWindow: 20000000000n, + blockWindowSize: 10, + tx: 10000000000n, + }, }, Base: { - attenuatedCttpBridgeAddress: '0xB6615B2662b35fc3533F8479002e62D0523341De', + attenuatedCttpBridgeAddresses: [ + '0xB6615B2662b35fc3533F8479002e62D0523341De', + ], cctpTokenMessengerAddress: '0x1682Ae6375C4E4A97e4B583BC394c861A46D8962', chainId: 8453, confirmations: 2, + rateLimits: { + blockWindow: 20000000000n, + blockWindowSize: 10, + tx: 10000000000n, + }, }, Ethereum: { - attenuatedCttpBridgeAddress: '0xBC8552339dA68EB65C8b88B414B5854E0E366cFc', + attenuatedCttpBridgeAddresses: [ + '0xBC8552339dA68EB65C8b88B414B5854E0E366cFc', + ], cctpTokenMessengerAddress: '0xBd3fa81B58Ba92a82136038B25aDec7066af3155', chainId: 1, confirmations: 2, + rateLimits: { + blockWindow: 20000000000n, + blockWindowSize: 10, + tx: 10000000000n, + }, }, Optimism: { - attenuatedCttpBridgeAddress: '0x48C5417ED570928eC85D5e3AD4e7E0EeD7dB1E2A', + attenuatedCttpBridgeAddresses: [ + '0x48C5417ED570928eC85D5e3AD4e7E0EeD7dB1E2A', + ], cctpTokenMessengerAddress: '0x2B4069517957735bE00ceE0fadAE88a26365528f', chainId: 10, confirmations: 2, + rateLimits: { + blockWindow: 20000000000n, + blockWindowSize: 10, + tx: 10000000000n, + }, }, Polygon: { - attenuatedCttpBridgeAddress: '0x32cb9574650AFF312c80edc4B4343Ff5500767cA', + attenuatedCttpBridgeAddresses: [ + '0x32cb9574650AFF312c80edc4B4343Ff5500767cA', + ], cctpTokenMessengerAddress: '0x9daF8c91AEFAE50b9c0E69629D3F6Ca40cA3B3FE', chainId: 137, confirmations: 2, + rateLimits: { + blockWindow: 20000000000n, + blockWindowSize: 10, + tx: 10000000000n, + }, }, }, eventFilter: 'DepositForBurn(uint64,address,uint256,address,bytes32,uint32,bytes32,bytes32)', diff --git a/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.snap b/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.snap index e0cfacdb33d..852bea26923 100644 Binary files a/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.snap and b/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.snap differ diff --git a/packages/builders/scripts/fast-usdc/init-fast-usdc.js b/packages/builders/scripts/fast-usdc/init-fast-usdc.js index fade01cea83..44187137f4b 100644 --- a/packages/builders/scripts/fast-usdc/init-fast-usdc.js +++ b/packages/builders/scripts/fast-usdc/init-fast-usdc.js @@ -17,7 +17,7 @@ import { parseArgs } from 'node:util'; /** * @import {CoreEvalBuilder, DeployScriptFunction} from '@agoric/deploy-script-support/src/externalTypes.js' * @import {ParseArgsConfig} from 'node:util' - * @import {FastUSDCConfig} from '@agoric/fast-usdc/src/types.js' + * @import {FastUSDCConfig, FeedPolicy} from '@agoric/fast-usdc/src/types.js' */ const { keys } = Object; @@ -114,6 +114,7 @@ export default async (homeP, endowments) => { }, } = parseArgs({ args: scriptArgs, options }); + /** @returns {FeedPolicy} */ const parseFeedPolicy = () => { if (net) { if (!(net in configurations)) { @@ -122,7 +123,17 @@ export default async (homeP, endowments) => { return configurations[net].feedPolicy; } if (!feedPolicy) throw Error(feedPolicyUsage); - return JSON.parse(feedPolicy); + const parsed = JSON.parse(feedPolicy); + if (!parsed.chainPolicies) { + return { + ...configurations.MAINNET.feedPolicy, + ...parsed, + }; + } else { + // consider having callers use `toExternalConfig` to pass in bigints and + // use `fromExternalConfig` here to parse + throw Error('TODO: support unmarshalling feedPolicy'); + } }; const parseOracleArgs = () => { diff --git a/packages/fast-usdc/src/fast-usdc-policy.core.js b/packages/fast-usdc/src/fast-usdc-policy.core.js index 49e1087ff1b..431c0c61524 100644 --- a/packages/fast-usdc/src/fast-usdc-policy.core.js +++ b/packages/fast-usdc/src/fast-usdc-policy.core.js @@ -1,6 +1,8 @@ /** @file core-eval to publish update to Fast USDC feedPolicy */ import { E } from '@endo/far'; +import { makeMarshal } from '@endo/marshal'; +import { Fail } from '@endo/errors'; import { fromExternalConfig } from './utils/config-marshal.js'; import { FeedPolicyShape } from './type-guards.js'; @@ -14,6 +16,7 @@ import { FeedPolicyShape } from './type-guards.js'; const contractName = 'fastUsdc'; const FEED_POLICY = 'feedPolicy'; +const marshalData = makeMarshal(_val => Fail`data only`); /** * XXX copied from fast-usdc.start.js @@ -23,7 +26,8 @@ const FEED_POLICY = 'feedPolicy'; */ const publishFeedPolicy = async (node, policy) => { const feedPolicy = E(node).makeChildNode(FEED_POLICY); - await E(feedPolicy).setValue(JSON.stringify(policy)); + const value = marshalData.toCapData(policy); + await E(feedPolicy).setValue(JSON.stringify(value)); }; /** diff --git a/packages/fast-usdc/src/fast-usdc.start.js b/packages/fast-usdc/src/fast-usdc.start.js index c8465635335..41042587aea 100644 --- a/packages/fast-usdc/src/fast-usdc.start.js +++ b/packages/fast-usdc/src/fast-usdc.start.js @@ -22,6 +22,7 @@ import { fromExternalConfig } from './utils/config-marshal.js'; * @import {Board} from '@agoric/vats' * @import {ManifestBundleRef} from '@agoric/deploy-script-support/src/externalTypes.js' * @import {BootstrapManifest} from '@agoric/vats/src/core/lib-boot.js' + * @import {Passable} from '@endo/pass-style' * @import {LegibleCapData} from './utils/config-marshal.js' * @import {FastUsdcSF} from './fast-usdc.contract.js' * @import {FeedPolicy, FastUSDCConfig} from './types.js' @@ -93,7 +94,8 @@ const POOL_METRICS = 'poolMetrics'; */ const publishFeedPolicy = async (node, policy) => { const feedPolicy = E(node).makeChildNode(FEED_POLICY); - await E(feedPolicy).setValue(JSON.stringify(policy)); + const value = marshalData.toCapData(policy); + await E(feedPolicy).setValue(JSON.stringify(value)); }; /** diff --git a/packages/fast-usdc/src/type-guards.js b/packages/fast-usdc/src/type-guards.js index 4f148fcaadd..5ece98c4ac9 100644 --- a/packages/fast-usdc/src/type-guards.js +++ b/packages/fast-usdc/src/type-guards.js @@ -122,10 +122,19 @@ harden(PoolMetricsShape); /** @type {TypedPattern} */ export const ChainPolicyShape = { - attenuatedCttpBridgeAddress: EvmHashShape, + attenuatedCttpBridgeAddresses: M.splitArray( + [EvmHashShape], + undefined, + M.arrayOf(EvmHashShape), + ), cctpTokenMessengerAddress: EvmHashShape, confirmations: M.number(), chainId: M.number(), + rateLimits: { + tx: M.bigint(), + blockWindow: M.bigint(), + blockWindowSize: M.number(), + }, }; harden(ChainPolicyShape); diff --git a/packages/fast-usdc/src/types.ts b/packages/fast-usdc/src/types.ts index 433ea135d97..bc9bf5cd4ca 100644 --- a/packages/fast-usdc/src/types.ts +++ b/packages/fast-usdc/src/types.ts @@ -82,27 +82,35 @@ export interface PoolMetrics extends PoolStats { export interface ChainPolicy { /** `msg.sender` of DepositAndBurn to TokenMessenger must be an attenuated wrapper contract that does not contain `replaceDepositForBurn` */ - attenuatedCttpBridgeAddress: EvmHash; + attenuatedCttpBridgeAddresses: EvmHash[]; /** @see {@link https://developers.circle.com/stablecoins/evm-smart-contracts} */ cctpTokenMessengerAddress: EvmHash; /** e.g., `1` for ETH mainnet 42161 for Arbitrum One. @see {@link https://chainlist.org/} */ chainId: EvmChainID; /** the number of block confirmations to observe before reporting */ confirmations: number; + rateLimits: { + /** do not advance more than this amount for an individual transaction */ + tx: bigint; + /** do not advance more than this amount per block window */ + blockWindow: bigint; + /** the number of blocks to consider for `blockWindow` */ + blockWindowSize: number; + }; } -export interface FeedPolicy { +export type FeedPolicy = { nobleDomainId: number; nobleAgoricChannelId: string; chainPolicies: Record; eventFilter?: string; -} +} & CopyRecord; export type FastUSDCConfig = { terms: FastUsdcTerms; oracles: Record; feeConfig: FeeConfig; - feedPolicy: FeedPolicy & Passable; + feedPolicy: FeedPolicy; noNoble: boolean; // support a3p-integration, which has no noble chain chainInfo: Record; assetInfo: [Denom, DenomDetail & { brandKey?: string }][]; diff --git a/packages/fast-usdc/src/utils/chain-policies.js b/packages/fast-usdc/src/utils/chain-policies.js index 33ede2d446f..cc52dc045f4 100644 --- a/packages/fast-usdc/src/utils/chain-policies.js +++ b/packages/fast-usdc/src/utils/chain-policies.js @@ -4,71 +4,136 @@ export const ChainPolicies = /** @type {const} */ ({ MAINNET: { Arbitrum: { - attenuatedCttpBridgeAddress: '0xe298b93ffB5eA1FB628e0C0D55A43aeaC268e347', + attenuatedCttpBridgeAddresses: [ + '0xe298b93ffB5eA1FB628e0C0D55A43aeaC268e347', + ], cctpTokenMessengerAddress: '0x19330d10D9Cc8751218eaf51E8885D058642E08A', chainId: 42161, - confirmations: 2, // TODO placeholder + // TODO confirm confirmations and rateLimits + confirmations: 2, + rateLimits: { + blockWindow: 20_000_000_000n, + blockWindowSize: 10, + tx: 10_000_000_000n, + }, }, Base: { - attenuatedCttpBridgeAddress: '0xB6615B2662b35fc3533F8479002e62D0523341De', + attenuatedCttpBridgeAddresses: [ + '0xB6615B2662b35fc3533F8479002e62D0523341De', + ], cctpTokenMessengerAddress: '0x1682Ae6375C4E4A97e4B583BC394c861A46D8962', chainId: 8453, - confirmations: 2, // TODO placeholder + // TODO confirm confirmations and rateLimits + confirmations: 2, + rateLimits: { + blockWindow: 20_000_000_000n, + blockWindowSize: 10, + tx: 10_000_000_000n, + }, }, Ethereum: { - attenuatedCttpBridgeAddress: '0xBC8552339dA68EB65C8b88B414B5854E0E366cFc', + attenuatedCttpBridgeAddresses: [ + '0xBC8552339dA68EB65C8b88B414B5854E0E366cFc', + ], cctpTokenMessengerAddress: '0xBd3fa81B58Ba92a82136038B25aDec7066af3155', chainId: 1, - confirmations: 2, // TODO placeholder + // TODO confirm confirmations and rateLimits + confirmations: 2, + rateLimits: { + blockWindow: 20_000_000_000n, + blockWindowSize: 10, + tx: 10_000_000_000n, + }, }, Optimism: { - attenuatedCttpBridgeAddress: '0x48C5417ED570928eC85D5e3AD4e7E0EeD7dB1E2A', + attenuatedCttpBridgeAddresses: [ + '0x48C5417ED570928eC85D5e3AD4e7E0EeD7dB1E2A', + ], cctpTokenMessengerAddress: '0x2B4069517957735bE00ceE0fadAE88a26365528f', chainId: 10, - confirmations: 2, // TODO placeholder + // TODO confirm confirmations and rateLimits + confirmations: 2, + rateLimits: { + blockWindow: 20_000_000_000n, + blockWindowSize: 10, + tx: 10_000_000_000n, + }, }, Polygon: { - attenuatedCttpBridgeAddress: '0x32cb9574650AFF312c80edc4B4343Ff5500767cA', + attenuatedCttpBridgeAddresses: [ + '0x32cb9574650AFF312c80edc4B4343Ff5500767cA', + ], cctpTokenMessengerAddress: '0x9daF8c91AEFAE50b9c0E69629D3F6Ca40cA3B3FE', chainId: 137, - confirmations: 2, // TODO placeholder + // TODO confirm confirmations and rateLimits + confirmations: 2, + rateLimits: { + blockWindow: 20_000_000_000n, + blockWindowSize: 10, + tx: 10_000_000_000n, + }, }, }, TESTNET: { // Arbitrum Sepolia Arbitrum: { - attenuatedCttpBridgeAddress: '0xTODO', + attenuatedCttpBridgeAddresses: ['0xTODO'], cctpTokenMessengerAddress: '0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5', chainId: 421614, - confirmations: 2, // TODO placeholder + confirmations: 2, + rateLimits: { + blockWindow: 20_000_000_000n, + blockWindowSize: 10, + tx: 10_000_000_000n, + }, }, // Base Sepolia Base: { - attenuatedCttpBridgeAddress: '0xTODO', + attenuatedCttpBridgeAddresses: ['0xTODO'], cctpTokenMessengerAddress: '0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5', chainId: 84532, - confirmations: 2, // TODO placeholder + confirmations: 2, + rateLimits: { + blockWindow: 20_000_000_000n, + blockWindowSize: 10, + tx: 10_000_000_000n, + }, }, // Ethereum Sepolia Ethereum: { - attenuatedCttpBridgeAddress: '0xTODO', + attenuatedCttpBridgeAddresses: ['0xTODO'], cctpTokenMessengerAddress: '0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5', chainId: 11155111, - confirmations: 2, // TODO placeholder + confirmations: 2, + rateLimits: { + blockWindow: 20_000_000_000n, + blockWindowSize: 10, + tx: 10_000_000_000n, + }, }, // OP Sepolia Optimism: { - attenuatedCttpBridgeAddress: '0xTODO', + attenuatedCttpBridgeAddresses: ['0xTODO'], cctpTokenMessengerAddress: '0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5', chainId: 11155420, - confirmations: 2, // TODO placeholder + confirmations: 2, + rateLimits: { + blockWindow: 20_000_000_000n, + blockWindowSize: 10, + tx: 10_000_000_000n, + }, }, // Polygon PoS Amoy Polygon: { - attenuatedCttpBridgeAddress: '0xTODO', + attenuatedCttpBridgeAddresses: ['0xTODO'], cctpTokenMessengerAddress: '0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5', chainId: 80002, - confirmations: 2, // TODO placeholder + confirmations: 2, + rateLimits: { + blockWindow: 20_000_000_000n, + blockWindowSize: 10, + tx: 10_000_000_000n, + }, }, }, }); diff --git a/packages/fast-usdc/test/type-guards.test.ts b/packages/fast-usdc/test/type-guards.test.ts index cefea175705..4528d3caa7c 100644 --- a/packages/fast-usdc/test/type-guards.test.ts +++ b/packages/fast-usdc/test/type-guards.test.ts @@ -1,11 +1,16 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { mustMatch } from '@endo/patterns'; +import { M, mustMatch } from '@endo/patterns'; import { TxStatus, PendingTxStatus } from '../src/constants.js'; -import { CctpTxEvidenceShape, PendingTxShape } from '../src/type-guards.js'; -import type { CctpTxEvidence } from '../src/types.js'; +import { + CctpTxEvidenceShape, + ChainPolicyShape, + PendingTxShape, +} from '../src/type-guards.js'; +import type { CctpTxEvidence, ChainPolicy } from '../src/types.js'; import { MockCctpTxEvidences } from './fixtures.js'; +import { ChainPolicies } from '../src/utils/chain-policies.js'; test('CctpTxEvidenceShape', t => { const specimen: CctpTxEvidence = harden( @@ -37,3 +42,46 @@ test('PendingTxShape', t => { ), ); }); + +test('ChainPolicyShape', t => { + const policy: ChainPolicy = harden({ + attenuatedCttpBridgeAddresses: [ + '0xe298b93ffB5eA1FB628e0C0D55A43aeaC268e347', + ], + rateLimits: { + blockWindow: 20_000_000_000n, + blockWindowSize: 10, + tx: 10_000_000_000n, + }, + cctpTokenMessengerAddress: '0x19330d10D9Cc8751218eaf51E8885D058642E08A', + chainId: 42161, + confirmations: 2, + }); + t.notThrows(() => mustMatch(policy, ChainPolicyShape)); + + const a = policy.attenuatedCttpBridgeAddresses[0]; + const twoAddrs = harden({ + ...policy, + attenuatedCttpBridgeAddresses: [a, a], + }); + t.notThrows(() => mustMatch(twoAddrs, ChainPolicyShape)); + + const threeAddrs = harden({ + ...policy, + attenuatedCttpBridgeAddresses: [a, a, a], + }); + t.notThrows(() => mustMatch(threeAddrs, ChainPolicyShape)); + + t.notThrows(() => + mustMatch( + harden(Object.values(ChainPolicies.MAINNET)), + M.arrayOf(ChainPolicyShape), + ), + ); + + const noAddrs = harden({ + ...policy, + attenuatedCttpBridgeAddresses: [], + }); + t.throws(() => mustMatch(noAddrs, ChainPolicyShape)); +});