Skip to content

Commit

Permalink
ability to replace oracle operators in fast-usdc (#10755)
Browse files Browse the repository at this point in the history
refs: #10754

## Description

Add and test the ability to replace operators

Before closing: test coverage of adding new ones

### Security Considerations
removes `.admin` from operator offer result kit. (wasn't exploitable but this is better)

### Scaling Considerations
no changes


### Documentation Considerations
no changes

### Testing Considerations
new coverage suffices

### Upgrade Considerations
not yet deployed
  • Loading branch information
mergify[bot] authored Jan 11, 2025
2 parents b58d523 + 7cc7a5e commit d703190
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 73 deletions.
121 changes: 91 additions & 30 deletions packages/boot/test/fast-usdc/fast-usdc.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';

import type { TestFn } from 'ava';

import { encodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js';
import { configurations } from '@agoric/fast-usdc/src/utils/deploy-config.js';
import { MockCctpTxEvidences } from '@agoric/fast-usdc/test/fixtures.js';
import { documentStorageSchema } from '@agoric/governance/tools/storageDoc.js';
import { Fail } from '@endo/errors';
import {
BridgeId,
deeplyFulfilledObject,
NonNullish,
objectMap,
} from '@agoric/internal';
import { unmarshalFromVstorage } from '@agoric/internal/src/marshal.js';
import { defaultSerializer } from '@agoric/internal/src/storage-test-utils.js';
import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
import { Fail } from '@endo/errors';
import { makeMarshal } from '@endo/marshal';
import {
defaultMarshaller,
defaultSerializer,
} from '@agoric/internal/src/storage-test-utils.js';
import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
import { BridgeId, NonNullish } from '@agoric/internal';
AckBehavior,
insistManagerType,
makeSwingsetHarness,
} from '../../tools/supports.js';
import {
makeWalletFactoryContext,
type WalletFactoryTestContext,
} from '../bootstrapTests/walletFactory.js';
import {
makeSwingsetHarness,
insistManagerType,
AckBehavior,
} from '../../tools/supports.js';

const test: TestFn<
WalletFactoryTestContext & {
Expand Down Expand Up @@ -51,8 +53,8 @@ test.before('bootstrap', async t => {
test.after.always(t => t.context.shutdown?.());

test.serial('oracles provision before contract deployment', async t => {
const { walletFactoryDriver: wd } = t.context;
const watcherWallet = await wd.provideSmartWallet('agoric1watcher1');
const { walletFactoryDriver: wfd } = t.context;
const watcherWallet = await wfd.provideSmartWallet('agoric1watcher1');
t.truthy(watcherWallet);
});

Expand All @@ -66,12 +68,12 @@ test.serial(
evalProposal,
refreshAgoricNamesRemotes,
storage,
walletFactoryDriver: wd,
walletFactoryDriver: wfd,
} = t.context;

const { oracles } = configurations.MAINNET;
const [watcherWallet] = await Promise.all(
Object.values(oracles).map(addr => wd.provideSmartWallet(addr)),
Object.values(oracles).map(addr => wfd.provideSmartWallet(addr)),
);

// inbound `startChannelOpenInit` responses immediately.
Expand Down Expand Up @@ -201,8 +203,8 @@ test.serial('writes account addresses to vstorage', async t => {
});

test.serial('LP deposits', async t => {
const { walletFactoryDriver: wd, agoricNamesRemotes } = t.context;
const lp = await wd.provideSmartWallet(
const { walletFactoryDriver: wfd, agoricNamesRemotes } = t.context;
const lp = await wfd.provideSmartWallet(
'agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8',
);

Expand Down Expand Up @@ -270,15 +272,15 @@ test.serial('LP deposits', async t => {

test.serial('makes usdc advance', async t => {
const {
walletFactoryDriver: wd,
walletFactoryDriver: wfd,
storage,
agoricNamesRemotes,
harness,
} = t.context;
const oracles = await Promise.all([
wd.provideSmartWallet('agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8'),
wd.provideSmartWallet('agoric1krunjcqfrf7la48zrvdfeeqtls5r00ep68mzkr'),
wd.provideSmartWallet('agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj'),
wfd.provideSmartWallet('agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8'),
wfd.provideSmartWallet('agoric1krunjcqfrf7la48zrvdfeeqtls5r00ep68mzkr'),
wfd.provideSmartWallet('agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj'),
]);
await Promise.all(
oracles.map(wallet =>
Expand All @@ -298,7 +300,7 @@ test.serial('makes usdc advance', async t => {
const lastNodeValue = storage.getValues('published.fastUsdc').at(-1);
const { settlementAccount } = JSON.parse(NonNullish(lastNodeValue));
const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(
// mock with the read settlementAccount address
// mock with the real settlementAccount address
encodeAddressHook(settlementAccount, { EUD }),
);

Expand Down Expand Up @@ -344,18 +346,18 @@ test.serial('makes usdc advance', async t => {
});

test.serial('skips usdc advance when risks identified', async t => {
const { walletFactoryDriver: wd, storage } = t.context;
const { walletFactoryDriver: wfd, storage } = t.context;
const oracles = await Promise.all([
wd.provideSmartWallet('agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8'),
wd.provideSmartWallet('agoric1krunjcqfrf7la48zrvdfeeqtls5r00ep68mzkr'),
wd.provideSmartWallet('agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj'),
wfd.provideSmartWallet('agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8'),
wfd.provideSmartWallet('agoric1krunjcqfrf7la48zrvdfeeqtls5r00ep68mzkr'),
wfd.provideSmartWallet('agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj'),
]);

const EUD = 'dydx1riskyeud';
const lastNodeValue = storage.getValues('published.fastUsdc').at(-1);
const { settlementAccount } = JSON.parse(NonNullish(lastNodeValue));
const evidence = MockCctpTxEvidences.AGORIC_PLUS_DYDX(
// mock with the read settlementAccount address
// mock with the real settlementAccount address
encodeAddressHook(settlementAccount, { EUD }),
);

Expand Down Expand Up @@ -394,8 +396,8 @@ test.serial('skips usdc advance when risks identified', async t => {
});

test.serial('LP withdraws', async t => {
const { walletFactoryDriver: wd, agoricNamesRemotes } = t.context;
const lp = await wd.provideSmartWallet(
const { walletFactoryDriver: wfd, agoricNamesRemotes } = t.context;
const lp = await wfd.provideSmartWallet(
'agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8',
);

Expand Down Expand Up @@ -463,3 +465,62 @@ test.serial('restart contract', async t => {
const actual = await EV(kit.adminFacet).restartContract(kit.privateArgs);
t.deepEqual(actual, { incarnationNumber: 1 });
});

test.serial('replace operators', async t => {
const {
agoricNamesRemotes,
storage,
runUtils: { EV },
walletFactoryDriver: wfd,
} = t.context;
const { creatorFacet } = await EV.vat('bootstrap').consumeItem('fastUsdcKit');

const EUD = 'dydx1anything';
const lastNodeValue = storage.getValues('published.fastUsdc').at(-1);
const { settlementAccount } = JSON.parse(NonNullish(lastNodeValue));
const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(
// mock with the real settlementAccount address
encodeAddressHook(settlementAccount, { EUD }),
);

// Remove old oracle operators (nested in block to isolate bindings)
{
// old oracles, which were started with MAINNET config
const { oracles } = configurations.MAINNET;

for (const [name, address] of Object.entries(oracles)) {
t.log('Removing operator', name, 'at', address);
await EV(creatorFacet).removeOperator(address);
}

const wallets = await Promise.all(
Object.values(oracles).map(addr => wfd.provideSmartWallet(addr)),
);

await Promise.all(
wallets.map(wallet =>
wallet.sendOffer({
id: 'submit-while-disabled',
invitationSpec: {
source: 'continuing',
previousOffer: 'claim-oracle-invitation',
invitationMakerName: 'SubmitEvidence',
invitationArgs: [evidence],
},
proposal: {},
}),
),
);
for (const wd of wallets) {
t.like(wd.getLatestUpdateRecord(), {
status: {
id: 'submit-while-disabled',
error: 'Error: submitEvidence for disabled operator',
},
});
}
}

// TODO test adding new operators
// The naive approach is failing under XS. A new CoreEval may be necessary.
});
28 changes: 21 additions & 7 deletions packages/fast-usdc/src/exos/transaction-feed.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { prepareOperatorKit } from './operator-kit.js';

const trace = makeTracer('TxFeed', true);

/**
* @typedef {Pick<OperatorKit, 'invitationMakers' | 'operator'>} OperatorOfferResult
*/

/** Name in the invitation purse (keyed also by this contract instance) */
export const INVITATION_MAKERS_DESC = 'oracle operator invitation';

Expand All @@ -27,8 +31,10 @@ const TransactionFeedKitI = harden({
).returns(),
}),
creator: M.interface('Transaction Feed Creator', {
// TODO narrow the return shape to OperatorKit
initOperator: M.call(M.string()).returns(M.record()),
initOperator: M.call(M.string()).returns({
invitationMakers: M.remotable(),
operator: M.remotable(),
}),
makeOperatorInvitation: M.call(M.string()).returns(M.promise()),
removeOperator: M.call(M.string()).returns(),
}),
Expand Down Expand Up @@ -92,22 +98,25 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
* CCTP transactions.
*
* @param {string} operatorId unique per contract instance
* @returns {Promise<Invitation<OperatorKit>>}
* @returns {Promise<Invitation<OperatorOfferResult>>}
*/
makeOperatorInvitation(operatorId) {
const { creator } = this.facets;
trace('makeOperatorInvitation', operatorId);

return zcf.makeInvitation(
/** @type {OfferHandler<OperatorKit>} */
/** @type {OfferHandler<OperatorOfferResult>} */
seat => {
seat.exit();
return creator.initOperator(operatorId);
},
INVITATION_MAKERS_DESC,
);
},
/** @param {string} operatorId */
/**
* @param {string} operatorId
* @returns {OperatorOfferResult}
*/
initOperator(operatorId) {
const { operators, pending, risks } = this.state;
trace('initOperator', operatorId);
Expand All @@ -123,11 +132,16 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
);
risks.init(operatorId, zone.detached().mapStore('risk assessments'));

return operatorKit;
// Subset facets to all the off-chain operator needs
const { invitationMakers, operator } = operatorKit;
return {
invitationMakers,
operator,
};
},

/** @param {string} operatorId */
async removeOperator(operatorId) {
removeOperator(operatorId) {
const { operators } = this.state;
trace('removeOperator', operatorId);
const operatorKit = operators.get(operatorId);
Expand Down
29 changes: 6 additions & 23 deletions packages/fast-usdc/src/fast-usdc.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { prepareStatusManager } from './exos/status-manager.js';
import { prepareTransactionFeedKit } from './exos/transaction-feed.js';
import * as flows from './fast-usdc.flows.js';
import { FastUSDCTermsShape, FeeConfigShape } from './type-guards.js';
import { defineInertInvitation } from './utils/zoe.js';

const trace = makeTracer('FastUsdc');

Expand All @@ -37,7 +36,7 @@ const ADDRESSES_BAGGAGE_KEY = 'addresses';
* @import {Remote} from '@agoric/internal';
* @import {Marshaller, StorageNode} from '@agoric/internal/src/lib-chainStorage.js'
* @import {Zone} from '@agoric/zone';
* @import {OperatorKit} from './exos/operator-kit.js';
* @import {OperatorOfferResult} from './exos/transaction-feed.js';
* @import {CctpTxEvidence, FeeConfig, RiskAssessment} from './types.js';
*/

Expand Down Expand Up @@ -151,18 +150,17 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
{ makeRecorderKit },
);

const makeTestInvitation = defineInertInvitation(
zcf,
'test of forcing evidence',
);

const { makeLocalAccount, makeNobleAccount } = orchestrateAll(flows, {});

const creatorFacet = zone.exo('Fast USDC Creator', undefined, {
/** @type {(operatorId: string) => Promise<Invitation<OperatorKit>>} */
/** @type {(operatorId: string) => Promise<Invitation<OperatorOfferResult>>} */
async makeOperatorInvitation(operatorId) {
return feedKit.creator.makeOperatorInvitation(operatorId);
},
/** @type {(operatorId: string) => void} */
removeOperator(operatorId) {
return feedKit.creator.removeOperator(operatorId);
},
async connectToNoble() {
return vowTools.when(nobleAccountV, nobleAccount => {
trace('nobleAccount', nobleAccount);
Expand Down Expand Up @@ -193,21 +191,6 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
});

const publicFacet = zone.exo('Fast USDC Public', undefined, {
// XXX to be removed before production
/**
* NB: Any caller with access to this invitation maker has the ability to
* force handling of evidence.
*
* Provide an API call in the form of an invitation maker, so that the
* capability is available in the smart-wallet bridge during UI testing.
*
* @param {CctpTxEvidence} evidence
* @param {RiskAssessment} [risk]
*/
makeTestPushInvitation(evidence, risk = {}) {
void advancer.handleTransactionEvent({ evidence, risk });
return makeTestInvitation();
},
makeDepositInvitation() {
return poolKit.public.makeDepositInvitation();
},
Expand Down
1 change: 1 addition & 0 deletions packages/fast-usdc/src/fast-usdc.start.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ const publishFeedPolicy = async (node, policy) => {
* }} FastUSDCCorePowers
*
* @typedef {StartedInstanceKitWithLabel & {
* creatorFacet: Awaited<ReturnType<FastUsdcSF>>['creatorFacet'];
* privateArgs: StartParams<FastUsdcSF>['privateArgs'];
* }} FastUSDCKit
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/fast-usdc/test/exos/transaction-feed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,15 +189,15 @@ test('disagreement after publishing', async t => {
});
});

test('disabled operator', async t => {
test('remove operator', async t => {
const feedKit = makeFeedKit();
const { op1 } = await makeOperators(feedKit);
const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO();

// works before disabling
op1.operator.submitEvidence(evidence);

op1.admin.disable();
await feedKit.creator.removeOperator('op1');

t.throws(() => op1.operator.submitEvidence(evidence), {
message: 'submitEvidence for disabled operator',
Expand Down
Loading

0 comments on commit d703190

Please sign in to comment.