Skip to content

Commit

Permalink
chore: fusdc multichain support (#10626)
Browse files Browse the repository at this point in the history
refs: #10597

## Description

Primary changes:

- `@agoric/orchestration`: exposes `intermediateRecipient: ChainAddress['value']` in `ForwardOpts`, part of `IBCMsgTransferOpts`, allowing callers to provide their own value. Currently defaults to `"pfm"`
- `@agoric/fast-usdc`: creates a Noble ICA in the `StartFn` so we have an address value for `intermediateRecipient`. Noble's chain does not permit non-bech32 values.

Ancillary: 
- `@agoric/boot`'s `bridgeUtils` helpers:
   - `setAckBehavior` helper to control whether a `IBCDowncallMethod` acknowledgments are queued or inbounded immediately
   - `setBech32Prefix` helper to control address prefix for ICA accounts
   - ICA addresses are unique per account
- `@agoric/fast-usdc`: remove `blockTimestamp` from `CctpTxEvidence`


### Security Considerations
No new considerations

### Scaling Considerations
None

### Documentation Considerations
None

### Testing Considerations
Updated existing tests.

### Upgrade Considerations
N/A, library code part of FUSDC or NPM Orch release
  • Loading branch information
mergify[bot] authored Dec 6, 2024
2 parents e596a01 + 1847618 commit ef9088c
Show file tree
Hide file tree
Showing 27 changed files with 236 additions and 71 deletions.
24 changes: 11 additions & 13 deletions packages/boot/test/bootstrapTests/orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,19 +378,19 @@ test.serial('basic-flows', async t => {
[
'request-coa',
{
account: 'published.basicFlows.cosmos1test',
account: 'published.basicFlows.cosmos1test1',
},
],
],
});
t.like(wd.getLatestUpdateRecord(), {
status: { id: 'request-coa', numWantsSatisfied: 1 },
});
t.deepEqual(readPublished('basicFlows.cosmos1test'), {
t.deepEqual(readPublished('basicFlows.cosmos1test1'), {
localAddress:
'/ibc-port/icacontroller-2/ordered/{"version":"ics27-1","controllerConnectionId":"connection-8","hostConnectionId":"connection-649","address":"cosmos1test","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-2',
'/ibc-port/icacontroller-2/ordered/{"version":"ics27-1","controllerConnectionId":"connection-8","hostConnectionId":"connection-649","address":"cosmos1test1","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-2',
remoteAddress:
'/ibc-hop/connection-8/ibc-port/icahost/ordered/{"version":"ics27-1","controllerConnectionId":"connection-8","hostConnectionId":"connection-649","address":"cosmos1test","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-2',
'/ibc-hop/connection-8/ibc-port/icahost/ordered/{"version":"ics27-1","controllerConnectionId":"connection-8","hostConnectionId":"connection-649","address":"cosmos1test1","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-2',
});

// create a local orchestration account
Expand Down Expand Up @@ -599,25 +599,23 @@ test.serial('basic-flows - portfolio holder', async t => {
'request-portfolio-acct',
{
agoric: 'published.basicFlows.agoric1fakeLCAAddress1',
cosmoshub: 'published.basicFlows.cosmos1test',
// XXX support multiple chain addresses in ibc mocks
osmosis: 'published.basicFlows.cosmos1test',
cosmoshub: 'published.basicFlows.cosmos1test2',
osmosis: 'published.basicFlows.cosmos1test3',
},
],
],
});
t.like(wd.getLatestUpdateRecord(), {
status: { id: 'request-portfolio-acct', numWantsSatisfied: 1 },
});
// XXX this overrides a previous account, since mocks only provide one address
t.deepEqual(readPublished('basicFlows.cosmos1test'), {

t.deepEqual(readPublished('basicFlows.cosmos1test3'), {
localAddress:
'/ibc-port/icacontroller-4/ordered/{"version":"ics27-1","controllerConnectionId":"connection-1","hostConnectionId":"connection-1649","address":"cosmos1test","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-4',
'/ibc-port/icacontroller-4/ordered/{"version":"ics27-1","controllerConnectionId":"connection-1","hostConnectionId":"connection-1649","address":"cosmos1test3","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-4',
remoteAddress:
'/ibc-hop/connection-1/ibc-port/icahost/ordered/{"version":"ics27-1","controllerConnectionId":"connection-1","hostConnectionId":"connection-1649","address":"cosmos1test","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-4',
'/ibc-hop/connection-1/ibc-port/icahost/ordered/{"version":"ics27-1","controllerConnectionId":"connection-1","hostConnectionId":"connection-1649","address":"cosmos1test3","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-4',
});
// XXX this overrides a previous account, since mocks only provide one address
t.is(readPublished('basicFlows.agoric1fakeLCAAddress'), '');
t.is(readPublished('basicFlows.agoric1fakeLCAAddress1'), '');

const { BLD } = agoricNamesRemotes.brand;
BLD || Fail`BLD missing from agoricNames`;
Expand Down
14 changes: 13 additions & 1 deletion packages/boot/test/fast-usdc/fast-usdc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import { unmarshalFromVstorage } from '@agoric/internal/src/marshal.js';
import { makeMarshal } from '@endo/marshal';
import { defaultMarshaller } from '@agoric/internal/src/storage-test-utils.js';
import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
import { BridgeId } from '@agoric/internal';
import {
makeWalletFactoryContext,
type WalletFactoryTestContext,
} from '../bootstrapTests/walletFactory.js';
import {
makeSwingsetHarness,
insistManagerType,
AckBehavior,
} from '../../tools/supports.js';

const test: TestFn<
Expand Down Expand Up @@ -54,8 +56,9 @@ test.serial(
async t => {
const {
agoricNamesRemotes,
evalProposal,
bridgeUtils,
buildProposal,
evalProposal,
refreshAgoricNamesRemotes,
storage,
walletFactoryDriver: wd,
Expand All @@ -67,6 +70,15 @@ test.serial(
wd.provideSmartWallet('agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj'),
]);

// inbound `startChannelOpenInit` responses immediately.
// needed since the Fusdc StartFn relies on an ICA being created
bridgeUtils.setAckBehavior(
BridgeId.DIBC,
'startChannelOpenInit',
AckBehavior.Immediate,
);
bridgeUtils.setBech32Prefix('noble');

const materials = buildProposal(
'@agoric/builders/scripts/fast-usdc/init-fast-usdc.js',
['--net', 'MAINNET'],
Expand Down
25 changes: 16 additions & 9 deletions packages/boot/tools/ibc/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @ts-check
import { createMockAckMap } from '@agoric/orchestration/tools/ibc-mocks.js';
import type { IBCChannelID, IBCEvent, IBCMethod } from '@agoric/vats';

/** @import { IBCChannelID, IBCMethod, IBCEvent } from '@agoric/vats'; */

Expand Down Expand Up @@ -65,6 +66,11 @@ export const protoMsgMocks = {
msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ25zS0tTOXBZbU11WVhCd2JHbGpZWFJwYjI1ekxuUnlZVzV6Wm1WeUxuWXhMazF6WjFSeVlXNXpabVZ5RWs0S0NIUnlZVzV6Wm1WeUVndGphR0Z1Ym1Wc0xUVXpOaG9UQ2cxcFltTXZkWFZ6WkdOb1lYTm9FZ0l4TUNJTFkyOXpiVzl6TVhSbGMzUXFDbTV2WW14bE1YUmxjM1F5QURpQThKTEwzUWc9IiwibWVtbyI6IiJ9',
ack: responses.ibcTransfer,
},
// MsgTransfer 10 ibc/uusdchash from cosmos1test1 to noble1test through channel-536
ibcTransfer2: {
msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ253S0tTOXBZbU11WVhCd2JHbGpZWFJwYjI1ekxuUnlZVzV6Wm1WeUxuWXhMazF6WjFSeVlXNXpabVZ5RWs4S0NIUnlZVzV6Wm1WeUVndGphR0Z1Ym1Wc0xUVXpOaG9UQ2cxcFltTXZkWFZ6WkdOb1lYTm9FZ0l4TUNJTVkyOXpiVzl6TVhSbGMzUXhLZ3B1YjJKc1pURjBaWE4wTWdBNGdQQ1N5OTBJIiwibWVtbyI6IiJ9',
ack: responses.ibcTransfer,
},
error: {
msg: '',
ack: responses.error5,
Expand Down Expand Up @@ -101,17 +107,18 @@ export const addParamsIfJsonVersion = (version, params) => {
export const icaMocks = {
/**
* ICA Channel Creation
* @param {IBCMethod<'startChannelOpenInit'>} obj
* @returns {IBCEvent<'channelOpenAck'>}
* @param obj
* @param bech32Prefix
*/
channelOpenAck: obj => {
channelOpenAck: (
obj: IBCMethod<'startChannelOpenInit'>,
bech32Prefix: string = 'cosmos',
): IBCEvent<'channelOpenAck'> => {
// Fake a channel IDs from port suffixes. _Ports have no relation to channels, and hosts
// and controllers will likely have different channel IDs for the same channel._
const mocklID = Number(obj.packet.source_port.split('-').at(-1));
/** @type {IBCChannelID} */
const mockLocalChannelID = `channel-${mocklID}`;
/** @type {IBCChannelID} */
const mockRemoteChannelID = `channel-${mocklID}`;
const mockLocalChannelID: IBCChannelID = `channel-${mocklID}`;
const mockRemoteChannelID: IBCChannelID = `channel-${mocklID}`;

return {
type: 'IBC_EVENT',
Expand All @@ -125,8 +132,8 @@ export const icaMocks = {
channel_id: mockRemoteChannelID,
},
counterpartyVersion: addParamsIfJsonVersion(obj.version, {
// TODO, parameterize
address: 'cosmos1test',
// mockID expected to increase monotonically since icacontroller ports are sequential
address: `${bech32Prefix}1test${mocklID < 2 ? '' : mocklID - 1}`,
}),
connectionHops: obj.hops,
order: obj.order,
Expand Down
68 changes: 64 additions & 4 deletions packages/boot/tools/supports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import type { ExecutionContext as AvaT } from 'ava';
import type { CoreEvalSDKType } from '@agoric/cosmic-proto/swingset/swingset.js';
import type { EconomyBootstrapPowers } from '@agoric/inter-protocol/src/proposals/econ-behaviors.js';
import type { SwingsetController } from '@agoric/swingset-vat/src/controller/controller.js';
import type { BridgeHandler, IBCMethod } from '@agoric/vats';
import type { BridgeHandler, IBCDowncallMethod, IBCMethod } from '@agoric/vats';
import type { BootstrapRootObject } from '@agoric/vats/src/core/lib-boot.js';
import type { EProxy } from '@endo/eventual-send';
import type { FastUSDCCorePowers } from '@agoric/fast-usdc/src/fast-usdc.start.js';
Expand Down Expand Up @@ -289,6 +289,14 @@ export const matchIter = (t: AvaT, iter, valueRef) => {
matchValue(t, iter.value, valueRef);
};

export const AckBehavior = {
/** inbound responses are queued. use `flushInboundQueue()` to simulate the remote response */
Queued: 'QUEUED',
/** inbound messages are delivered immediately */
Immediate: 'IMMEDIATE',
} as const;
type AckBehaviorType = (typeof AckBehavior)[keyof typeof AckBehavior];

/**
* Start a SwingSet kernel to be used by tests and benchmarks.
*
Expand Down Expand Up @@ -365,7 +373,30 @@ export const makeSwingsetTestKit = async (
console.log('inbound', ...args);
bridgeInbound!(...args);
};
/**
* Config DIBC bridge behavior.
* Defaults to `Queued` unless specified.
* Current only configured for `channelOpenInit` but can be
* extended to support `sendPacket`.
*/
const ackBehaviors: Partial<
Record<BridgeId, Partial<Record<IBCDowncallMethod, AckBehaviorType>>>
> = {
[BridgeId.DIBC]: {
startChannelOpenInit: AckBehavior.Queued,
},
};

const shouldAckImmediately = (
bridgeId: BridgeId,
method: IBCDowncallMethod,
) => ackBehaviors?.[bridgeId]?.[method] === AckBehavior.Immediate;

/**
* configurable `bech32Prefix` for DIBC bridge
* messages that involve creating an ICA.
*/
let bech32Prefix = 'cosmos';
/**
* Adds the sequence so the bridge knows what response to connect it to.
* Then queue it send it over the bridge over this returns.
Expand Down Expand Up @@ -404,7 +435,7 @@ export const makeSwingsetTestKit = async (
* Mock the bridge outbound handler. The real one is implemented in Golang so
* changes there will sometimes require changes here.
*/
const bridgeOutbound = (bridgeId: string, obj: any) => {
const bridgeOutbound = (bridgeId: BridgeId, obj: any) => {
// store all messages for querying by tests
if (!outboundMessages.has(bridgeId)) {
outboundMessages.set(bridgeId, []);
Expand Down Expand Up @@ -476,9 +507,17 @@ export const makeSwingsetTestKit = async (
case `${BridgeId.DIBC}:IBC_METHOD`:
case `${BridgeId.VTRANSFER}:IBC_METHOD`: {
switch (obj.method) {
case 'startChannelOpenInit':
pushInbound(BridgeId.DIBC, icaMocks.channelOpenAck(obj));
case 'startChannelOpenInit': {
const message = icaMocks.channelOpenAck(obj, bech32Prefix);
const handle = shouldAckImmediately(
bridgeId,
'startChannelOpenInit',
)
? inbound
: pushInbound;
handle(BridgeId.DIBC, message);
return undefined;
}
case 'sendPacket': {
if (protoMsgMockMap[obj.packet.data]) {
return ackLater(obj, protoMsgMockMap[obj.packet.data]);
Expand Down Expand Up @@ -629,6 +668,27 @@ export const makeSwingsetTestKit = async (
getOutboundMessages: (bridgeId: string) =>
harden([...outboundMessages.get(bridgeId)]),
getInboundQueueLength: () => inboundQueue.length,
setAckBehavior(
bridgeId: BridgeId,
method: IBCDowncallMethod,
behavior: AckBehaviorType,
): void {
if (!ackBehaviors?.[bridgeId]?.[method])
throw Fail`ack behavior not yet configurable for ${bridgeId} ${method}`;
console.log('setting', bridgeId, method, 'ack behavior to', behavior);
ackBehaviors[bridgeId][method] = behavior;
},
lookupAckBehavior(
bridgeId: BridgeId,
method: IBCDowncallMethod,
): AckBehaviorType {
if (!ackBehaviors?.[bridgeId]?.[method])
throw Fail`ack behavior not yet configurable for ${bridgeId} ${method}`;
return ackBehaviors[bridgeId][method];
},
setBech32Prefix(prefix: string): void {
bech32Prefix = prefix;
},
/**
* @param {number} max the max number of messages to flush
* @returns {Promise<number>} the number of messages flushed
Expand Down
22 changes: 22 additions & 0 deletions packages/builders/test/snapshots/orchestration-imports.test.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,17 @@ Generated by [AVA](https://avajs.dev).
payload: [
{},
{
intermediateRecipient: {
chainId: Object @match:string {
payload: [],
},
encoding: Object @match:string {
payload: [],
},
value: Object @match:string {
payload: [],
},
},
retries: Object @match:kind {
payload: 'number',
},
Expand Down Expand Up @@ -423,6 +434,17 @@ Generated by [AVA](https://avajs.dev).
payload: [
{},
{
intermediateRecipient: {
chainId: Object @match:string {
payload: [],
},
encoding: Object @match:string {
payload: [],
},
value: Object @match:string {
payload: [],
},
},
retries: Object @match:kind {
payload: 'number',
},
Expand Down
Binary file not shown.
1 change: 0 additions & 1 deletion packages/fast-usdc/src/cli/operator-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ export const addOperatorCommands = (
.requiredOption('--recipientAddress <string>', 'bech32 address', String)
.requiredOption('--blockHash <0xhex>', 'hex hash', parseHex)
.requiredOption('--blockNumber <number>', 'number', parseNat)
.requiredOption('--blockTimestamp <number>', 'number', parseNat)
.requiredOption('--chainId <string>', 'chain id', Number)
.requiredOption('--amount <number>', 'number', parseNat)
.requiredOption('--forwardingAddress <string>', 'bech32 address', String)
Expand Down
20 changes: 15 additions & 5 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export const prepareAdvancerKit = (
* notifyFacet: import('./settler.js').SettlerKit['notify'];
* borrowerFacet: LiquidityPoolKit['borrower'];
* poolAccount: HostInterface<OrchestrationAccount<{chainId: 'agoric'}>>;
* intermediateRecipient: ChainAddress;
* }} config
*/
config => harden(config),
Expand Down Expand Up @@ -187,12 +188,20 @@ export const prepareAdvancerKit = (
* @param {AdvancerVowCtx & { tmpSeat: ZCFSeat }} ctx
*/
onFulfilled(result, ctx) {
const { poolAccount } = this.state;
const { poolAccount, intermediateRecipient } = this.state;
const { destination, advanceAmount, ...detail } = ctx;
const transferV = E(poolAccount).transfer(destination, {
denom: usdc.denom,
value: advanceAmount.value,
});
const transferV = E(poolAccount).transfer(
destination,
{
denom: usdc.denom,
value: advanceAmount.value,
},
{
forwardOpts: {
intermediateRecipient,
},
},
);
return watch(transferV, this.facets.transferHandler, {
destination,
advanceAmount,
Expand Down Expand Up @@ -250,6 +259,7 @@ export const prepareAdvancerKit = (
notifyFacet: M.remotable(),
borrowerFacet: M.remotable(),
poolAccount: M.remotable(),
intermediateRecipient: ChainAddressShape,
}),
},
);
Expand Down
10 changes: 9 additions & 1 deletion packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AmountMath } from '@agoric/ertp';
import { assertAllDefined, makeTracer } from '@agoric/internal';
import { ChainAddressShape } from '@agoric/orchestration';
import { atob } from '@endo/base64';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
Expand Down Expand Up @@ -93,6 +94,7 @@ export const prepareSettler = (
* remoteDenom: Denom;
* repayer: LiquidityPoolKit['repayer'];
* settlementAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>
* intermediateRecipient: ChainAddress;
* }} config
*/
config => {
Expand Down Expand Up @@ -255,14 +257,19 @@ export const prepareSettler = (
* @param {string} EUD
*/
forward(txHash, sender, fullValue, EUD) {
const { settlementAccount } = this.state;
const { settlementAccount, intermediateRecipient } = this.state;

const dest = chainHub.makeChainAddress(EUD);

// TODO? statusManager.forwarding(txHash, sender, amount);
const txfrV = E(settlementAccount).transfer(
dest,
AmountMath.make(USDC, fullValue),
{
forwardOpts: {
intermediateRecipient,
},
},
);
void vowTools.watch(txfrV, this.facets.transferHandler, {
txHash,
Expand Down Expand Up @@ -305,6 +312,7 @@ export const prepareSettler = (
sourceChannel: M.string(),
remoteDenom: M.string(),
mintedEarly: M.remotable('mintedEarly'),
intermediateRecipient: ChainAddressShape,
}),
},
);
Expand Down
Loading

0 comments on commit ef9088c

Please sign in to comment.