diff --git a/packages/run-protocol/src/vpool-xyk-amm/constantProduct/AMM-Trade.jpeg b/packages/run-protocol/src/vpool-xyk-amm/constantProduct/AMM-Trade.jpeg new file mode 100644 index 000000000000..000fa005c44f Binary files /dev/null and b/packages/run-protocol/src/vpool-xyk-amm/constantProduct/AMM-Trade.jpeg differ diff --git a/packages/run-protocol/src/vpool-xyk-amm/constantProduct/README.md b/packages/run-protocol/src/vpool-xyk-amm/constantProduct/README.md index dff568951c06..bf0901d4bc4c 100644 --- a/packages/run-protocol/src/vpool-xyk-amm/constantProduct/README.md +++ b/packages/run-protocol/src/vpool-xyk-amm/constantProduct/README.md @@ -11,25 +11,26 @@ actions of arbitrageurs. At any time a trader can trade with the pool by offering to deposit one of the two assets. They will receive an amount of the complementary asset that will maintain the invariant that the product of the balances doesn't decrease. (Rounding is done in favor of the -pool.) A fee is charged on the swap to reward the liquidity providers. +pool.) The user can specify a maximum amount they want to pay or a minimum amount they want to receive. Unlike Uniswap, this approach will charge less than the user offered or pay more than they asked for when appropriate. By analogy, if a user is willing to pay up to $20 when the price of soda is $3 per bottle, it would -give 6 bottles and only charge $18. Uniswap doesn't adjust the provided price, -so it charges $20. This matters whenever the values of the smallest unit of the -currencies are significantly different, which is common in DeFi. (We refer to -these as "improved" prices.) +give 6 bottles and only charge $18. (We refer to these as "improved" prices.) +Uniswap doesn't adjust the provided price, so it would charge $20 in that trade. +This matters whenever the values of the smallest unit of the currencies are +significantly different, which is common in DeFi. The rules that drive the design include -* When the user names an input (or output) price, they shouldn't pay more +* When the user specifies an input (or output) price, they shouldn't pay more (or receive less) than they said. * The pool fee is charged against the side not specified by the user (the - "computed side"). + "computed side"). This increases the pool balance without impacting the amount + that the user specified. * The protocol fee is always charged in RUN. -* The fees should be calculated based on the pool balances before a transaction. +* The fees are calculated based on the pool balances before the transaction. * Computations are rounded in favor of the pool. We start by estimating the exchange rate, and calculate fees based on that. Once @@ -41,11 +42,23 @@ extracted from the pools to adhere to those rules. In these tables BLD represents any collateral. The user can specify how much they want or how much they're willing to pay. We'll call the value they specified **sGive** or **sGet** and bold it. We'll always refer to the currency -being added as X (regardless of whether it's what they pay or what they receive) -and the currency the user gets as Y. This table shows which brands the -amounts each have, as well as what is computed vs. given. The PoolFee is -computed based on the calculated amount (BLD in rows 1 and 2; RUN in rows 3 and -4). The Protocol fee is always in RUN. +being added as X and the currency the user gets as Y. X and Y are the amounts +used to maintain the constant product invariant. On every transaction the pool +balance will also increase on one side or the other by the pool fee, and the +protocol fee will be extracted outside the pool. So the amount produced is less +(or the amount paid is more) because the invariant is maintained on the amounts +remaining after fees have been charged. + +In the final table, we'll see what the user pays or gets (sGet, sGive), how much +the pool balances change (xIncr, yDecr) and the changes that impact the constant +product calculation(ΔX, ΔY). The amount paid and received by the +trader and changes to the pool are calculated relative to ΔX and ΔY +so that the pool grows by the poolFee and the protocolFee can be paid from the +proceeds. + +This table shows which brands the amounts each have, as well as what is computed +vs. given. The PoolFee is computed based on the calculated amount (BLD in rows 1 +and 2; RUN in rows 3 and 4). The Protocol fee is always in RUN. | | In (X) | Out (Y) | PoolFee | Protocol Fee | Specified | Computed | |---------|-----|-----|--------|-----|------|-----| @@ -54,53 +67,64 @@ computed based on the calculated amount (BLD in rows 1 and 2; RUN in rows 3 and | **BLD in** | BLD | RUN | RUN | RUN | **sGive** | sGet | | **BLD out** | RUN | BLD | RUN | RUN | **sGet** | sGive | -We'll estimate how much the pool balances would change in the no-fee, improved -price case using the constant product formulas. We call these estimates -δX, and δY. The fees are based on δX, and δY. ρ is -the poolFee (e.g. .003). +We'll calculate how much the pool balances would change in the no-fee, improved +price case using the constant product formulas. We call these results δX, +and δY. This table uses lower case δ to distinguish estimates from +final values, which use capital Δ. -The pool fee will be ρ times whichever of δX and δY was -calculated. The protocol fee will be ρ * δX when RUN is paid in, and -ρ * δY when BLD is paid in. +The fees are based on δX, and δY. ρ is the poolFee (e.g., 30 +basis points). φ is the protocol fee (e.g., 6 basis points). The pool fee +will be ρ times whichever of δX and δY was calculated. The +protocol fee will be φ * δX when RUN is paid in, and φ * δY +when BLD is paid in. (α and β in this table are the no-fee versions.) | | δX | δY | PoolFee | Protocol Fee | |---------|-----|-----|--------|-----| -| **RUN in** | **sGive** | calc | ρ × δY | ρ × **sGive** (= ρ × δX) | -| **RUN out** | calc | **sGet** | ρ × δY | ρ × **sGet** (= ρ × δY) | -| **BLD in** | **sGive** | calc | ρ × δX | ρ × δY | -| **BLD out** | calc | **sGet** | ρ × δX | ρ × δX | +| **RUN in** | **sGive** | y * α / (1 + α) | ρ × δY | φ × **sGive** (= φ × δX) | +| **RUN out** | x * β / (1 - β) | **sGet** | ρ × δX | φ × **sGet** (= φ × δY) | +| **BLD in** | **sGive** | y * α / (1 + α) | ρ × δY | φ × δY | +| **BLD out** | x * β / (1 - β) | **sGet** | ρ × δX | φ × δX | In rows 1 and 3, **sGive** was specified and sGet will be calculated. In rows 2 and 4, **sGet** was specified and sGive will be calculated. Once we know the fees, we can add or subtract the fees and calculate the pool changes. -Notice that the ProtocolFee always affects the inputs to the constant product -calculation (because it is collected outside the pool). The PoolFee is visible -in the formulas in this table when the input to the calculation is in RUN. +The next table shows how the amount added to and subtracted from the pool (xIncr +and yIncr) differs from what the trader gives or gets and the change in the pool +values. While X' and Y' are the vaules resulting from the constant product +formula, xHat and yHat are the new values in the pool, reflecting the +contribution of the pool fee. From the diagram, xIncr is the difference between +X and xHat, while yIncr is the difference between Y and yHat. -| | input estimate | output estimate | -|---------|-----|-----| -| **RUN in** | **sGive** - ProtocolFee | | -| **RUN out** | | **sGet** + ProtocolFee + PoolFee | -| **BLD in** | **sGive** - ProtocolFee - PoolFee | | -| **BLD out** | | **sGet** + ProtocolFee | +The protocol fee (φ) is always charged on the RUN side, and doesn't go in +the pool. The pool fee (ρ) is charged on the amount that is calculated +(ΔX or ΔY) and is included in yHat. We use the estimate of the amount in or out to calculate improved values of ΔX and ΔY. These values tell us how much the trader will pay, the changes in pool balances, and what the trader will receive. As before, ΔX reflects a balance that will be growing, and ΔY one that will be -shrinking. If **sGive** is known, we subtract fees to get ΔX and calculate -ΔY. If **sGet** is known, we add fees to get ΔY and calculate -ΔX. ΔY and ΔX are the values that maintain the constant -product invariant. The amount paid and received by the trader and changes to the -pool are calculated relative to ΔX and ΔY so that the pool grows by -the poolFee and the protocolFee can be paid from the proceeds. - -| | xIncr | yDecr | pay In | pay Out | +shrinking. + +![AMM Diagram](./AMM-trade.jpeg) + +When RUN is specified (in or out), we adjust by the protocol Fee to get the +change to the RUN balance (which gives X' or Y'); use that to calculate the +BLD component of the constant product, and then adjust δ by the pool fee. +When BLD is specified, the entire amount will affect the pool balance (so the +value in the constant product formula is the final BLD balance.) Both the pool +and protocol fees are charged in RUN so they respectively reduce sGet or +increase sGive. + +This table shows how the pool's growth and trader's values differ from the +amounts that match the constant product formula. X is in RUN on lines 1 and 4, +and BLD on lines 2 and 3. + +| | xIncr | yDecr | pay In (sGive) | pay Out (sGet) | |---------|-----|-----|-----|-----| | **RUN in** | ΔX | ΔY - PoolFee | ΔX + protocolFee | ΔY - PoolFee | -| **RUN out** | ΔX | ΔY - PoolFee | ΔX + protocolFee | ΔY - PoolFee | -| **BLD in** | ΔX + PoolFee | ΔY | ΔX + PoolFee + ProtocolFee | ΔY | +| **RUN out** | ΔX + PoolFee | ΔY | ΔX + protocolFee + PoolFee | ΔY | +| **BLD in** | ΔX | ΔY - PoolFee | ΔX + ProtocolFee | ΔY - PoolFee | | **BLD out** | ΔX + PoolFee | ΔY | ΔX + PoolFee + ProtocolFee | ΔY | In the two right columns the protocolFee is either added to the amount the @@ -108,6 +132,20 @@ trader pays, or subtracted from the proceeds. The poolFee does the same on the left side, and it is either added to the amount deposited in the pool (xIncr) or deducted from the amout removed from the pool (yDecr). + +* line 1: the trader provides **sGive** RUN; the pool will gain that minus + the protocol fee in RUN. The value in the RUN pool after (X') will be used to + calculate Y'. The pool fee is calculated from δY and the trader gets + &deltaY; - poolFee +* line 2: the trader asked for **sGet** RUN; the pool will be reduced by that + amount plus the protocol fee. Y' will be used to calculate X'. The trader + must provide &deltaX; plus poolFee +* Line 3: the trader pays **sGive** BLD; the pool will increase by that minus + both fees +* line 4: the trader specified **sGet** BLD; the pool will be reduced by that + plus the protocol fee. + + ## Example For example, let's say the pool has 40,000,000 RUN and 3,000,000 BLD. Alice diff --git a/packages/vats/src/nameHub.js b/packages/vats/src/nameHub.js index b475a13f9c97..a56dfc856f8f 100644 --- a/packages/vats/src/nameHub.js +++ b/packages/vats/src/nameHub.js @@ -33,19 +33,26 @@ export const makeNameHubKit = () => { return E(firstValue).lookup(...remaining); }, entries() { - return mapIterable(keyToRecord.entries(), ([key, record]) => [ - key, - record.promise || record.value, - ]); + return [ + ...mapIterable( + keyToRecord.entries(), + ([key, record]) => /** @type {[string, ERef]} */ ([ + key, + record.promise || record.value, + ]), + ), + ]; }, values() { - return mapIterable( - keyToRecord.values(), - record => record.promise || record.value, - ); + return [ + ...mapIterable( + keyToRecord.values(), + record => record.promise || record.value, + ), + ]; }, keys() { - return keyToRecord.keys(); + return [...keyToRecord.keys()]; }, }); diff --git a/packages/vats/src/types.js b/packages/vats/src/types.js index ff97844e4ab2..787526615cbd 100644 --- a/packages/vats/src/types.js +++ b/packages/vats/src/types.js @@ -15,14 +15,19 @@ /** * @typedef {Object} NameHub + * + * NOTE: We need to return arrays, not iterables, because even if marshal could + * allow passing a remote iterable, there would be an inordinate number of round + * trips for the contents of even the simplest nameHub. + * * @property {(...path: Array) => Promise} lookup Look up a * path of keys starting from the current NameHub. Wait on any reserved * promises. - * @property {() => Iterable<[string, unknown]>} entries get all the entries + * @property {() => [string, unknown][]} entries get all the entries * available in the current NameHub - * @property {() => Iterable} keys get all names available in the + * @property {() => string[]} keys get all names available in the * current NameHub - * @property {() => Iterable} values get all values available in the + * @property {() => unknown[]} values get all values available in the * current NameHub */ diff --git a/packages/vats/test/test-name-hub.js b/packages/vats/test/test-name-hub.js index 91038979a877..f2129ee64fcf 100644 --- a/packages/vats/test/test-name-hub.js +++ b/packages/vats/test/test-name-hub.js @@ -7,15 +7,15 @@ test('makeNameHubKit - lookup paths', async t => { const { nameAdmin: na2, nameHub: nh2 } = makeNameHubKit(); const { nameAdmin: na3, nameHub: nh3 } = makeNameHubKit(); - t.deepEqual([...nh1.keys()], []); - t.deepEqual([...nh1.values()], []); - t.deepEqual([...nh1.entries()], []); - t.deepEqual([...nh2.keys()], []); - t.deepEqual([...nh2.values()], []); - t.deepEqual([...nh2.entries()], []); - t.deepEqual([...nh3.keys()], []); - t.deepEqual([...nh3.values()], []); - t.deepEqual([...nh3.entries()], []); + t.deepEqual(nh1.keys(), []); + t.deepEqual(nh1.values(), []); + t.deepEqual(nh1.entries(), []); + t.deepEqual(nh2.keys(), []); + t.deepEqual(nh2.values(), []); + t.deepEqual(nh2.entries(), []); + t.deepEqual(nh3.keys(), []); + t.deepEqual(nh3.values(), []); + t.deepEqual(nh3.entries(), []); na1.update('path1', nh2); t.is(await nh1.lookup('path1'), nh2); @@ -24,15 +24,15 @@ test('makeNameHubKit - lookup paths', async t => { na3.update('path3', 'finish'); t.is(await nh3.lookup('path3'), 'finish'); - t.deepEqual([...nh1.keys()], ['path1']); - t.deepEqual([...nh1.values()], [nh2]); - t.deepEqual([...nh1.entries()], [['path1', nh2]]); - t.deepEqual([...nh2.keys()], ['path2']); - t.deepEqual([...nh2.values()], [nh3]); - t.deepEqual([...nh2.entries()], [['path2', nh3]]); - t.deepEqual([...nh3.keys()], ['path3']); - t.deepEqual([...nh3.values()], ['finish']); - t.deepEqual([...nh3.entries()], [['path3', 'finish']]); + t.deepEqual(nh1.keys(), ['path1']); + t.deepEqual(nh1.values(), [nh2]); + t.deepEqual(nh1.entries(), [['path1', nh2]]); + t.deepEqual(nh2.keys(), ['path2']); + t.deepEqual(nh2.values(), [nh3]); + t.deepEqual(nh2.entries(), [['path2', nh3]]); + t.deepEqual(nh3.keys(), ['path3']); + t.deepEqual(nh3.values(), ['finish']); + t.deepEqual(nh3.entries(), [['path3', 'finish']]); t.is(await nh1.lookup(), nh1); t.is(await nh1.lookup('path1'), nh2); @@ -50,9 +50,9 @@ test('makeNameHubKit - reserve and update', async t => { message: /"nameKey" not found: .*/, }); - t.deepEqual([...nameHub.keys()], []); - t.deepEqual([...nameHub.values()], []); - t.deepEqual([...nameHub.entries()], []); + t.deepEqual(nameHub.keys(), []); + t.deepEqual(nameHub.values(), []); + t.deepEqual(nameHub.entries(), []); // Try reserving and looking up. nameAdmin.reserve('hello'); @@ -61,16 +61,16 @@ test('makeNameHubKit - reserve and update', async t => { const lookupHelloP = nameHub .lookup('hello') .finally(() => (lookedUpHello = true)); - t.deepEqual([...nameHub.keys()], ['hello']); - const helloP = [...nameHub.values()][0]; + t.deepEqual(nameHub.keys(), ['hello']); + const helloP = nameHub.values()[0]; t.assert(helloP instanceof Promise); - t.deepEqual([...nameHub.entries()], [['hello', helloP]]); + t.deepEqual(nameHub.entries(), [['hello', helloP]]); t.falsy(lookedUpHello); nameAdmin.update('hello', 'foo'); - t.deepEqual([...nameHub.keys()], ['hello']); - t.deepEqual([...nameHub.values()], ['foo']); - t.deepEqual([...nameHub.entries()], [['hello', 'foo']]); + t.deepEqual(nameHub.keys(), ['hello']); + t.deepEqual(nameHub.values(), ['foo']); + t.deepEqual(nameHub.entries(), [['hello', 'foo']]); t.is(await lookupHelloP, 'foo'); t.truthy(lookedUpHello); @@ -85,9 +85,9 @@ test('makeNameHubKit - reserve and delete', async t => { message: /"nameKey" not found: .*/, }); - t.deepEqual([...nameHub.keys()], []); - t.deepEqual([...nameHub.values()], []); - t.deepEqual([...nameHub.entries()], []); + t.deepEqual(nameHub.keys(), []); + t.deepEqual(nameHub.values(), []); + t.deepEqual(nameHub.entries(), []); nameAdmin.reserve('goodbye'); let lookedUpGoodbye = false; @@ -96,15 +96,15 @@ test('makeNameHubKit - reserve and delete', async t => { .finally(() => (lookedUpGoodbye = true)); t.falsy(lookedUpGoodbye); - t.deepEqual([...nameHub.keys()], ['goodbye']); - const goodbyeP = [...nameHub.values()][0]; + t.deepEqual(nameHub.keys(), ['goodbye']); + const goodbyeP = nameHub.values()[0]; t.assert(goodbyeP instanceof Promise); - t.deepEqual([...nameHub.entries()], [['goodbye', goodbyeP]]); + t.deepEqual(nameHub.entries(), [['goodbye', goodbyeP]]); nameAdmin.delete('goodbye'); - t.deepEqual([...nameHub.keys()], []); - t.deepEqual([...nameHub.values()], []); - t.deepEqual([...nameHub.entries()], []); + t.deepEqual(nameHub.keys(), []); + t.deepEqual(nameHub.values(), []); + t.deepEqual(nameHub.entries(), []); await t.throwsAsync(lookupGoodbyeP, { message: /"nameKey" not found: .*/, }); diff --git a/packages/zoe/src/contractFacet/zcfZygote.js b/packages/zoe/src/contractFacet/zcfZygote.js index 3c821b5f0fc3..0d67e9b7efcb 100644 --- a/packages/zoe/src/contractFacet/zcfZygote.js +++ b/packages/zoe/src/contractFacet/zcfZygote.js @@ -332,7 +332,17 @@ export const makeZCFZygote = ( let contractCode; - /** @type {ZCFZygote} */ + /** + * A zygote is a pre-image of a vat that can quickly be instantiated because + * the code has already been evaluated. SwingSet doesn't support zygotes yet. + * Once it does the code will be evaluated once when creating the zcfZygote, + * then the start() function will be called each time an instance is started. + * + * Currently, Zoe's buildRootObject calls makeZCFZygote, evaluateContract, and + * startContract every time a contract instance is created. + * + * @type {ZCFZygote} + * */ const zcfZygote = { evaluateContract: bundle => { contractCode = evalContractBundle(bundle); diff --git a/packages/zoe/src/zoeService/installationStorage.js b/packages/zoe/src/zoeService/installationStorage.js index c9b651306bc9..2383ae83a2ad 100644 --- a/packages/zoe/src/zoeService/installationStorage.js +++ b/packages/zoe/src/zoeService/installationStorage.js @@ -12,8 +12,11 @@ export const makeInstallationStorage = () => { const installations = new WeakSet(); /** - * Create an installation by permanently storing the bundle. It will be - * evaluated each time it is used to make a new instance of a contract. + * Create an installation by permanently storing the bundle. The code is + * currently evaluated each time it is used to make a new instance of a + * contract. When SwingSet supports zygotes, the code will be evaluated once + * when creating a zcfZygote, then the start() function will be called each + * time an instance is started. */ /** @type {Install} */ const install = async bundle => {