diff --git a/a3p-integration/proposals/z:acceptance/lib/vaults.mts b/a3p-integration/proposals/z:acceptance/lib/vaults.mts new file mode 100644 index 00000000000..4ce6697f1ee --- /dev/null +++ b/a3p-integration/proposals/z:acceptance/lib/vaults.mts @@ -0,0 +1,209 @@ +/* eslint-disable @jessie.js/safe-await-separator */ +/* eslint-env node */ + +import { strict as assert } from 'node:assert'; + +import { + addUser, + agops, + agoric, + ATOM_DENOM, + executeOffer, + GOV1ADDR, + GOV2ADDR, + GOV3ADDR, + provisionSmartWallet, + waitForBlock, +} from '@agoric/synthetic-chain'; + +const govAccounts = [GOV1ADDR, GOV2ADDR, GOV3ADDR]; + +export const ISTunit = 1_000_000n; // aka displayInfo: { decimalPlaces: 6 } + +// XXX likely longer than necessary +const VOTING_WAIT_MS = 65 * 1000; + +const proposeNewAuctionParams = async ( + address: string, + startFequency: any, + clockStep: any, + priceLockPeriod: any, +) => { + const charterAcceptOfferId = await agops.ec( + 'find-continuing-id', + '--for', + `${'charter\\ member\\ invitation'}`, + '--from', + address, + ); + + return executeOffer( + address, + agops.auctioneer( + 'proposeParamChange', + '--charterAcceptOfferId', + charterAcceptOfferId, + '--start-frequency', + startFequency, + '--clock-step', + clockStep, + '--price-lock-period', + priceLockPeriod, + ), + ); +}; + +const voteForNewParams = (accounts: string[], position: number) => { + console.log('ACTIONS voting for position', position, 'using', accounts); + return Promise.all( + accounts.map((account: string) => + agops.ec('vote', '--forPosition', position, '--send-from', account), + ), + ); +}; + +const paramChangeOfferGeneration = async ( + previousOfferId: string, + voteDur: number, + debtLimit: number | bigint, +) => { + const voteDurSec = BigInt(voteDur); + const debtLimitValue = BigInt(debtLimit) * ISTunit; + const toSec = (ms: number) => BigInt(Math.round(ms / 1000)); + + const id = `propose-${Date.now()}`; + const deadline = toSec(Date.now()) + voteDurSec; + + const zip = (xs: any[], ys: { [x: string]: any }) => + xs.map((x: any, i: string | number) => [x, ys[i]]); + const fromSmallCapsEntries = (txt: string) => { + 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 = [] as string[]; // XXX global mutable state + const smallCaps = { + Nat: (n: any) => `+${n}`, + // XXX mutates obj + ref: (obj: { ix: string; boardID: any; iface: any }) => { + if (obj.ix) return obj.ix; + const ix = slots.length; + slots.push(obj.boardID); + obj.ix = `$${ix}.Alleged: ${obj.iface}`; + return obj.ix; + }, + }; + + const instance = fromSmallCapsEntries( + await agoric.follow('-lF', ':published.agoricNames.instance', '-o', 'text'), + ); + assert(instance.VaultFactory); + + const brand = fromSmallCapsEntries( + await agoric.follow('-lF', ':published.agoricNames.brand', '-o', 'text'), + ); + assert(brand.IST); + assert(brand.ATOM); + + const body = { + method: 'executeOffer', + offer: { + id, + invitationSpec: { + invitationMakerName: 'VoteOnParamChange', + previousOffer: previousOfferId, + source: 'continuing', + }, + offerArgs: { + deadline: smallCaps.Nat(deadline), + instance: smallCaps.ref(instance.VaultFactory), + params: { + DebtLimit: { + brand: smallCaps.ref(brand.IST), + value: smallCaps.Nat(debtLimitValue), + }, + }, + path: { + paramPath: { + key: { + collateralBrand: smallCaps.ref(brand.ATOM), + }, + }, + }, + }, + proposal: {}, + }, + }; + + const capData = { body: `#${JSON.stringify(body)}`, slots }; + return JSON.stringify(capData); +}; +export const provisionWallet = async (user: string) => { + const userAddress = await addUser(user); + + await provisionSmartWallet( + userAddress, + `20000000ubld,100000000${ATOM_DENOM}`, + ); + await waitForBlock(); +}; + +export const implementNewAuctionParams = async ( + address: string, + oracles: { address: string; id: string }[], + startFequency: number, + clockStep: number, + priceLockPeriod: number, +) => { + await waitForBlock(3); + + await proposeNewAuctionParams( + address, + startFequency, + clockStep, + priceLockPeriod, + ); + + console.log('ACTIONS voting for new auction params'); + await voteForNewParams(govAccounts, 0); + + console.log('ACTIONS wait for the vote deadline to pass'); + await new Promise(r => setTimeout(r, VOTING_WAIT_MS)); +}; + +export const proposeNewDebtCeiling = async ( + address: string, + debtLimit: number | bigint, +) => { + const charterAcceptOfferId = await agops.ec( + 'find-continuing-id', + '--for', + `${'charter\\ member\\ invitation'}`, + '--from', + address, + ); + + return executeOffer( + address, + paramChangeOfferGeneration(charterAcceptOfferId, 30, debtLimit), + ); +}; + +export const setDebtLimit = async ( + address: string, + debtLimit: number | bigint, +) => { + console.log('ACTIONS Setting debt limit'); + + await proposeNewDebtCeiling(address, debtLimit); + await voteForNewParams(govAccounts, 0); + + console.log('ACTIONS wait for the vote to pass'); + await new Promise(r => setTimeout(r, VOTING_WAIT_MS)); +}; diff --git a/a3p-integration/proposals/z:acceptance/package.json b/a3p-integration/proposals/z:acceptance/package.json index ab84f74b613..0c0432f19a5 100644 --- a/a3p-integration/proposals/z:acceptance/package.json +++ b/a3p-integration/proposals/z:acceptance/package.json @@ -1,6 +1,6 @@ { "agoricProposal": { - "type": "/cosmos.params.v1beta1.ParameterChangeProposal" + "type": "/agoric.swingset.CoreEvalProposal" }, "type": "module", "license": "Apache-2.0", diff --git a/a3p-integration/proposals/z:acceptance/scripts/test-vaults.mts b/a3p-integration/proposals/z:acceptance/scripts/test-vaults.mts new file mode 100755 index 00000000000..7fdbcef510a --- /dev/null +++ b/a3p-integration/proposals/z:acceptance/scripts/test-vaults.mts @@ -0,0 +1,55 @@ +#!/usr/bin/env tsx +/* eslint-disable @jessie.js/safe-await-separator */ + +import { + agoric, + GOV1ADDR, + GOV2ADDR, + newOfferId, +} from '@agoric/synthetic-chain'; +import assert from 'node:assert/strict'; +import { + implementNewAuctionParams, + ISTunit, + provisionWallet, + setDebtLimit, +} from '../lib/vaults.mjs'; + +const START_FREQUENCY = 600; // StartFrequency: 600s (auction runs every 10m) +const CLOCK_STEP = 20; // ClockStep: 20s (ensures auction completes in time) +const PRICE_LOCK_PERIOD = 300; +const oraclesAddresses = [GOV1ADDR, GOV2ADDR]; + +const oracles = [] as { address: string; id: string }[]; +for (const oracle of oraclesAddresses) { + const offerId = await newOfferId(); + oracles.push({ address: oracle, id: offerId }); +} + +console.log('Ensure user2 provisioned'); +await provisionWallet('user2'); + +console.log('Ensure auction params have changed'); +await implementNewAuctionParams( + GOV1ADDR, + oracles, + START_FREQUENCY, + CLOCK_STEP, + PRICE_LOCK_PERIOD, +); + +const govParams = await agoric.follow('-lF', ':published.auction.governance'); +assert.equal(govParams.current.ClockStep.value.relValue, CLOCK_STEP.toString()); +assert.equal( + govParams.current.StartFrequency.value.relValue, + START_FREQUENCY.toString(), +); + +console.log('Ensure debt ceiling changes'); +const limit = 45_000_000n; +await setDebtLimit(GOV1ADDR, limit); +const params = await agoric.follow( + '-lF', + ':published.vaultFactory.managers.manager0.governance', +); +assert.equal(params.current.DebtLimit.value.value, String(limit * ISTunit)); diff --git a/a3p-integration/proposals/z:acceptance/test.sh b/a3p-integration/proposals/z:acceptance/test.sh index 7e4b0145a04..0aade020e3b 100755 --- a/a3p-integration/proposals/z:acceptance/test.sh +++ b/a3p-integration/proposals/z:acceptance/test.sh @@ -1,5 +1,5 @@ #!/bin/bash - +set -ueo pipefail # Place here any test that should be executed using the executed proposal. # The effects of this step are not persisted in further proposal layers. @@ -10,6 +10,9 @@ yarn ava initial.test.js GLOBIGNORE=initial.test.js yarn ava ./*.test.js +npm install -g tsx +scripts/test-vaults.mts + ./create-kread-item-test.sh ./state-sync-snapshots-test.sh