Skip to content

Commit

Permalink
feat: use feeCollectorDepositFacet
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed May 21, 2021
1 parent 60f7ea0 commit 7e97cc1
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 125 deletions.
33 changes: 22 additions & 11 deletions packages/vats/src/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function makeVattpFrom(vats) {
});
}

const CENTRAL_DENOM_NAME = 'urun';
export function buildRootObject(vatPowers, vatParameters) {
const { D } = vatPowers;
async function setupCommandDevice(httpVat, cmdDevice, roles) {
Expand Down Expand Up @@ -163,17 +164,27 @@ export function buildRootObject(vatPowers, vatParameters) {
const epochTimerService = chainTimerService;
const distributorParams = {
epochInterval: 60n * 60n, // 1 hour
runIssuer: centralIssuer,
runBrand: centralBrand,
};
E(vats.distributeFees)
.buildDistributor(
E(vats.distributeFees).makeTreasuryFeeCollector(zoe, treasuryCreator),
E(bankManager).getDepositFacet(),
epochTimerService,
harden(distributorParams),
)
.catch(e => console.error('Error distributing fees', e));
const feeCollectorDepositFacet = await E(bankManager)
.getFeeCollectorDepositFacet(CENTRAL_DENOM_NAME, {
issuer: centralIssuer,
brand: centralBrand,
})
.catch(e => {
console.error('Cannot create fee collector', e);
return undefined;
});
if (feeCollectorDepositFacet) {
// Only distribute fees if there is a collector.
E(vats.distributeFees)
.buildDistributor(
E(vats.distributeFees).makeTreasuryFeeCollector(zoe, treasuryCreator),
feeCollectorDepositFacet,
epochTimerService,
harden(distributorParams),
)
.catch(e => console.error('Error distributing fees', e));
}

/** @type {undefined | import('@agoric/eventual-send').EOnly<Purse>} */
let centralBootstrapPurse;
Expand All @@ -185,7 +196,7 @@ export function buildRootObject(vatPowers, vatParameters) {
return;
}
await E(bankManager).addAsset(
'urun',
CENTRAL_DENOM_NAME,
CENTRAL_ISSUER_NAME,
'Agoric RUN currency',
harden({ issuer: centralIssuer, brand: centralBrand }),
Expand Down
74 changes: 20 additions & 54 deletions packages/vats/src/distributeFees.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@

import { observeNotifier } from '@agoric/notifier';
import { E } from '@agoric/eventual-send';
import { natSafeMath } from '@agoric/zoe/src/contractSupport';
import { AmountMath } from '@agoric/ertp';
import { Far } from '@agoric/marshal';

const { add, subtract, multiply, floorDivide } = natSafeMath;

// wrapper to take the treasury's creatorFacet, and make a function that will
// request an invitation and return a promise for a payment.
export function makeTreasuryFeeCollector(zoe, treasuryCreatorFacet) {
Expand All @@ -21,65 +17,35 @@ export function makeTreasuryFeeCollector(zoe, treasuryCreatorFacet) {
});
}

// A distributor of fees from treasury to the Bank module, which has a mapping
// from accounts to purses. Each time the epochTimer signals the end of an
// Epoch, it will ask the bank's notifier for a fresh list of accounts and ask
// the treasury for the fees that have been collected to date. It will then
// divide the funds evenly among the accounts, and send payments.
// When the payment doesn't divide evenly among the accounts, it holds the
// remainder till the next epoch.

/** @type {BuildFeeDistributor} */
export function buildDistributor(treasury, bank, epochTimer, params) {
/**
* A distributor of fees from treasury to the Bank module. Each time the
* epochTimer signals the end of an Epoch, it will ask the treasury for the fees
* that have been collected to date and send that payment to the depositFacet.
*
* @type {BuildFeeDistributor}
*/
export async function buildDistributor(
treasury,
feeDepositFacet,
epochTimer,
params,
) {
const {
// By default, we assume the epochTimer fires once per epoch.
epochInterval = 1n,
runIssuer,
runBrand,
} = params;
const accountsNotifier = E(bank).getAccountsNotifier();
let leftOverPayment;
let leftOverValue = 0n;

async function schedulePayments() {
let payment = E(treasury).collectFees();
const [amount, { value: accounts }] = await Promise.all([
E(runIssuer).getAmountOf(payment),
E(accountsNotifier).getUpdateSince(),
]);
const totalValue = add(leftOverValue, amount.value);
const valuePerAccount = floorDivide(totalValue, accounts.length);

const amounts = Array(accounts.length).fill(
AmountMath.make(runBrand, valuePerAccount),
);
if (leftOverValue) {
payment = E(runIssuer).combine([payment, leftOverPayment]);
}
leftOverValue = subtract(
totalValue,
multiply(accounts.length, valuePerAccount),
);
amounts.push(AmountMath.make(runBrand, leftOverValue));

const manyPayments = await E(runIssuer).splitMany(payment, amounts);
// manyPayments is hardened, so we can't use pop()
leftOverPayment = manyPayments[manyPayments.length - 1];
const actualPayments = manyPayments.slice(0, manyPayments.length - 1);

// eslint-disable-next-line no-unused-vars
const settledResultsP = Promise.allSettled(
// The scheduler will decide how fast to process the deposits.
accounts.map((acct, i) =>
E(bank).deposit(runBrand, acct, actualPayments[i]),
),
);

// TODO: examine settledResultsP for failures, and reclaim those payments.
const payment = await E(treasury).collectFees();
return E(feeDepositFacet).receive(payment);
}

const timeObserver = {
updateState: _ => schedulePayments(),
updateState: _ =>
schedulePayments().catch(e => {
console.error('failed with', e);
throw e;
}),
fail: reason => {
throw Error(`Treasury epoch timer failed: ${reason}`);
},
Expand Down
17 changes: 6 additions & 11 deletions packages/vats/src/types.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
// @ts-check

/**
* @template T
* @typedef {import('@agoric/eventual-send').EOnly<T>} EOnly
*/

/**
* @typedef {Object} Board
* @property {(id: string) => any} getValue
Expand Down Expand Up @@ -50,20 +55,11 @@
* @property {() => ERef<Payment>} collectFees
*/

/**
* @typedef {Object} BankDepositFacet
*
* @property {(brand: Brand, account: string, payment: Payment) => Promise<Amount>} deposit
* @property {() => Notifier<string[]>} getAccountsNotifier
*/

/**
* @typedef {Object} DistributorParams
*
* @property {bigint} [epochInterval=1n] - parameter to the epochTimer
* controlling the interval at which rewards should be sent to the bank.
* @property {Issuer} runIssuer
* @property {Brand} runBrand
*/

/**
Expand All @@ -72,8 +68,7 @@
* @param {ERef<FeeCollector>} treasuryCollector - an object with a
* collectFees() method, which will return a payment. can be populated with
* makeTreasuryFeeCollector(zoe, treasuryCreatorFacet)
* @param {ERef<BankDepositFacet>} bank - object with getAccountsNotifier() and
* deposit()
* @param {EOnly<DepositFacet>} feeDepositFacet - object with receive()
* @param {ERef<TimerService>} epochTimer - timer that notifies at the end of
* each Epoch. The epochInterval parameter controls the interval.
* @param {DistributorParams} params
Expand Down
76 changes: 27 additions & 49 deletions packages/vats/test/test-distributeFees.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava';

import { AmountMath } from '@agoric/ertp';
import { makeNotifierKit } from '@agoric/notifier';
import buildManualTimer from '@agoric/zoe/tools/manualTimer';
import { setup } from '@agoric/zoe/test/unitTests/setupBasicMints';

import { makePromiseKit } from '@agoric/promise-kit';
import { assertPayoutAmount } from '@agoric/zoe/test/zoeTestHelpers';
import { E } from '@agoric/eventual-send';
import { buildDistributor } from '../src/distributeFees';

// Some notifier updates aren't propogating sufficiently quickly for the tests.
Expand All @@ -21,25 +21,20 @@ async function waitForPromisesToSettle() {
return pk.promise;
}

function makeFakeBank() {
const depositAccounts = [];
/**
* @param {Issuer} feeIssuer
*/
function makeFakeFeeDepositFacet(feeIssuer) {
const depositPayments = [];
const { notifier, updater } = makeNotifierKit();

return {
getAccountsNotifier: () => notifier,
deposit: async (brand, a, p) => {
depositAccounts.push(a);
depositPayments.push(p);
// success or failure is all that matters for the test
return AmountMath.makeEmpty(brand);
const feeDepositFacet = {
async receive(pmt) {
depositPayments.push(pmt);
return E(feeIssuer).getAmountOf(pmt);
},

// tools for the fake:
getUpdater: _ => updater,
getAccounts: _ => depositAccounts,
getPayments: _ => depositPayments,
};

return { feeDepositFacet, getPayments: _ => depositPayments };
}

function makeFakeTreasury() {
Expand All @@ -61,73 +56,56 @@ function assertPaymentArray(t, payments, count, value, issuer, brand) {
test('fee distribution', async t => {
const { brands, moolaIssuer: issuer, moolaMint: runMint } = setup();
const brand = brands.get('moola');
const bank = makeFakeBank();
const bankUpdater = bank.getUpdater();
const { feeDepositFacet, getPayments } = makeFakeFeeDepositFacet(issuer);
const treasury = makeFakeTreasury();
const epochTimer = buildManualTimer(console.log);
const distributorParams = {
epochInterval: 1n,
runIssuer: issuer,
runBrand: brand,
};
buildDistributor(treasury, bank, epochTimer, distributorParams);
buildDistributor(
treasury,
feeDepositFacet,
epochTimer,
distributorParams,
).catch(e => {
t.fail(e.stack);
});

treasury.pushFees(runMint.mintPayment(AmountMath.make(brand, 500n)));
bankUpdater.updateState(['a37', 'a2389', 'a274', 'a16', 'a1772']);

t.deepEqual(bank.getAccounts(), []);
t.deepEqual(bank.getPayments(), []);
t.deepEqual(getPayments(), []);

await epochTimer.tick();
await waitForPromisesToSettle();

t.deepEqual(bank.getAccounts(), ['a37', 'a2389', 'a274', 'a16', 'a1772']);
assertPaymentArray(t, bank.getPayments(), 5, 100, issuer, brand);
assertPaymentArray(t, getPayments(), 1, 500, issuer, brand);
});

test('fee distribution, leftovers', async t => {
const { brands, moolaIssuer: issuer, moolaMint: runMint } = setup();
const brand = brands.get('moola');
const bank = makeFakeBank();
const bankUpdater = bank.getUpdater();
const { feeDepositFacet, getPayments } = makeFakeFeeDepositFacet(issuer);
const treasury = makeFakeTreasury();
const epochTimer = buildManualTimer(console.log);
const distributorParams = {
epochInterval: 1n,
runIssuer: issuer,
runBrand: brand,
};
buildDistributor(treasury, bank, epochTimer, distributorParams);
buildDistributor(treasury, feeDepositFacet, epochTimer, distributorParams);

treasury.pushFees(runMint.mintPayment(AmountMath.make(brand, 12n)));
bankUpdater.updateState(['a37', 'a2389', 'a274', 'a16', 'a1772']);

t.deepEqual(bank.getAccounts(), []);
t.deepEqual(bank.getPayments(), []);
t.deepEqual(getPayments(), []);

await epochTimer.tick();
await waitForPromisesToSettle();

t.deepEqual(bank.getAccounts(), ['a37', 'a2389', 'a274', 'a16', 'a1772']);
assertPaymentArray(t, bank.getPayments(), 5, 2, issuer, brand);
assertPaymentArray(t, getPayments(), 1, 12, issuer, brand);

// Pay them again
treasury.pushFees(runMint.mintPayment(AmountMath.make(brand, 13n)));

await epochTimer.tick();
await waitForPromisesToSettle();

t.deepEqual(bank.getAccounts(), [
'a37',
'a2389',
'a274',
'a16',
'a1772',
'a37',
'a2389',
'a274',
'a16',
'a1772',
]);
assertPaymentArray(t, bank.getPayments().slice(5), 5, 3, issuer, brand);
assertPaymentArray(t, getPayments().slice(1), 1, 13, issuer, brand);
});

0 comments on commit 7e97cc1

Please sign in to comment.