Skip to content

Commit

Permalink
chore: fusdc chain policy limits (#10814)
Browse files Browse the repository at this point in the history
closes: #10812

## Description
1. Adds `rateLimits` to ChainPolicy:
```js
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;
};
```
2. Renames `attenuatedCttpBridgeAddress` to `attenuatedCttpBridgeAddresses` to accept a list of attenuated bridge contracts.
3. Adds a `fast-usdc-tool.ts` script to `multichain-testing` to bootstrap `fast-usdc` and set up an environment for testing with the OCW.

### Security Considerations
This is an additional line of defense for FUSDC to limit the total value flowing through the system over a window of blocks.

### Scaling Considerations
None, handled in off-chain oracle operator software.

### Documentation Considerations
None

### Testing Considerations
Updates existing tests

### Upgrade Considerations
None, targeting initial release of FUSDC.
  • Loading branch information
mergify[bot] authored Jan 11, 2025
2 parents 537ffd0 + 1b1b52d commit b58d523
Show file tree
Hide file tree
Showing 17 changed files with 515 additions and 65 deletions.
2 changes: 1 addition & 1 deletion multichain-testing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
277 changes: 277 additions & 0 deletions multichain-testing/scripts/fast-usdc-tool.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof commonSetup>>['vstorageClient'];
const agoricNamesQ = (vsc: VStorageClient) =>
harden({
brands: <K extends AssetKind>(_assetKind: K) =>
vsc
.queryData('published.agoricNames.brand')
.then(pairs => Object.fromEntries(pairs) as Record<string, Brand<K>>),
});

const fastLPQ = (vsc: VStorageClient) =>
harden({
metrics: () =>
vsc.queryData(`published.fastUsdc.poolMetrics`) as Promise<PoolMetrics>,
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);
});
17 changes: 6 additions & 11 deletions multichain-testing/test/fast-usdc/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IBCChannelID } from '@agoric/vats';
import type { FeedPolicy } from '@agoric/fast-usdc/src/types.js';

export const oracleMnemonics = {
oracle1:
Expand All @@ -10,19 +11,13 @@ export const oracleMnemonics = {
};
harden(oracleMnemonics);

export const makeFeedPolicy = (nobleAgoricChannelId: IBCChannelID) => {
export const makeFeedPolicyPartial = (
nobleAgoricChannelId: IBCChannelID,
): Omit<FeedPolicy, 'chainPolicies'> => {
// 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);
8 changes: 5 additions & 3 deletions multichain-testing/test/fast-usdc/fast-usdc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
});

Expand Down
2 changes: 1 addition & 1 deletion multichain-testing/test/fast-usdc/noble-forwarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const test = anyTest as TestFn<SetupContext>;

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;
Expand Down
7 changes: 5 additions & 2 deletions multichain-testing/test/support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit b58d523

Please sign in to comment.