From 1eb24640571a017767d741c3f8321df8cadc7d90 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Mon, 20 Sep 2021 16:02:33 -0700 Subject: [PATCH] feat!: use contractGovernor to govern Treasury using ParamManager extract params to a separate file integrate contract governance into treasury swingset test for treasury governance closes #3189 closes #3473 --- packages/treasury/bundles/install-on-chain.js | 68 +- packages/treasury/package.json | 4 +- packages/treasury/scripts/build-bundles.js | 12 + packages/treasury/src/params.js | 84 +++ packages/treasury/src/stablecoinMachine.js | 126 +++- packages/treasury/src/types.js | 40 +- packages/treasury/src/vault.js | 8 +- packages/treasury/src/vaultManager.js | 31 +- .../swingsetTests/governance/bootstrap.js | 242 ++++++ .../governance/test-governance.js | 103 +++ .../{ => governance}/vat-alice.js | 0 .../swingsetTests/governance/vat-owner.js | 138 ++++ .../{ => governance}/vat-priceAuthority.js | 0 .../swingsetTests/governance/vat-voter.js | 85 +++ .../test/swingsetTests/governance/vat-zoe.js | 24 + .../swingsetTests/{ => treasury}/bootstrap.js | 47 +- .../{ => treasury}/test-treasury.js | 12 +- .../test/swingsetTests/treasury/vat-alice.js | 60 ++ .../swingsetTests/{ => treasury}/vat-owner.js | 39 +- .../treasury/vat-priceAuthority.js | 11 + .../test/swingsetTests/treasury/vat-zoe.js | 22 + .../treasury/test/swingsetTests/vat-zoe.js | 13 - .../treasury/test/test-bootstrapPayment.js | 303 ++++++-- packages/treasury/test/test-stablecoin.js | 703 +++++++----------- .../treasury/test/vault-contract-wrapper.js | 17 +- packages/vats/src/bootstrap.js | 6 +- 26 files changed, 1583 insertions(+), 615 deletions(-) create mode 100644 packages/treasury/src/params.js create mode 100644 packages/treasury/test/swingsetTests/governance/bootstrap.js create mode 100644 packages/treasury/test/swingsetTests/governance/test-governance.js rename packages/treasury/test/swingsetTests/{ => governance}/vat-alice.js (100%) create mode 100644 packages/treasury/test/swingsetTests/governance/vat-owner.js rename packages/treasury/test/swingsetTests/{ => governance}/vat-priceAuthority.js (100%) create mode 100644 packages/treasury/test/swingsetTests/governance/vat-voter.js create mode 100644 packages/treasury/test/swingsetTests/governance/vat-zoe.js rename packages/treasury/test/swingsetTests/{ => treasury}/bootstrap.js (66%) rename packages/treasury/test/swingsetTests/{ => treasury}/test-treasury.js (79%) create mode 100644 packages/treasury/test/swingsetTests/treasury/vat-alice.js rename packages/treasury/test/swingsetTests/{ => treasury}/vat-owner.js (72%) create mode 100644 packages/treasury/test/swingsetTests/treasury/vat-priceAuthority.js create mode 100644 packages/treasury/test/swingsetTests/treasury/vat-zoe.js delete mode 100644 packages/treasury/test/swingsetTests/vat-zoe.js diff --git a/packages/treasury/bundles/install-on-chain.js b/packages/treasury/bundles/install-on-chain.js index f22af1a70be..facb5456d39 100644 --- a/packages/treasury/bundles/install-on-chain.js +++ b/packages/treasury/bundles/install-on-chain.js @@ -1,9 +1,16 @@ // @ts-check import { E } from '@agoric/eventual-send'; +import '../../governance/exported'; + import liquidateBundle from './bundle-liquidateMinimum.js'; import autoswapBundle from './bundle-multipoolAutoswap.js'; import stablecoinBundle from './bundle-stablecoinMachine.js'; +import contractGovernorBundle from './bundle-contractGovernor.js'; +import committeeRegistrarBundle from './bundle-committeeRegistrar.js'; +import binaryVoteCounterBundle from './bundle-binaryVoteCounter.js'; +import { governedParameterTerms } from '../src/params'; +import { Far } from '@agoric/marshal'; const SECONDS_PER_HOUR = 60n * 60n; const SECONDS_PER_DAY = 24n * SECONDS_PER_HOUR; @@ -58,11 +65,18 @@ export async function installOnChain({ ['liquidate', liquidateBundle], ['autoswap', autoswapBundle], ['stablecoin', stablecoinBundle], + ['contractGovernor', contractGovernorBundle], + ['committeeRegistrar', committeeRegistrarBundle], + ['binaryCounter', binaryVoteCounterBundle], + ]; const [ liquidationInstall, autoswapInstall, stablecoinMachineInstall, + contractGovernorInstall, + committeeRegistrarInstall, + binaryCounterInstall, ] = await Promise.all( nameBundles.map(async ([name, bundle]) => { // Install the bundle in Zoe. @@ -74,6 +88,15 @@ export async function installOnChain({ }), ); + const registrarTerms = { committeeName: 'TreasuryBoard', committeeSize: 5 }; + // The registrarCreatorFacet has `getVoterInvitations()`, which returns + // invitations for the people who can vote on changes to the treasury. We + // don't currently hand those out to anyone, but that is not visible on chain. + const { + creatorFacet: registrarCreatorFacet, + instance: registrarInstance, + } = await E(zoeWPurse).startInstance(committeeRegistrarInstall, {}, registrarTerms); + const loanParams = { chargingPeriod: SECONDS_PER_HOUR, recordingPeriod: SECONDS_PER_DAY, @@ -81,24 +104,36 @@ export async function installOnChain({ protocolFee, }; - const terms = harden({ + const treasuryTerms = harden({ autoswapInstall, liquidationInstall, priceAuthority, loanParams, timerService: chainTimerService, + governedParams: governedParameterTerms, bootstrapPaymentValue, }); + const governorTerms = harden({ + timer: chainTimerService, + registrarInstance, + governedContractInstallation: stablecoinMachineInstall, + governed: { + terms: treasuryTerms, + issuerKeywordRecord: {}, + privateArgs: harden({ feeMintAccess }), + }, + }); - const privateArgs = harden({ feeMintAccess }); - - const { instance, creatorFacet } = await E(zoeWPurse).startInstance( - stablecoinMachineInstall, + const { + creatorFacet: governorCreatorFacet, + } = await E(zoeWPurse).startInstance( + contractGovernorInstall, undefined, - terms, - privateArgs, + governorTerms, + harden({ registrarCreatorFacet }), ); + const treasuryInstance = await E(governorCreatorFacet).getInstance(); const [ ammInstance, invitationIssuer, @@ -106,10 +141,12 @@ export async function installOnChain({ issuers: { Governance: govIssuer, RUN: centralIssuer }, brands: { Governance: govBrand, RUN: centralBrand }, }, + treasuryCreator, ] = await Promise.all([ - E(creatorFacet).getAMM(), + E(E(governorCreatorFacet).getCreatorFacet()).getAMM(), E(zoeWPurse).getInvitationIssuer(), - E(zoeWPurse).getTerms(instance), + E(zoeWPurse).getTerms(treasuryInstance), + E(governorCreatorFacet).getCreatorFacet() ]); const treasuryUiDefaults = { @@ -123,12 +160,15 @@ export async function installOnChain({ // Look up all the board IDs. const boardIdValue = [ - ['INSTANCE_BOARD_ID', instance], + ['INSTANCE_BOARD_ID', treasuryInstance], ['INSTALLATION_BOARD_ID', stablecoinMachineInstall], ['RUN_ISSUER_BOARD_ID', centralIssuer], ['RUN_BRAND_BOARD_ID', centralBrand], ['AMM_INSTALLATION_BOARD_ID', autoswapInstall], ['LIQ_INSTALLATION_BOARD_ID', liquidationInstall], + ['BINARY_COUNTER_INSTALLATION_BOARD_ID', binaryCounterInstall], + ['COMMITTEE_REGISTRAR_INSTALLATION_BOARD_ID', committeeRegistrarInstall], + ['CONTRACT_GOVERNOR_INSTALLATION_BOARD_ID', contractGovernorInstall], ['AMM_INSTANCE_BOARD_ID', ammInstance], ['INVITE_BRAND_BOARD_ID', E(invitationIssuer).getBrand()], ]; @@ -146,7 +186,7 @@ export async function installOnChain({ // Install the names in agoricNames. const nameAdminUpdates = [ [uiConfigAdmin, treasuryUiDefaults.CONTRACT_NAME, treasuryUiDefaults], - [instanceAdmin, treasuryUiDefaults.CONTRACT_NAME, instance], + [instanceAdmin, treasuryUiDefaults.CONTRACT_NAME, treasuryInstance], [instanceAdmin, treasuryUiDefaults.AMM_NAME, ammInstance], [brandAdmin, 'TreasuryGovernance', govBrand], [issuerAdmin, 'TreasuryGovernance', govIssuer], @@ -159,5 +199,9 @@ export async function installOnChain({ ), ); - return creatorFacet; + const voteCreator = Far('treasury vote creator', { + voteOnParamChange: E(governorCreatorFacet).voteOnParamChange, + }); + + return { treasuryCreator, voteCreator }; } diff --git a/packages/treasury/package.json b/packages/treasury/package.json index ff9219bf41b..fd9a100c93e 100644 --- a/packages/treasury/package.json +++ b/packages/treasury/package.json @@ -42,7 +42,9 @@ "@agoric/promise-kit": "^0.2.26", "@agoric/store": "^0.6.4", "@agoric/swingset-vat": "^0.22.0", - "@agoric/zoe": "^0.19.0" + "@agoric/zoe": "^0.19.0", + "@agoric/governance": "^0.2.0", + "@agoric/same-structure": "^0.1.20" }, "devDependencies": { "@agoric/babel-standalone": "^7.14.3", diff --git a/packages/treasury/scripts/build-bundles.js b/packages/treasury/scripts/build-bundles.js index 8b13cfbe3de..ecf9502ee2e 100644 --- a/packages/treasury/scripts/build-bundles.js +++ b/packages/treasury/scripts/build-bundles.js @@ -42,6 +42,18 @@ async function main() { `@agoric/zoe/src/contracts/multipoolAutoswap/multipoolAutoswap.js`, `${dirname}/../bundles/bundle-multipoolAutoswap.js`, ], + [ + '@agoric/governance/src/contractGovernor.js', + `${dirname}/../bundles/bundle-contractGovernor.js`, + ], + [ + '@agoric/governance/src/committeeRegistrar.js', + `${dirname}/../bundles/bundle-committeeRegistrar.js`, + ], + [ + '@agoric/governance/src/binaryVoteCounter.js', + `${dirname}/../bundles/bundle-binaryVoteCounter.js`, + ], ]; for (const [contractFilename, outputPath] of contractOutputs) { // eslint-disable-next-line no-await-in-loop diff --git a/packages/treasury/src/params.js b/packages/treasury/src/params.js new file mode 100644 index 00000000000..a79feea918c --- /dev/null +++ b/packages/treasury/src/params.js @@ -0,0 +1,84 @@ +// @ts-check + +import { buildParamManager, ParamType } from '@agoric/governance'; + +export const POOL_FEE_KEY = 'PoolFee'; +export const PROTOCOL_FEE_KEY = 'ProtocolFee'; + +export const CHARGING_PERIOD_KEY = 'ChargingPeriod'; +export const RECORDING_PERIOD_KEY = 'RecordingPeriod'; + +export const INITIAL_MARGIN_KEY = 'InitialMargin'; +export const LIQUIDATION_MARGIN_KEY = 'LiquidationMargin'; +export const INTEREST_RATE_KEY = 'InterestRate'; +export const LOAN_FEE_KEY = 'LoanFee'; + +export const governedParameterTerms = { + loanParams: [POOL_FEE_KEY, PROTOCOL_FEE_KEY], + poolParams: [ + CHARGING_PERIOD_KEY, + RECORDING_PERIOD_KEY, + INITIAL_MARGIN_KEY, + LIQUIDATION_MARGIN_KEY, + INTEREST_RATE_KEY, + LOAN_FEE_KEY, + ], +}; + +/** @type {{ FEE: 'fee', POOL: 'pool' }} */ +export const ParamKey = { + FEE: 'fee', + POOL: 'pool', +}; + +export const makeFeeParamManager = loanParams => { + /** @type {FeeParamManager} */ + return buildParamManager([ + { + name: POOL_FEE_KEY, + value: loanParams.poolFee, + type: ParamType.NAT, + }, + { + name: PROTOCOL_FEE_KEY, + value: loanParams.protocolFee, + type: ParamType.NAT, + }, + ]); +}; + +export const makePoolParamManager = (loanParams, rates) => { + /** @type {PoolParamManager} */ + return buildParamManager([ + { + name: CHARGING_PERIOD_KEY, + value: loanParams.chargingPeriod, + type: ParamType.NAT, + }, + { + name: RECORDING_PERIOD_KEY, + value: loanParams.recordingPeriod, + type: ParamType.NAT, + }, + { + name: INITIAL_MARGIN_KEY, + value: rates.initialMargin, + type: ParamType.RATIO, + }, + { + name: LIQUIDATION_MARGIN_KEY, + value: rates.liquidationMargin, + type: ParamType.RATIO, + }, + { + name: INTEREST_RATE_KEY, + value: rates.interestRate, + type: ParamType.RATIO, + }, + { + name: LOAN_FEE_KEY, + value: rates.loanFee, + type: ParamType.RATIO, + }, + ]); +}; diff --git a/packages/treasury/src/stablecoinMachine.js b/packages/treasury/src/stablecoinMachine.js index c9cf1a919a6..d8ee05f21a7 100644 --- a/packages/treasury/src/stablecoinMachine.js +++ b/packages/treasury/src/stablecoinMachine.js @@ -1,5 +1,4 @@ // @ts-check -import { Far } from '@agoric/marshal'; import '@agoric/zoe/exported.js'; import '@agoric/zoe/src/contracts/exported.js'; @@ -19,8 +18,9 @@ import '@agoric/zoe/src/contracts/exported.js'; // can't redeem them outright, that would drain the utility from the economy. import { E } from '@agoric/eventual-send'; -import { assert, details, q } from '@agoric/assert'; -import { makeStore } from '@agoric/store'; +import '@agoric/governance/src/exported'; + +import makeStore from '@agoric/store'; import { assertProposalShape, offerTo, @@ -28,19 +28,45 @@ import { getAmountIn, } from '@agoric/zoe/src/contractSupport/index.js'; import { HIGH_FEE, LONG_EXP } from '@agoric/zoe/src/constants.js'; - import { multiplyBy, makeRatioFromAmounts, } from '@agoric/zoe/src/contractSupport/ratio.js'; import { AmountMath } from '@agoric/ertp'; +import { sameStructure } from '@agoric/same-structure'; +import { Far } from '@agoric/marshal'; + import { makeTracer } from './makeTracer.js'; import { makeVaultManager } from './vaultManager.js'; import { makeLiquidationStrategy } from './liquidateMinimum.js'; import { makeMakeCollectFeesInvitation } from './collectRewardFees.js'; +import { + makePoolParamManager, + makeFeeParamManager, + PROTOCOL_FEE_KEY, + POOL_FEE_KEY, + governedParameterTerms as governedParameterLocal, + ParamKey, +} from './params.js'; + +const { quote: q, details: X } = assert; const trace = makeTracer('ST'); +// The StableCoinMachine owns a number of VaultManagers, and a mint for the +// "RUN" stablecoin. This overarching SCM will hold ownershipTokens in the +// individual per-type vaultManagers. +// +// makeAddTypeInvitation is a closely held method that adds a brand new +// collateral type. It specifies the initial exchange rate for that type. +// +// a second closely held method (not implemented yet) would add collateral of a +// type for which there is an existing pool. It gets the current price from the +// pool. +// +// ownershipTokens for vaultManagers entitle holders to distributions, but you +// can't redeem them outright, that would drain the utility from the economy. + /** @type {ContractStartFn} */ export async function start(zcf, privateArgs) { // loanParams has time limits for charging interest @@ -51,22 +77,29 @@ export async function start(zcf, privateArgs) { timerService, liquidationInstall, bootstrapPaymentValue = 0n, + electionManager, + governedParams, } = zcf.getTerms(); + const governorPublic = E(zcf.getZoeService()).getPublicFacet(electionManager); const { feeMintAccess } = privateArgs; assert.typeof( loanParams.chargingPeriod, 'bigint', - details`chargingPeriod (${q(loanParams.chargingPeriod)}) must be a BigInt`, + X`chargingPeriod (${q(loanParams.chargingPeriod)}) must be a BigInt`, ); assert.typeof( loanParams.recordingPeriod, 'bigint', - details`recordingPeriod (${q( - loanParams.recordingPeriod, - )}) must be a BigInt`, + X`recordingPeriod (${q(loanParams.recordingPeriod)}) must be a BigInt`, + ); + + assert( + sameStructure(governedParams, harden(governedParameterLocal)), + X`Terms must match ${governedParameterLocal}`, ); + const feeParams = makeFeeParamManager(loanParams); const [runMint, govMint] = await Promise.all([ zcf.registerFeeMint('RUN', feeMintAccess), @@ -118,11 +151,15 @@ export async function start(zcf, privateArgs) { { Central: runIssuer }, { timer: timerService, - poolFee: loanParams.poolFee, - protocolFee: loanParams.protocolFee, + // TODO(hibbert): make the AMM use a paramManager. For now, the values + // are fixed after creation of an autoswap instance. + poolFee: feeParams.getParam(POOL_FEE_KEY).value, + protocolFee: feeParams.getParam(PROTOCOL_FEE_KEY).value, }, ); + const poolParamManagers = makeStore('brand'); // Brand -> poolGovernor + // We process only one offer per collateralType. They must tell us the // dollar value of their collateral, and we create that many RUN. // collateralKeyword = 'aEth' @@ -135,6 +172,10 @@ export async function start(zcf, privateArgs) { const collateralBrand = zcf.getBrandForIssuer(collateralIssuer); assert(!collateralTypes.has(collateralBrand)); + assert(!poolParamManagers.has(collateralBrand)); + const poolParamManager = makePoolParamManager(loanParams, rates); + poolParamManagers.init(collateralBrand, poolParamManager); + const { creatorFacet: liquidationFacet } = await E(zoe).startInstance( liquidationInstall, { RUN: runIssuer }, @@ -151,6 +192,7 @@ export async function start(zcf, privateArgs) { want: { Governance: _govOut }, // ownership of the whole stablecoin machine } = seat.getProposal(); assert(!collateralTypes.has(collateralBrand)); + // initialPrice is in rates, but only used at creation, so not in governor const runAmount = multiplyBy(collateralIn, rates.initialPrice); // arbitrarily, give governance tokens equal to RUN tokens const govAmount = AmountMath.make(runAmount.value, govBrand); @@ -227,10 +269,9 @@ export async function start(zcf, privateArgs) { runMint, collateralBrand, priceAuthority, - rates, + poolParamManager.getParams, reallocateReward, timerService, - loanParams, liquidationStrategy, ); collateralTypes.init(collateralBrand, vm); @@ -258,7 +299,7 @@ export async function start(zcf, privateArgs) { const { brand: brandIn } = collateralAmount; assert( collateralTypes.has(brandIn), - details`Not a supported collateral type ${brandIn}`, + X`Not a supported collateral type ${brandIn}`, ); /** @type {VaultManager} */ const mgr = collateralTypes.get(brandIn); @@ -331,7 +372,7 @@ export async function start(zcf, privateArgs) { if (expectedAmount) { assert( AmountMath.isGTE(bootstrapAmount, expectedAmount), - details`${bootstrapAmount} is not at least ${expectedAmount}`, + X`${bootstrapAmount} is not at least ${expectedAmount}`, ); } return bootstrapPayment; @@ -339,7 +380,29 @@ export async function start(zcf, privateArgs) { return getBootstrapPayment; } - const getBootstrapPayment = mintBootstrapPayment(); + const getParams = paramDesc => { + switch (paramDesc.key) { + case ParamKey.FEE: + return feeParams.getParams(); + case ParamKey.POOL: + return poolParamManagers.get(paramDesc.collateralBrand).getParams(); + default: + throw Error(`Unrecognized param key for Params '${paramDesc.key}'`); + } + }; + + const getParamState = paramDesc => { + switch (paramDesc.keyl) { + case ParamKey.FEE: + return feeParams.getParam(paramDesc.parameterName); + case ParamKey.POOL: + return poolParamManagers + .get(paramDesc.collateralBrand) + .getParam(paramDesc.parameterName); + default: + throw Error(`Unrecognized param key for State '${paramDesc.key}'`); + } + }; const publicFacet = Far('stablecoin public facet', { getAMM() { @@ -352,6 +415,9 @@ export async function start(zcf, privateArgs) { getRunIssuer() { return runIssuer; }, + getParamState, + getParams, + getContractGovernor: () => governorPublic, }); const { makeCollectFeesInvitation } = makeMakeCollectFeesInvitation( @@ -361,6 +427,22 @@ export async function start(zcf, privateArgs) { runBrand, ); + const getParamMgrRetriever = () => + Far('paramManagerAccessor', { + get: paramDesc => { + switch (paramDesc.key) { + case ParamKey.FEE: + return feeParams; + case ParamKey.POOL: + return poolParamManagers.get(paramDesc.collateralBrand); + default: + throw Error( + `Unrecognized param key for accessor '${paramDesc.key}'`, + ); + } + }, + }); + /** @type {StablecoinMachine} */ const stablecoinMachine = Far('stablecoin machine', { makeAddTypeInvitation, @@ -369,9 +451,19 @@ export async function start(zcf, privateArgs) { }, getCollaterals, getRewardAllocation, - getBootstrapPayment, + getBootstrapPayment: mintBootstrapPayment(), makeCollectFeesInvitation, + getContractGovernor: () => electionManager, + }); + + const stablecoinMachineWrapper = Far('powerful stablecoinMachine wrapper', { + getParamMgrRetriever, + getLimitedCreatorFacet: () => stablecoinMachine, }); - return harden({ creatorFacet: stablecoinMachine, publicFacet }); + return harden({ + creatorFacet: stablecoinMachineWrapper, + publicFacet, + ParamKey, + }); } diff --git a/packages/treasury/src/types.js b/packages/treasury/src/types.js index 532afc2571a..597f9c8044b 100644 --- a/packages/treasury/src/types.js +++ b/packages/treasury/src/types.js @@ -32,6 +32,10 @@ * @property {(collateralIssuer: Issuer, collateralKeyword: Keyword, rates: Rates) => Promise} makeAddTypeInvitation * @property {() => Instance} getAMM * @property {() => Promise>} getCollaterals + * @property {() => Allocation} getRewardAllocation, + * @property {() => ERef} getBootstrapPayment + * @property {(Brand) => Governor} getContractGovernor + * @property {() => Invitation} makeCollectFeesInvitation */ /** @@ -65,6 +69,10 @@ * @property {() => Promise} getCollateralQuote * @property {() => Ratio} getInitialMargin * @property {() => Ratio} getInterestRate - The annual interest rate on a loan + * @property {() => RelativeTime} getChargingPeriod - The period (in seconds) at + * which interest is charged to the loan. + * @property {() => RelativeTime} getRecordingPeriod - The period (in seconds) + * at which interest is recorded to the loan. * @property {ReallocateReward} reallocateReward */ @@ -117,6 +125,12 @@ * @property {RelativeTime} recordingPeriod */ +/** + * @typedef {Object} AMMFees + * @property {bigint} poolFee + * @property {bigint} protocolFee + */ + /** * @typedef {Object} LiquidationStrategy * @property {() => KeywordKeywordRecord} keywordMapping @@ -131,10 +145,9 @@ * @param {ZCFMint} runMint * @param {Brand} collateralBrand * @param {ERef} priceAuthority - * @param {Rates} rates - * @param {StageReward} rewardPoolStaging + * @param {GetParams} getLoanParams + * @param {ReallocateReward} reallocateReward * @param {TimerService} timerService - * @param {LoanParams} loanParams * @param {LiquidationStrategy} liquidationStrategy * @returns {VaultManager} */ @@ -146,7 +159,7 @@ * @param {ZCFMint} runMint * @param {ERef} autoswap * @param {ERef} priceAuthority - * @param {LoanParams} loanParams + * @param {GetParams} paramManager * @param {Timestamp} startTimeStamp * @returns {VaultKit} */ @@ -183,3 +196,22 @@ * @param {RelativeTime} recordingPeriod * @returns {CalculatorKit} */ + +/** + * @typedef {Object} FeeParamManager + * @property {() => Record} getParams + * @property {(bigint) => void} updateProtocolFee + * @property {(bigint) => void} updatePoolFee + */ + +/** + * @typedef {Object} PoolParamManager + * @property {() => Record} getParams + * @property {(bigint) => void} updateChargingPeriod + * @property {(bigint) => void} updateRecordingPeriod + * @property {(Ratio) => void} updateInitialMargin + * @property {(Ratio) => void} updateLiquidationMargin + * @property {(Ratio) => void} updateInitialPrice + * @property {(Ratio) => void} updateInterestRate + * @property {(Ratio) => void} updateLoanFee + */ diff --git a/packages/treasury/src/vault.js b/packages/treasury/src/vault.js index f1652f112e9..2e2bc5bcaf2 100644 --- a/packages/treasury/src/vault.js +++ b/packages/treasury/src/vault.js @@ -41,7 +41,7 @@ export function makeVaultKit( runMint, autoswap, priceAuthority, - loanParams, + loanParamManager, startTimeStamp, ) { const { updater: uiUpdater, notifier } = makeNotifierKit(); @@ -58,6 +58,8 @@ export function makeVaultKit( assert(vaultState === VaultState.ACTIVE, 'vault must still be active'); } + console.log(`VALT ${q(manager)}`); + const collateralBrand = manager.getCollateralBrand(); // timestamp of most recent update to interest let latestInterestUpdate = startTimeStamp; @@ -74,8 +76,8 @@ export function makeVaultKit( const interestCalculator = makeInterestCalculator( runBrand, manager.getInterestRate(), - loanParams.chargingPeriod, - loanParams.recordingPeriod, + manager.getChargingPeriod(), + manager.getRecordingPeriod(), ); function getCollateralAllocated(seat) { diff --git a/packages/treasury/src/vaultManager.js b/packages/treasury/src/vaultManager.js index 66dd3ca8027..838e14ac8db 100644 --- a/packages/treasury/src/vaultManager.js +++ b/packages/treasury/src/vaultManager.js @@ -19,6 +19,14 @@ import { makeVaultKit } from './vault.js'; import { makePrioritizedVaults } from './prioritizedVaults.js'; import { liquidate } from './liquidation.js'; import { makeTracer } from './makeTracer.js'; +import { + RECORDING_PERIOD_KEY, + LIQUIDATION_MARGIN_KEY, + INITIAL_MARGIN_KEY, + LOAN_FEE_KEY, + INTEREST_RATE_KEY, + CHARGING_PERIOD_KEY, +} from './params.js'; const { details: X } = assert; @@ -36,28 +44,35 @@ export function makeVaultManager( runMint, collateralBrand, priceAuthority, - rates, + getLoanParams, reallocateReward, timerService, - loanParams, liquidationStrategy, ) { const { brand: runBrand } = runMint.getIssuerRecord(); + const getLoanParamValue = key => getLoanParams()[key].value; + const shared = { // loans below this margin may be liquidated getLiquidationMargin() { - return rates.liquidationMargin; + return getLoanParamValue(LIQUIDATION_MARGIN_KEY); }, // loans must initially have at least 1.2x collateralization getInitialMargin() { - return rates.initialMargin; + return getLoanParamValue(INITIAL_MARGIN_KEY); }, getLoanFee() { - return rates.loanFee; + return getLoanParamValue(LOAN_FEE_KEY); }, getInterestRate() { - return rates.interestRate; + return getLoanParamValue(INTEREST_RATE_KEY); + }, + getChargingPeriod() { + return getLoanParamValue(CHARGING_PERIOD_KEY); + }, + getRecordingPeriod() { + return getLoanParamValue(RECORDING_PERIOD_KEY); }, async getCollateralQuote() { // get a quote for one unit of the collateral @@ -178,7 +193,7 @@ export function makeVaultManager( const periodNotifier = E(timerService).makeNotifier( 0n, - loanParams.recordingPeriod, + /** @type {bigint} */ (getLoanParamValue(RECORDING_PERIOD_KEY)), ); const { zcfSeat: poolIncrementSeat } = zcf.makeEmptySeatKit(); @@ -219,7 +234,7 @@ export function makeVaultManager( runMint, autoswap, priceAuthority, - loanParams, + getLoanParams, startTimeStamp, ); diff --git a/packages/treasury/test/swingsetTests/governance/bootstrap.js b/packages/treasury/test/swingsetTests/governance/bootstrap.js new file mode 100644 index 00000000000..d2fef30643f --- /dev/null +++ b/packages/treasury/test/swingsetTests/governance/bootstrap.js @@ -0,0 +1,242 @@ +// @ts-check + +import { E } from '@agoric/eventual-send'; +import { Far } from '@agoric/marshal'; +import { makeIssuerKit, AmountMath } from '@agoric/ertp'; +import buildManualTimer from '@agoric/zoe/tools/manualTimer'; +import { q } from '@agoric/assert'; +import { makeRatio } from '@agoric/zoe/src/contractSupport'; + +import { POOL_FEE_KEY, ParamKey, INTEREST_RATE_KEY } from '../../../src/params'; + +const ONE_DAY = 24n * 60n * 60n; +const BASIS_POINTS = 10000n; + +const setupBasicMints = () => { + const all = [makeIssuerKit('moola')]; + const mints = all.map(objs => objs.mint); + const issuers = all.map(objs => objs.issuer); + const brands = all.map(objs => objs.brand); + + return harden({ + mints, + issuers, + brands, + }); +}; + +const installContracts = async (zoe, cb) => { + const [ + liquidateMinimum, + autoswap, + treasury, + registrar, + counter, + governor, + ] = await Promise.all([ + E(zoe).install(cb.liquidateMinimum), + E(zoe).install(cb.autoswap), + E(zoe).install(cb.treasury), + E(zoe).install(cb.committeeRegistrar), + E(zoe).install(cb.binaryVoteCounter), + E(zoe).install(cb.contractGovernor), + ]); + + const installations = { + liquidateMinimum, + autoswap, + treasury, + registrar, + counter, + governor, + }; + return installations; +}; + +const startRegistrar = async (zoe, installations) => { + const registrarTerms = { committeeName: 'TwentyCommittee', committeeSize: 5 }; + const { + creatorFacet: registrarCreatorFacet, + instance: registrarInstance, + } = await E(zoe).startInstance(installations.registrar, {}, registrarTerms); + return { registrarCreatorFacet, registrarInstance }; +}; + +const createCommittee = async (registrarCreatorFacet, voterCreator) => { + const invitations = await E(registrarCreatorFacet).getVoterInvitations(); + + const floraP = E(voterCreator).createVoter('Flora', invitations[0]); + const bobP = E(voterCreator).createVoter('Bob', invitations[1]); + const carolP = E(voterCreator).createVoter('Carol', invitations[2]); + const daveP = E(voterCreator).createVoter('Dave', invitations[3]); + const emmaP = E(voterCreator).createVoter('Emma', invitations[4]); + return Promise.all([bobP, carolP, daveP, emmaP, floraP]); +}; + +const votersVote = async (detailsP, votersP, selections) => { + const [voters, { positions, questionHandle }] = await Promise.all([ + votersP, + detailsP, + ]); + + await Promise.all( + voters.map((v, i) => { + return E(v).castBallotFor(questionHandle, positions[selections[i]]); + }), + ); +}; + +const oneVoterValidate = async ( + votersP, + details, + governedInstanceP, + registrarInstance, + governorInstanceP, +) => { + const [voters, governedInstance, governorInstance] = await Promise.all([ + votersP, + governedInstanceP, + governorInstanceP, + ]); + const { counterInstance, issue } = await details; + + E(voters[0]).validate( + counterInstance, + governedInstance, + registrarInstance, + governorInstance, + issue, + ); +}; + +const setUpVote = async ( + newValue, + deadline, + votersP, + votes, + paramDesc, + contracts, +) => { + const { treasury, installations, registrarInstance, governor } = contracts; + + const { details: feeDetails } = await E( + treasury.voteCreator, + ).voteOnParamChange(paramDesc, newValue, installations.counter, deadline); + await votersVote(feeDetails, votersP, votes); + + await oneVoterValidate( + votersP, + feeDetails, + treasury.instance, + registrarInstance, + governor.instance, + ); + return E.get(feeDetails).counterInstance; +}; + +const makeBootstrap = (argv, cb, vatPowers) => async (vats, devices) => { + const log = vatPowers.testLog; + const vatAdminSvc = await E(vats.vatAdmin).createVatAdminService( + devices.vatAdmin, + ); + const { zoe, feeMintAccess } = await E(vats.zoe).buildZoe(vatAdminSvc); + + const installations = await installContracts(zoe, cb); + const voterCreator = E(vats.voter).build(zoe); + + const [testName, startingValues] = argv; + const timer = buildManualTimer(console.log, 0n, ONE_DAY); + const { mints, issuers, brands } = setupBasicMints(); + const makePayments = values => + mints.map((mint, i) => + mint.mintPayment(AmountMath.make(values[i], brands[i])), + ); + const [aliceValues, ownerValues] = startingValues; + + const { registrarCreatorFacet, registrarInstance } = await startRegistrar( + zoe, + installations, + ); + + const votersP = createCommittee(registrarCreatorFacet, voterCreator); + log(`=> voter and registrar vats are set up`); + + // Setup Alice + const aliceP = E(vats.alice).build( + zoe, + brands, + makePayments(aliceValues), + timer, + ); + + // Setup Owner + const { governor, governed: treasury, runBrand } = await E(vats.owner).build( + zoe, + issuers, + brands, + makePayments(ownerValues), + installations, + timer, + vats.priceAuthority, + feeMintAccess, + registrarInstance, + registrarCreatorFacet, + ); + log(`=> alice and the treasury are set up`); + + const feeParamsStateAnte = await E(treasury.publicFacet).getParams({ + key: ParamKey.FEE, + }); + log(`param values before ${q(feeParamsStateAnte)}`); + + const contracts = { treasury, installations, registrarInstance, governor }; + const votes = [0, 1, 1, 0, 0]; + + const fees = { + key: ParamKey.FEE, + parameterName: POOL_FEE_KEY, + }; + + const counter = await setUpVote( + 37n, + 3n * ONE_DAY, + votersP, + votes, + fees, + contracts, + ); + + const poolParams = { + key: ParamKey.POOL, + parameterName: INTEREST_RATE_KEY, + collateralBrand: brands[0], + }; + const newRate = makeRatio(500n, runBrand, BASIS_POINTS); + + await setUpVote(newRate, 3n * ONE_DAY, votersP, votes, poolParams, contracts); + + E(E(zoe).getPublicFacet(counter)) + .getOutcome() + .then(async outcome => { + const feeParamsStatePost = await E(treasury.publicFacet).getParams({ + key: ParamKey.FEE, + }); + log( + `param values after vote on (${outcome.changeParam.parameterName}) ${q( + feeParamsStatePost, + )}`, + ); + }) + .catch(e => log(`BOOT fail: ${e}`)); + + await E(timer).tick(); + await E(timer).tick(); + await E(timer).tick(); + + await E(aliceP).startTest(testName, treasury.publicFacet); +}; + +export function buildRootObject(vatPowers, vatParameters) { + const { argv, contractBundles: cb } = vatParameters; + return Far('root', { bootstrap: makeBootstrap(argv, cb, vatPowers) }); +} diff --git a/packages/treasury/test/swingsetTests/governance/test-governance.js b/packages/treasury/test/swingsetTests/governance/test-governance.js new file mode 100644 index 00000000000..d9c57f698bc --- /dev/null +++ b/packages/treasury/test/swingsetTests/governance/test-governance.js @@ -0,0 +1,103 @@ +// @ts-check + +// TODO Remove babel-standalone preinitialization +// https://github.com/endojs/endo/issues/768 +import '@agoric/babel-standalone'; +// eslint-disable-next-line import/no-extraneous-dependencies +import '@agoric/install-ses'; +// eslint-disable-next-line import/no-extraneous-dependencies +import rawTest from 'ava'; +import { buildVatController, buildKernelBundles } from '@agoric/swingset-vat'; +import bundleSource from '@agoric/bundle-source'; +import { E } from '@agoric/eventual-send'; +import path from 'path'; + +import liquidateBundle from '../../../bundles/bundle-liquidateMinimum.js'; +import autoswapBundle from '../../../bundles/bundle-multipoolAutoswap.js'; +import stablecoinBundle from '../../../bundles/bundle-stablecoinMachine.js'; +import contractGovernorBundle from '../../../bundles/bundle-contractGovernor.js'; +import committeeRegistrarBundle from '../../../bundles/bundle-committeeRegistrar.js'; +import binaryVoteCounterBundle from '../../../bundles/bundle-binaryVoteCounter.js'; + +const filename = new URL(import.meta.url).pathname; +const dirname = path.dirname(filename); + +/** @type {import('ava').TestInterface<{ data: { kernelBundles: any, config: any } }>} */ +const test = rawTest; + +test.before(async t => { + const kernelBundles = await buildKernelBundles(); + + const nameToBundle = [ + ['liquidateMinimum', liquidateBundle], + ['autoswap', autoswapBundle], + ['treasury', stablecoinBundle], + ['committeeRegistrar', committeeRegistrarBundle], + ['contractGovernor', contractGovernorBundle], + ['binaryVoteCounter', binaryVoteCounterBundle], + ]; + const contractBundles = {}; + nameToBundle.forEach(([name, bundle]) => { + contractBundles[name] = bundle; + }); + + const vatNames = ['alice', 'owner', 'priceAuthority', 'voter', 'zoe']; + const vatNameToSource = vatNames.map(name => { + const source = `${dirname}/vat-${name}.js`; + return [name, source]; + }); + const bootstrapSource = `${dirname}/bootstrap.js`; + vatNameToSource.push(['bootstrap', bootstrapSource]); + + const bundles = await Promise.all( + vatNameToSource.map(([_, source]) => bundleSource(source)), + ); + const vats = {}; + [...vatNames, 'bootstrap'].forEach( + (name, index) => (vats[name] = { bundle: bundles[index] }), + ); + + vats.bootstrap.parameters = { contractBundles }; + + const config = { bootstrap: 'bootstrap', vats }; + config.defaultManagerType = 'xs-worker'; + + t.context.data = { kernelBundles, config }; +}); + +async function main(t, argv) { + const { kernelBundles, config } = t.context.data; + const controller = buildVatController(config, argv, { kernelBundles }); + await E(controller).run(); + return E(controller).dump(); +} + +const expectedTreasuryLog = [ + '=> voter and registrar vats are set up', + '=> alice and the treasury are set up', + 'param values before {"PoolFee":{"name":"PoolFee","type":"nat","value":"[24n]"},"ProtocolFee":{"name":"ProtocolFee","type":"nat","value":"[6n]"}}', + 'Voter Bob cast a ballot for {"changeParam":{"key":"fee","parameterName":"PoolFee"},"proposedValue":"[37n]"}', + 'Voter Carol cast a ballot for {"noChange":{"key":"fee","parameterName":"PoolFee"}}', + 'Voter Dave cast a ballot for {"noChange":{"key":"fee","parameterName":"PoolFee"}}', + 'Voter Emma cast a ballot for {"changeParam":{"key":"fee","parameterName":"PoolFee"},"proposedValue":"[37n]"}', + 'Voter Flora cast a ballot for {"changeParam":{"key":"fee","parameterName":"PoolFee"},"proposedValue":"[37n]"}', + 'governor from governed matches governor instance', + 'Param "PoolFee" is in the question', + 'Voter Bob cast a ballot for {"changeParam":{"collateralBrand":"[Alleged: moola brand]","key":"pool","parameterName":"InterestRate"},"proposedValue":{"denominator":{"brand":"[Alleged: RUN brand]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[500n]"}}}', + 'Voter Carol cast a ballot for {"noChange":{"collateralBrand":"[Alleged: moola brand]","key":"pool","parameterName":"InterestRate"}}', + 'Voter Dave cast a ballot for {"noChange":{"collateralBrand":"[Alleged: moola brand]","key":"pool","parameterName":"InterestRate"}}', + 'Voter Emma cast a ballot for {"changeParam":{"collateralBrand":"[Alleged: moola brand]","key":"pool","parameterName":"InterestRate"},"proposedValue":{"denominator":{"brand":"[Alleged: RUN brand]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[500n]"}}}', + 'Voter Flora cast a ballot for {"changeParam":{"collateralBrand":"[Alleged: moola brand]","key":"pool","parameterName":"InterestRate"},"proposedValue":{"denominator":{"brand":"[Alleged: RUN brand]","value":"[10000n]"},"numerator":{"brand":"[Seen]","value":"[500n]"}}}', + '=> alice.oneLoanWithInterest called', + 'param values after vote on (PoolFee) {"PoolFee":{"name":"PoolFee","type":"nat","value":"[37n]"},"ProtocolFee":{"name":"ProtocolFee","type":"nat","value":"[6n]"}}', + 'governor from governed matches governor instance', + 'Param "InterestRate" is in the question', + 'Alice owes {"brand":"[Alleged: RUN brand]","value":"[510000n]"} after borrowing', + 'Alice owes {"brand":"[Alleged: RUN brand]","value":"[510068n]"} after interest', +]; + +test.serial('treasury', async t => { + const startingValues = [[100], [1000]]; + const dump = await main(t, ['oneLoanWithInterest', startingValues]); + t.deepEqual(dump.log, expectedTreasuryLog); +}); diff --git a/packages/treasury/test/swingsetTests/vat-alice.js b/packages/treasury/test/swingsetTests/governance/vat-alice.js similarity index 100% rename from packages/treasury/test/swingsetTests/vat-alice.js rename to packages/treasury/test/swingsetTests/governance/vat-alice.js diff --git a/packages/treasury/test/swingsetTests/governance/vat-owner.js b/packages/treasury/test/swingsetTests/governance/vat-owner.js new file mode 100644 index 00000000000..ea3337a34d7 --- /dev/null +++ b/packages/treasury/test/swingsetTests/governance/vat-owner.js @@ -0,0 +1,138 @@ +// @ts-check + +import { E } from '@agoric/eventual-send'; +import { Far } from '@agoric/marshal'; +import { makeRatio } from '@agoric/zoe/src/contractSupport'; +import { AmountMath } from '@agoric/ertp'; +import { governedParameterTerms } from '../../../src/params'; + +const SECONDS_PER_HOUR = 60n * 60n; +const SECONDS_PER_DAY = 24n * SECONDS_PER_HOUR; +const BASIS_POINTS = 10000n; + +// Treasury owner + +const build = async ( + log, + zoe, + issuers, + brands, + payments, + installations, + timer, + priceAuthorityVat, + feeMintAccess, + registrarInstance, + registrarCreatorFacet, +) => { + const [moolaBrand] = brands; + const [moolaPayment] = payments; + const [moolaIssuer] = issuers; + + const loanParams = { + chargingPeriod: SECONDS_PER_DAY, + recordingPeriod: SECONDS_PER_DAY, + poolFee: 24n, + protocolFee: 6n, + }; + + const priceAuthorityKit = await E(priceAuthorityVat).makePriceAuthority(); + const terms = harden({ + autoswapInstall: installations.autoswap, + priceAuthority: priceAuthorityKit.priceAuthority, + loanParams, + liquidationInstall: installations.liquidateMinimum, + timerService: timer, + governedParams: governedParameterTerms, + }); + const privateArgs = harden({ registrarCreatorFacet }); + const privateTreasuryArgs = { feeMintAccess }; + + const governorFacets = await E(zoe).startInstance( + installations.governor, + {}, + { + timer, + registrarInstance, + governedContractInstallation: installations.treasury, + governed: { + terms, + issuerKeywordRecord: {}, + privateArgs: privateTreasuryArgs, + }, + }, + privateArgs, + ); + + const governedInstance = await E(governorFacets.creatorFacet).getInstance(); + const governedPublicFacet = E(zoe).getPublicFacet(governedInstance); + + const { + issuers: { RUN: runIssuer }, + brands: { Governance: govBrand, RUN: runBrand }, + } = await E(zoe).getTerms(governedInstance); + + const rates = { + initialPrice: makeRatio(10000n, runBrand, 5n, moolaBrand), + initialMargin: makeRatio(120n, runBrand), + liquidationMargin: makeRatio(105n, runBrand), + interestRate: makeRatio(250n, runBrand, BASIS_POINTS), + loanFee: makeRatio(200n, runBrand, BASIS_POINTS), + }; + + const addTypeInvitation = await E( + // get the governed creatorFacet from the governor + E(governorFacets.creatorFacet).getCreatorFacet(), + ).makeAddTypeInvitation(moolaIssuer, 'Moola', rates); + const proposal = harden({ + give: { + Collateral: AmountMath.make(moolaBrand, 1000n), + }, + want: { Governance: AmountMath.makeEmpty(govBrand) }, + }); + + const seat = await E(zoe).offer( + addTypeInvitation, + proposal, + harden({ Collateral: moolaPayment }), + ); + await E(seat).getOfferResult(); + + const QUOTE_INTERVAL = 24n * 60n * 60n; + const moolaPriceAuthority = await E(priceAuthorityVat).makeFakePriceAuthority( + harden({ + issuerIn: moolaIssuer, + issuerOut: runIssuer, + actualBrandIn: moolaBrand, + actualBrandOut: runBrand, + priceList: [100000n, 120000n, 110000n, 80000n], + timer, + quoteInterval: QUOTE_INTERVAL, + }), + ); + + await E(priceAuthorityKit.adminFacet).registerPriceAuthority( + moolaPriceAuthority, + moolaBrand, + runBrand, + ); + + const voteCreator = Far('treasury vote creator', { + voteOnParamChange: E(governorFacets.creatorFacet).voteOnParamChange, + }); + + const governed = { + instance: governedInstance, + creatorFacet: E(governorFacets.creatorFacet).getCreatorFacet(), + publicFacet: governedPublicFacet, + voteCreator, + }; + + return { governor: governorFacets, governed, runBrand }; +}; + +export function buildRootObject(vatPowers) { + return Far('root', { + build: (...args) => build(vatPowers.testLog, ...args), + }); +} diff --git a/packages/treasury/test/swingsetTests/vat-priceAuthority.js b/packages/treasury/test/swingsetTests/governance/vat-priceAuthority.js similarity index 100% rename from packages/treasury/test/swingsetTests/vat-priceAuthority.js rename to packages/treasury/test/swingsetTests/governance/vat-priceAuthority.js diff --git a/packages/treasury/test/swingsetTests/governance/vat-voter.js b/packages/treasury/test/swingsetTests/governance/vat-voter.js new file mode 100644 index 00000000000..48b57c86584 --- /dev/null +++ b/packages/treasury/test/swingsetTests/governance/vat-voter.js @@ -0,0 +1,85 @@ +// @ts-check + +import { E } from '@agoric/eventual-send'; +import { Far } from '@agoric/marshal'; +import { q } from '@agoric/assert'; +import { sameStructure } from '@agoric/same-structure'; +import { validateQuestionFromCounter } from '@agoric/governance/src/contractGovernor'; +import { assertContractRegistrar } from '@agoric/governance/src/validators'; +import { assertBallotConcernsQuestion } from '@agoric/governance/src/governParam'; + +const build = async (log, zoe) => { + return Far('voter', { + createVoter: async (name, invitation) => { + const seat = E(zoe).offer(invitation); + const voteFacet = E(seat).getOfferResult(); + + return Far(`Voter ${name}`, { + castBallotFor: async (handle, choice) => { + log(`Voter ${name} cast a ballot for ${q(choice)}`); + return E(voteFacet).castBallotFor(handle, [choice]); + }, + validate: async ( + counterInstance, + governedInstance, + registrarInstance, + governorInstance, + issue, + ) => { + // I'd like to validate Installations, but there doesn't seem to be a + // way to get it from an Instance. I'd verify the Registrar, + // ballotCounter, and contractGovernor. + const governedTermsP = E(zoe).getTerms(governedInstance); + const electionManagerP = E.get(governedTermsP).electionManager; + + const counterPublicP = E(zoe).getPublicFacet(counterInstance); + const ballotDetailsP = E(counterPublicP).getDetails(); + + const [electionManager, ballotDetails] = await Promise.all([ + electionManagerP, + ballotDetailsP, + ]); + + await validateQuestionFromCounter( + zoe, + registrarInstance, + counterInstance, + ); + + const governorMatches = electionManager === governorInstance; + log( + `governor from governed ${ + governorMatches ? 'matches' : 'does not match' + } governor instance`, + ); + + const included = sameStructure( + ballotDetails.issue.paramSpec, + issue.paramSpec, + ); + log( + `Param "${issue.paramSpec.parameterName}" ${ + included ? 'is' : 'is not' + } in the question`, + ); + + assertBallotConcernsQuestion( + issue.paramSpec.parameterName, + ballotDetails, + ); + + await assertContractRegistrar( + zoe, + governorInstance, + registrarInstance, + ); + }, + }); + }, + }); +}; + +export const buildRootObject = vatPowers => + Far('root', { + build: (...args) => build(vatPowers.testLog, ...args), + }); diff --git a/packages/treasury/test/swingsetTests/governance/vat-zoe.js b/packages/treasury/test/swingsetTests/governance/vat-zoe.js new file mode 100644 index 00000000000..f7c05dabada --- /dev/null +++ b/packages/treasury/test/swingsetTests/governance/vat-zoe.js @@ -0,0 +1,24 @@ +// @ts-check + +import { Far } from '@agoric/marshal'; + +// noinspection ES6PreferShortImport +import { E } from '@agoric/eventual-send'; +import { makeZoeKit } from '@agoric/zoe'; + +export function buildRootObject(vatPowers) { + return Far('root', { + buildZoe: vatAdminSvc => { + const shutdownZoeVat = vatPowers.exitVatWithFailure; + const { zoeService, feeMintAccess } = makeZoeKit( + vatAdminSvc, + shutdownZoeVat, + ); + const feePurse = E(zoeService).makeFeePurse(); + const zoe = E(zoeService).bindDefaultFeePurse(feePurse); + + console.log(`ZOE Vat ${zoe}`); + return { zoe, feeMintAccess }; + }, + }); +} diff --git a/packages/treasury/test/swingsetTests/bootstrap.js b/packages/treasury/test/swingsetTests/treasury/bootstrap.js similarity index 66% rename from packages/treasury/test/swingsetTests/bootstrap.js rename to packages/treasury/test/swingsetTests/treasury/bootstrap.js index 1dcf68c00d7..cf9a9aad1df 100644 --- a/packages/treasury/test/swingsetTests/bootstrap.js +++ b/packages/treasury/test/swingsetTests/treasury/bootstrap.js @@ -20,7 +20,16 @@ const setupBasicMints = () => { }); }; -const makeVats = ( +const startRegistrar = async (zoe, installations) => { + const registrarTerms = { committeeName: 'TwentyCommittee', committeeSize: 5 }; + const { + creatorFacet: committeeCreator, + instance: registrarInstance, + } = await E(zoe).startInstance(installations.registrar, {}, registrarTerms); + return { committeeCreator, registrarInstance }; +}; + +const makeVats = async ( log, vats, zoe, @@ -44,6 +53,11 @@ const makeVats = ( timer, ); + const { committeeCreator, registrarInstance } = await startRegistrar( + zoe, + installations, + ); + // Setup Owner const treasuryPublicFacet = E(vats.owner).build( zoe, @@ -54,6 +68,8 @@ const makeVats = ( timer, vats.priceAuthority, feeMintAccess, + committeeCreator, + registrarInstance, ); const result = { aliceP, treasuryPublicFacet }; @@ -67,21 +83,34 @@ function makeBootstrap(argv, cb, vatPowers) { const vatAdminSvc = await E(vats.vatAdmin).createVatAdminService( devices.vatAdmin, ); - const { zoeService, feeMintAccess } = await E(vats.zoe).buildZoe( - vatAdminSvc, - ); - const feePurse = E(zoeService).makeFeePurse(); - const zoe = E(zoeService).bindDefaultFeePurse(feePurse); - const [liquidateMinimum, autoswap, treasury] = await Promise.all([ + const { zoe, feeMintAccess } = await E(vats.zoe).buildZoe(vatAdminSvc); + const [ + liquidateMinimum, + autoswap, + treasury, + registrar, + counter, + governor, + ] = await Promise.all([ E(zoe).install(cb.liquidateMinimum), E(zoe).install(cb.autoswap), E(zoe).install(cb.treasury), + E(zoe).install(cb.committeeRegistrar), + E(zoe).install(cb.binaryVoteCounter), + E(zoe).install(cb.contractGovernor), ]); - const installations = { liquidateMinimum, autoswap, treasury }; + const installations = { + liquidateMinimum, + autoswap, + treasury, + registrar, + counter, + governor, + }; const [testName, startingValues] = argv; - const { aliceP, treasuryPublicFacet } = makeVats( + const { aliceP, treasuryPublicFacet } = await makeVats( vatPowers.testLog, vats, zoe, diff --git a/packages/treasury/test/swingsetTests/test-treasury.js b/packages/treasury/test/swingsetTests/treasury/test-treasury.js similarity index 79% rename from packages/treasury/test/swingsetTests/test-treasury.js rename to packages/treasury/test/swingsetTests/treasury/test-treasury.js index a8d3865f29c..3d252c2b287 100644 --- a/packages/treasury/test/swingsetTests/test-treasury.js +++ b/packages/treasury/test/swingsetTests/treasury/test-treasury.js @@ -12,9 +12,12 @@ import { buildVatController, buildKernelBundles } from '@agoric/swingset-vat'; import bundleSource from '@agoric/bundle-source'; import { E } from '@agoric/eventual-send'; -import liquidateBundle from '../../bundles/bundle-liquidateMinimum.js'; -import autoswapBundle from '../../bundles/bundle-multipoolAutoswap.js'; -import stablecoinBundle from '../../bundles/bundle-stablecoinMachine.js'; +import liquidateBundle from '../../../bundles/bundle-liquidateMinimum.js'; +import autoswapBundle from '../../../bundles/bundle-multipoolAutoswap.js'; +import stablecoinBundle from '../../../bundles/bundle-stablecoinMachine.js'; +import committeeRegistrarBundle from '../../../bundles/bundle-committeeRegistrar.js'; +import contractGovernorBundle from '../../../bundles/bundle-contractGovernor.js'; +import binaryVoteCounterBundle from '../../../bundles/bundle-binaryVoteCounter.js'; const filename = new URL(import.meta.url).pathname; const dirname = path.dirname(filename); @@ -29,6 +32,9 @@ test.before(async t => { ['liquidateMinimum', liquidateBundle], ['autoswap', autoswapBundle], ['treasury', stablecoinBundle], + ['committeeRegistrar', committeeRegistrarBundle], + ['contractGovernor', contractGovernorBundle], + ['binaryVoteCounter', binaryVoteCounterBundle], ]; const contractBundles = {}; nameToBundle.forEach(([name, bundle]) => { diff --git a/packages/treasury/test/swingsetTests/treasury/vat-alice.js b/packages/treasury/test/swingsetTests/treasury/vat-alice.js new file mode 100644 index 00000000000..6e7e97b69b3 --- /dev/null +++ b/packages/treasury/test/swingsetTests/treasury/vat-alice.js @@ -0,0 +1,60 @@ +// @ts-check + +import { E } from '@agoric/eventual-send'; +import { Far } from '@agoric/marshal'; +import { assert, details as X, q } from '@agoric/assert'; +import { AmountMath } from '@agoric/ertp'; + +const build = async (log, zoe, brands, payments, timer) => { + const [moolaBrand] = brands; + const [moolaPayment] = payments; + + const oneLoanWithInterest = async treasury => { + log(`=> alice.oneLoanWithInterest called`); + + console.log(`ALICE ${treasury}`); + + const t = await treasury; + console.log(`ALICE t: ${t}`); + + const runIssuer = await E(treasury).getRunIssuer(); + const runBrand = await E(runIssuer).getBrand(); + + console.log(`ALICE ${runIssuer}`); + + const loanSeat = await E(zoe).offer( + E(treasury).makeLoanInvitation(), + harden({ + give: { Collateral: AmountMath.make(moolaBrand, 100n) }, + want: { RUN: AmountMath.make(runBrand, 500000n) }, + }), + harden({ + Collateral: moolaPayment, + }), + ); + + const { vault, _liquidationPayout } = await E(loanSeat).getOfferResult(); + log(`Alice owes ${q(await E(vault).getDebtAmount())} after borrowing`); + await E(timer).tick(); + log(`Alice owes ${q(await E(vault).getDebtAmount())} after interest`); + }; + + return Far('build', { + startTest: async (testName, treasury) => { + switch (testName) { + case 'oneLoanWithInterest': { + return oneLoanWithInterest(treasury); + } + default: { + assert.fail(X`testName ${testName} not recognized`); + } + } + }, + }); +}; + +export function buildRootObject(vatPowers) { + return Far('root', { + build: (...args) => build(vatPowers.testLog, ...args), + }); +} diff --git a/packages/treasury/test/swingsetTests/vat-owner.js b/packages/treasury/test/swingsetTests/treasury/vat-owner.js similarity index 72% rename from packages/treasury/test/swingsetTests/vat-owner.js rename to packages/treasury/test/swingsetTests/treasury/vat-owner.js index 3d220f0517b..c7c5ea14082 100644 --- a/packages/treasury/test/swingsetTests/vat-owner.js +++ b/packages/treasury/test/swingsetTests/treasury/vat-owner.js @@ -4,6 +4,7 @@ import { E } from '@agoric/eventual-send'; import { Far } from '@agoric/marshal'; import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js'; import { AmountMath } from '@agoric/ertp'; +import { governedParameterTerms } from '../../../src/params'; const SECONDS_PER_HOUR = 60n * 60n; const SECONDS_PER_DAY = 24n * SECONDS_PER_HOUR; @@ -21,6 +22,8 @@ const build = async ( timer, priceAuthorityVat, feeMintAccess, + committeeCreator, + registrarInstance, ) => { const [moolaBrand] = brands; const [moolaPayment] = payments; @@ -40,19 +43,33 @@ const build = async ( loanParams, liquidationInstall: installations.liquidateMinimum, timerService: timer, + governedParams: governedParameterTerms, }); + const privateTreasuryArgs = { feeMintAccess }; - const { publicFacet, creatorFacet, instance } = await E(zoe).startInstance( - installations.treasury, - undefined, - terms, - harden({ feeMintAccess }), + const { creatorFacet: governor } = await E(zoe).startInstance( + installations.governor, + {}, + { + timer, + registrarInstance, + governedContractInstallation: installations.treasury, + governed: { + terms, + issuerKeywordRecord: {}, + privateArgs: privateTreasuryArgs, + }, + }, + { registrarCreatorFacet: committeeCreator }, ); + const governedInstance = await E(governor).getInstance(); + const governedPublicFacet = E(zoe).getPublicFacet(governedInstance); + const { issuers: { RUN: runIssuer }, brands: { Governance: govBrand, RUN: runBrand }, - } = await E(zoe).getTerms(instance); + } = await E(zoe).getTerms(governedInstance); const rates = { initialPrice: makeRatio(10000n, runBrand, 5n, moolaBrand), @@ -62,11 +79,9 @@ const build = async ( loanFee: makeRatio(200n, runBrand, BASIS_POINTS), }; - const addTypeInvitation = await E(creatorFacet).makeAddTypeInvitation( - moolaIssuer, - 'Moola', - rates, - ); + const addTypeInvitation = await E( + E(governor).getCreatorFacet(), + ).makeAddTypeInvitation(moolaIssuer, 'Moola', rates); const proposal = harden({ give: { Collateral: AmountMath.make(moolaBrand, 1000n), @@ -100,7 +115,7 @@ const build = async ( runBrand, ); - return publicFacet; + return governedPublicFacet; }; export function buildRootObject(vatPowers) { diff --git a/packages/treasury/test/swingsetTests/treasury/vat-priceAuthority.js b/packages/treasury/test/swingsetTests/treasury/vat-priceAuthority.js new file mode 100644 index 00000000000..521b56c583c --- /dev/null +++ b/packages/treasury/test/swingsetTests/treasury/vat-priceAuthority.js @@ -0,0 +1,11 @@ +import { Far } from '@agoric/marshal'; +import { makePriceAuthorityRegistry } from '@agoric/zoe/tools/priceAuthorityRegistry'; +import { makeScriptedPriceAuthority } from '@agoric/zoe/tools/scriptedPriceAuthority'; + +export function buildRootObject(_vatPowers) { + return Far('root', { + makePriceAuthority: makePriceAuthorityRegistry, + makeFakePriceAuthority: async options => + makeScriptedPriceAuthority(options), + }); +} diff --git a/packages/treasury/test/swingsetTests/treasury/vat-zoe.js b/packages/treasury/test/swingsetTests/treasury/vat-zoe.js new file mode 100644 index 00000000000..37d9144a093 --- /dev/null +++ b/packages/treasury/test/swingsetTests/treasury/vat-zoe.js @@ -0,0 +1,22 @@ +// @ts-check + +import { Far } from '@agoric/marshal'; + +// noinspection ES6PreferShortImport +import { E } from '@agoric/eventual-send'; +import { makeZoeKit } from '@agoric/zoe'; + +export function buildRootObject(vatPowers) { + return Far('root', { + buildZoe: vatAdminSvc => { + const shutdownZoeVat = vatPowers.exitVatWithFailure; + const { zoeService, feeMintAccess } = makeZoeKit( + vatAdminSvc, + shutdownZoeVat, + ); + const feePurse = E(zoeService).makeFeePurse(); + const zoe = E(zoeService).bindDefaultFeePurse(feePurse); + return { zoe, feeMintAccess }; + }, + }); +} diff --git a/packages/treasury/test/swingsetTests/vat-zoe.js b/packages/treasury/test/swingsetTests/vat-zoe.js deleted file mode 100644 index 934583f2c0f..00000000000 --- a/packages/treasury/test/swingsetTests/vat-zoe.js +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-check - -import { Far } from '@agoric/marshal'; -import { makeZoeKit } from '@agoric/zoe'; - -export function buildRootObject(vatPowers) { - return Far('root', { - buildZoe: vatAdminSvc => { - const shutdownZoeVat = vatPowers.exitVatWithFailure; - return makeZoeKit(vatAdminSvc, shutdownZoeVat); - }, - }); -} diff --git a/packages/treasury/test/test-bootstrapPayment.js b/packages/treasury/test/test-bootstrapPayment.js index baadae71895..8a3153ff8e4 100644 --- a/packages/treasury/test/test-bootstrapPayment.js +++ b/packages/treasury/test/test-bootstrapPayment.js @@ -8,117 +8,237 @@ import '../src/types.js'; import path from 'path'; import { E } from '@agoric/eventual-send'; import bundleSource from '@agoric/bundle-source'; -import fakeVatAdmin from '@agoric/zoe/tools/fakeVatAdmin.js'; +import { makeFakeVatAdmin } from '@agoric/zoe/tools/fakeVatAdmin.js'; import { makeZoeKit } from '@agoric/zoe'; import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; import { AmountMath } from '@agoric/ertp'; import { resolve as importMetaResolve } from 'import-meta-resolve'; +import { makeLoopback } from '@agoric/captp'; +import { governedParameterTerms } from '../src/params.js'; const filename = new URL(import.meta.url).pathname; const dirname = path.dirname(filename); const stablecoinRoot = `${dirname}/../src/stablecoinMachine.js`; const liquidationRoot = `${dirname}/../src/liquidateMinimum.js`; +const autoswapRoot = + '@agoric/zoe/src/contracts/multipoolAutoswap/multipoolAutoswap.js'; +const governanceRoot = '@agoric/governance/src/contractGovernor.js'; +const registrarRoot = '@agoric/governance/src/committeeRegistrar.js'; + +async function makeBundle(sourceRoot) { + const url = await importMetaResolve(sourceRoot, import.meta.url); + const contractBundle = await bundleSource(new URL(url).pathname); + console.log(`makeBundle ${sourceRoot}`); + return contractBundle; +} + +const [ + autoswapBundle, + stablecoinBundle, + liquidationBundle, + governanceBundle, + registrarBundle, +] = await Promise.all([ + makeBundle(autoswapRoot), + makeBundle(stablecoinRoot), + makeBundle(liquidationRoot), + makeBundle(governanceRoot), + makeBundle(registrarRoot), +]); + +function installBundle(zoe, contractBundle) { + return E(zoe).install(contractBundle); +} + +const setupGovernor = async ( + zoe, + registrarInstall, + registrarTerms, + governedContractInstallation, + governanceInstall, + governed, + timer, +) => { + const { + instance: registrarInstance, + creatorFacet: registrarCreatorFacet, + } = await E(zoe).startInstance(registrarInstall, {}, registrarTerms); + + const governorTerms = { + registrarInstance, + timer, + governedContractInstallation, + governed, + }; + return E(zoe).startInstance( + governanceInstall, + {}, + governorTerms, + harden({ registrarCreatorFacet }), + ); +}; -const autoswapRootP = importMetaResolve( - '@agoric/zoe/src/contracts/multipoolAutoswap/multipoolAutoswap.js', - import.meta.url, -).then(url => new URL(url).pathname); - -const makeInstall = async (root, zoe) => { - const bundle = await bundleSource(root); - // install the contract - const installationP = E(zoe).install(bundle); - return installationP; +const setUpZoeForTest = async setJig => { + const { makeFar, makeNear } = makeLoopback('zoeTest'); + let isFirst = true; + function makeRemote(arg) { + const result = isFirst ? makeNear(arg) : arg; + // this seems fragile. It relies on one contract being created first by Zoe + isFirst = !isFirst; + return result; + } + + /** + * These properties will be assigned by `setJig` in the contract. + * + * @typedef {Object} TestContext + * @property {ContractFacet} zcf + * @property {IssuerRecord} runIssuerRecord + * @property {IssuerRecord} govIssuerRecord + * @property {ERef} autoswap + */ + + const { + zoeService: nonFarZoeService, + feeMintAccess: nonFarFeeMintAccess, + } = makeZoeKit(makeFakeVatAdmin(setJig, makeRemote).admin); + const feePurse = E(nonFarZoeService).makeFeePurse(); + const zoeService = await E(nonFarZoeService).bindDefaultFeePurse(feePurse); + /** @type {ERef} */ + const zoe = makeFar(zoeService); + const feeMintAccess = await makeFar(nonFarFeeMintAccess); + return { + zoe, + feeMintAccess, + }; }; test('bootstrap payment', async t => { - const { zoeService, feeMintAccess } = makeZoeKit(fakeVatAdmin); - const feePurse = E(zoeService).makeFeePurse(); - const zoe = E(zoeService).bindDefaultFeePurse(feePurse); - const autoswapRoot = await autoswapRootP; - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); + let testJig; + const setJig = jig => { + testJig = jig; + }; + const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); + + const autoswapInstall = await installBundle(zoe, autoswapBundle); + const stablecoinInstall = await installBundle(zoe, stablecoinBundle); + const liquidationInstall = await installBundle(zoe, liquidationBundle); + const registrarInstall = await installBundle(zoe, registrarBundle); + const governanceInstall = await installBundle(zoe, governanceBundle); const loanParams = { chargingPeriod: 2n, recordingPeriod: 10n, + poolFee: 24n, + protocolFee: 6n, }; const manualTimer = buildManualTimer(console.log); + // This test value is not a statement about the actual value to // be minted const bootstrapPaymentValue = 20000n * 10n ** 6n; - const { creatorFacet: stablecoinMachine, instance } = await E( + const treasuryTerms = { + autoswapInstall, + priceAuthority: Promise.resolve(), + loanParams, + timerService: manualTimer, + liquidationInstall, + governedParams: governedParameterTerms, + bootstrapPaymentValue, + }; + const governed = { + terms: treasuryTerms, + issuerKeywordRecord: {}, + privateArgs: { feeMintAccess }, + }; + + const registrarTerms = { committeeName: 'bandOfAngels', committeeSize: 5 }; + const { creatorFacet } = await setupGovernor( zoe, - ).startInstance( + registrarInstall, + registrarTerms, stablecoinInstall, - {}, - { - autoswapInstall, - priceAuthority: Promise.resolve(), - loanParams, - timerService: manualTimer, - liquidationInstall, - - bootstrapPaymentValue, - }, - harden({ feeMintAccess }), + governanceInstall, + governed, + manualTimer, ); - const issuers = await E(zoe).getIssuers(instance); + const bootstrapPayment = E( + E(creatorFacet).getCreatorFacet(), + ).getBootstrapPayment(); - const bootstrapPayment = E(stablecoinMachine).getBootstrapPayment(); + const { runIssuerRecord } = testJig; - const bootstrapAmount = await E(issuers.RUN).getAmountOf(bootstrapPayment); - - const runBrand = await E(issuers.RUN).getBrand(); + const bootstrapAmount = await E(runIssuerRecord.issuer).getAmountOf( + bootstrapPayment, + ); t.true( AmountMath.isEqual( bootstrapAmount, - AmountMath.make(runBrand, bootstrapPaymentValue), + AmountMath.make(runIssuerRecord.brand, bootstrapPaymentValue), ), ); }); test('bootstrap payment - only minted once', async t => { - const { zoeService, feeMintAccess } = makeZoeKit(fakeVatAdmin); - const feePurse = E(zoeService).makeFeePurse(); - const zoe = E(zoeService).bindDefaultFeePurse(feePurse); - const autoswapRoot = await autoswapRootP; - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); + let testJig; + const setJig = jig => { + testJig = jig; + }; + const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); + + const autoswapInstall = await installBundle(zoe, autoswapBundle); + const stablecoinInstall = await installBundle(zoe, stablecoinBundle); + const liquidationInstall = await installBundle(zoe, liquidationBundle); + const registrarInstall = await installBundle(zoe, registrarBundle); + const governanceInstall = await installBundle(zoe, governanceBundle); const loanParams = { chargingPeriod: 2n, recordingPeriod: 10n, + poolFee: 24n, + protocolFee: 6n, }; const manualTimer = buildManualTimer(console.log); + // This test value is not a statement about the actual value to // be minted const bootstrapPaymentValue = 20000n * 10n ** 6n; - const { creatorFacet: stablecoinMachine, instance } = await E( + + const treasuryTerms = { + autoswapInstall, + priceAuthority: Promise.resolve(), + loanParams, + timerService: manualTimer, + liquidationInstall, + governedParams: governedParameterTerms, + bootstrapPaymentValue, + }; + const governed = { + terms: treasuryTerms, + issuerKeywordRecord: {}, + privateArgs: { feeMintAccess }, + }; + + const registrarTerms = { committeeName: 'bandOfAngels', committeeSize: 5 }; + const { creatorFacet } = await setupGovernor( zoe, - ).startInstance( + registrarInstall, + registrarTerms, stablecoinInstall, - {}, - { - autoswapInstall, - priceAuthority: Promise.resolve(), - loanParams, - timerService: manualTimer, - liquidationInstall, - - bootstrapPaymentValue, - }, - harden({ feeMintAccess }), + governanceInstall, + governed, + manualTimer, ); - const issuers = await E(zoe).getIssuers(instance); + const bootstrapPayment = E( + E(creatorFacet).getCreatorFacet(), + ).getBootstrapPayment(); - const bootstrapPayment = E(stablecoinMachine).getBootstrapPayment(); + const { runIssuerRecord } = testJig; + const issuers = { RUN: runIssuerRecord.issuer }; const claimedPayment = await E(issuers.RUN).claim(bootstrapPayment); const bootstrapAmount = await E(issuers.RUN).getAmountOf(claimedPayment); @@ -134,7 +254,9 @@ test('bootstrap payment - only minted once', async t => { // Try getting another payment - const bootstrapPayment2 = E(stablecoinMachine).getBootstrapPayment(); + const bootstrapPayment2 = E( + E(creatorFacet).getCreatorFacet(), + ).getBootstrapPayment(); await t.throwsAsync(() => E(issuers.RUN).claim(bootstrapPayment2), { message: /^payment not found for "RUN"/, @@ -142,37 +264,58 @@ test('bootstrap payment - only minted once', async t => { }); test('bootstrap payment - default value is 0n', async t => { - const { zoeService, feeMintAccess } = makeZoeKit(fakeVatAdmin); - const feePurse = E(zoeService).makeFeePurse(); - const zoe = E(zoeService).bindDefaultFeePurse(feePurse); - const autoswapRoot = await autoswapRootP; - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); + let testJig; + const setJig = jig => { + testJig = jig; + }; + const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); + + const autoswapInstall = await installBundle(zoe, autoswapBundle); + const stablecoinInstall = await installBundle(zoe, stablecoinBundle); + const liquidationInstall = await installBundle(zoe, liquidationBundle); + const registrarInstall = await installBundle(zoe, registrarBundle); + const governanceInstall = await installBundle(zoe, governanceBundle); const loanParams = { chargingPeriod: 2n, recordingPeriod: 10n, + poolFee: 24n, + protocolFee: 6n, }; const manualTimer = buildManualTimer(console.log); - const { creatorFacet: stablecoinMachine, instance } = await E( + + const registrarTerms = { committeeName: 'bandOfAngels', committeeSize: 5 }; + + const treasuryTerms = { + autoswapInstall, + priceAuthority: Promise.resolve(), + loanParams, + timerService: manualTimer, + liquidationInstall, + governedParams: governedParameterTerms, + }; + const governed = { + terms: treasuryTerms, + issuerKeywordRecord: {}, + privateArgs: { feeMintAccess }, + }; + + const { creatorFacet } = await setupGovernor( zoe, - ).startInstance( + registrarInstall, + registrarTerms, stablecoinInstall, - {}, - { - autoswapInstall, - priceAuthority: Promise.resolve(), - loanParams, - timerService: manualTimer, - liquidationInstall, - }, - harden({ feeMintAccess }), + governanceInstall, + governed, + manualTimer, ); - const issuers = await E(zoe).getIssuers(instance); + const { runIssuerRecord } = testJig; + const issuers = { RUN: runIssuerRecord.issuer }; - const bootstrapPayment = E(stablecoinMachine).getBootstrapPayment(); + const bootstrapPayment = E( + E(creatorFacet).getCreatorFacet(), + ).getBootstrapPayment(); const bootstrapAmount = await E(issuers.RUN).getAmountOf(bootstrapPayment); diff --git a/packages/treasury/test/test-stablecoin.js b/packages/treasury/test/test-stablecoin.js index d7f20f655d3..6c1b07ae62a 100644 --- a/packages/treasury/test/test-stablecoin.js +++ b/packages/treasury/test/test-stablecoin.js @@ -28,14 +28,45 @@ import { makeTracer } from '../src/makeTracer.js'; import { SECONDS_PER_YEAR } from '../src/interest.js'; import { VaultState } from '../src/vault.js'; +import { governedParameterTerms } from '../src/params.js'; + const stablecoinRoot = '../src/stablecoinMachine.js'; const liquidationRoot = '../src/liquidateMinimum.js'; const autoswapRoot = '@agoric/zoe/src/contracts/newSwap/multipoolAutoswap.js'; + +const contractGovernorRoot = '@agoric/governance/src/contractGovernor.js'; +const committeeRegistrarRoot = '@agoric/governance/src/committeeRegistrar.js'; +const voteCounterRoot = '@agoric/governance/src/binaryVoteCounter.js'; + const trace = makeTracer('TestST'); const BASIS_POINTS = 10000n; const PERCENT = 100n; +async function makeBundle(sourceRoot) { + const url = await importMetaResolve(sourceRoot, import.meta.url); + const path = new URL(url).pathname; + const contractBundle = await bundleSource(path); + trace('makeBundle', sourceRoot); + return contractBundle; +} + +const [ + autoswapBundle, + stablecoinBundle, + liquidationBundle, + contractGovernorBundle, + committeeRegistrarBundle, + voteCounterBundle, +] = await Promise.all([ + makeBundle(autoswapRoot), + makeBundle(stablecoinRoot), + makeBundle(liquidationRoot), + makeBundle(contractGovernorRoot), + makeBundle(committeeRegistrarRoot), + makeBundle(voteCounterRoot), +]); + const setUpZoeForTest = async setJig => { const { makeFar, makeNear } = makeLoopback('zoeTest'); let isFirst = true; @@ -72,13 +103,8 @@ const setUpZoeForTest = async setJig => { }; }; -async function makeInstall(sourceRoot, zoe) { - const url = await importMetaResolve(sourceRoot, import.meta.url); - const path = new URL(url).pathname; - const contractBundle = await bundleSource(path); - const install = E(zoe).install(contractBundle); - trace('install', sourceRoot, install); - return install; +function installBundle(zoe, contractBundle) { + return E(zoe).install(contractBundle); } function setupAssets() { @@ -104,7 +130,6 @@ const makePriceAuthority = ( brandIn, brandOut, priceList, - tradeList, timer, quoteMint, unitAmountIn, @@ -114,7 +139,6 @@ const makePriceAuthority = ( actualBrandIn: brandIn, actualBrandOut: brandOut, priceList, - tradeList, timer, quoteMint, unitAmountIn, @@ -138,51 +162,93 @@ function makeRates(runBrand, aethBrand) { }); } -test('first', async t => { - /* @type {TestContext} */ +async function setupServices( + loanParams, + priceList, + unitAmountIn, + aethBrand, + registrarTerms, + timer = buildManualTimer(console.log), + quoteInterval, +) { let testJig; const setJig = jig => { testJig = jig; }; const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); + const [ + autoswap, + stablecoin, + liquidation, + governor, + registrar, + counter, + ] = await Promise.all([ + installBundle(zoe, autoswapBundle), + installBundle(zoe, stablecoinBundle), + installBundle(zoe, liquidationBundle), + installBundle(zoe, contractGovernorBundle), + installBundle(zoe, committeeRegistrarBundle), + installBundle(zoe, voteCounterBundle), + ]); + + const installs = { + autoswap, + stablecoin, + liquidation, + governor, + registrar, + counter, + }; const { - aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, - } = setupAssets(); + creatorFacet: committeeCreator, + instance: registrarInstance, + } = await E(zoe).startInstance(installs.registrar, {}, registrarTerms); const priceAuthorityPromiseKit = makePromiseKit(); const priceAuthorityPromise = priceAuthorityPromiseKit.promise; - const loanParams = { - chargingPeriod: 2n, - recordingPeriod: 10n, - poolFee: 24n, - protocolFee: 6n, + const treasuryTerms = { + autoswapInstall: installs.autoswap, + priceAuthority: priceAuthorityPromise, + loanParams, + liquidationInstall: installs.liquidation, + timerService: timer, + governedParams: governedParameterTerms, }; - const manualTimer = buildManualTimer(console.log); - const { creatorFacet: stablecoinMachine, publicFacet: lender } = await E( - zoe, - ).startInstance( - stablecoinInstall, - {}, - { - autoswapInstall, - priceAuthority: priceAuthorityPromise, - loanParams, - timerService: manualTimer, - liquidationInstall, + const governorTerms = { + timer, + registrarInstance, + governedContractInstallation: installs.stablecoin, + governed: { + terms: treasuryTerms, + issuerKeywordRecord: {}, + privateArgs: { feeMintAccess }, }, - harden({ feeMintAccess }), - ); + }; - const { runIssuerRecord, govIssuerRecord, autoswap: _autoswapAPI } = testJig; + const { + instance: governorInstance, + publicFacet: governorPublicFacet, + creatorFacet: governorCreatorFacet, + } = await E(zoe).startInstance(installs.governor, {}, governorTerms, { + registrarCreatorFacet: committeeCreator, + }); + + const stablecoinMachineP = E(governorCreatorFacet).getCreatorFacet(); + const lenderP = E(governorCreatorFacet).getPublicFacet(); - const { issuer: runIssuer, brand: runBrand } = runIssuerRecord; + const [stablecoinMachine, lender] = await Promise.all([ + stablecoinMachineP, + lenderP, + ]); - const { brand: govBrand } = govIssuerRecord; + const g = { governorInstance, governorPublicFacet, governorCreatorFacet }; + + const s = { stablecoinMachine, lender }; + const { runIssuerRecord, govIssuerRecord, autoswap: autoswapAPI } = testJig; + const issuers = { run: runIssuerRecord, gov: govIssuerRecord }; const quoteMint = makeIssuerKit('quote', AssetKind.SET).mint; @@ -190,15 +256,49 @@ test('first', async t => { // stablecoinMachine has been built, so resolve priceAuthorityPromiseKit here const priceAuthority = makePriceAuthority( aethBrand, - runBrand, - [500n, 15n], - null, - manualTimer, + runIssuerRecord.brand, + priceList, + timer, quoteMint, - AmountMath.make(900n, aethBrand), + unitAmountIn, + quoteInterval, ); priceAuthorityPromiseKit.resolve(priceAuthority); + return { + zoe, + installs, + registrar, + governor: g, + stablecoin: s, + issuers, + priceAuthority, + autoswapAPI, + }; +} + +test('first', async t => { + const { + aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, + } = setupAssets(); + const loanParams = { + chargingPeriod: 2n, + recordingPeriod: 10n, + poolFee: 24n, + protocolFee: 6n, + }; + const services = await setupServices( + loanParams, + [500n, 15n], + AmountMath.make(900n, aethBrand), + aethBrand, + { committeeName: 'TheCabal', committeeSize: 5 }, + ); + const { issuer: runIssuer, brand: runBrand } = services.issuers.run; + const { brand: govBrand } = services.issuers.gov; + const zoe = services.zoe; + const { stablecoinMachine, lender } = services.stablecoin; + // Add a pool with 900 aeth collateral at a 201 aeth/RUN rate const capitalAmount = AmountMath.make(900n, aethBrand); const rates = makeRates(runBrand, aethBrand); @@ -308,67 +408,33 @@ test('first', async t => { }); test('price drop', async t => { - /* @type {TestContext} */ - let testJig; - const setJig = jig => { - testJig = jig; - }; - const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); - - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); - const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); - const priceAuthorityPromiseKit = makePromiseKit(); - const priceAuthorityPromise = priceAuthorityPromiseKit.promise; + const manualTimer = buildManualTimer(console.log); // When the price falls to 636, the loan will get liquidated. 636 for 900 // Aeth is 1.4 each. The loan is 270 RUN. The margin is 1.05, so at 636, 400 // Aeth collateral could support a loan of 268. - const loanParams = { chargingPeriod: 2n, recordingPeriod: 10n, - poolFee: 24, - protocolFee: 6, + poolFee: 24n, + protocolFee: 6n, }; - const manualTimer = buildManualTimer(console.log); - const { creatorFacet: stablecoinMachine, publicFacet: lender } = await E( - zoe, - ).startInstance( - stablecoinInstall, - {}, - { - autoswapInstall, - priceAuthority: priceAuthorityPromise, - loanParams, - timerService: manualTimer, - liquidationInstall, - }, - harden({ feeMintAccess }), - ); - - const { runIssuerRecord, govIssuerRecord } = testJig; - const { brand: runBrand } = runIssuerRecord; - const { brand: govBrand } = govIssuerRecord; - const quoteMint = makeIssuerKit('quote', AssetKind.SET).mint; - - const priceAuthority = makePriceAuthority( - aethBrand, - runBrand, + const services = await setupServices( + loanParams, [1000n, 677n, 636n], - null, - manualTimer, - quoteMint, AmountMath.make(900n, aethBrand), + aethBrand, + { committeeName: 'TheCabal', committeeSize: 5 }, + manualTimer, ); - // priceAuthority needs runDebt, which isn't available till the - // stablecoinMachine has been built, so resolve priceAuthorityPromiseKit here - priceAuthorityPromiseKit.resolve(priceAuthority); + const { brand: runBrand } = services.issuers.run; + const { brand: govBrand } = services.issuers.gov; + const zoe = services.zoe; + const { stablecoinMachine, lender } = services.stablecoin; // Add a pool with 900 aeth at a 201 RUN/aeth rate const capitalAmount = AmountMath.make(900n, aethBrand); @@ -466,19 +532,9 @@ test('price drop', async t => { }); test('price falls precipitously', async t => { - /* @type {TestContext} */ - let testJig; - const setJig = jig => { - testJig = jig; - }; - const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); - - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); - - const priceAuthorityPromiseKit = makePromiseKit(); - const priceAuthorityPromise = priceAuthorityPromiseKit.promise; + const { + aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, + } = setupAssets(); const loanParams = { chargingPeriod: 2n, recordingPeriod: 10n, @@ -486,10 +542,6 @@ test('price falls precipitously', async t => { protocolFee: 6n, }; - const { - aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, - } = setupAssets(); - // The borrower will deposit 4 Aeth, and ask to borrow 470 RUN. The // PriceAuthority's initial quote is 180. The max loan on 4 Aeth would be 600 // (to make the margin 20%). @@ -499,43 +551,18 @@ test('price falls precipitously', async t => { // gets 41 back const manualTimer = buildManualTimer(console.log); - const { creatorFacet: stablecoinMachine, publicFacet: lender } = await E( - zoe, - ).startInstance( - stablecoinInstall, - {}, - { - autoswapInstall, - priceAuthority: priceAuthorityPromise, - loanParams, - timerService: manualTimer, - liquidationInstall, - }, - harden({ feeMintAccess }), - ); - - const { runIssuerRecord, govIssuerRecord, autoswap: autoswapAPI } = testJig; - - const { brand: runBrand } = runIssuerRecord; - const { brand: govBrand } = govIssuerRecord; - // Our wrapper gives us a Vault which holds 5 Collateral, has lent out 10 - // RUN, which uses an autoswap that presents a fixed price of 4 RUN - // per Collateral. - - const quoteMint = makeIssuerKit('quote', AssetKind.SET).mint; - - // priceAuthority needs the RUN brand, which isn't available till the - // stablecoinMachine has been built, so resolve priceAuthorityPromiseKit here - const priceAuthority = makePriceAuthority( - aethBrand, - runBrand, + const services = await setupServices( + loanParams, [2200n, 19180n, 1650n, 150n], - null, - manualTimer, - quoteMint, AmountMath.make(900n, aethBrand), + aethBrand, + { committeeName: 'TheCabal', committeeSize: 5 }, + manualTimer, ); - priceAuthorityPromiseKit.resolve(priceAuthority); + const { brand: runBrand } = services.issuers.run; + const { brand: govBrand } = services.issuers.gov; + const zoe = services.zoe; + const { stablecoinMachine, lender } = services.stablecoin; // Add a pool with 900 aeth at a 201 RUN/aeth rate const capitalAmount = AmountMath.make(900n, aethBrand); @@ -587,7 +614,7 @@ test('price falls precipitously', async t => { ); // Sell some Eth to drive the value down - const swapInvitation = E(autoswapAPI).makeSwapInvitation(); + const swapInvitation = E(services.autoswapAPI).makeSwapInvitation(); const proposal = { give: { In: AmountMath.make(200n, aethBrand) }, want: { Out: AmountMath.makeEmpty(runBrand) }, @@ -624,58 +651,27 @@ test('price falls precipitously', async t => { }); test('stablecoin display collateral', async t => { - /* @type {TestContext} */ - let testJig; - const setJig = jig => { - testJig = jig; - }; - const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); - - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); - - const { - aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, - } = setupAssets(); - - const priceAuthorityPromiseKit = makePromiseKit(); - const priceAuthorityPromise = priceAuthorityPromiseKit.promise; const loanParams = { chargingPeriod: 2n, recordingPeriod: 6n, poolFee: 24n, protocolFee: 6n, }; - const manualTimer = buildManualTimer(console.log); - const { creatorFacet: stablecoinMachine } = await E(zoe).startInstance( - stablecoinInstall, - {}, - { - autoswapInstall, - priceAuthority: priceAuthorityPromise, - loanParams, - timerService: manualTimer, - liquidationInstall, - }, - harden({ feeMintAccess }), - ); - - const { runIssuerRecord, govIssuerRecord, autoswap: _autoswapAPI } = testJig; - const { brand: runBrand } = runIssuerRecord; - const { brand: govBrand } = govIssuerRecord; - const quoteMint = makeIssuerKit('quote', AssetKind.SET).mint; + const { + aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, + } = setupAssets(); - const priceAuthority = makePriceAuthority( - aethBrand, - runBrand, + const services = await setupServices( + loanParams, [500n, 1500n], - null, - manualTimer, - quoteMint, AmountMath.make(90n, aethBrand), + aethBrand, + { committeeName: 'TheCabal', committeeSize: 5 }, ); - priceAuthorityPromiseKit.resolve(priceAuthority); + const { brand: runBrand } = services.issuers.run; + const { brand: govBrand } = services.issuers.gov; + const zoe = services.zoe; + const { stablecoinMachine } = services.stablecoin; // Add a vaultManager with 900 aeth collateral at a 201 aeth/RUN rate const capitalAmount = AmountMath.make(900n, aethBrand); @@ -713,23 +709,6 @@ test('stablecoin display collateral', async t => { // charging period is 1 week. Clock ticks by days test('interest on multiple vaults', async t => { - /* @type {TestContext} */ - let testJig; - const setJig = jig => { - testJig = jig; - }; - const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); - - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); - - const { - aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, - } = setupAssets(); - - const priceAuthorityPromiseKit = makePromiseKit(); - const priceAuthorityPromise = priceAuthorityPromiseKit.promise; const secondsPerDay = SECONDS_PER_YEAR / 365n; const loanParams = { chargingPeriod: secondsPerDay * 7n, @@ -739,37 +718,23 @@ test('interest on multiple vaults', async t => { }; // Clock ticks by days const manualTimer = buildManualTimer(console.log, 0n, secondsPerDay); - const { creatorFacet: stablecoinMachine, publicFacet: lender } = await E( - zoe, - ).startInstance( - stablecoinInstall, - {}, - { - autoswapInstall, - priceAuthority: priceAuthorityPromise, - loanParams, - timerService: manualTimer, - liquidationInstall, - }, - harden({ feeMintAccess }), - ); - - const { runIssuerRecord, govIssuerRecord, autoswap: _autoswapAPI } = testJig; - const { issuer: runIssuer, brand: runBrand } = runIssuerRecord; - const { brand: govBrand } = govIssuerRecord; - const quoteMint = makeIssuerKit('quote', AssetKind.SET).mint; - const priceAuthority = makePriceAuthority( - aethBrand, - runBrand, + const { + aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, + } = setupAssets(); + const services = await setupServices( + loanParams, [500n, 1500n], - null, - manualTimer, - quoteMint, AmountMath.make(90n, aethBrand), + aethBrand, + { committeeName: 'TheCabal', committeeSize: 5 }, + manualTimer, secondsPerDay, ); - priceAuthorityPromiseKit.resolve(priceAuthority); + const { issuer: runIssuer, brand: runBrand } = services.issuers.run; + const { brand: govBrand } = services.issuers.gov; + const zoe = services.zoe; + const { stablecoinMachine, lender } = services.stablecoin; // Add a vaultManager with 900 aeth collateral at a 201 aeth/RUN rate const capitalAmount = AmountMath.make(900n, aethBrand); @@ -921,60 +886,27 @@ test('interest on multiple vaults', async t => { }); test('adjust balances', async t => { - /* @type {TestContext} */ - let testJig; - const setJig = jig => { - testJig = jig; - }; - const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); - - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); - - const { - aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, - } = setupAssets(); - - const priceAuthorityPromiseKit = makePromiseKit(); - const priceAuthorityPromise = priceAuthorityPromiseKit.promise; const loanParams = { chargingPeriod: 2n, recordingPeriod: 6n, poolFee: 24n, protocolFee: 6n, }; - const manualTimer = buildManualTimer(console.log); - const { creatorFacet: stablecoinMachine, publicFacet: lender } = await E( - zoe, - ).startInstance( - stablecoinInstall, - {}, - { - autoswapInstall, - priceAuthority: priceAuthorityPromise, - loanParams, - timerService: manualTimer, - liquidationInstall, - }, - harden({ feeMintAccess }), - ); - - const { runIssuerRecord, govIssuerRecord } = testJig; - const { issuer: runIssuer, brand: runBrand } = runIssuerRecord; - const { brand: govBrand } = govIssuerRecord; - const quoteMint = makeIssuerKit('quote', AssetKind.SET).mint; - const priceAuthority = makePriceAuthority( - aethBrand, - runBrand, + const { + aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, + } = setupAssets(); + const services = await setupServices( + loanParams, [15n], - null, - manualTimer, - quoteMint, AmountMath.make(1n, aethBrand), + aethBrand, + { committeeName: 'TheCabal', committeeSize: 5 }, ); - priceAuthorityPromiseKit.resolve(priceAuthority); + const { issuer: runIssuer, brand: runBrand } = services.issuers.run; + const { brand: govBrand } = services.issuers.gov; + const zoe = services.zoe; + const { stablecoinMachine, lender } = services.stablecoin; const priceConversion = makeRatio(15n, runBrand, 1n, aethBrand); // Add a vaultManager with 900 aeth collateral at a 201 aeth/RUN rate @@ -1224,60 +1156,27 @@ test('adjust balances', async t => { // Alice will over repay her borrowed RUN. In order to make that possible, // Bob will also take out a loan and will give her the proceeds. test('overdeposit', async t => { - /* @type {TestContext} */ - let testJig; - const setJig = jig => { - testJig = jig; - }; - const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); - - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); - - const { - aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, - } = setupAssets(); - - const priceAuthorityPromiseKit = makePromiseKit(); - const priceAuthorityPromise = priceAuthorityPromiseKit.promise; const loanParams = { chargingPeriod: 2n, recordingPeriod: 6n, poolFee: 24n, protocolFee: 6n, }; - const manualTimer = buildManualTimer(console.log); - const { creatorFacet: stablecoinMachine, publicFacet: lender } = await E( - zoe, - ).startInstance( - stablecoinInstall, - {}, - { - autoswapInstall, - priceAuthority: priceAuthorityPromise, - loanParams, - timerService: manualTimer, - liquidationInstall, - }, - harden({ feeMintAccess }), - ); - const { runIssuerRecord, govIssuerRecord } = testJig; - const { issuer: runIssuer, brand: runBrand } = runIssuerRecord; - const { brand: govBrand } = govIssuerRecord; - const quoteMint = makeIssuerKit('quote', AssetKind.SET).mint; - - const priceAuthority = makePriceAuthority( - aethBrand, - runBrand, + const { + aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, + } = setupAssets(); + const services = await setupServices( + loanParams, [15n], - null, - manualTimer, - quoteMint, AmountMath.make(1n, aethBrand), + aethBrand, + { committeeName: 'TheCabal', committeeSize: 5 }, ); - priceAuthorityPromiseKit.resolve(priceAuthority); + const { issuer: runIssuer, brand: runBrand } = services.issuers.run; + const { brand: govBrand } = services.issuers.gov; + const zoe = services.zoe; + const { stablecoinMachine, lender } = services.stablecoin; // Add a vaultManager with 900 aeth collateral at a 201 aeth/RUN rate const capitalAmount = AmountMath.make(900n, aethBrand); @@ -1418,64 +1317,32 @@ test('overdeposit', async t => { // enough of the overage that she'll get caught when prices drop. Bob will be // charged interest (twice), which will trigger liquidation. test('mutable liquidity triggers and interest', async t => { - /* @type {TestContext} */ - let testJig; - const setJig = jig => { - testJig = jig; - }; - const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); - - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); - const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); - const priceAuthorityPromiseKit = makePromiseKit(); - const priceAuthorityPromise = priceAuthorityPromiseKit.promise; - const secondsPerDay = SECONDS_PER_YEAR / 365n; - // charge interest on every tick const loanParams = { chargingPeriod: secondsPerDay * 7n, recordingPeriod: secondsPerDay * 7n, poolFee: 24n, protocolFee: 6n, }; + // charge interest on every tick const manualTimer = buildManualTimer(console.log, 0n, secondsPerDay * 7n); - const { creatorFacet: stablecoinMachine, publicFacet: lender } = await E( - zoe, - ).startInstance( - stablecoinInstall, - {}, - { - autoswapInstall, - priceAuthority: priceAuthorityPromise, - loanParams, - timerService: manualTimer, - liquidationInstall, - }, - harden({ feeMintAccess }), - ); - - const { runIssuerRecord, govIssuerRecord } = testJig; - const { issuer: runIssuer, brand: runBrand } = runIssuerRecord; - const { brand: govBrand } = govIssuerRecord; - const quoteMint = makeIssuerKit('quote', AssetKind.SET).mint; - - const priceAuthority = makePriceAuthority( - aethBrand, - runBrand, + const services = await setupServices( + loanParams, [10n, 7n], - null, - manualTimer, - quoteMint, AmountMath.make(1n, aethBrand), + aethBrand, + { committeeName: 'TheCabal', committeeSize: 5 }, + manualTimer, secondsPerDay * 7n, ); - priceAuthorityPromiseKit.resolve(priceAuthority); + const { issuer: runIssuer, brand: runBrand } = services.issuers.run; + const { brand: govBrand } = services.issuers.gov; + const zoe = services.zoe; + const { stablecoinMachine, lender } = services.stablecoin; // Add a vaultManager with 10000 aeth collateral at a 200 aeth/RUN rate const capitalAmount = AmountMath.make(10000n, aethBrand); @@ -1649,13 +1516,18 @@ test('mutable liquidity triggers and interest', async t => { }); test('bad chargingPeriod', async t => { - /* @type {TestContext} */ const setJig = () => {}; const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); + const [ + autoswapInstall, + stablecoinInstall, + liquidationInstall, + ] = await Promise.all([ + installBundle(zoe, autoswapBundle), + installBundle(zoe, stablecoinBundle), + installBundle(zoe, liquidationBundle), + ]); const priceAuthorityPromiseKit = makePromiseKit(); const priceAuthorityPromise = priceAuthorityPromiseKit.promise; @@ -1678,6 +1550,7 @@ test('bad chargingPeriod', async t => { loanParams, timerService: manualTimer, liquidationInstall, + governedParams: governedParameterTerms, }, harden({ feeMintAccess }), ), @@ -1686,62 +1559,33 @@ test('bad chargingPeriod', async t => { }); test('coll fees from loan and AMM', async t => { - /* @type {TestContext} */ - let testJig; - const setJig = jig => { - testJig = jig; - }; - const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); - - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); - - const { - aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, - } = setupAssets(); - - const priceAuthorityPromiseKit = makePromiseKit(); - const priceAuthorityPromise = priceAuthorityPromiseKit.promise; const loanParams = { chargingPeriod: 2n, recordingPeriod: 10n, poolFee: 24n, protocolFee: 6n, }; - const manualTimer = buildManualTimer(console.log); - const { creatorFacet: stablecoinMachine, publicFacet: lender } = await E( - zoe, - ).startInstance( - stablecoinInstall, - {}, - { - autoswapInstall, - priceAuthority: priceAuthorityPromise, - loanParams, - timerService: manualTimer, - liquidationInstall, - }, - harden({ feeMintAccess }), - ); - const { runIssuerRecord, govIssuerRecord, autoswap: _autoswapAPI } = testJig; - const { brand: runBrand } = runIssuerRecord; - const { brand: govBrand } = govIssuerRecord; - const quoteMint = makeIssuerKit('quote', AssetKind.SET).mint; + const { + aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, + } = setupAssets(); + const priceList = [500n, 15n]; + const unitAmountIn = AmountMath.make(900n, aethBrand); + const registrarTerms = { committeeName: 'TheCabal', committeeSize: 5 }; + const manualTimer = buildManualTimer(console.log); - // priceAuthority needs the RUN brand, which isn't available until the - // stablecoinMachine has been built, so resolve priceAuthorityPromiseKit here - const priceAuthority = makePriceAuthority( + const services = await setupServices( + loanParams, + priceList, + unitAmountIn, aethBrand, - runBrand, - [500n, 15n], - null, + registrarTerms, manualTimer, - quoteMint, - AmountMath.make(900n, aethBrand), ); - priceAuthorityPromiseKit.resolve(priceAuthority); + const { brand: runBrand } = services.issuers.run; + const { brand: govBrand } = services.issuers.gov; + const zoe = services.zoe; + const { stablecoinMachine, lender } = services.stablecoin; // Add a pool with 900 aeth collateral at a 201 aeth/RUN rate const capitalAmount = AmountMath.make(900n, aethBrand); @@ -1824,59 +1668,26 @@ test('coll fees from loan and AMM', async t => { }); test('close loan', async t => { - /* @type {TestContext} */ - let testJig; - const setJig = jig => { - testJig = jig; - }; - const { zoe, feeMintAccess } = await setUpZoeForTest(setJig); - - const autoswapInstall = await makeInstall(autoswapRoot, zoe); - const stablecoinInstall = await makeInstall(stablecoinRoot, zoe); - const liquidationInstall = await makeInstall(liquidationRoot, zoe); - const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); - - const priceAuthorityPromiseKit = makePromiseKit(); - const priceAuthorityPromise = priceAuthorityPromiseKit.promise; const loanParams = { chargingPeriod: 2n, recordingPeriod: 6n, poolFee: 24n, protocolFee: 6n, }; - const manualTimer = buildManualTimer(console.log); - const { creatorFacet: stablecoinMachine, publicFacet: lender } = await E( - zoe, - ).startInstance( - stablecoinInstall, - {}, - { - autoswapInstall, - priceAuthority: priceAuthorityPromise, - loanParams, - timerService: manualTimer, - liquidationInstall, - }, - harden({ feeMintAccess }), - ); - const { runIssuerRecord, govIssuerRecord } = testJig; - const { issuer: runIssuer, brand: runBrand } = runIssuerRecord; - const { brand: govBrand } = govIssuerRecord; - const quoteMint = makeIssuerKit('quote', AssetKind.SET).mint; - - const priceAuthority = makePriceAuthority( - aethBrand, - runBrand, + const services = await setupServices( + loanParams, [15n], - null, - manualTimer, - quoteMint, AmountMath.make(1n, aethBrand), + aethBrand, + { committeeName: 'Star Chamber', committeeSize: 5 }, ); - priceAuthorityPromiseKit.resolve(priceAuthority); + const { issuer: runIssuer, brand: runBrand } = services.issuers.run; + const { brand: govBrand } = services.issuers.gov; + const zoe = services.zoe; + const { stablecoinMachine, lender } = services.stablecoin; // Add a vaultManager with 900 aeth collateral at a 201 aeth/RUN rate const capitalAmount = AmountMath.make(900n, aethBrand); diff --git a/packages/treasury/test/vault-contract-wrapper.js b/packages/treasury/test/vault-contract-wrapper.js index 435346f226d..963fc0005c9 100644 --- a/packages/treasury/test/vault-contract-wrapper.js +++ b/packages/treasury/test/vault-contract-wrapper.js @@ -10,11 +10,12 @@ import { makeFakePriceAuthority } from '@agoric/zoe/tools/fakePriceAuthority.js' import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; import { Far } from '@agoric/marshal'; +import { buildParamManager } from '@agoric/governance/src/paramManager'; import { makeVaultKit } from '../src/vault.js'; import { paymentFromZCFMint } from '../src/burn.js'; -import { SECONDS_PER_YEAR } from '../src/interest.js'; const BASIS_POINTS = 10000n; +const SECONDS_PER_HOUR = 60n * 60n; /** @type {ContractStartFn} */ export async function start(zcf, privateArgs) { @@ -68,10 +69,15 @@ export async function start(zcf, privateArgs) { getCollateralBrand() { return collateralBrand; }, + getChargingPeriod() { + return SECONDS_PER_HOUR * 24n; + }, + getRecordingPeriod() { + return SECONDS_PER_HOUR * 24n * 7n; + }, reallocateReward, }); - const SECONDS_PER_HOUR = SECONDS_PER_YEAR / 365n / 24n; const timer = buildManualTimer(console.log, 0n, SECONDS_PER_HOUR * 24n); const options = { actualBrandIn: collateralBrand, @@ -83,16 +89,15 @@ export async function start(zcf, privateArgs) { }; const priceAuthority = makeFakePriceAuthority(options); + const { publicFacet } = buildParamManager([]); + const { vault, openLoan, accrueInterestAndAddToPool } = await makeVaultKit( zcf, managerMock, runMint, autoswapMock, priceAuthority, - { - chargingPeriod: SECONDS_PER_HOUR * 24n, - recordingPeriod: SECONDS_PER_HOUR * 24n * 7n, - }, + publicFacet, timer.getCurrentTimestamp(), ); diff --git a/packages/vats/src/bootstrap.js b/packages/vats/src/bootstrap.js index 03b5f571700..2c9807d8db4 100644 --- a/packages/vats/src/bootstrap.js +++ b/packages/vats/src/bootstrap.js @@ -198,7 +198,11 @@ export function buildRootObject(vatPowers, vatParameters) { // Now we can bootstrap the economy! const bootstrapPaymentValue = Nat(BigInt(centralBootstrapSupply.amount)); - const treasuryCreator = await installEconomy(bootstrapPaymentValue); + // NOTE: no use of the voteCreator. We'll need it to initiate votes on + // changing Treasury parameters. + const { treasuryCreator, _voteCreator } = await installEconomy( + bootstrapPaymentValue, + ); const [ centralIssuer,