@agoric/zoe
Version:
Zoe: the Smart Contract Framework for Offer Enforcement
178 lines (156 loc) • 6.61 kB
JavaScript
/// <reference path="./types-ambient.js" />
import { Fail } from '@endo/errors';
import { makePromiseKit } from '@endo/promise-kit';
import { E } from '@endo/eventual-send';
import { Far } from '@endo/marshal';
import { AmountMath } from '@agoric/ertp';
import {
assertProposalShape,
depositToSeat,
assertNatAssetKind,
makeRatio,
ceilMultiplyBy,
} from '../../contractSupport/index.js';
import { makePayoffHandler } from './payoffHandler.js';
import { Position } from './position.js';
/**
* @import {PriceAuthority, PriceDescription, PriceQuote, PriceQuoteValue, PriceQuery,} from '@agoric/zoe/tools/types.js';
*/
const PERCENT_BASE = 100n;
const BASIS_POINTS = 10000n;
/**
* 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 invitations are produced in pairs. The
* creatorFacet has a method makeInvitationPair(longCollateralShare) whose
* argument must be a number between 0 and 100. makeInvitationPair() returns two
* invitations which require depositing amounts summing to the settlement amount
* in the proportions longCollateralShare and (100 - longCollateralShare) to
* redeem the respective options/invitations. (They are returned under the
* Keyword 'Option'.) The options are ERTP invitations that are suitable for
* resale.
*
* This option contract is settled financially. There is no requirement that the
* creator 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.
*
* terms include:
* `timer` is a timer, and must be recognized by `priceAuthority`.
* `expiration` is a time recognized by the `timer`.
* `underlyingAmount` is passed to `priceAuthority`. It could be an NFT or a
* fungible amount.
* `strikePrice2` must be greater than `strikePrice1`.
* `settlementAmount` is the amount deposited by the funder and split between
* the holders of the options. It uses Collateral.
* `priceAuthority` is an oracle that has a timer so it can respond to requests
* for prices as of a stated time. After the deadline, it will issue a
* PriceQuote giving the value of the underlying asset in the strike currency.
*
* Future enhancements:
* + issue multiple option pairs with the same expiration from a single instance
*
* @param {ZCF<{
* strikePrice1: Amount<'nat'>,
* strikePrice2: Amount<'nat'>,
* settlementAmount: Amount<'nat'>,
* priceAuthority: PriceAuthority,
* expiration: bigint,
* underlyingAmount: Amount<'nat'>,
* }>} zcf
*/
const start = zcf => {
const { brands, strikePrice1, strikePrice2, settlementAmount } =
zcf.getTerms();
assertNatAssetKind(zcf, brands.Collateral);
assertNatAssetKind(zcf, brands.Strike);
// notice that we don't assert that the Underlying is fungible.
assert(
AmountMath.isGTE(strikePrice2, strikePrice1),
'strikePrice2 must be greater than strikePrice1',
);
void zcf.saveIssuer(zcf.getInvitationIssuer(), 'Options');
// We will create the two options early and allocate them to this seat.
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
// allocate the payouts when those promises resolve.
/** @type {Record<PositionKind,PromiseRecord<ZCFSeat>>} */
const seatPromiseKits = {
[Position.LONG]: makePromiseKit(),
[Position.SHORT]: makePromiseKit(),
};
/** @type {PayoffHandler} */
const payoffHandler = makePayoffHandler(zcf, seatPromiseKits, collateralSeat);
async function makeOptionInvitation(position, deposit) {
const option = payoffHandler.makeOptionInvitation(position);
const invitationIssuer = zcf.getInvitationIssuer();
const payment = harden({ Option: option });
const Option = await E(invitationIssuer).getAmountOf(option);
/** @type {any} */
const spreadAmount = harden({
Option,
});
// AWAIT ////
await depositToSeat(zcf, collateralSeat, spreadAmount, payment);
// AWAIT ////
/** @type {OfferHandler} */
const optionPosition = depositSeat => {
assertProposalShape(depositSeat, {
give: { Collateral: null },
want: { Option: null },
exit: { onDemand: null },
});
const {
give: { Collateral: newCollateral },
want: { Option: desiredOption },
} = depositSeat.getProposal();
// assert that the allocation includes the amount of collateral required
AmountMath.isEqual(newCollateral, deposit) ||
Fail`Collateral required: ${deposit.value}`;
// assert that the requested option was the right one.
assert(
spreadAmount.Option.value[0].instance ===
desiredOption.value[0].instance,
'wanted option not a match',
);
zcf.atomicRearrange(
harden([
[collateralSeat, depositSeat, spreadAmount],
[depositSeat, collateralSeat, { Collateral: newCollateral }],
]),
);
depositSeat.exit();
};
return zcf.makeInvitation(optionPosition, `call spread ${position}`, {
position,
collateral: deposit.value,
option: spreadAmount.Option,
});
}
// TODO(2282): change the API so the caller can provide share in basis points
// rather than percent
function makeInvitationPair(longCollateralShare) {
const longPercent = makeRatio(
(longCollateralShare * BASIS_POINTS) / PERCENT_BASE,
brands.Collateral,
BASIS_POINTS,
);
// if there's round-off, the long side pays the extra fraction
const longRequired = ceilMultiplyBy(settlementAmount, longPercent);
const shortRequired = AmountMath.subtract(settlementAmount, longRequired);
const longInvitation = makeOptionInvitation(Position.LONG, longRequired);
const shortInvitation = makeOptionInvitation(Position.SHORT, shortRequired);
payoffHandler.schedulePayoffs();
return { longInvitation, shortInvitation };
}
const creatorFacet = Far('creatorFacet', { makeInvitationPair });
return harden({ creatorFacet });
};
harden(start);
export { start };