diff --git a/packages/zoe/src/contracts/callSpread.js b/packages/zoe/src/contracts/callSpread.js new file mode 100644 index 000000000000..7a483508d30a --- /dev/null +++ b/packages/zoe/src/contracts/callSpread.js @@ -0,0 +1,219 @@ +// @ts-check +import '../../exported'; + +import { assert, details } from '@agoric/assert'; + +// Eventually will be importable from '@agoric/zoe-contract-support' +import { makePromiseKit } from '@agoric/promise-kit'; +import { E } from '@agoric/eventual-send'; +import { + assertProposalShape, + depositToSeat, + natSafeMath, + trade, + assertUsesNatMath, +} from '../contractSupport'; + +const { subtract, multiply, floorDivide } = natSafeMath; + +/** + * Constants for long and short positions. + * + * @type {{ LONG: 'long', SHORT: 'short' }} + */ +const Position = { + LONG: 'long', + SHORT: 'short', +}; + +const PERCENT_BASE = 100; +const inverse = percent => subtract(PERCENT_BASE, percent); + +/** + * This contract implements a fully collateralized call spread. This is a + * combination of a call option bought at one strike price and a second call + * option sold at a higher price. The contracts are sold in pairs, and the + * purchaser pays the entire amount that will be paid out. The individual + * options are ERTP invitations that are suitable for resale. + * + * This option is settled financially. There is no requirement that the original + * purchaser have ownership of the underlying asset at the start, and the + * beneficiaries shouldn't expect to take delivery at closing. + * + * The issuerKeywordRecord specifies the issuers for three keywords: Underlying, + * Strike, and Collateral. The payout is in Collateral. Strike amounts are used + * for the price oracle's quotes as to the value of the Underlying, as well as + * the strike prices in the terms. The terms include { expiration, + * underlyingAmount, priceAuthority, strikePrice1, strikePrice2, + * settlementAmount }. expiration is a time recognized by the priceAuthority. + * underlyingAmount is passed to the priceAuthority, so it could be an NFT or a + * fungible amount. strikePrice2 must be greater than strikePrice1. + * settlementAmount uses Collateral. + * + * creatorInvitation has terms that include the amounts of the two options as + * longOption and shortOption. When the creatorInvitation is exercised, the + * payout includes the two option positions, which are themselves invitations + * which can be exercised for free, and are valuable for their payouts. + * + * Future enhancements: + * + issue multiple option pairs with the same expiration from a single instance + * + create separate invitations to purchase the pieces of the option pair. + * (This would remove the current requirement that an intermediary have the + * total collateral available before the option descriptions have been + * created.) + * + exit the contract when both seats have been paid. + * + * @type {ContractStartFn} + */ +const start = zcf => { + // terms: underlyingAmount, priceAuthority, strike1, strike2, + // settlementAmount, expiration + + const terms = zcf.getTerms(); + const { + maths: { Collateral: collateralMath, Strike: strikeMath }, + } = terms; + assertUsesNatMath(zcf, collateralMath.getBrand()); + assertUsesNatMath(zcf, strikeMath.getBrand()); + // notice that we don't assert that the Underlying is fungible. + + assert( + strikeMath.isGTE(terms.strikePrice2, terms.strikePrice1), + details`strikePrice2 must be greater than strikePrice1`, + ); + + const { zcfSeat: collateralSeat } = zcf.makeEmptySeatKit(); + + // Since the seats for the payout of the settlement aren't created until the + // invitations for the options themselves are exercised, we don't have those + // seats at the time of creation of the options, so we use Promises, and + // resolve the payments when those promises resolve. + const seatPromiseKits = {}; + + seatPromiseKits[Position.LONG] = makePromiseKit(); + seatPromiseKits[Position.SHORT] = makePromiseKit(); + + function reallocateToSeat(position, sharePercent) { + seatPromiseKits[position].promise.then(seat => { + const currentCollateral = collateralSeat.getCurrentAllocation() + .Collateral; + const collateral = terms.settlementAmount; + const collateralShare = floorDivide( + multiply(collateral.value, sharePercent), + PERCENT_BASE, + ); + const seatPortion = collateralMath.make(collateralShare); + const collateralRemainder = collateralMath.subtract( + currentCollateral, + seatPortion, + ); + zcf.reallocate( + seat.stage({ Collateral: seatPortion }), + collateralSeat.stage({ Collateral: collateralRemainder }), + ); + seat.exit(); + }); + } + + function calculateLongShare(price) { + // longShare is the value of the underlying at close of the strikePrice + // percentage (base:100) computed from strikePrice + // scale that will be used to calculate the portion of collateral + // allocated to each party. + + if (strikeMath.isGTE(terms.strikePrice1, price)) { + return 0; + } else if (strikeMath.isGTE(price, terms.strikePrice2)) { + return 100; + } + + const denominator = strikeMath.subtract( + terms.strikePrice2, + terms.strikePrice1, + ).value; + const numerator = strikeMath.subtract(price, terms.strikePrice1).value; + return floorDivide(multiply(PERCENT_BASE, numerator), denominator); + } + + function payoffOptions(price) { + // either offer might be exercised late, so we pay the two seats + // separately. + const longShare = calculateLongShare(price); + reallocateToSeat(Position.LONG, longShare, terms); + reallocateToSeat(Position.SHORT, inverse(longShare), terms); + } + + function schedulePayoffs() { + terms.priceAuthority + .priceAtTime(terms.expiration, terms.underlyingAmount) + .then(price => payoffOptions(price)); + } + + function makeOptionInvitation(dir) { + const optionsTerms = harden({ + ...terms, + position: dir, + }); + return zcf.makeInvitation( + seat => seatPromiseKits[dir].resolve(seat), + `collect ${dir} payout`, + optionsTerms, + ); + } + + async function makeOptionPair() { + return { + longInvitation: makeOptionInvitation(Position.LONG), + shortInvitation: makeOptionInvitation(Position.SHORT), + }; + } + + async function makeInvitationToBuy() { + const { longInvitation, shortInvitation } = await makeOptionPair(); + const invitationIssuer = zcf.getInvitationIssuer(); + const longAmount = await E(invitationIssuer).getAmountOf(longInvitation); + const shortAmount = await E(invitationIssuer).getAmountOf(shortInvitation); + depositToSeat( + zcf, + collateralSeat, + { LongOption: longAmount, ShortOption: shortAmount }, + { LongOption: longInvitation, ShortOption: shortInvitation }, + ); + + // transfer collateral from longSeat to collateralSeat, then return a pair + // of callSpread invitations + /** @type {OfferHandler} */ + const pairBuyerPosition = longSeat => { + assertProposalShape(longSeat, { + give: { Collateral: null }, + want: { LongOption: null, ShortOption: null }, + }); + + trade( + zcf, + { + seat: collateralSeat, + gains: { Collateral: terms.settlementAmount }, + }, + { + seat: longSeat, + gains: { LongOption: longAmount, ShortOption: shortAmount }, + }, + ); + schedulePayoffs(); + longSeat.exit(); + }; + + const longTerms = harden({ + ...terms, + LongOption: longAmount, + ShortOption: shortAmount, + }); + return zcf.makeInvitation(pairBuyerPosition, `call spread pair`, longTerms); + } + + return harden({ creatorInvitation: makeInvitationToBuy() }); +}; + +harden(start); +export { start }; diff --git a/packages/zoe/test/unitTests/contracts/test-callSpread.js b/packages/zoe/test/unitTests/contracts/test-callSpread.js new file mode 100644 index 000000000000..e12b64ac70b1 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/test-callSpread.js @@ -0,0 +1,664 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import '@agoric/install-ses'; +// eslint-disable-next-line import/no-extraneous-dependencies +import test from 'ava'; +import { E } from '@agoric/eventual-send'; +import '../../../exported'; +import { makePromiseKit } from '@agoric/promise-kit'; +import buildManualTimer from '../../../tools/manualTimer'; + +import { setup } from '../setupBasicMints'; +import { installationPFromSource } from '../installFromSource'; +import { assertPayoutDeposit, assertPayoutAmount } from '../../zoeTestHelpers'; + +const callSpread = `${__dirname}/../../../src/contracts/callSpread`; +const simpleExchange = `${__dirname}/../../../src/contracts/simpleExchange`; + +function makeFakePriceAuthority( + timer, + underlyingAmountMath, + strikeAmountMath, + priceSchedule, +) { + function priceFromSchedule(strikeTime) { + let freshestPrice = 0; + let freshestTime = -1; + for (const tick of priceSchedule) { + if (tick.time > freshestTime && tick.time <= strikeTime) { + freshestTime = tick.time; + freshestPrice = tick.price; + } + } + return freshestPrice; + } + + const priceAuthority = { + getCurrentPrice: underlyingAmount => { + const underlyingValue = underlyingAmountMath.getValue(underlyingAmount); + return E(timer) + .getCurrentTimestamp() + .then(now => { + const price = priceFromSchedule(now); + return strikeAmountMath.make(price * underlyingValue); + }); + }, + priceAtTime: (timeStamp, underlyingAmount) => { + const { promise, resolve } = makePromiseKit(); + + underlyingAmountMath.getValue(underlyingAmount); + E(timer).setWakeup( + timeStamp, + harden({ + wake: () => { + return resolve(priceAuthority.getCurrentPrice(underlyingAmount)); + }, + }), + ); + return promise; + }, + }; + return priceAuthority; +} + +// Underlying is in Simoleans. Collateral, strikePrice and Payout are in bucks. +// Value is in Moola. The price oracle takes an amount in Underlying, and +// gives the value in Moola. +test('callSpread below Strike1', async t => { + const { + moolaIssuer, + simoleanIssuer, + moola, + simoleans, + bucksIssuer, + bucksMint, + bucks, + zoe, + amountMaths, + } = setup(); + const installation = await installationPFromSource(zoe, callSpread); + const invitationIssuer = await E(zoe).getInvitationIssuer(); + + // Alice will create and fund a call spread contract, and give the invitations + // to Bob and Carol. Bob and Carol will promptly schedule collection of funds. + // The spread will then mature at a low price, and carol will get paid. + + // Setup Alice + const aliceBucksPayment = bucksMint.mintPayment(bucks(300)); + // Setup Bob + const bobBucksPurse = bucksIssuer.makeEmptyPurse(); + // Setup Carol + const carolBucksPurse = bucksIssuer.makeEmptyPurse(); + + // Alice creates a callSpread instance + const issuerKeywordRecord = harden({ + Underlying: simoleanIssuer, + Collateral: bucksIssuer, + Strike: moolaIssuer, + LongOption: invitationIssuer, + }); + + const manualTimer = buildManualTimer(console.log, 1); + const priceAuthority = makeFakePriceAuthority( + manualTimer, + amountMaths.get('simoleans'), + amountMaths.get('moola'), + [ + { time: 0, price: 20 }, + { time: 1, price: 35 }, + { time: 2, price: 15 }, + { time: 3, price: 28 }, + ], + ); + // underlying is 2 Simoleans, strike range is 30-50 (doubled) + const terms = harden({ + expiration: 3, + underlyingAmount: simoleans(2), + priceAuthority, + strikePrice1: moola(60), + strikePrice2: moola(100), + settlementAmount: bucks(300), + }); + const { creatorInvitation } = await zoe.startInstance( + installation, + issuerKeywordRecord, + terms, + ); + + const optionAmount = await invitationIssuer.getAmountOf(creatorInvitation); + const longOptionAmount = optionAmount.value[0].LongOption; + const shortOptionAmount = optionAmount.value[0].ShortOption; + + const aliceProposal = harden({ + want: { LongOption: longOptionAmount, ShortOption: shortOptionAmount }, + give: { Collateral: bucks(300) }, + }); + const alicePayments = { Collateral: aliceBucksPayment }; + const aliceSeat = await zoe.offer( + creatorInvitation, + aliceProposal, + alicePayments, + ); + const aliceOption = await aliceSeat.getOfferResult(); + t.truthy(invitationIssuer.isLive(aliceOption)); + const { + LongOption: bobLongOption, + ShortOption: carolShortOption, + } = await aliceSeat.getPayouts(); + + const bobOptionSeat = await zoe.offer(bobLongOption); + const bobPayout = bobOptionSeat.getPayout('Collateral'); + const bobDeposit = assertPayoutDeposit(t, bobPayout, bobBucksPurse, bucks(0)); + + const carolOptionSeat = await zoe.offer(carolShortOption); + const carolPayout = carolOptionSeat.getPayout('Collateral'); + const carolDeposit = assertPayoutDeposit( + t, + carolPayout, + carolBucksPurse, + bucks(300), + ); + + manualTimer.tick(); + manualTimer.tick(); + await Promise.all([bobDeposit, carolDeposit]); +}); + +// Underlying is in Simoleans. Collateral, strikePrice and Payout are in bucks. +// Value is in Moola. +test('callSpread above Strike2', async t => { + const { + moolaIssuer, + simoleanIssuer, + moola, + simoleans, + bucksIssuer, + bucksMint, + bucks, + zoe, + amountMaths, + } = setup(); + const installation = await installationPFromSource(zoe, callSpread); + const invitationIssuer = await E(zoe).getInvitationIssuer(); + + // Alice will create and fund a call spread contract, and give the invitations + // to Bob and Carol. Bob and Carol will promptly schedule collection of funds. + // The spread will then mature at a high price, and bob will get paid. + + // Setup Alice + const aliceBucksPayment = bucksMint.mintPayment(bucks(300)); + // Setup Bob + const bobBucksPurse = bucksIssuer.makeEmptyPurse(); + // Setup Carol + const carolBucksPurse = bucksIssuer.makeEmptyPurse(); + + // Alice creates a callSpread instance + const issuerKeywordRecord = harden({ + Underlying: simoleanIssuer, + Collateral: bucksIssuer, + Strike: moolaIssuer, + LongOption: invitationIssuer, + }); + + const manualTimer = buildManualTimer(console.log, 1); + const priceAuthority = makeFakePriceAuthority( + manualTimer, + amountMaths.get('simoleans'), + amountMaths.get('moola'), + [ + { time: 0, price: 20 }, + { time: 3, price: 55 }, + ], + ); + // underlying is 2 Simoleans, strike range is 30-50 (doubled) + const terms = harden({ + expiration: 3, + underlyingAmount: simoleans(2), + priceAuthority, + strikePrice1: moola(60), + strikePrice2: moola(100), + settlementAmount: bucks(300), + }); + + const { creatorInvitation } = await zoe.startInstance( + installation, + issuerKeywordRecord, + terms, + ); + + const optionAmount = await invitationIssuer.getAmountOf(creatorInvitation); + const longOptionAmount = optionAmount.value[0].LongOption; + const shortOptionAmount = optionAmount.value[0].ShortOption; + + const aliceProposal = harden({ + want: { LongOption: longOptionAmount, ShortOption: shortOptionAmount }, + give: { Collateral: bucks(300) }, + }); + const alicePayments = { Collateral: aliceBucksPayment }; + const aliceSeat = await zoe.offer( + creatorInvitation, + aliceProposal, + alicePayments, + ); + const aliceOption = await aliceSeat.getOfferResult(); + t.truthy(invitationIssuer.isLive(aliceOption)); + const { + LongOption: bobLongOption, + ShortOption: carolShortOption, + } = await aliceSeat.getPayouts(); + + const bobOptionSeat = await zoe.offer(bobLongOption); + const bobPayout = bobOptionSeat.getPayout('Collateral'); + const bobDeposit = assertPayoutDeposit( + t, + bobPayout, + bobBucksPurse, + bucks(300), + ); + + const carolOptionSeat = await zoe.offer(carolShortOption); + const carolPayout = carolOptionSeat.getPayout('Collateral'); + const carolDeposit = assertPayoutDeposit( + t, + carolPayout, + carolBucksPurse, + bucks(0), + ); + + manualTimer.tick(); + manualTimer.tick(); + await Promise.all([bobDeposit, carolDeposit]); +}); + +// Underlying is in Simoleans. Collateral, strikePrice and Payout are in bucks. +// Value is in Moola. +test('callSpread, mid-strike', async t => { + const { + moolaIssuer, + simoleanIssuer, + moola, + simoleans, + bucksIssuer, + bucksMint, + bucks, + zoe, + amountMaths, + } = setup(); + const installation = await installationPFromSource(zoe, callSpread); + const invitationIssuer = await E(zoe).getInvitationIssuer(); + + // Alice will create and fund a call spread contract, and give the invitations + // to Bob and Carol. Bob and Carol will promptly schedule collection of funds. + // The spread will then mature, and both will get paid. + + // Setup Alice + const aliceBucksPayment = bucksMint.mintPayment(bucks(300)); + // Setup Bob + const bobBucksPurse = bucksIssuer.makeEmptyPurse(); + // Setup Carol + const carolBucksPurse = bucksIssuer.makeEmptyPurse(); + + // Alice creates a callSpread instance + const issuerKeywordRecord = harden({ + Underlying: simoleanIssuer, + Collateral: bucksIssuer, + Strike: moolaIssuer, + LongOption: invitationIssuer, + }); + + const manualTimer = buildManualTimer(console.log, 1); + const priceAuthority = makeFakePriceAuthority( + manualTimer, + amountMaths.get('simoleans'), + amountMaths.get('moola'), + [ + { time: 0, price: 20 }, + { time: 3, price: 45 }, + ], + ); + // underlying is 2 Simoleans, strike range is 30-50 (doubled) + const terms = harden({ + expiration: 3, + underlyingAmount: simoleans(2), + priceAuthority, + strikePrice1: moola(60), + strikePrice2: moola(100), + settlementAmount: bucks(300), + }); + const { creatorInvitation } = await zoe.startInstance( + installation, + issuerKeywordRecord, + terms, + ); + + const optionAmount = await invitationIssuer.getAmountOf(creatorInvitation); + const longOptionAmount = optionAmount.value[0].LongOption; + const shortOptionAmount = optionAmount.value[0].ShortOption; + + const aliceProposal = harden({ + want: { LongOption: longOptionAmount, ShortOption: shortOptionAmount }, + give: { Collateral: bucks(300) }, + }); + const alicePayments = { Collateral: aliceBucksPayment }; + const aliceSeat = await zoe.offer( + creatorInvitation, + aliceProposal, + alicePayments, + ); + const aliceOption = await aliceSeat.getOfferResult(); + t.truthy(invitationIssuer.isLive(aliceOption)); + const { + LongOption: bobLongOption, + ShortOption: carolShortOption, + } = await aliceSeat.getPayouts(); + + const bobOptionSeat = await zoe.offer(bobLongOption); + const bobPayout = bobOptionSeat.getPayout('Collateral'); + const bobDeposit = assertPayoutDeposit( + t, + bobPayout, + bobBucksPurse, + bucks(225), + ); + + const carolOptionSeat = await zoe.offer(carolShortOption); + const carolPayout = carolOptionSeat.getPayout('Collateral'); + const carolDeposit = assertPayoutDeposit( + t, + carolPayout, + carolBucksPurse, + bucks(75), + ); + + manualTimer.tick(); + manualTimer.tick(); + await Promise.all([bobDeposit, carolDeposit]); +}); + +// Underlying is in Simoleans. Collateral, strikePrice and Payout are in bucks. +// Value is in Moola. Carol waits to collect until after settlement +test('callSpread, late exercise', async t => { + const { + moolaIssuer, + simoleanIssuer, + moola, + simoleans, + bucksIssuer, + bucksMint, + bucks, + zoe, + amountMaths, + } = setup(); + const installation = await installationPFromSource(zoe, callSpread); + const invitationIssuer = await E(zoe).getInvitationIssuer(); + + // Alice will create and fund a call spread contract, and give the invitations + // to Bob and Carol. Bob and Carol will promptly schedule collection of funds. + // The spread will then mature, and both will get paid. + + // Setup Alice + const aliceBucksPayment = bucksMint.mintPayment(bucks(300)); + // Setup Bob + const bobBucksPurse = bucksIssuer.makeEmptyPurse(); + // Setup Carol + const carolBucksPurse = bucksIssuer.makeEmptyPurse(); + + // Alice creates a callSpread instance + const issuerKeywordRecord = harden({ + Underlying: simoleanIssuer, + Collateral: bucksIssuer, + Strike: moolaIssuer, + LongOption: invitationIssuer, + }); + + const manualTimer = buildManualTimer(console.log, 1); + const priceAuthority = makeFakePriceAuthority( + manualTimer, + amountMaths.get('simoleans'), + amountMaths.get('moola'), + [ + { time: 0, price: 20 }, + { time: 3, price: 45 }, + ], + ); + // underlying is 2 Simoleans, strike range is 30-50 (doubled) + const terms = harden({ + expiration: 3, + underlyingAmount: simoleans(2), + priceAuthority, + strikePrice1: moola(60), + strikePrice2: moola(100), + settlementAmount: bucks(300), + }); + const { creatorInvitation } = await zoe.startInstance( + installation, + issuerKeywordRecord, + terms, + ); + + const optionAmount = await invitationIssuer.getAmountOf(creatorInvitation); + const longOptionAmount = optionAmount.value[0].LongOption; + const shortOptionAmount = optionAmount.value[0].ShortOption; + + const aliceProposal = harden({ + want: { LongOption: longOptionAmount, ShortOption: shortOptionAmount }, + give: { Collateral: bucks(300) }, + }); + const alicePayments = { Collateral: aliceBucksPayment }; + const aliceSeat = await zoe.offer( + creatorInvitation, + aliceProposal, + alicePayments, + ); + const aliceOption = await aliceSeat.getOfferResult(); + t.truthy(invitationIssuer.isLive(aliceOption)); + const { + LongOption: bobLongOption, + ShortOption: carolShortOption, + } = await aliceSeat.getPayouts(); + + const bobOptionSeat = await zoe.offer(bobLongOption); + const bobPayout = bobOptionSeat.getPayout('Collateral'); + const bobDeposit = assertPayoutDeposit( + t, + bobPayout, + bobBucksPurse, + bucks(225), + ); + + manualTimer.tick(); + manualTimer.tick(); + + const carolOptionSeat = await zoe.offer(carolShortOption); + const carolPayout = await carolOptionSeat.getPayout('Collateral'); + const carolDepositAmount = await E(carolBucksPurse).deposit(carolPayout); + await t.deepEqual( + carolDepositAmount, + bucks(75), + `payout was ${carolDepositAmount.value}, expected 75`, + ); + await Promise.all([bobDeposit]); +}); + +test('callSpread, sell options', async t => { + const { + moolaIssuer, + simoleanIssuer, + moola, + simoleans, + bucksIssuer, + bucksMint, + bucks, + zoe, + amountMaths, + } = setup(); + const installation = await installationPFromSource(zoe, callSpread); + const invitationIssuer = await E(zoe).getInvitationIssuer(); + + // Alice will create and fund a call spread contract, and sell the invitations + // to Bob and Carol. Bob and Carol will promptly schedule collection of funds. + // The spread will then mature, and both will get paid. + + // Setup Alice + const aliceBucksPayment = bucksMint.mintPayment(bucks(300)); + const aliceBucksPurse = bucksIssuer.makeEmptyPurse(); + // Setup Bob + const bobBucksPurse = bucksIssuer.makeEmptyPurse(); + const bobBucksPayment = bucksMint.mintPayment(bucks(200)); + // Setup Carol + const carolBucksPurse = bucksIssuer.makeEmptyPurse(); + const carolBucksPayment = bucksMint.mintPayment(bucks(100)); + + // Alice creates a callSpread instance + const issuerKeywordRecord = harden({ + Underlying: simoleanIssuer, + Collateral: bucksIssuer, + Strike: moolaIssuer, + LongOption: invitationIssuer, + }); + + const manualTimer = buildManualTimer(console.log, 1); + const priceAuthority = makeFakePriceAuthority( + manualTimer, + amountMaths.get('simoleans'), + amountMaths.get('moola'), + [ + { time: 0, price: 20 }, + { time: 3, price: 45 }, + ], + ); + // underlying is 2 Simoleans, strike range is 30-50 (doubled) + const terms = harden({ + expiration: 3, + underlyingAmount: simoleans(2), + priceAuthority, + strikePrice1: moola(60), + strikePrice2: moola(100), + settlementAmount: bucks(300), + }); + const { creatorInvitation } = await zoe.startInstance( + installation, + issuerKeywordRecord, + terms, + ); + + const optionAmount = await invitationIssuer.getAmountOf(creatorInvitation); + const longOptionAmount = optionAmount.value[0].LongOption; + const shortOptionAmount = optionAmount.value[0].ShortOption; + + const aliceProposal = harden({ + want: { LongOption: longOptionAmount, ShortOption: shortOptionAmount }, + give: { Collateral: bucks(300) }, + }); + const alicePayments = { Collateral: aliceBucksPayment }; + const aliceSeat = await zoe.offer( + creatorInvitation, + aliceProposal, + alicePayments, + ); + const aliceOption = await aliceSeat.getOfferResult(); + t.truthy(invitationIssuer.isLive(aliceOption)); + const { + LongOption: longOption, + ShortOption: shortOption, + } = await aliceSeat.getPayouts(); + + const exchangeInstallation = await installationPFromSource( + zoe, + simpleExchange, + ); + const { publicFacet: exchangePublic } = await zoe.startInstance( + exchangeInstallation, + { + Asset: invitationIssuer, + Price: bucksIssuer, + }, + ); + + // Alice offers to sell the long invitation + const aliceLongInvitation = E(exchangePublic).makeInvitation(); + const proposalLong = harden({ + give: { Asset: longOptionAmount }, + want: { Price: bucks(200) }, + }); + const aliceSellLongSeat = await zoe.offer(aliceLongInvitation, proposalLong, { + Asset: longOption, + }); + const aliceLong = assertPayoutDeposit( + t, + aliceSellLongSeat.getPayout('Price'), + aliceBucksPurse, + bucks(200), + ); + + // Alice offers to sell the short invitation + const aliceShortInvitation = E(exchangePublic).makeInvitation(); + const proposalShort = harden({ + give: { Asset: shortOptionAmount }, + want: { Price: bucks(100) }, + }); + const aliceSellShortSeat = await zoe.offer( + aliceShortInvitation, + proposalShort, + { Asset: shortOption }, + ); + const aliceShort = assertPayoutDeposit( + t, + aliceSellShortSeat.getPayout('Price'), + carolBucksPurse, + bucks(100), + ); + + // Bob buys the long invitation + const bobLongInvitation = E(exchangePublic).makeInvitation(); + const bobProposal = harden({ + give: { Price: bucks(200) }, + want: { Asset: longOptionAmount }, + }); + const bobBuySeat = await zoe.offer(bobLongInvitation, bobProposal, { + Price: bobBucksPayment, + }); + const longInvitationPayout = await bobBuySeat.getPayout('Asset'); + assertPayoutAmount( + t, + invitationIssuer, + longInvitationPayout, + longOptionAmount, + ); + const bobOptionSeat = await zoe.offer(longInvitationPayout); + const bobPayout = bobOptionSeat.getPayout('Collateral'); + const bobDeposit = assertPayoutDeposit( + t, + bobPayout, + bobBucksPurse, + bucks(225), + ); + + // Bob buys the Short invitation + const carolShortInvitation = E(exchangePublic).makeInvitation(); + const carolProposal = harden({ + give: { Price: bucks(100) }, + want: { Asset: shortOptionAmount }, + }); + const carolBuySeat = await zoe.offer(carolShortInvitation, carolProposal, { + Price: carolBucksPayment, + }); + const ShortInvitationPayout = await carolBuySeat.getPayout('Asset'); + assertPayoutAmount( + t, + invitationIssuer, + ShortInvitationPayout, + shortOptionAmount, + ); + const carolOptionSeat = await zoe.offer(ShortInvitationPayout); + const carolPayout = carolOptionSeat.getPayout('Collateral'); + const carolDeposit = assertPayoutDeposit( + t, + carolPayout, + carolBucksPurse, + bucks(75), + ); + + manualTimer.tick(); + manualTimer.tick(); + await Promise.all([aliceLong, aliceShort, bobDeposit, carolDeposit]); +}); diff --git a/packages/zoe/test/zoeTestHelpers.js b/packages/zoe/test/zoeTestHelpers.js index cc962016104a..f020a02bd7a8 100644 --- a/packages/zoe/test/zoeTestHelpers.js +++ b/packages/zoe/test/zoeTestHelpers.js @@ -13,17 +13,18 @@ export const assertPayoutAmount = async ( t.deepEqual(amount, expectedAmount, `${label} payout was ${amount.value}`); }; +// Returns a promise that can be awaited in tests to ensure the check completes. export const assertPayoutDeposit = (t, payout, purse, amount) => { - payout.then(payment => { + return payout.then(payment => { E(purse) .deposit(payment) - .then(payoutAmount => { + .then(payoutAmount => t.deepEqual( payoutAmount, amount, `payout was ${payoutAmount.value}, expected ${amount}.value`, - ); - }); + ), + ); }); };