Skip to content

Instantly share code, notes, and snippets.

@katelynsills
Created February 12, 2021 03:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save katelynsills/48d6436fdefcec6ffe549693c1e6c93d to your computer and use it in GitHub Desktop.
Save katelynsills/48d6436fdefcec6ffe549693c1e6c93d to your computer and use it in GitHub Desktop.
Reputation-based OTC Desk contract
// @ts-check
import { E } from '@agoric/eventual-send';
import { assert, details } from '@agoric/assert';
import { trade } from '../contractSupport';
import '../../exported';
/**
* This OTC Desk contract does not assume that the underlying assets
* have been escrowed. Rather, a reputation mechanism
* is used to inform potential users of how often the quote is
* accurate, and how often the trade fails.
*
* @type {ContractStartFn}
*/
const start = zcf => {
const { zcfSeat: marketMakerSeat } = zcf.makeEmptySeatKit();
let numTradesAttempted = 0;
let numTradesDefected = 0;
const makeQuote = (price, assets, timerAuthority, deadline) => {
let quoteActive = true;
E(timerAuthority).setWakeup(
deadline,
harden({ wake: () => (quoteActive = false) }),
);
const acceptQuote = seat => {
assert(quoteActive, details`The quote was no longer active`);
const { want: userWantActual, give: userGiveActual } = seat.getProposal();
numTradesAttempted += 1;
const hasInventory = Object.entries(assets).every(([keyword, amount]) => {
const allocatedAmount = marketMakerSeat.getAmountAllocated(
keyword,
amount.brand,
);
return zcf.getAmountMath(amount.brand).isGTE(allocatedAmount, amount);
});
if (!hasInventory) {
numTradesDefected += 1;
throw Error(
`The market maker did not have the inventory they promised. Their rating has been adjusted accordingly`,
);
}
// If this trade does not conserve rights, it will throw an
// error and the user will get their funds back.
try {
trade(
zcf,
{
seat: marketMakerSeat,
gains: price,
losses: assets,
},
{ seat, gains: userWantActual, losses: userGiveActual },
);
} catch (err) {
console.log('actual proposal: ', seat.getProposal());
console.log('quote: ', {
assets,
price,
});
throw Error(
'Your offer did not match the quote. See the console log for more details.',
);
}
seat.exit();
return 'Trade successful';
};
const customProperties = harden({
underlyingAssets: assets,
strikePrice: price,
byTimerAuthority: timerAuthority,
beforeOrAtDeadline: deadline,
});
const acceptQuoteInvitation = zcf.makeInvitation(
acceptQuote,
'acceptQuote',
customProperties,
);
return acceptQuoteInvitation;
};
const addInventory = seat => {
// Take everything in this seat and add it to the marketMakerSeat
trade(
zcf,
{ seat: marketMakerSeat, gains: seat.getCurrentAllocation() },
{ seat, gains: {} },
);
seat.exit();
return 'Inventory added';
};
const removeInventory = seat => {
const { want } = seat.getProposal();
trade(zcf, { seat: marketMakerSeat, gains: {} }, { seat, gains: want });
seat.exit();
return 'Inventory removed';
};
const creatorFacet = {
// The inventory can be added in bulk before any quotes are made
// or can be added immediately before a quote.
makeAddInventoryInvitation: async (issuerKeywordRecord = undefined) => {
const { issuers } = zcf.getTerms();
const issuersPSaved = Object.entries(issuerKeywordRecord).map(
([keyword, issuer]) => {
// If the keyword does not yet exist, add it and the
// associated issuer.
if (issuers.keyword === undefined) {
return zcf.saveIssuer(issuer, keyword);
}
return undefined;
},
);
await Promise.all(issuersPSaved);
return zcf.makeInvitation(addInventory, 'addInventory');
},
makeRemoveInventoryInvitation: () =>
zcf.makeInvitation(removeInventory, 'removeInventory'),
makeQuote,
};
const publicFacet = {
getRating: () =>
numTradesAttempted > 0
? `${Math.floor(
((numTradesAttempted - numTradesDefected) / numTradesAttempted) *
100,
)}%`
: 'No trades have occurred',
};
return harden({ creatorFacet, publicFacet });
};
export { start };
// eslint-disable-next-line import/no-extraneous-dependencies
import '@agoric/install-ses';
// eslint-disable-next-line import/no-extraneous-dependencies
import test from 'ava';
// eslint-disable-next-line import/no-extraneous-dependencies
import bundleSource from '@agoric/bundle-source';
import { E } from '@agoric/eventual-send';
import { setup } from '../setupBasicMints';
import buildManualTimer from '../../../tools/manualTimer';
import { assertPayoutAmount } from '../../zoeTestHelpers';
const root = `${__dirname}/../../../src/contracts/otcDeskReputation`;
const installCode = async zoe => {
const bundle = await bundleSource(root);
const installation = await E(zoe).install(bundle);
return installation;
};
const makeAlice = async (t, zoe, installation, issuers, origPayments) => {
let creatorFacet;
const {
moolaIssuer,
simoleanIssuer,
bucksIssuer,
moola,
simoleans,
bucks,
} = issuers;
const { moolaPayment, simoleanPayment, bucksPayment } = origPayments;
const simoleanPurse = await E(simoleanIssuer).makeEmptyPurse();
return {
installCode: async () => {},
startInstance: async () => {
({ creatorFacet } = await E(zoe).startInstance(installation));
},
addInventory: async () => {
const invitation = await E(creatorFacet).makeAddInventoryInvitation({
Moola: moolaIssuer,
Simolean: simoleanIssuer,
Buck: bucksIssuer,
});
const proposal = harden({
give: {
Moola: moola(10000),
Simolean: simoleans(10000),
Buck: bucks(10000),
},
});
const payments = {
Moola: moolaPayment,
Simolean: simoleanPayment,
Buck: bucksPayment,
};
const seat = await E(zoe).offer(invitation, proposal, payments);
const offerResult = await E(seat).getOfferResult();
t.is(offerResult, 'Inventory added');
},
makeQuoteForBob: async (userGiveQuote, userWantQuote, timer, deadline) => {
const bobInvitation = await E(creatorFacet).makeQuote(
userGiveQuote,
userWantQuote,
timer,
deadline,
);
return bobInvitation;
},
removeInventory: async simoleanAmount => {
const invitation = await E(creatorFacet).makeRemoveInventoryInvitation();
const proposal = harden({ want: { Simolean: simoleanAmount } });
const seat = await E(zoe).offer(invitation, proposal);
const offerResult = await E(seat).getOfferResult();
t.is(offerResult, 'Inventory removed');
const simoleanPayout = await E(seat).getPayout('Simolean');
const amountDeposited = await simoleanPurse.deposit(simoleanPayout);
t.deepEqual(amountDeposited, simoleanAmount);
},
};
};
const makeBob = (t, zoe, installation, issuers, origPayments) => {
const {
moolaIssuer,
simoleanIssuer,
bucksIssuer,
moola,
simoleans,
bucks,
} = issuers;
const { moolaPayment, simoleanPayment, bucksPayment } = origPayments;
const moolaPurse = moolaIssuer.makeEmptyPurse();
const simoleanPurse = simoleanIssuer.makeEmptyPurse();
const bucksPurse = bucksIssuer.makeEmptyPurse();
moolaPurse.deposit(moolaPayment);
simoleanPurse.deposit(simoleanPayment);
bucksPurse.deposit(bucksPayment);
return harden({
offerGood: async untrustedInvitation => {
const invitationIssuer = await E(zoe).getInvitationIssuer();
const invitation = await invitationIssuer.claim(untrustedInvitation);
const invitationValue = await E(zoe).getInvitationDetails(invitation);
t.is(
invitationValue.installation,
installation,
'installation is otcDesk',
);
t.deepEqual(
invitationValue.underlyingAssets,
{ Moola: moola(3) },
`bob will get 3 moola`,
);
t.deepEqual(
invitationValue.strikePrice,
{ Simolean: simoleans(4) },
`bob must give 4 simoleans`,
);
// Bob can use whatever keywords he wants
const proposal = harden({
give: { Whatever1: simoleans(4) },
want: { Whatever2: moola(3) },
exit: { onDemand: null },
});
const simoleanPayment1 = simoleanPurse.withdraw(simoleans(4));
const payments = { Whatever1: simoleanPayment1 };
const seat = await zoe.offer(invitation, proposal, payments);
t.is(await E(seat).getOfferResult(), 'Trade successful');
await assertPayoutAmount(
t,
moolaIssuer,
E(seat).getPayout('Whatever2'),
moola(3),
'bob moola',
);
await assertPayoutAmount(
t,
simoleanIssuer,
E(seat).getPayout('Whatever1'),
simoleans(0),
'bob simolean',
);
},
offerExpired: async untrustedInvitation => {
const invitationIssuer = await E(zoe).getInvitationIssuer();
const invitation = await invitationIssuer.claim(untrustedInvitation);
const invitationValue = await E(zoe).getInvitationDetails(invitation);
t.is(
invitationValue.installation,
installation,
'installation is otcDesk',
);
t.deepEqual(
invitationValue.underlyingAssets,
{ Moola: moola(3) },
`bob will get 3 moola`,
);
t.deepEqual(
invitationValue.strikePrice,
{ Simolean: simoleans(4) },
`bob must give 4 simoleans`,
);
// Bob can use whatever keywords he wants
const proposal = harden({
give: { Whatever1: simoleans(4) },
want: { Whatever2: moola(3) },
exit: { onDemand: null },
});
const simoleanPayment1 = simoleanPurse.withdraw(simoleans(4));
const payments = { Whatever1: simoleanPayment1 };
const offerExpiredSeat = await zoe.offer(invitation, proposal, payments);
await t.throwsAsync(() => E(offerExpiredSeat).getOfferResult(), {
message: 'The quote was no longer active',
});
await assertPayoutAmount(
t,
moolaIssuer,
E(offerExpiredSeat).getPayout('Whatever2'),
moola(0),
'bob moola',
);
await assertPayoutAmount(
t,
simoleanIssuer,
E(offerExpiredSeat).getPayout('Whatever1'),
simoleans(4),
'bob simolean',
);
},
offerWantTooMuch: async untrustedInvitation => {
const invitationIssuer = await E(zoe).getInvitationIssuer();
const invitation = await invitationIssuer.claim(untrustedInvitation);
const invitationValue = await E(zoe).getInvitationDetails(invitation);
t.is(
invitationValue.installation,
installation,
'installation is otcDesk',
);
t.deepEqual(
invitationValue.underlyingAssets,
{ Simolean: simoleans(15) },
`bob will get 15 simoleans`,
);
t.deepEqual(
invitationValue.strikePrice,
{ Buck: bucks(500), Moola: moola(35) },
`bob must give 500 bucks and 35 moola`,
);
// Bob can use whatever keywords he wants
const proposal = harden({
give: { Whatever1: bucks(500), Whatever2: moola(35) },
want: { Whatever3: simoleans(16) },
exit: { onDemand: null },
});
const bucks500Payment = bucksPurse.withdraw(bucks(500));
const moola35Payment = moolaPurse.withdraw(moola(35));
const payments = {
Whatever1: bucks500Payment,
Whatever2: moola35Payment,
};
const seat = await zoe.offer(invitation, proposal, payments);
await t.throwsAsync(() => E(seat).getOfferResult(), {
message:
'Your offer did not match the quote. See the console log for more details.',
});
await assertPayoutAmount(
t,
bucksIssuer,
E(seat).getPayout('Whatever1'),
bucks(500),
'bob bucks',
);
await assertPayoutAmount(
t,
moolaIssuer,
E(seat).getPayout('Whatever2'),
moola(35),
'bob moola',
);
await assertPayoutAmount(
t,
simoleanIssuer,
E(seat).getPayout('Whatever3'),
simoleans(0),
'bob simolean',
);
},
offerNotCovered: async untrustedInvitation => {
const invitationIssuer = await E(zoe).getInvitationIssuer();
const invitation = await invitationIssuer.claim(untrustedInvitation);
const invitationValue = await E(zoe).getInvitationDetails(invitation);
t.is(
invitationValue.installation,
installation,
'installation is otcDesk',
);
t.deepEqual(
invitationValue.underlyingAssets,
{ Simolean: simoleans(15) },
`bob will get 15 simoleans`,
);
t.deepEqual(
invitationValue.strikePrice,
{ Buck: bucks(500), Moola: moola(35) },
`bob must give 500 bucks and 35 moola`,
);
const publicFacet = await E(zoe).getPublicFacet(invitationValue.instance);
t.is(await E(publicFacet).getRating(), '100%');
// Bob can use whatever keywords he wants
const proposal = harden({
give: { Whatever1: bucks(500), Whatever2: moola(35) },
want: { Whatever3: simoleans(15) },
exit: { onDemand: null },
});
const bucks500Payment = bucksPurse.withdraw(bucks(500));
const moola35Payment = moolaPurse.withdraw(moola(35));
const payments = {
Whatever1: bucks500Payment,
Whatever2: moola35Payment,
};
const seat = await zoe.offer(invitation, proposal, payments);
await t.throwsAsync(() => E(seat).getOfferResult(), {
message:
'The market maker did not have the inventory they promised. Their rating has been adjusted accordingly',
});
t.is(await E(publicFacet).getRating(), '66%');
await assertPayoutAmount(
t,
bucksIssuer,
E(seat).getPayout('Whatever1'),
bucks(500),
'bob bucks',
);
await assertPayoutAmount(
t,
moolaIssuer,
E(seat).getPayout('Whatever2'),
moola(35),
'bob moola',
);
await assertPayoutAmount(
t,
simoleanIssuer,
E(seat).getPayout('Whatever3'),
simoleans(0),
'bob simolean',
);
},
});
};
const {
moolaKit,
simoleanKit,
bucksKit,
moola,
simoleans,
moolaIssuer,
simoleanIssuer,
bucksIssuer,
bucks,
zoe,
} = setup();
const issuers = {
moolaIssuer,
simoleanIssuer,
bucksIssuer,
moola,
simoleans,
bucks,
};
const makeInitialPayments = (moolaValue, simoleanValue, bucksValue) => ({
moolaPayment: moolaKit.mint.mintPayment(moola(moolaValue)),
simoleanPayment: simoleanKit.mint.mintPayment(simoleans(simoleanValue)),
bucksPayment: bucksKit.mint.mintPayment(bucks(bucksValue)),
});
const timer = buildManualTimer(console.log);
test('zoe - otcDesk', async t => {
const installation = await installCode(zoe);
// Make Alice
const alicePayments = makeInitialPayments(10000, 10000, 10000);
const alice = await makeAlice(t, zoe, installation, issuers, alicePayments);
// Make Bob
const bobPayments = makeInitialPayments(10000, 10000, 10000);
const bob = await makeBob(t, zoe, installation, issuers, bobPayments);
await alice.startInstance();
// Alice wants to make a custom quote for Bob. If Bob gives 4
// simoleans, he can get 3 moola.
// First, Alice must add enough inventory. If the contract hasn't
// been told of an issuer yet, she must add the issuer to the
// contract in the call to make the invitation
await alice.addInventory();
// Alice makes a custom quote for Bob
const invitation1 = await alice.makeQuoteForBob(
{ Simolean: simoleans(4) },
{ Moola: moola(3) },
timer,
1,
);
await bob.offerGood(invitation1);
await alice.removeInventory(simoleans(2));
// Alice makes a custom quote for Bob
const invitation2 = await alice.makeQuoteForBob(
{ Simolean: simoleans(4) },
{ Moola: moola(3) },
timer,
1,
);
timer.tick();
// Bob tries to offer but the quote is expired.
await bob.offerExpired(invitation2);
const invitation3 = await alice.makeQuoteForBob(
{ Buck: bucks(500), Moola: moola(35) },
{ Simolean: simoleans(15) },
timer,
100,
);
// Bob tries to offer but he wants more than what was quoted.
await bob.offerWantTooMuch(invitation3);
await alice.removeInventory(simoleans(10000));
// Alice makes a quote that is currently funded, but removes the funding before Bob receives it.
const invitation4 = await alice.makeQuoteForBob(
{ Buck: bucks(500), Moola: moola(35) },
{ Simolean: simoleans(15) },
timer,
100,
);
await bob.offerNotCovered(invitation4);
// TODO:
// Alice makes a quote for Bob that isn't yet funded. She might or might not fund it before Bob exercises.
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment