Skip to content

Commit

Permalink
feat: lp assetManager facet
Browse files Browse the repository at this point in the history
  • Loading branch information
0xpatrickdev committed Nov 15, 2024
1 parent 06ca4dd commit cbbdd0b
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 63 deletions.
198 changes: 149 additions & 49 deletions packages/fast-usdc/src/exos/liquidity-pool.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
import {
AmountMath,
AmountShape,
PaymentShape,
RatioShape,
} from '@agoric/ertp';
import { AmountMath, AmountShape, PaymentShape } from '@agoric/ertp';
import {
makeRecorderTopic,
TopicsRecordShape,
} from '@agoric/zoe/src/contractSupport/topics.js';
import { depositToSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';
import {
depositToSeat,
withdrawFromSeat,
} from '@agoric/zoe/src/contractSupport/zoeHelpers.js';
import { SeatShape } from '@agoric/zoe/src/typeGuards.js';
import { M } from '@endo/patterns';
import { Fail, q } from '@endo/errors';
import {
borrowCalc,
depositCalc,
makeParity,
repayCalc,
withdrawCalc,
withFees,
} from '../pool-share-math.js';
import { makeProposalShapes } from '../type-guards.js';
import { PoolMetricsShape } from '../typeGuards.js';

/**
* @import {Zone} from '@agoric/zone';
* @import {Remote, TypedPattern} from '@agoric/internal'
* @import {StorageNode} from '@agoric/internal/src/lib-chainStorage.js'
* @import {MakeRecorderKit, RecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'
* @import {USDCProposalShapes, ShareWorth} from '../pool-share-math.js'
* @import {PoolMetrics, PoolStats} from '../types.js';
*/

const { add, isEqual } = AmountMath;
const { add, isEqual, makeEmpty } = AmountMath;

/** @param {Brand} brand */
const makeDust = brand => AmountMath.make(brand, 1n);
Expand All @@ -45,31 +46,36 @@ const makeDust = brand => AmountMath.make(brand, 1n);
* @param {ZCFSeat} poolSeat
* @param {ShareWorth} shareWorth
* @param {Brand} USDC
* @param {Amount<'nat'>} outstandingLends
*/
const checkPoolBalance = (poolSeat, shareWorth, USDC) => {
const checkPoolBalance = (poolSeat, shareWorth, USDC, outstandingLends) => {
const available = poolSeat.getAmountAllocated('USDC', USDC);
const dust = makeDust(USDC);
isEqual(add(available, dust), shareWorth.numerator) ||
const virtualTotal = add(add(available, dust), outstandingLends);
isEqual(virtualTotal, shareWorth.numerator) ||
Fail`🚨 pool balance ${q(available)} inconsistent with shareWorth ${q(shareWorth)}`;
};

/**
* @param {Zone} zone
* @param {ZCF} zcf
* @param {Brand<'nat'>} USDC
* @param {object} caps
* @param {ZCF} caps.zcf
* @param {{brand: Brand<'nat'>; issuer: Issuer<'nat'>;}} caps.usdc
* @param {{
* makeRecorderKit: MakeRecorderKit;
* }} tools
* }} caps.tools
*/
export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
export const prepareLiquidityPoolKit = (zone, { zcf, usdc, tools }) => {
return zone.exoClassKit(
'Liquidity Pool',
{
feeSink: M.interface('feeSink', {
receive: M.call(AmountShape, PaymentShape).returns(M.promise()),
assetManager: M.interface('assetManager', {
lookupBalance: M.call().returns(AmountShape),
borrow: M.callWhen(AmountShape).returns(PaymentShape),
repay: M.callWhen(PaymentShape, PaymentShape).returns(M.undefined()),
}),
external: M.interface('external', {
publishShareWorth: M.call().returns(),
publishPoolMetrics: M.call().returns(),
}),
depositHandler: M.interface('depositHandler', {
handle: M.call(SeatShape, M.any()).returns(M.promise()),
Expand All @@ -89,60 +95,145 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
*/
(shareMint, node) => {
const { brand: PoolShares } = shareMint.getIssuerRecord();
const proposalShapes = makeProposalShapes({ USDC, PoolShares });
const shareWorth = makeParity(makeDust(USDC), PoolShares);
const proposalShapes = makeProposalShapes({
USDC: usdc.brand,
PoolShares,
});
const shareWorth = makeParity(makeDust(usdc.brand), PoolShares);
const { zcfSeat: poolSeat } = zcf.makeEmptySeatKit();
const shareWorthRecorderKit = tools.makeRecorderKit(node, RatioShape);
const poolMetricsRecorderKit = tools.makeRecorderKit(
node,
PoolMetricsShape,
);
/** used for `checkPoolBalance` invariant */
const outstandingLends = makeEmpty(usdc.brand);
const poolStats = /** @type {PoolStats} */ harden({
totalFees: makeEmpty(usdc.brand),
totalBorrows: makeEmpty(usdc.brand),
totalReturns: makeEmpty(usdc.brand),
});
return {
shareMint,
shareWorth,
outstandingLends,
poolStats,
poolMetricsRecorderKit,
poolSeat,
PoolShares,
proposalShapes,
shareWorthRecorderKit,
shareMint,
shareWorth,
};
},
{
feeSink: {
assetManager: {
lookupBalance() {
const { poolSeat } = this.state;
return poolSeat.getAmountAllocated('USDC', usdc.brand);
},
/**
* @param {Amount<'nat'>} amount
* @param {Payment<'nat'>} payment
* @returns {Promise<Payment<'nat'>>}
*/
async receive(amount, payment) {
const { poolSeat, shareWorth } = this.state;
const { external } = this.facets;
await depositToSeat(
zcf,
poolSeat,
harden({ USDC: amount }),
harden({ USDC: payment }),
async borrow(amount) {
const { outstandingLends, poolSeat, poolStats } = this.state;
const available = poolSeat.getAmountAllocated('USDC', usdc.brand);
// Validate amount is available in pool
const post = borrowCalc(
amount,
available,
outstandingLends,
poolStats,
);
this.state.shareWorth = withFees(shareWorth, amount);
external.publishShareWorth();

// XXX COMMIT POINT?
const { USDC: paymentP } = await withdrawFromSeat(zcf, poolSeat, {
USDC: amount,
});
const payment = await paymentP;
Object.assign(this.state, post);
this.facets.external.publishPoolMetrics();

return payment;
},

/**
* @param {{ Principal: Amount<'nat'>; PoolFee: Amount<'nat'>; }} amounts
* @param {{ Principal: Payment<'nat'>; PoolFee: Payment<'nat'>; }} payments
*/
async repay(amounts, payments) {
const { outstandingLends, poolSeat, poolStats, shareWorth } =
this.state;
checkPoolBalance(poolSeat, shareWorth, usdc.brand, outstandingLends);
// ensure repay calcs pass, but wait until we commit to use them to ensure
// we have the latest state
repayCalc(shareWorth, amounts, outstandingLends, poolStats);
const { zcfSeat: tmpSeat } = zcf.makeEmptySeatKit();

/**
* XXX COMMIT POINT?
* Note: this is turn boundary after preconditions have been established.
*/
await depositToSeat(zcf, tmpSeat, amounts, payments);
zcf.atomicRearrange(
harden([
[
tmpSeat,
poolSeat,
amounts,
// TODO: confirm this is a valid approach
{ USDC: add(amounts.PoolFee, amounts.Principal) },
],
]),
);
const post = repayCalc(
shareWorth,
amounts,
outstandingLends,
poolStats,
);
Object.assign(this.state, post);
try {
checkPoolBalance(
poolSeat,
post.shareWorth,
usdc.brand,
post.outstandingLends,
);
} catch (cause) {
const reason = Error('🚨 repay invariant failed', { cause });
console.error(reason.message, cause);
}
this.facets.external.publishPoolMetrics();
},
},

external: {
publishShareWorth() {
const { shareWorth } = this.state;
const { recorder } = this.state.shareWorthRecorderKit;
publishPoolMetrics() {
const { poolStats, shareWorth, poolSeat } = this.state;
const { recorder } = this.state.poolMetricsRecorderKit;
// Consumers of this .write() are off-chain / outside the VM.
// And there's no way to recover from a failed write.
// So don't await.
void recorder.write(shareWorth);
void recorder.write(
/** @type {PoolMetrics} */ ({
availableBalance: poolSeat.getAmountAllocated('USDC', usdc.brand),
shareWorth,
...poolStats,
}),
);
},
},

depositHandler: {
/** @param {ZCFSeat} lp */
async handle(lp) {
const { shareWorth, shareMint, poolSeat } = this.state;
const { shareWorth, shareMint, poolSeat, outstandingLends } =
this.state;
const { external } = this.facets;

/** @type {USDCProposalShapes['deposit']} */
// @ts-expect-error ensured by proposalShape
const proposal = lp.getProposal();
checkPoolBalance(poolSeat, shareWorth, USDC);
checkPoolBalance(poolSeat, shareWorth, usdc.brand, outstandingLends);
const post = depositCalc(shareWorth, proposal);

// COMMIT POINT
Expand All @@ -165,20 +256,21 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
console.error(reason.message, cause);
zcf.shutdownWithFailure(reason);
}
external.publishShareWorth();
external.publishPoolMetrics();
},
},
withdrawHandler: {
/** @param {ZCFSeat} lp */
async handle(lp) {
const { shareWorth, shareMint, poolSeat } = this.state;
const { shareWorth, shareMint, poolSeat, outstandingLends } =
this.state;
const { external } = this.facets;

/** @type {USDCProposalShapes['withdraw']} */
// @ts-expect-error ensured by proposalShape
const proposal = lp.getProposal();
const { zcfSeat: burn } = zcf.makeEmptySeatKit();
checkPoolBalance(poolSeat, shareWorth, USDC);
checkPoolBalance(poolSeat, shareWorth, usdc.brand, outstandingLends);
const post = withdrawCalc(shareWorth, proposal);

// COMMIT POINT
Expand All @@ -201,7 +293,7 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
console.error(reason.message, cause);
zcf.shutdownWithFailure(reason);
}
external.publishShareWorth();
external.publishPoolMetrics();
},
},
public: {
Expand All @@ -222,18 +314,26 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
);
},
getPublicTopics() {
const { shareWorthRecorderKit } = this.state;
const { poolMetricsRecorderKit } = this.state;
return {
shareWorth: makeRecorderTopic('shareWorth', shareWorthRecorderKit),
poolMetrics: makeRecorderTopic(
'poolMetrics',
poolMetricsRecorderKit,
),
};
},
},
},
{
finish: ({ facets: { external } }) => {
void external.publishShareWorth();
void external.publishPoolMetrics();
},
},
);
};
harden(prepareLiquidityPoolKit);

/**
* @typedef {ReturnType<ReturnType<typeof prepareLiquidityPoolKit>>} LiquidityPoolKit
* @typedef {LiquidityPoolKit['assetManager']} AssetManagerFacet
*/
18 changes: 10 additions & 8 deletions packages/fast-usdc/src/fast-usdc.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,14 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
}
},
});
const makeLiquidityPoolKit = prepareLiquidityPoolKit(
zone,
const makeLiquidityPoolKit = prepareLiquidityPoolKit(zone, {
zcf,
terms.brands.USDC,
{ makeRecorderKit },
);
usdc: {
brand: terms.brands.USDC,
issuer: terms.issuers.USDC,
},
tools: { makeRecorderKit },
});

const makeTestInvitation = defineInertInvitation(
zcf,
Expand All @@ -105,10 +107,10 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
async makeOperatorInvitation(operatorId) {
return feedKit.creator.makeOperatorInvitation(operatorId);
},
simulateFeesFromAdvance(amount, payment) {
simulateFeesFromAdvance(/* amount, payment */) {
console.log('🚧🚧 UNTIL: advance fees are implemented 🚧🚧');
// eslint-disable-next-line no-use-before-define
return poolKit.feeSink.receive(amount, payment);
// XXX FIXME
// return poolKit.feeSink.receive(amount, payment);
},
});

Expand Down
Loading

0 comments on commit cbbdd0b

Please sign in to comment.