From f01f6b4eaa134380279e8413eda8e78f10b8ca75 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Wed, 26 Feb 2025 11:39:04 -0800 Subject: [PATCH 1/3] refactor: use a single zcf.atomicReallocate for repaying --- packages/fast-usdc/src/exos/liquidity-pool.js | 46 ++++++--------- packages/fast-usdc/src/exos/settler.js | 7 +-- packages/fast-usdc/src/pool-share-math.js | 32 ++++------- packages/fast-usdc/test/exos/settler.test.ts | 22 +++----- .../fast-usdc/test/pool-share-math.test.ts | 56 +++---------------- 5 files changed, 46 insertions(+), 117 deletions(-) diff --git a/packages/fast-usdc/src/exos/liquidity-pool.js b/packages/fast-usdc/src/exos/liquidity-pool.js index f71a6d6fdff..c86652b4839 100644 --- a/packages/fast-usdc/src/exos/liquidity-pool.js +++ b/packages/fast-usdc/src/exos/liquidity-pool.js @@ -1,5 +1,7 @@ import { AmountMath, AmountShape, RatioShape } from '@agoric/ertp'; import { + fromOnly, + toOnly, makeRecorderTopic, RecorderKitShape, TopicsRecordShape, @@ -7,6 +9,7 @@ import { import { SeatShape } from '@agoric/zoe/src/typeGuards.js'; import { M } from '@endo/patterns'; import { Fail, q } from '@endo/errors'; +import { TransferPartShape } from '@agoric/zoe/src/contractSupport/atomicTransfer.js'; import { borrowCalc, checkPoolBalance, @@ -83,7 +86,7 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => { }), repayer: M.interface('repayer', { repay: M.call( - SeatShape, + TransferPartShape, harden({ Principal: makeNatAmountShape(USDC, 1n), PoolFee: makeNatAmountShape(USDC, 0n), @@ -179,7 +182,6 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => { * @param {Amount<'nat'>} amount */ returnToPool(borrowSeat, amount) { - const { zcfSeat: repaySeat } = zcf.makeEmptySeatKit(); const returnAmounts = harden({ Principal: amount, PoolFee: makeEmpty(USDC), @@ -188,21 +190,18 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => { const borrowSeatAllocation = borrowSeat.getCurrentAllocation(); isGTE(borrowSeatAllocation.USDC, amount) || Fail`⚠️ borrowSeatAllocation ${q(borrowSeatAllocation)} less than amountKWR ${q(amount)}`; - // arrange payments in a format repay is expecting - zcf.atomicRearrange( - harden([[borrowSeat, repaySeat, { USDC: amount }, returnAmounts]]), - ); - this.facets.repayer.repay(repaySeat, returnAmounts); + + const transferSourcePart = fromOnly(borrowSeat, { USDC: amount }); + this.facets.repayer.repay(transferSourcePart, returnAmounts); borrowSeat.exit(); - repaySeat.exit(); }, }, repayer: { /** - * @param {ZCFSeat} fromSeat - * @param {RepayAmountKWR} amounts + * @param {TransferPart} sourceTransfer + * @param {RepayAmountKWR} split */ - repay(fromSeat, amounts) { + repay(sourceTransfer, split) { const { encumberedBalance, feeSeat, @@ -215,32 +214,21 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => { shareWorth, encumberedBalance, ); - - const fromSeatAllocation = fromSeat.getCurrentAllocation(); - // Validate allocation equals amounts and Principal <= encumberedBalance + // Validate Principal <= encumberedBalance and produce poolStats after const post = repayCalc( shareWorth, - fromSeatAllocation, - amounts, + split, encumberedBalance, poolStats, ); - const { ContractFee, ...rest } = amounts; - // COMMIT POINT // UNTIL #10684: ability to terminate an incarnation w/o terminating the contract - zcf.atomicRearrange( - harden([ - [ - fromSeat, - poolSeat, - rest, - { USDC: add(amounts.PoolFee, amounts.Principal) }, - ], - [fromSeat, feeSeat, { ContractFee }, { USDC: ContractFee }], - ]), - ); + zcf.atomicRearrange([ + sourceTransfer, + toOnly(poolSeat, { USDC: add(split.PoolFee, split.Principal) }), + toOnly(feeSeat, { USDC: split.ContractFee }), + ]); Object.assign(this.state, post); this.facets.external.publishPoolMetrics(); diff --git a/packages/fast-usdc/src/exos/settler.js b/packages/fast-usdc/src/exos/settler.js index e7d69b4c188..c137118331f 100644 --- a/packages/fast-usdc/src/exos/settler.js +++ b/packages/fast-usdc/src/exos/settler.js @@ -6,6 +6,7 @@ import { E } from '@endo/far'; import { M } from '@endo/patterns'; import { decodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js'; +import { fromOnly } from '@agoric/zoe/src/contractSupport/index.js'; import { PendingTxStatus } from '../constants.js'; import { makeFeeTools } from '../utils/fees.js'; import { @@ -323,10 +324,8 @@ export const prepareSettler = ( harden({ In: received }), ), ); - zcf.atomicRearrange( - harden([[settlingSeat, settlingSeat, { In: received }, split]]), - ); - repayer.repay(settlingSeat, split); + const transferPart = fromOnly(settlingSeat, { In: received }); + repayer.repay(transferPart, split); settlingSeat.exit(); // update status manager, marking tx `DISBURSED` diff --git a/packages/fast-usdc/src/pool-share-math.js b/packages/fast-usdc/src/pool-share-math.js index c3ddb57feee..474edfeab13 100644 --- a/packages/fast-usdc/src/pool-share-math.js +++ b/packages/fast-usdc/src/pool-share-math.js @@ -201,35 +201,23 @@ export const borrowCalc = ( /** * @param {ShareWorth} shareWorth - * @param {Allocation} fromSeatAllocation - * @param {RepayAmountKWR} amounts + * @param {RepayAmountKWR} split * @param {Amount<'nat'>} encumberedBalance aka 'outstanding borrows' * @param {PoolStats} poolStats - * @throws {Error} if allocations do not match amounts or Principal exceeds encumberedBalance + * @throws {Error} if Principal exceeds encumberedBalance */ -export const repayCalc = ( - shareWorth, - fromSeatAllocation, - amounts, - encumberedBalance, - poolStats, -) => { - (isEqual(fromSeatAllocation.Principal, amounts.Principal) && - isEqual(fromSeatAllocation.PoolFee, amounts.PoolFee) && - isEqual(fromSeatAllocation.ContractFee, amounts.ContractFee)) || - Fail`Cannot repay. From seat allocation ${q(fromSeatAllocation)} does not equal amounts ${q(amounts)}.`; - - isGTE(encumberedBalance, amounts.Principal) || - Fail`Cannot repay. Principal ${q(amounts.Principal)} exceeds encumberedBalance ${q(encumberedBalance)}.`; +export const repayCalc = (shareWorth, split, encumberedBalance, poolStats) => { + isGTE(encumberedBalance, split.Principal) || + Fail`Cannot repay. Principal ${q(split.Principal)} exceeds encumberedBalance ${q(encumberedBalance)}.`; return harden({ - shareWorth: withFees(shareWorth, amounts.PoolFee), - encumberedBalance: subtract(encumberedBalance, amounts.Principal), + shareWorth: withFees(shareWorth, split.PoolFee), + encumberedBalance: subtract(encumberedBalance, split.Principal), poolStats: { ...poolStats, - totalRepays: add(poolStats.totalRepays, amounts.Principal), - totalPoolFees: add(poolStats.totalPoolFees, amounts.PoolFee), - totalContractFees: add(poolStats.totalContractFees, amounts.ContractFee), + totalRepays: add(poolStats.totalRepays, split.Principal), + totalPoolFees: add(poolStats.totalPoolFees, split.PoolFee), + totalContractFees: add(poolStats.totalContractFees, split.ContractFee), }, }); }; diff --git a/packages/fast-usdc/test/exos/settler.test.ts b/packages/fast-usdc/test/exos/settler.test.ts index 31e5c4b2ece..eb449c13901 100644 --- a/packages/fast-usdc/test/exos/settler.test.ts +++ b/packages/fast-usdc/test/exos/settler.test.ts @@ -198,8 +198,8 @@ const makeTestContext = async t => { }; const repayer = zone.exo('Repayer Mock', undefined, { - repay(fromSeat: ZCFSeat, amounts: AmountKeywordRecord) { - callLog.push(harden({ method: 'repay', fromSeat, amounts })); + repay(sourceTransfer: TransferPart, amounts: AmountKeywordRecord) { + callLog.push(harden({ method: 'repay', sourceTransfer, amounts })); }, }); @@ -261,8 +261,8 @@ test('happy path: disburse to LPs; StatusManager removes tx', async t => { t.log('Funds were disbursed to LP.'); const calls = peekCalls(); - t.is(calls.length, 3); - const [withdraw, rearrange, repay] = calls; + t.is(calls.length, 2); + const [withdraw, repay] = calls; t.deepEqual( withdraw, @@ -284,16 +284,6 @@ test('happy path: disburse to LPs; StatusManager removes tx', async t => { Principal: usdc.make(146999999n), }); - t.like( - rearrange, - { method: 'atomicRearrange' }, - '2. settler called atomicRearrange ', - ); - t.is(rearrange.parts.length, 1); - const [s1, s2, a1, a2] = rearrange.parts[0]; - t.is(s1, s2, 'src and dest seat are the same'); - t.deepEqual([a1, a2], [{ In }, expectedSplit]); - t.like( repay, { @@ -302,7 +292,9 @@ test('happy path: disburse to LPs; StatusManager removes tx', async t => { }, '3. settler called repay() on liquidity pool repayer facet', ); - t.is(repay.fromSeat, s1); + t.like(repay.sourceTransfer[2], { + In: usdc.units(150), + }); t.deepEqual( statusManager.lookupPending( diff --git a/packages/fast-usdc/test/pool-share-math.test.ts b/packages/fast-usdc/test/pool-share-math.test.ts index 6fbb1922590..912157112a6 100644 --- a/packages/fast-usdc/test/pool-share-math.test.ts +++ b/packages/fast-usdc/test/pool-share-math.test.ts @@ -425,15 +425,8 @@ test('basic repay calculation', t => { }; const encumberedBalance = make(USDC, 200n); const poolStats = makeInitialPoolStats(); - const fromSeatAllocation = amounts; - const result = repayCalc( - shareWorth, - fromSeatAllocation, - amounts, - encumberedBalance, - poolStats, - ); + const result = repayCalc(shareWorth, amounts, encumberedBalance, poolStats); t.deepEqual( result.encumberedBalance, @@ -489,23 +482,13 @@ test('repay fails when principal exceeds encumbered balance', t => { const fromSeatAllocation = amounts; - t.throws( - () => - repayCalc( - shareWorth, - fromSeatAllocation, - amounts, - encumberedBalance, - poolStats, - ), - { - message: /Cannot repay. Principal .* exceeds encumberedBalance/, - }, - ); + t.throws(() => repayCalc(shareWorth, amounts, encumberedBalance, poolStats), { + message: /Cannot repay. Principal .* exceeds encumberedBalance/, + }); t.notThrows( () => - repayCalc(shareWorth, fromSeatAllocation, amounts, make(USDC, 200n), { + repayCalc(shareWorth, amounts, make(USDC, 200n), { ...makeInitialPoolStats(), totalBorrows: make(USDC, 200n), }), @@ -528,24 +511,9 @@ test('repay fails when seat allocation does not equal amounts', t => { totalBorrows: make(USDC, 100n), }; - const fromSeatAllocation = { - ...amounts, - ContractFee: make(USDC, 1n), - }; - - t.throws( - () => - repayCalc( - shareWorth, - fromSeatAllocation, - amounts, - encumberedBalance, - poolStats, - ), - { - message: /Cannot repay. From seat allocation .* does not equal amounts/, - }, - ); + t.throws(() => repayCalc(shareWorth, amounts, encumberedBalance, poolStats), { + message: /Cannot repay. Principal .* exceeds encumberedBalance/, + }); }); test('repay succeeds with no Pool or Contract Fee', t => { @@ -563,13 +531,7 @@ test('repay succeeds with no Pool or Contract Fee', t => { totalBorrows: make(USDC, 100n), }; const fromSeatAllocation = amounts; - const actual = repayCalc( - shareWorth, - fromSeatAllocation, - amounts, - encumberedBalance, - poolStats, - ); + const actual = repayCalc(shareWorth, amounts, encumberedBalance, poolStats); t.like(actual, { shareWorth, encumberedBalance: { From abe4a95ea47e74f2324eb6da02003c2025d567bf Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Wed, 26 Feb 2025 11:40:44 -0800 Subject: [PATCH 2/3] refactor: use arrow function style https://github.com/Agoric/agoric-sdk/wiki/Arrow-Function-Style --- packages/fast-usdc/test/pool-share-math.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/fast-usdc/test/pool-share-math.test.ts b/packages/fast-usdc/test/pool-share-math.test.ts index 912157112a6..c2565320885 100644 --- a/packages/fast-usdc/test/pool-share-math.test.ts +++ b/packages/fast-usdc/test/pool-share-math.test.ts @@ -209,9 +209,8 @@ const scaleAmount = (frac: number, amount: Amount<'nat'>) => { }; // ack: https://stackoverflow.com/a/2901298/7963 -function numberWithCommas(x) { - return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); -} +const numberWithCommas = x => + x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); const logAmt = amt => [ Number(amt.value), From 6973a09fe1cbbd9033d13ade671f1bf03b4ddcdf Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Thu, 27 Feb 2025 16:35:27 -0800 Subject: [PATCH 3/3] chore: harden arguments to atomicRearrange --- packages/fast-usdc/src/exos/liquidity-pool.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/fast-usdc/src/exos/liquidity-pool.js b/packages/fast-usdc/src/exos/liquidity-pool.js index c86652b4839..09903c5c89d 100644 --- a/packages/fast-usdc/src/exos/liquidity-pool.js +++ b/packages/fast-usdc/src/exos/liquidity-pool.js @@ -224,11 +224,13 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => { // COMMIT POINT // UNTIL #10684: ability to terminate an incarnation w/o terminating the contract - zcf.atomicRearrange([ - sourceTransfer, - toOnly(poolSeat, { USDC: add(split.PoolFee, split.Principal) }), - toOnly(feeSeat, { USDC: split.ContractFee }), - ]); + zcf.atomicRearrange( + harden([ + sourceTransfer, + toOnly(poolSeat, { USDC: add(split.PoolFee, split.Principal) }), + toOnly(feeSeat, { USDC: split.ContractFee }), + ]), + ); Object.assign(this.state, post); this.facets.external.publishPoolMetrics();