Created
February 12, 2021 03:55
-
-
Save katelynsills/48d6436fdefcec6ffe549693c1e6c93d to your computer and use it in GitHub Desktop.
Reputation-based OTC Desk contract
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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