From 0f2dbe68fbaf14ed89c0e48ce3b0aeb8a8678b53 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Fri, 9 Oct 2020 09:37:50 -0700 Subject: [PATCH] feat: a call spread option contract and tests. Implementation of a fully collateralized call spread option, following Joe Clark's description. This is a combination of a bought call option and a sold call option at a higher strike price. The contracts are sold in pairs, and the buyers of the two positions together invest the entire amount that will be paid out. This option is settled financially. Neither party is expected to have ownership of the underlying asset at the start, and neither expects to take delivery at closing. zoe.startInstance() takes an issuerKeywordRecord that specifies the issuers for the keywords Underlying, Strike, and Collateral. The payout uses Collateral. The price oracle quotes the value of the Underlying in the same units as the Strike prices. creatorFacet has a method makeInvitationPair(), that takes terms that specifies { expiration, underlyingAmount, priceAuthority, strikePrice1, strikePrice2, settlementAmount, buyPercent }. ownerFacet.makeInvitationPair() returns two invitations, which can be sold separately. They settle when the priceAuthority announces the settlement amout as of it's pre-programmed closing time. closes: #1829 --- packages/zoe/src/contracts/callSpread.js | 224 +++++++ .../unitTests/contracts/test-callSpread.js | 559 ++++++++++++++++++ 2 files changed, 783 insertions(+) create mode 100644 packages/zoe/src/contracts/callSpread.js create mode 100644 packages/zoe/test/unitTests/contracts/test-callSpread.js diff --git a/packages/zoe/src/contracts/callSpread.js b/packages/zoe/src/contracts/callSpread.js new file mode 100644 index 000000000000..f00770ec6e4c --- /dev/null +++ b/packages/zoe/src/contracts/callSpread.js @@ -0,0 +1,224 @@ +// @ts-check +import '../../exported'; + +import { assert, details } from '@agoric/assert'; +import { E } from '@agoric/eventual-send'; + +// Eventually will be importable from '@agoric/zoe-contract-support' +import { makePromiseKit } from '@agoric/promise-kit'; +import { assertProposalShape, natSafeMath } from '../contractSupport'; + +const { subtract, multiply, floorDivide } = natSafeMath; + +/** + * Constants for buy and sell positions. + * + * @type {{ BUY: 'buy', SELL: 'sell' }} + */ +const Position = { + BUY: 'buy', + SELL: 'sell', +}; + +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 bought call option and a sold call option at a higher strike + * price. The contracts are sold in pairs, and the buyers of the two positions + * together invest the entire amount that will be paid out. + * + * This option is settled financially. Neither party is expected to have + * ownership of the underlying asset at the start, and neither expects to take + * delivery at closing. + * + * zoe.startInstance() takes an issuerKeywordRecord that specifies the issuers + * for the keywords Underlying, Strike, and Collateral. The payout uses + * collateral. The price oracle quotes the value of the Underlying in the same + * units as the Strike prices. + * + * creatorFacet has a method makeInvitationPair(), that takes terms + * that specifies { expiration, underlyingAmount, priceAuthority, strikePrice1, + * strikePrice2, settlementAmount, buyPercent }. + * ownerFacet.makeInvitationPair() returns two invitations, which can be + * exercised for free, and is valuable for its payouts. + * + * @type {ContractStartFn} + */ +const start = zcf => { + const terms = zcf.getTerms(); + const { + maths: { + // Underlying: underlyingMath, + Collateral: collateralMath, + Strike: strikeMath, + }, + } = terms; + + assert( + strikeMath.isGTE(terms.strikePrice2, terms.strikePrice1), + `strikePrice2 must be greater than strikePrice1`, + ); + + const { zcfSeat: collateralSeat } = zcf.makeEmptySeatKit(); + // promises for option seats. The seats aren't reified until offer() is + // called, but we want to set payouts when options mature, regardless + const seatPromiseKits = {}; + seatPromiseKits[Position.BUY] = makePromiseKit(); + seatPromiseKits[Position.SELL] = makePromiseKit(); + + function scheduleMaturity() { + function reallocateToSeat(position, sharePercent) { + seatPromiseKits[position].promise.then( + seat => { + const collateral = collateralSeat.getCurrentAllocation().Collateral; + const collateralShare = floorDivide( + multiply(collateral.value, sharePercent), + PERCENT_BASE, + ); + const seatPortion = collateralMath.make(collateralShare); + const collateralRemainder = collateralMath.subtract( + collateral, + seatPortion, + ); + zcf.reallocate( + seat.stage({ Collateral: seatPortion }), + collateralSeat.stage({ Collateral: collateralRemainder }), + ); + seat.exit(); + }, + () => zcf.shutdown(), + ); + } + + terms.priceAuthority + .priceAtTime(terms.expiration, terms.underlyingAmount) + .then( + price => { + // buyerShare 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. + let buyerShare; + + if (strikeMath.isGTE(terms.strikePrice1, price)) { + buyerShare = 0; + } else if (strikeMath.isGTE(price, terms.strikePrice2)) { + buyerShare = 100; + } else { + const denominator = strikeMath.subtract( + terms.strikePrice2, + terms.strikePrice1, + ).value; + const numerator = strikeMath.subtract(price, terms.strikePrice1) + .value; + buyerShare = floorDivide( + multiply(PERCENT_BASE, numerator), + denominator, + ); + } + + // either offer might be exercised late, so we pay the two seats + // separately. + const sellerShare = inverse(buyerShare); + reallocateToSeat(Position.BUY, buyerShare, terms); + reallocateToSeat(Position.SELL, sellerShare, terms); + }, + () => zcf.shutdown(), + ); + } + + function makeOptionInvitation(dir) { + function makePayoutHandler() { + return seat => seatPromiseKits[dir].resolve(seat); + } + + // transfer collateral from depositSeat to collateralSeat, then return an + // invitation for the payout. + /** @type {OfferHandler} */ + const optionPosition = depositSeat => { + assertProposalShape(depositSeat, { + give: { Collateral: null }, + // TODO(cth): is this right? Do the option buyers 'want' an option/invitation? + // want: { Spread: null }, + // exit: null, + }); + + const { + give: { Collateral: newCollateral }, + } = depositSeat.getProposal(); + let oldCollateral = collateralSeat.getCurrentAllocation().Collateral; + if (!oldCollateral) { + oldCollateral = collateralMath.getEmpty(); + } + + const numerator = + (dir === Position.BUY) ? terms.buyPercent : inverse(terms.buyPercent); + const required = floorDivide( + multiply(terms.settlementAmount.value, numerator), + 100, + ); + + assert( + collateralMath.isEqual(newCollateral, collateralMath.make(required)), + details`Collateral required: ${required}`, + ); + + const newTotal = collateralMath.add(newCollateral, oldCollateral); + zcf.reallocate( + depositSeat.stage({ Collateral: collateralMath.getEmpty() }), + collateralSeat.stage({ Collateral: newTotal }), + ); + depositSeat.exit(); + // TODO(cth): allocate the invitation to the seat rather than returning it. + + return zcf.makeInvitation(makePayoutHandler(), 'collect payout', terms); + }; + + return zcf.makeInvitation(optionPosition, `call spread ${dir}`, terms); + } + + function makeInvitationPair() { + const buyPercent = terms.buyPercent; + assert( + buyPercent >= 0 && buyPercent <= 100, + 'percentages must be between 0 and 100.', + ); + + const buyInvitation = makeOptionInvitation(Position.BUY); + const sellInvitation = makeOptionInvitation(Position.SELL); + scheduleMaturity(); + return { buyInvitation, sellInvitation }; + } + + const creatorFacet = harden({ makeInvitationPair }); + return harden({ creatorFacet }); +}; + +harden(start); +export { start }; + +/** + * makeInvitePair(fraction, characteristics) ===> two invitations: + * + * give: amount, want: + * generate invitations for the options + * construct inner invitations which will be paid out later + * collect the deposits, return the inner invitations. + + * set up a timer/price query and wait fro it [scheduleMaturity] + * pay out on timer firing + + * Start out with a comment saying we should leave a grace period for closing, + * but don't do anything about it. + * Initial seat + */ + +// const customProps = harden({ +// expirationDate: terms.exit.afterDeadline.deadline, +// underlyingAssets: terms.give, +// strikePrice1: seat.getProposal().want, +// strikePrice2: seat.getProposal().want, +// priceAuthority: seat.getInvitationDetails(), +// }); 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..c2b07907de27 --- /dev/null +++ b/packages/zoe/test/unitTests/contracts/test-callSpread.js @@ -0,0 +1,559 @@ +// 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 { assertPayoutAmount } from '../../zoeTestHelpers'; + +const callSpread = `${__dirname}/../../../src/contracts/callSpread`; + +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: () => { + console.log( + `TEST PrAuth triggering resolution ${underlyingAmount.value}`, + ); + 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 an call spread contract, and give the invitations to Bob + // and Carol. Bob and Carol will promptly deposit funds and schedule + // collection of funds. The spread will then mature, and both will get paid. + + // Setup Bob + const bobBucksPayment = bucksMint.mintPayment(bucks(105)); + const bobBucksPurse = bucksIssuer.makeEmptyPurse(); + // Setup Carol + const carolBucksPayment = bucksMint.mintPayment(bucks(195)); + + // Alice creates a callSpread instance + const issuerKeywordRecord = harden({ + Underlying: simoleanIssuer, + Collateral: bucksIssuer, + Strike: moolaIssuer, + }); + + 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: 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), + buyPercent: 35, + }); + const { creatorFacet } = await zoe.startInstance( + installation, + issuerKeywordRecord, + terms, + ); + + const { buyInvitation, sellInvitation } = await E( + creatorFacet, + ).makeInvitationPair(); + + const bobProposal = harden({ + // want: { Spread: }, + give: { Collateral: bucks(105) }, + }); + const bobPayments = { Collateral: bobBucksPayment }; + const bobSeat = await zoe.offer(buyInvitation, bobProposal, bobPayments); + const bobOption = await bobSeat.getOfferResult(); + t.truthy(invitationIssuer.isLive(bobOption)); + const bobOptionProposal = harden({ + want: { Collateral: bucks(0) }, + }); + const bobOptionSeat = await zoe.offer(bobOption, bobOptionProposal); + bobOptionSeat.getPayout('Collateral').then(bobCollateral => { + bobBucksPurse.deposit(bobCollateral, bucks(0)); + console.log(`TEST bob payout`); + }); + + const carolProposal = harden({ + // want: { Spread: }, + give: { Collateral: bucks(195) }, + }); + const carolPayments = { Collateral: carolBucksPayment }; + const carolSeat = await zoe.offer( + sellInvitation, + carolProposal, + carolPayments, + ); + const carolOption = await carolSeat.getOfferResult(); + t.truthy(invitationIssuer.isLive(carolOption)); + const carolOptionProposal = harden({ + want: { Collateral: bucks(0) }, + }); + const carolOptionSeat = await zoe.offer(carolOption, carolOptionProposal); + carolOptionSeat.getPayout('Collateral').then(carolCollateral => { + assertPayoutAmount(t, bucksIssuer, carolCollateral, bucks(300)); + console.log(`TEST carol payout`); + }); + + manualTimer.tick(); + manualTimer.tick(); +}); + +// 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 an call spread contract, and give the invitations to Bob + // and Carol. Bob and Carol will promptly deposit funds and schedule + // collection of funds. The spread will then mature, and both will get paid. + + // Setup Bob + const bobBucksPayment = bucksMint.mintPayment(bucks(105)); + const bobBucksPurse = bucksIssuer.makeEmptyPurse(); + // Setup Carol + const carolBucksPayment = bucksMint.mintPayment(bucks(195)); + + // Alice creates a callSpread instance + const issuerKeywordRecord = harden({ + Underlying: simoleanIssuer, + Collateral: bucksIssuer, + Strike: moolaIssuer, + }); + + 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), + buyPercent: 35, + }); + const { creatorFacet } = await zoe.startInstance( + installation, + issuerKeywordRecord, + terms, + ); + + const { buyInvitation, sellInvitation } = await E( + creatorFacet, + ).makeInvitationPair(); + + const bobProposal = harden({ + // want: { Spread: }, + give: { Collateral: bucks(105) }, + }); + const bobPayments = { Collateral: bobBucksPayment }; + const bobSeat = await zoe.offer(buyInvitation, bobProposal, bobPayments); + const bobOption = await bobSeat.getOfferResult(); + t.truthy(invitationIssuer.isLive(bobOption)); + const bobOptionProposal = harden({ + want: { Collateral: bucks(0) }, + }); + const bobOptionSeat = await zoe.offer(bobOption, bobOptionProposal); + bobOptionSeat.getPayout('Collateral').then(bobCollateral => { + bobBucksPurse.deposit(bobCollateral, bucks(300)); + }); + + const carolProposal = harden({ + give: { Collateral: bucks(195) }, + }); + const carolPayments = { Collateral: carolBucksPayment }; + const carolSeat = await zoe.offer( + sellInvitation, + carolProposal, + carolPayments, + ); + const carolOption = await carolSeat.getOfferResult(); + t.truthy(invitationIssuer.isLive(carolOption)); + const carolOptionProposal = harden({ + want: { Collateral: bucks(0) }, + }); + const carolOptionSeat = await zoe.offer(carolOption, carolOptionProposal); + carolOptionSeat.getPayout('Collateral').then(carolCollateral => { + assertPayoutAmount(t, bucksIssuer, carolCollateral, bucks(0)); + }); + + manualTimer.tick(); + manualTimer.tick(); +}); + +test.only('callSpread specify want', 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 an call spread contract, and give the invitations to Bob + // and Carol. Bob and Carol will promptly deposit funds and schedule + // collection of funds. The spread will then mature, and both will get paid. + + // Setup Bob + const bobBucksPayment = bucksMint.mintPayment(bucks(105)); + const bobBucksPurse = bucksIssuer.makeEmptyPurse(); + // Setup Carol + const carolBucksPayment = bucksMint.mintPayment(bucks(195)); + + // Alice creates a callSpread instance + const issuerKeywordRecord = harden({ + Underlying: simoleanIssuer, + Collateral: bucksIssuer, + Strike: moolaIssuer, + Spread: 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), + buyPercent: 35, + }); + const { creatorFacet } = await zoe.startInstance( + installation, + issuerKeywordRecord, + terms, + ); + + const { buyInvitation, sellInvitation } = await E( + creatorFacet, + ).makeInvitationPair(); + const emptyInvitation = invitationIssuer.makeEmptyPurse().getCurrentAmount(); + + const bobProposal = harden({ + want: { Spread: emptyInvitation }, + give: { Collateral: bucks(105) }, + }); + const bobPayments = { Collateral: bobBucksPayment }; + const bobSeat = await zoe.offer(buyInvitation, bobProposal, bobPayments); + const bobOption = await bobSeat.getOfferResult(); + t.truthy(invitationIssuer.isLive(bobOption)); + const bobOptionProposal = harden({ + want: { Collateral: bucks(0) }, + }); + const bobOptionSeat = await zoe.offer(bobOption, bobOptionProposal); + bobOptionSeat.getPayout('Collateral').then(bobCollateral => { + bobBucksPurse.deposit(bobCollateral, bucks(300)); + }); + + const carolProposal = harden({ + give: { Collateral: bucks(195) }, + }); + const carolPayments = { Collateral: carolBucksPayment }; + const carolSeat = await zoe.offer( + sellInvitation, + carolProposal, + carolPayments, + ); + const carolOption = await carolSeat.getOfferResult(); + t.truthy(invitationIssuer.isLive(carolOption)); + const carolOptionProposal = harden({ + want: { Collateral: bucks(0) }, + }); + const carolOptionSeat = await zoe.offer(carolOption, carolOptionProposal); + carolOptionSeat.getPayout('Collateral').then(carolCollateral => { + assertPayoutAmount(t, bucksIssuer, carolCollateral, bucks(0)); + }); + + manualTimer.tick(); + manualTimer.tick(); +}); + +// Underlying is in Simoleans. Collateral, strikePrice and Payout are in bucks. +// Value is in Moola. +test('callSpread between strikes', 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 an call spread contract, and give the invitations to Bob + // and Carol. Bob and Carol will promptly deposit funds and schedule + // collection of funds. The spread will then mature, and both will get paid. + + // Setup Bob + const bobBucksPayment = bucksMint.mintPayment(bucks(105)); + const bobBucksPurse = bucksIssuer.makeEmptyPurse(); + // Setup Carol + const carolBucksPayment = bucksMint.mintPayment(bucks(195)); + + // Alice creates a callSpread instance + const issuerKeywordRecord = harden({ + Underlying: simoleanIssuer, + Collateral: bucksIssuer, + Strike: moolaIssuer, + }); + + 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), + buyPercent: 35, + }); + const { creatorFacet } = await zoe.startInstance( + installation, + issuerKeywordRecord, + terms, + ); + + const { buyInvitation, sellInvitation } = await E( + creatorFacet, + ).makeInvitationPair(); + + const bobProposal = harden({ + // want: { Spread: }, + give: { Collateral: bucks(105) }, + }); + const bobPayments = { Collateral: bobBucksPayment }; + const bobSeat = await zoe.offer(buyInvitation, bobProposal, bobPayments); + const bobOption = await bobSeat.getOfferResult(); + t.truthy(invitationIssuer.isLive(bobOption)); + const bobOptionProposal = harden({ + want: { Collateral: bucks(0) }, + }); + const bobOptionSeat = await zoe.offer(bobOption, bobOptionProposal); + bobOptionSeat.getPayout('Collateral').then(bobCollateral => { + bobBucksPurse.deposit(bobCollateral, bucks(225)); + }); + + const carolProposal = harden({ + give: { Collateral: bucks(195) }, + }); + const carolPayments = { Collateral: carolBucksPayment }; + const carolSeat = await zoe.offer( + sellInvitation, + carolProposal, + carolPayments, + ); + const carolOption = await carolSeat.getOfferResult(); + t.truthy(invitationIssuer.isLive(carolOption)); + const carolOptionProposal = harden({ + want: { Collateral: bucks(75) }, + }); + const carolOptionSeat = await zoe.offer(carolOption, carolOptionProposal); + carolOptionSeat.getPayout('Collateral').then(carolCollateral => { + assertPayoutAmount(t, bucksIssuer, carolCollateral, bucks(0)); + }); + + manualTimer.tick(); + manualTimer.tick(); +}); + +// 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 insufficient collateral', async t => { + const { + moolaIssuer, + simoleanIssuer, + moola, + simoleans, + bucksIssuer, + bucksMint, + bucks, + zoe, + amountMaths, + } = setup(); + const installation = await installationPFromSource(zoe, callSpread); + + // Alice will create an call spread contract, and give the invitations to Bob + // and Carol. Bob and Carol will promptly deposit funds and schedule + // collection of funds. The spread will then mature, and both will get paid. + + // Setup Bob + const bobBucksPayment = bucksMint.mintPayment(bucks(10)); + + // Alice creates an callSpread instance + const issuerKeywordRecord = harden({ + Underlying: simoleanIssuer, + Collateral: bucksIssuer, + Strike: moolaIssuer, + }); + + 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: 3, price: 28 }, + ], + ); + const terms = harden({ + expiration: 3, + underlyingAmount: simoleans(50), + priceAuthority, + strikePrice1: moola(30), + strikePrice2: moola(50), + settlementAmount: bucks(300), + buyPercent: 35, + }); + const { creatorFacet } = await zoe.startInstance( + installation, + issuerKeywordRecord, + terms, + ); + + const { buyInvitation } = await E(creatorFacet).makeInvitationPair(); + + const bobProposal = harden({ + // want: { Spread: }, + give: { Collateral: bucks(10) }, + }); + const bobPayments = { Collateral: bobBucksPayment }; + const bobSeat = await zoe.offer(buyInvitation, bobProposal, bobPayments); + + await t.throwsAsync(() => E(bobSeat).getOfferResult(), { + message: 'Collateral required: (a number)\nSee console for error data.', + }); + + // Bob gets his deposit back + assertPayoutAmount( + t, + bucksIssuer, + await bobSeat.getPayout('Collateral'), + bucks(10), + ); +});