Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make Chainlink the canonical priceAggregator #6629

Merged
merged 26 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a960dda
build: priceAggregatorChainlink
turadg Dec 5, 2022
ed950f5
feat(oracle): support continuing invitation
turadg Dec 5, 2022
e1f7488
feat(oracle!): roundId in price results
turadg Dec 5, 2022
a03a92d
test: cleanup
turadg Dec 6, 2022
d63ddb2
test: context type
turadg Dec 6, 2022
28d1564
chore(types): cast ManualTimer
turadg Dec 6, 2022
274a10b
chores(types): RoundData
turadg Dec 6, 2022
a28c8f7
docs: why not `brands` key on terms
turadg Dec 7, 2022
2a2e559
style: remove superflous underscores
turadg Dec 7, 2022
9f6fd6b
refactor: clarity
turadg Dec 7, 2022
642cb37
test: getRoundStartNotifier
turadg Dec 7, 2022
9e2617b
feat(rpc): publish latestRound
turadg Dec 7, 2022
4899d40
test: chainlink-style price feed
turadg Dec 7, 2022
efe9d11
style: organize imports
turadg Dec 8, 2022
8f7601c
test: refactor to named params
turadg Dec 8, 2022
e3e3984
feat: publish price quotes
turadg Dec 8, 2022
30d3966
feat!: publish PriceDescription instead of PriceQuote
turadg Dec 8, 2022
d7ad277
chore: chainlink aggregator uses bech32 key
turadg Dec 12, 2022
5e58585
test: second oracle for smoketest
turadg Dec 12, 2022
9914899
fix(chainlink): publish new quotes
turadg Dec 12, 2022
bb2b503
refactor: clearer error message handling
turadg Dec 13, 2022
443ea1c
docs
turadg Dec 13, 2022
a8c836c
chore(chainlink)!: 'data' string to 'unitPrice' bigint
turadg Dec 19, 2022
61662e7
refactor(chainlink): simplify pushResult fn
turadg Dec 20, 2022
21018a1
refactor: rename PushPrice invitationMaker
turadg Dec 21, 2022
8e61373
chore(chainlink)!: only smart-wallet oracles
turadg Dec 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions packages/agoric-cli/src/commands/oracle.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const makeOracleCommand = async logger => {

oracle
.command('pushPrice')
.description('add a current price sample')
.description('add a current price sample to a priceAggregator')
.option('--offerId [number]', 'Offer id', Number, Date.now())
.requiredOption(
'--oracleAdminAcceptOfferId [number]',
Expand All @@ -114,7 +114,7 @@ export const makeOracleCommand = async logger => {
invitationSpec: {
source: 'continuing',
previousOffer: opts.oracleAdminAcceptOfferId,
invitationMakerName: 'makePushPriceInvitation',
invitationMakerName: 'PushPrice',
invitationArgs: harden([opts.price]),
},
proposal: {},
Expand All @@ -128,6 +128,42 @@ export const makeOracleCommand = async logger => {
console.warn('Now execute the prepared offer');
});

oracle
.command('pushPriceRound')
.description('add a price for a round to a priceAggregatorChainlink')
.option('--offerId [number]', 'Offer id', Number, Date.now())
.requiredOption(
'--oracleAdminAcceptOfferId [number]',
'offer that had continuing invitation result',
Number,
)
.requiredOption('--price [number]', 'price (per unitAmount)', BigInt)
.requiredOption('--roundId [number]', 'round', Number)
.action(async function () {
// @ts-expect-error this implicit any
const opts = this.opts();

/** @type {import('../lib/psm.js').OfferSpec} */
const offer = {
id: Number(opts.offerId),
invitationSpec: {
source: 'continuing',
previousOffer: opts.oracleAdminAcceptOfferId,
invitationMakerName: 'PushPrice',
invitationArgs: harden([
{ unitPrice: opts.price, roundId: opts.roundId },
]),
},
proposal: {},
};

outputAction({
method: 'executeOffer',
offer,
});

console.warn('Now execute the prepared offer');
});
oracle
.command('query')
.description('return current aggregated (median) price')
Expand Down
37 changes: 35 additions & 2 deletions packages/agoric-cli/test/agops-oracle-smoketest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ if [ -z "$WALLET" ]; then
fi
set -x

# this is in economy-template.json in the oracleAddresses list (agoric1dy0yegdsev4xvce3dx7zrz2ad9pesf5svzud6y)
# to use it run `agd keys oracle2 --interactive` and enter this mnenomic:
# dizzy scale gentle good play scene certain acquire approve alarm retreat recycle inch journey fitness grass minimum learn funny way unlock what buzz upon
WALLET2=oracle2

# Accept invitation to admin an oracle
ORACLE_OFFER=$(mktemp -t agops.XXX)
bin/agops oracle accept >|"$ORACLE_OFFER"
Expand All @@ -33,17 +38,45 @@ agoric wallet send --from "$WALLET" --offer "$ORACLE_OFFER"
agoric wallet show --from "$WALLET"
ORACLE_OFFER_ID=$(jq ".body | fromjson | .offer.id" <"$ORACLE_OFFER")

### Now we have the continuing invitationMakers saved in the wallet
# repeat for oracle2
ORACLE_OFFER=$(mktemp -t agops.XXX)
bin/agops oracle accept >|"$ORACLE_OFFER"
jq ".body | fromjson" <"$ORACLE_OFFER"
agoric wallet send --from "$WALLET2" --offer "$ORACLE_OFFER"
ORACLE2_OFFER_ID=$(jq ".body | fromjson | .offer.id" <"$ORACLE_OFFER")

### Now we have the continuing invitationMakers saved in the wallets

# Use invitation result, with continuing invitationMakers to propose a vote
PROPOSAL_OFFER=$(mktemp -t agops.XXX)
bin/agops oracle pushPrice --price 1.01 --oracleAdminAcceptOfferId "$ORACLE_OFFER_ID" >|"$PROPOSAL_OFFER"
bin/agops oracle pushPriceRound --price 101 --roundId 1 --oracleAdminAcceptOfferId "$ORACLE_OFFER_ID" >|"$PROPOSAL_OFFER"
Comment on lines -40 to +52
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me wonder whether the oracle operators aware of quoting prices in integers without decimals. I'm looking around.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vstorage path segment is still ATOM-USD_price_feed. Is the number of decimals available at a nearby vstorage key?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not presently. I expect the proposal config to suffice:

priceFeedOptions: {
IN_BRAND_NAME: inBrandName,
IN_BRAND_DECIMALS: '6',
OUT_BRAND_NAME: outBrandName,
OUT_BRAND_DECIMALS: '6',

jq ".body | fromjson" <"$PROPOSAL_OFFER"
agoric wallet send --from "$WALLET" --offer "$PROPOSAL_OFFER"

# verify that the offer was satisfied
echo "Offer $ORACLE_OFFER_ID should have numWantsSatisfied: 1"
agoric wallet show --from "$WALLET"

# verify feed publishing
agd query vstorage keys published.priceFeed

# verify that the round started
agoric follow :published.priceFeed.ATOM-USD_price_feed.latestRound

# submit another price in the round from the second oracle
PROPOSAL_OFFER=$(mktemp -t agops.XXX)
bin/agops oracle pushPriceRound --price 201 --roundId 1 --oracleAdminAcceptOfferId "$ORACLE2_OFFER_ID" >|"$PROPOSAL_OFFER"
jq ".body | fromjson" <"$PROPOSAL_OFFER"
agoric wallet send --from "$WALLET2" --offer "$PROPOSAL_OFFER"

# second round, first oracle
PROPOSAL_OFFER=$(mktemp -t agops.XXX)
bin/agops oracle pushPriceRound --price 1102 --roundId 2 --oracleAdminAcceptOfferId "$ORACLE_OFFER_ID" >|"$PROPOSAL_OFFER"
agoric wallet send --from "$WALLET" --offer "$PROPOSAL_OFFER"
# second round, second oracle
PROPOSAL_OFFER=$(mktemp -t agops.XXX)
bin/agops oracle pushPriceRound --price 1202 --roundId 2 --oracleAdminAcceptOfferId "$ORACLE2_OFFER_ID" >|"$PROPOSAL_OFFER"
agoric wallet send --from "$WALLET2" --offer "$PROPOSAL_OFFER"

# see new price
agoric follow :published.priceFeed.ATOM-USD_price_feed
3 changes: 2 additions & 1 deletion packages/cosmic-swingset/economy-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@
{
"AGORIC_INSTANCE_NAME": "ATOM-USD price feed",
"oracleAddresses": [
"@PRIMARY_ADDRESS@"
"@PRIMARY_ADDRESS@",
"agoric1dy0yegdsev4xvce3dx7zrz2ad9pesf5svzud6y"
],
"IN_BRAND_LOOKUP": [
"agoricNames",
Expand Down
4 changes: 2 additions & 2 deletions packages/inter-protocol/scripts/price-feed-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ export const defaultProposalBuilder = async (
brandOutRef: brandOut && publishRef(brandOut),
priceAggregatorRef: publishRef(
install(
'@agoric/zoe/src/contracts/priceAggregator.js',
'../bundles/bundle-priceAggregator.js',
'@agoric/zoe/src/contracts/priceAggregatorChainlink.js',
'../bundles/bundle-priceAggregatorChainlink.js',
),
),
},
Expand Down
9 changes: 9 additions & 0 deletions packages/inter-protocol/scripts/start-local-chain.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,12 @@ sleep 15
# verify
agoric wallet list
agoric wallet show --from "$WALLET"

echo "Repeating for oracle2 account..."
# this is in economy-template.json in the oracleAddresses list (agoric1dy0yegdsev4xvce3dx7zrz2ad9pesf5svzud6y)
# to use it run `agd keys oracle2 --interactive` and enter this mnenomic:
# dizzy scale gentle good play scene certain acquire approve alarm retreat recycle inch journey fitness grass minimum learn funny way unlock what buzz upon
WALLET2=oracle2
WALLET2_BECH32=$(agd keys show "$WALLET2" --output json | jq -r .address)
make ACCT_ADDR="$WALLET2_BECH32" FUNDS=20000000ubld,20000000ibc/usdc1234 fund-acct
agoric wallet provision --spend --account "$WALLET2"
18 changes: 11 additions & 7 deletions packages/inter-protocol/src/proposals/price-feed-proposal.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const ensureOracleBrands = async (

/**
* @param {ChainBootstrapSpace} powers
* @param {{options: {priceFeedOptions: {AGORIC_INSTANCE_NAME: string, oracleAddresses: string[], contractTerms: unknown, IN_BRAND_NAME: string, OUT_BRAND_NAME: string}}}} config
* @param {{options: {priceFeedOptions: {AGORIC_INSTANCE_NAME: string, oracleAddresses: string[], contractTerms: import('@agoric/zoe/src/contracts/priceAggregatorChainlink.js').ChainlinkConfig, IN_BRAND_NAME: string, OUT_BRAND_NAME: string}}}} config
*/
export const createPriceFeed = async (
{
Expand Down Expand Up @@ -129,7 +129,7 @@ export const createPriceFeed = async (
/**
* Values come from economy-template.json, which at this writing had IN:ATOM, OUT:USD
*
* @type {[[Brand<'nat'>, Brand<'nat'>], [Installation<import('@agoric/zoe/src/contracts/priceAggregator.js').start>]]}
* @type {[[Brand<'nat'>, Brand<'nat'>], [Installation<import('@agoric/zoe/src/contracts/priceAggregatorChainlink.js').start>]]}
*/
const [[brandIn, brandOut], [priceAggregator]] = await Promise.all([
reserveThenGetNames(E(agoricNamesAdmin).lookupAdmin('oracleBrand'), [
Expand All @@ -142,7 +142,6 @@ export const createPriceFeed = async (
]);

const unitAmountIn = await unitAmount(brandIn);
/** @type {import('@agoric/zoe/src/contracts/priceAggregator.js').PriceAggregatorContract['terms']} */
const terms = await deeplyFulfilledObject(
harden({
...contractTerms,
Expand Down Expand Up @@ -188,11 +187,11 @@ export const createPriceFeed = async (
.then(deleter => E(aggregators).set(terms, { aggregator, deleter }));

/**
* Send an invitation to one of the oracles.
* Initialize a new oracle and send an invitation to administer it.
*
* @param {string} addr
*/
const distributeInvitation = async addr => {
const addOracle = async addr => {
const invitation = await E(aggregator.creatorFacet).makeOracleInvitation(
addr,
);
Expand All @@ -205,7 +204,7 @@ export const createPriceFeed = async (
};

trace('distributing invitations', oracleAddresses);
await Promise.all(oracleAddresses.map(distributeInvitation));
await Promise.all(oracleAddresses.map(addOracle));
trace('createPriceFeed complete');
};

Expand Down Expand Up @@ -306,7 +305,12 @@ export const startPriceFeeds = async (
priceFeedOptions: {
AGORIC_INSTANCE_NAME: `${inBrandName}-${outBrandName} price feed`,
contractTerms: {
POLL_INTERVAL: 1n,
minSubmissionCount: 2,
minSubmissionValue: 1,
maxSubmissionCount: 5,
maxSubmissionValue: 99999,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this limitation of 100k - 1? I aim to look around a bit more...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing purposeful. It's much bigger in

maxSubmissionValue: 2n ** 256n,

Any suggestions?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the purpose of the limitation. What is it limiting? What bad thing might happen if there's no limit? Why is 100k a plausible number?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is it limiting?

It's limiting the maximum value in a submission.

I don't understand the purpose of the limitation.

One is that it came in the port of https://github.com/smartcontractkit/chainlink/blob/b045416ebca769aa69bde2da23b5109abe07a8b5/contracts/src/v0.6/FluxAggregator.sol#L155-L156

What bad thing might happen if there's no limit?

If enough oracles were compromised they could push the median up to a ludicrous price. Or if there is a software bug affecting all of them it wouldn't even require malice. TMK it's a hedge against run-away values.

Why is 100k a plausible number?

I don't know that it is. This code isn't of the economics or risk, just the functionality. Isn't that sufficient?

restartDelay: 1n,
timeout: 10,
},
oracleAddresses: demoOracleAddresses,
IN_BRAND_NAME: inBrandName,
Expand Down
11 changes: 8 additions & 3 deletions packages/inter-protocol/test/smartWallet/contexts.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ export const makeDefaultTestContext = async (t, makeSpace) => {
'installation',
);
const paBundle = await bundleCache.load(
'../zoe/src/contracts/priceAggregator.js',
'../zoe/src/contracts/priceAggregatorChainlink.js',
'priceAggregator',
);
/** @type {Promise<Installation<import('@agoric/zoe/src/contracts/priceAggregator.js').start>>} */
/** @type {Promise<Installation<import('@agoric/zoe/src/contracts/priceAggregatorChainlink.js').start>>} */
const paInstallation = E(zoe).install(paBundle);
await E(installAdmin).update('priceAggregator', paInstallation);

Expand All @@ -87,7 +87,12 @@ export const makeDefaultTestContext = async (t, makeSpace) => {
priceFeedOptions: {
AGORIC_INSTANCE_NAME: `${inBrandName}-${outBrandName} price feed`,
contractTerms: {
POLL_INTERVAL: 1n,
minSubmissionCount: 2,
minSubmissionValue: 1,
maxSubmissionCount: 5,
maxSubmissionValue: 99999,
restartDelay: 1n,
timeout: 10,
},
oracleAddresses,
IN_BRAND_NAME: inBrandName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import {
import { eventLoopIteration } from '@agoric/zoe/tools/eventLoopIteration.js';
import { E } from '@endo/far';

import { INVITATION_MAKERS_DESC } from '@agoric/zoe/src/contracts/priceAggregator.js';
import buildManualTimer from '@agoric/zoe/tools/manualTimer.js';
import { AmountMath } from '@agoric/ertp';
import { coalesceUpdates } from '@agoric/smart-wallet/src/utils.js';
import { INVITATION_MAKERS_DESC } from '@agoric/zoe/src/contracts/priceAggregatorChainlink.js';
import buildManualTimer from '@agoric/zoe/tools/manualTimer.js';
import { ensureOracleBrands } from '../../src/proposals/price-feed-proposal.js';
import { makeDefaultTestContext } from './contexts.js';
import { headValue } from '../supports.js';
import { makeDefaultTestContext } from './contexts.js';
Comment on lines -13 to +18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything to this change besides line order? I can't tell. Is there some motivation for changing the order?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was some import to remove and I used Organize imports in VS Code (from TypeScript LSP)


/**
* @type {import('ava').TestFn<Awaited<ReturnType<makeDefaultTestContext>>
Expand Down Expand Up @@ -87,18 +86,15 @@ test('admin price', async t => {
const currentSub = E(wallet).getCurrentSubscriber();

await t.context.simpleCreatePriceFeed([operatorAddress], 'ATOM', 'USD');
const atomBrand = await E(agoricNames).lookup('oracleBrand', 'ATOM');
const usdBrand = await E(agoricNames).lookup('oracleBrand', 'USD');

const offersFacet = wallet.getOffersFacet();

/** @type {import('@agoric/zoe/src/zoeService/utils.js').Instance<import('@agoric/zoe/src/contracts/priceAggregatorChainlink.js').start>} */
const priceAggregator = await E(agoricNames).lookup(
'instance',
'ATOM-USD price feed',
);
/** @type {import('@agoric/zoe/src/contracts/priceAggregator.js').PriceAggregatorContract['publicFacet']} */
const paPublicFacet = await E(zoe).getPublicFacet(priceAggregator);
const priceAuthority = await E(paPublicFacet).getPriceAuthority();

/**
* get invitation details the way a user would
Expand Down Expand Up @@ -159,12 +155,15 @@ test('admin price', async t => {

// Push a new price result /////////////////////////

/** @type {import('@agoric/zoe/src/contracts/priceAggregatorChainlink.js').PriceRound} */
const result = { roundId: 1, unitPrice: 123n };

/** @type {import('@agoric/smart-wallet/src/invitations.js').ContinuingInvitationSpec} */
const proposeInvitationSpec = {
source: 'continuing',
previousOffer: 44,
invitationMakerName: 'makePushPriceInvitation',
invitationArgs: harden([123n]),
invitationMakerName: 'PushPrice',
invitationArgs: harden([result]),
};

/** @type {import('@agoric/smart-wallet/src/offers').OfferSpec} */
Expand All @@ -185,17 +184,10 @@ test('admin price', async t => {
// trigger an aggregation (POLL_INTERVAL=1n in context)
E(manualTimer).tickN(1);

const quote = await priceAuthority.quoteGiven(
AmountMath.make(atomBrand, 1_000n),
usdBrand,
);
const latestRoundSubscriber = await E(paPublicFacet).getRoundStartNotifier();

t.deepEqual(quote.quoteAmount.value[0].amountIn, {
brand: atomBrand,
value: 1_000n,
});
t.deepEqual(quote.quoteAmount.value[0].amountOut, {
brand: usdBrand,
value: 123_000n,
t.deepEqual((await latestRoundSubscriber.subscribeAfter()).head.value, {
roundId: 1n,
startedAt: 0n,
});
});
4 changes: 4 additions & 0 deletions packages/zoe/scripts/build-bundles.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const sourceToBundle = [
'../src/contracts/priceAggregator.js',
'../bundles/bundle-priceAggregator.js',
],
[
'../src/contracts/priceAggregatorChainlink.js',
'../bundles/bundle-priceAggregatorChainlink.js',
],
];

await createBundles(sourceToBundle, dirname);
2 changes: 1 addition & 1 deletion packages/zoe/src/contractSupport/priceAuthority.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export function makeOnewayPriceAuthorityKit(opts) {
amountIn,
amountOut: calcAmountOut(amountIn),
}));
assert(quote);
assert(quote, 'createQuote returned falsey');

const value = await quote;
return harden({
Expand Down
Loading