Skip to content

Commit

Permalink
9584 upgrade price feeds (#10074)
Browse files Browse the repository at this point in the history
closes: #9584
closes: #9928
refs: #9827 
refs: #9748 
refs: #9382
closes: #10031

## Description

We added upgrading the scaledPriceAuthority to the steps in upgrading vaults, auctions, and priceFeeds, and didn't notice that it broke things. The problem turned out to be that the "priceAuthority" object registered with the priceFeedRegistry was an ephemeral object that was not upgraded. This fixes that by re-registering the new priceAuthority.

Then, to simplify the process of cleaning up the uncollected cycles reported in #9483, we switched to replacing the scaledPriceAuthorities rather than upgrading them.

We also realized that we would need different coreEvals in different environments, since the Oracle addresses and particular assets vary for each (test and mainNet) chain environment.

#9748 addressed some of the issues in the original coreEval. #9999 showed what was needed for upgrading priceFeeds, which was completed by #9827.  #10021 added some details on replacing scaledPriceAuthorities.

### Security Considerations

N/A

### Scaling Considerations

Addresses one of our biggest scaling issues.

### Documentation Considerations

N/A

### Testing Considerations

Thorough testing in a3p, and our testnets.  #9886 discusses some testing to ensure Oracles will work with the upgrade.

### Upgrade Considerations

See above
  • Loading branch information
mergify[bot] authored Oct 9, 2024
2 parents 6415ef6 + eb941d4 commit c42dff3
Show file tree
Hide file tree
Showing 34 changed files with 3,875 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: node-modules
10 changes: 10 additions & 0 deletions a3p-integration/proposals/f:replace-price-feeds/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# CoreEvalProposal to replace existing price_feed and scaledPriceAuthority vats
# with new contracts. Auctions will need to be replaced, and Vaults will need to
# get at least a null upgrade in order to make use of the new prices. Oracle
# operators will need to accept new invitations, and sync to the roundId (0) of
# the new contracts in order to feed the new pipelines.

The `submission` for this proposal is automatically generated during `yarn build`
in [a3p-integration](../..) using the code in agoric-sdk through
[build-all-submissions.sh](../../scripts/build-all-submissions.sh) and
[build-submission.sh](../../scripts/build-submission.sh).
23 changes: 23 additions & 0 deletions a3p-integration/proposals/f:replace-price-feeds/agd-tools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { agd } from '@agoric/synthetic-chain';

export const BID_OFFER_ID = 'bid-vaultUpgrade-test3';

/** @param {string} path */
export const queryVstorage = path =>
agd.query('vstorage', 'data', '--output', 'json', path);

export const getOracleInstance = async price => {
const instanceRec = await queryVstorage(`published.agoricNames.instance`);

const value = JSON.parse(instanceRec.value);
const body = JSON.parse(value.values.at(-1));

const feeds = JSON.parse(body.body.substring(1));
const feedName = `${price}-USD price feed`;

const key = Object.keys(feeds).find(k => feeds[k][0] === feedName);
if (key) {
return body.slots[key];
}
return null;
};
96 changes: 96 additions & 0 deletions a3p-integration/proposals/f:replace-price-feeds/agoric-tools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import assert from 'node:assert';
import { agops, agoric, executeOffer } from '@agoric/synthetic-chain';

export const generateVaultDirectorParamChange = async (
previousOfferId,
voteDur,
params,
paramsPath,
) => {
const voteDurSec = BigInt(voteDur);
const toSec = ms => BigInt(Math.round(ms / 1000));

const id = `propose-${Date.now()}`;
const deadline = toSec(Date.now()) + voteDurSec;

const zip = (xs, ys) => xs.map((x, i) => [x, ys[i]]);
// KLUDGE: partial deconstruction of smallCaps values
const fromSmallCapsEntries = txt => {
const { body, slots } = JSON.parse(txt);
const theEntries = zip(JSON.parse(body.slice(1)), slots).map(
([[name, ref], boardID]) => {
const iface = ref.replace(/^\$\d+\./, '');
return [name, { iface, boardID }];
},
);
return Object.fromEntries(theEntries);
};

const slots = []; // XXX global mutable state
const smallCaps = {
Nat: n => `+${n}`,
// XXX mutates obj
ref: obj => {
if (obj.ix) return obj.ix;
const ix = slots.length;
slots.push(obj.boardID);
obj.ix = `$${ix}.Alleged: ${obj.iface}`;
return obj.ix;
},
};

await null;
const instance = fromSmallCapsEntries(
await agoric.follow('-lF', ':published.agoricNames.instance', '-o', 'text'),
);
assert(instance.VaultFactory);

const body = {
method: 'executeOffer',
offer: {
id,
invitationSpec: {
invitationMakerName: 'VoteOnParamChange',
previousOffer: previousOfferId,
source: 'continuing',
},
offerArgs: {
deadline: smallCaps.Nat(deadline),
instance: smallCaps.ref(instance.VaultFactory),
params,
path: paramsPath,
},
proposal: {},
},
};

const capData = { body: `#${JSON.stringify(body)}`, slots };
return JSON.stringify(capData);
};

export const proposeVaultDirectorParamChange = async (
address,
params,
path,
) => {
const charterAcceptOfferId = await agops.ec(
'find-continuing-id',
'--for',
`${'charter\\ member\\ invitation'}`,
'--from',
address,
);

return executeOffer(
address,
generateVaultDirectorParamChange(charterAcceptOfferId, 30, params, path),
);
};

export const voteForNewParams = (accounts, position) => {
return Promise.all(
accounts.map(account =>
agops.ec('vote', '--forPosition', position, '--send-from', account),
),
);
};
15 changes: 15 additions & 0 deletions a3p-integration/proposals/f:replace-price-feeds/eval.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash

# Exit when any command fails
set -uxeo pipefail

# The upgrade-vaults proposal needs to know the existing vaultDirector
# parameters in order to cleanly upgrade the contract. The governance notifier
# it relies on doesn't give the most recent value if there were no updates to
# the parameters, so we'll do a governance action to reset them to their current
# values so the notifier will work.

./resetChargingPeriod.js

cp /usr/src/upgrade-test-scripts/eval_submission.js .
./eval_submission.js
28 changes: 28 additions & 0 deletions a3p-integration/proposals/f:replace-price-feeds/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"agoricProposal": {
"source": "subdir",
"sdk-generate": [
"inter-protocol/updatePriceFeeds.js submission A3P_INTEGRATION",
"vats/add-auction.js",
"vats/upgradeVaults.js"
],
"type": "/agoric.swingset.CoreEvalProposal"
},
"type": "module",
"license": "Apache-2.0",
"dependencies": {
"@agoric/synthetic-chain": "^0.3.0",
"ava": "^5.3.1"
},
"ava": {
"concurrency": 1,
"timeout": "2m",
"files": [
"!submission"
]
},
"scripts": {
"agops": "yarn --cwd /usr/src/agoric-sdk/ --silent agops"
},
"packageManager": "[email protected]"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import test from 'ava';

import {
agops,
ATOM_DENOM,
bankSend,
createBid,
generateOracleMap,
getDetailsMatchingVats,
getInstanceBoardId,
getISTBalance,
getLiveOffers,
getPriceQuote,
getVaultPrices,
getVatDetails,
openVault,
pushPrices,
registerOraclesForBrand,
USER1ADDR,
} from '@agoric/synthetic-chain';

import { BID_OFFER_ID } from './agd-tools.js';

export const checkForOracle = async (t, name) => {
const instanceName = `${name}-USD price feed`;
const instance = await getInstanceBoardId(instanceName);
t.truthy(instance);
};

const checkPriceFeedVatsUpdated = async t => {
const atomDetails = await getVatDetails('ATOM-USD_price_feed');
// both the original and the new ATOM vault are incarnation 0
t.is(atomDetails.incarnation, 0);
const stAtomDetails = await getVatDetails('stATOM');
t.is(stAtomDetails.incarnation, 0);
await checkForOracle(t, 'ATOM');
await checkForOracle(t, 'stATOM');
};

console.log('adding oracle for each brand');
const oraclesByBrand = generateOracleMap('f-priceFeeds', ['ATOM', 'stATOM']);
await registerOraclesForBrand('ATOM', oraclesByBrand);
await registerOraclesForBrand('stATOM', oraclesByBrand);

let roundId = 1;

const tryPushPrices = async t => {
// There are no old prices for the other currencies.
// const atomOutPre = await getPriceQuote('ATOM');
// t.is(atomOutPre, '+12010000');
// const stAtomOutPre = await getPriceQuote('stATOM');
// t.is(stAtomOutPre, '+12010000');

t.log('pushing new prices');
await pushPrices(13.4, 'ATOM', oraclesByBrand, roundId);
await pushPrices(13.7, 'stATOM', oraclesByBrand, roundId);
roundId += 1;

t.log('awaiting new quotes');
const atomOut = await getPriceQuote('ATOM');
t.is(atomOut, '+13400000');
const stAtomOut = await getPriceQuote('stATOM');
t.is(stAtomOut, '+13700000');
};

const createNewBid = async t => {
await createBid('20', USER1ADDR, BID_OFFER_ID);
const liveOffer = await getLiveOffers(USER1ADDR);
t.true(liveOffer[0].includes(BID_OFFER_ID));
};

const openMarginalVault = async t => {
let user1IST = await getISTBalance(USER1ADDR);
await bankSend(USER1ADDR, `20000000${ATOM_DENOM}`);
const currentVaults = await agops.vaults('list', '--from', USER1ADDR);

t.log('opening a vault');
await openVault(USER1ADDR, 5, 10);
user1IST += 5;
const istBalanceAfterVaultOpen = await getISTBalance(USER1ADDR);
t.is(istBalanceAfterVaultOpen, user1IST);

const activeVaultsAfter = await agops.vaults('list', '--from', USER1ADDR);
t.log(currentVaults, activeVaultsAfter);
t.true(
activeVaultsAfter.length > currentVaults.length,
`vaults count should increase, ${activeVaultsAfter.length}, ${currentVaults.length}`,
);
};

const triggerAuction = async t => {
await pushPrices(5.2, 'ATOM', oraclesByBrand, roundId);

const atomOut = await getPriceQuote('ATOM');
t.is(atomOut, '+5200000');
};

const checkNewAuctionVat = async t => {
const details = await getDetailsMatchingVats('auctioneer');
// This query matches both the auction and its governor, so double the count
t.is(Object.keys(details).length, 3 * 2);
};

const countPriceFeedVats = async t => {
// price_feed and governor, old and new for two tokens
const priceFeedDetails = await getDetailsMatchingVats('price_feed');
t.is(Object.keys(priceFeedDetails).length, 8);

// Two old SPAs, and two new ones
const details = await getDetailsMatchingVats('scaledPriceAuthority');
t.is(Object.keys(details).length, 4);

// ATOM vat name is something like zcf-DEADBEEF-ATOM_USD_price_feed
// initial '-' distinguishes this from stAOM
const atomDetails = await getDetailsMatchingVats('-ATOM-USD_price_feed');
t.is(Object.keys(atomDetails).length, 4);

const stAtomDetails = await getVatDetails('stATOM');
t.is(Object.keys(stAtomDetails).length, 4);
await Promise.all([checkForOracle(t, 'ATOM'), checkForOracle(t, 'stATOM')]);
};

const verifyVaultPriceUpdate = async t => {
const ATOMManagerIndex = 0;
const quote = await getVaultPrices(ATOMManagerIndex);
t.true(quote.value[0].amountIn.brand.includes(' ATOM '));
t.is(quote.value[0].amountOut.value, '+5200000');
};

// test.serial() isn't guaranteed to run tests in order, so we run the intended tests here
test('liquidation post upgrade', async t => {
t.log('starting upgrade vaults test');
await checkPriceFeedVatsUpdated(t);

t.log('starting pushPrices');
await tryPushPrices(t);

t.log('create a new Bid for the auction');
await createNewBid(t);

t.log('open a marginal vault');
await openMarginalVault(t);

t.log('trigger Auction');
await triggerAuction(t);

t.log('check new auction');
await checkNewAuctionVat(t);

t.log('count vats');
await countPriceFeedVats(t);

t.log('verify Vault priceUpdate');
await verifyVaultPriceUpdate(t);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env node

/* global setTimeout */

import {
getQuoteBody,
GOV1ADDR,
GOV2ADDR,
GOV3ADDR,
} from '@agoric/synthetic-chain';
import {
proposeVaultDirectorParamChange,
voteForNewParams,
} from './agoric-tools.js';

const GOV_ADDRESSES = [GOV1ADDR, GOV2ADDR, GOV3ADDR];

const readChargingPeriod = async () => {
const governanceBody = await getQuoteBody(
'published.vaultFactory.governance',
);
const period =
governanceBody.current.ChargingPeriod.value.match(/\+?(\d+)/)[1];
return `+${period}`;
};

const setChargingPeriod = async period => {
const params = {
ChargingPeriod: period,
};

const path = { paramPath: { key: 'governedParams' } };

await proposeVaultDirectorParamChange(GOV1ADDR, params, path);
await voteForNewParams(GOV_ADDRESSES, 0);

await new Promise(r => setTimeout(r, 65000));
};

const period = await readChargingPeriod();
await setChargingPeriod(period);
6 changes: 6 additions & 0 deletions a3p-integration/proposals/f:replace-price-feeds/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash

# Place here any test that should be executed using the proposal.
# The effects of this step are not persisted in further layers.

yarn ava ./*.test.js
Loading

0 comments on commit c42dff3

Please sign in to comment.