@gnosis.pm/hg-market-makers
Version:
Collection of Market Makers for Gnosis Mercury
447 lines (368 loc) • 22.3 kB
JavaScript
const _ = require('lodash')
const utils = require('./utils')
const { ONE, isClose, lmsrMarginalPrice, getParamFromTxEvent, assertRejects, Decimal, randnums } = utils
const { toBN, soliditySha3, toHex } = web3.utils
const PredictionMarketSystem = artifacts.require('PredictionMarketSystem')
const LMSRMarketMakerFactory = artifacts.require('LMSRMarketMakerFactory')
const LMSRMarketMaker = artifacts.require('LMSRMarketMaker')
const WETH9 = artifacts.require('WETH9')
contract('MarketMaker', function(accounts) {
let pmSystem
let lmsrMarketMakerFactory
let etherToken
beforeEach(async () => {
pmSystem = await PredictionMarketSystem.deployed()
lmsrMarketMakerFactory = await LMSRMarketMakerFactory.deployed()
etherToken = await WETH9.deployed()
})
it('should move price of an outcome to 0 after participants sell lots of that outcome to lmsrMarketMaker maker', async () => {
// Create event
const numOutcomes = 2
const netOutcomeTokensSold = new Array(numOutcomes).fill(0)
const questionId = '0xf00dcafef00dcafef00dcafef00dcafef00dcafef00dcafef00dcafef00dcafe'
const oracleAddress = accounts[1]
const conditionId = await getParamFromTxEvent(
await pmSystem.prepareCondition(oracleAddress, questionId, numOutcomes),
'conditionId')
const investor = 0
const feeFactor = 0 // 0%
// Create and fund lmsrMarketMaker
const funding = toBN(1e17)
await etherToken.deposit({ value: funding, from: accounts[investor] })
await etherToken.approve(lmsrMarketMakerFactory.address, funding, { from: accounts[investor] })
assert.equal(await etherToken.balanceOf.call(accounts[investor]).then(v => v.toString()), funding.toString())
const lmsrMarketMaker = await getParamFromTxEvent(
await lmsrMarketMakerFactory.createLMSRMarketMaker(pmSystem.address, etherToken.address, [conditionId], feeFactor, toBN(1e17),
{ from: accounts[investor] }),
'lmsrMarketMaker', LMSRMarketMaker)
assert.equal(await etherToken.balanceOf.call(accounts[investor]).then(v => v.toString()), '0')
// User buys all outcomes
const trader = 1
const outcome = 1
const tokenCountRaw = 1e18
const tokenCount = toBN(tokenCountRaw)
const loopCount = toBN(10)
await etherToken.deposit({ value: tokenCount.mul(loopCount), from: accounts[trader] })
await etherToken.approve(pmSystem.address, tokenCount.mul(loopCount), { from: accounts[trader] })
await pmSystem.splitPosition(etherToken.address, "0x00", conditionId, [...Array(numOutcomes).keys()].map(i => 1 << i), tokenCount.mul(loopCount), { from: accounts[trader] })
await pmSystem.setApprovalForAll(lmsrMarketMaker.address, true, { from: accounts[trader] })
// User sells tokens
const buyerBalance = await etherToken.balanceOf.call(accounts[trader])
let profit, outcomeTokenAmounts
for(const i of _.range(loopCount.toNumber())) {
// Calculate profit for selling tokens
outcomeTokenAmounts = Array.from({length: numOutcomes}, (v, i) => i === outcome ? tokenCount.neg() : toBN(0))
profit = (await lmsrMarketMaker.calcNetCost.call(outcomeTokenAmounts)).neg()
if(profit.eqn(0))
break
// Selling tokens
assert.equal((await getParamFromTxEvent(
await lmsrMarketMaker.trade(outcomeTokenAmounts, profit.neg(), { from: accounts[trader] }), 'outcomeTokenNetCost'
)).neg().toString(), profit.toString())
netOutcomeTokensSold[outcome] -= tokenCountRaw
const expected = lmsrMarginalPrice(funding, netOutcomeTokensSold, outcome)
const actual = new Decimal(await lmsrMarketMaker.calcMarginalPrice.call(toBN(outcome)).then(v => v.toString())).div(ONE)
assert(
isClose(actual, expected),
`Marginal price calculation is off for iteration ${i}:\n` +
` funding: ${funding}\n` +
` net outcome tokens sold: ${netOutcomeTokensSold}\n` +
` actual: ${actual}\n` +
` expected: ${expected}`
)
}
// Selling of tokens is worth less than 1 Wei
assert.equal(profit, 0)
// User's Ether balance increased
assert((await etherToken.balanceOf.call(accounts[trader])).gt(buyerBalance), 'trader balance did not increase')
})
it('should move price of an outcome to 1 after participants buy lots of that outcome from lmsrMarketMaker maker', async () => {
// Prepare condition
const numOutcomes = 2
const questionId = '0xf00dcafef00dcafef00dcafef00dcafef00dcafef00dcafef00dcafef00dcafe'
const oracleAddress = accounts[5]
const conditionId = await getParamFromTxEvent(
await pmSystem.prepareCondition(oracleAddress, questionId, toBN(numOutcomes)),
'conditionId')
for(const [investor, funding, tokenCountRaw] of [
[2, toBN(1e17), 1e18],
[3, toBN(1), 10],
[4, toBN(1), 1e18],
]) {
await etherToken.deposit({ value: funding, from: accounts[investor] })
await etherToken.approve(lmsrMarketMakerFactory.address, funding, { from: accounts[investor] })
const tokenCount = toBN(tokenCountRaw)
const netOutcomeTokensSold = new Array(numOutcomes).fill(0)
// Create and Fund lmsrMarketMaker
assert.equal(await etherToken.balanceOf.call(accounts[investor]).then(v => v.toString()), funding.toString())
const feeFactor = 0 // 0%
const lmsrMarketMaker = await getParamFromTxEvent(
await lmsrMarketMakerFactory.createLMSRMarketMaker(pmSystem.address, etherToken.address, [conditionId], feeFactor, funding,
{ from: accounts[investor] }),
'lmsrMarketMaker', LMSRMarketMaker)
assert.equal(await etherToken.balanceOf.call(accounts[investor]).then(v => v.toString()), '0')
// User buys ether tokens
const trader = 1
const outcome = 1
const loopCount = 10
await etherToken.deposit({ value: tokenCount.muln(loopCount), from: accounts[trader] })
// User buys outcome tokens from lmsrMarketMaker maker
let cost, outcomeTokenAmounts
for(const i of _.range(loopCount)) {
// Calculate cost of buying tokens
outcomeTokenAmounts = Array.from({length: numOutcomes}, (v, i) => i === outcome ? tokenCount : toBN(0))
cost = await lmsrMarketMaker.calcNetCost.call(outcomeTokenAmounts)
// Buying tokens
await etherToken.approve(lmsrMarketMaker.address, cost, { from: accounts[trader] })
assert.equal(await getParamFromTxEvent(
await lmsrMarketMaker.trade(outcomeTokenAmounts, cost, { from: accounts[trader] }), 'outcomeTokenNetCost'
).then(v => v.toString()), cost.toString())
netOutcomeTokensSold[outcome] += tokenCountRaw
const expected = lmsrMarginalPrice(funding, netOutcomeTokensSold, outcome)
const actual = new Decimal(await lmsrMarketMaker.calcMarginalPrice.call(toBN(outcome)).then(v => v.toString())).div(ONE)
assert(
isClose(actual, expected) || expected.toString() == 'NaN',
`Marginal price calculation is off for iteration ${i}:\n` +
` funding: ${funding}\n` +
` net outcome tokens sold: ${netOutcomeTokensSold}\n` +
` actual: ${actual}\n` +
` expected: ${expected}`
)
}
// Price is at least 1
assert(cost.gte(tokenCount))
}
})
it('should allow buying and selling outcome tokens in the same transaction', async () => {
// Create event
const numOutcomes = 4
const questionId = '0xf00dcafef00dcafef00dcafef00dcafef00dcafef00dcafef00dcafef00dcafe'
const investor = 5
const oracleAddress = accounts[1]
const conditionId = await getParamFromTxEvent(
await pmSystem.prepareCondition(oracleAddress, questionId, toBN(numOutcomes)),
'conditionId')
// Create and fund lmsrMarketMaker
const funding = toBN(1e18)
await etherToken.deposit({ value: funding, from: accounts[investor] })
await etherToken.approve(lmsrMarketMakerFactory.address, funding, { from: accounts[investor] })
assert.equal(await etherToken.balanceOf.call(accounts[investor]).then(v => v.toString()), funding.toString())
const feeFactor = toBN(0) // 0%
const lmsrMarketMaker = await getParamFromTxEvent(
await lmsrMarketMakerFactory.createLMSRMarketMaker(pmSystem.address, etherToken.address, [conditionId], feeFactor, funding,
{ from: accounts[investor] }),
'lmsrMarketMaker', LMSRMarketMaker)
assert.equal(await etherToken.balanceOf.call(accounts[investor]).then(v => v.toString()), '0')
const trader = 6
const initialOutcomeTokenCount = toBN(1e18)
const initialWETH9Count = toBN(10e18)
// User buys all outcomes
await etherToken.deposit({ value: initialOutcomeTokenCount.add(initialWETH9Count), from: accounts[trader] })
await etherToken.approve(pmSystem.address, initialOutcomeTokenCount, { from: accounts[trader] })
await pmSystem.splitPosition(etherToken.address, "0x00", conditionId, [...Array(numOutcomes).keys()].map(i => toBN(1 << i)), initialOutcomeTokenCount, { from: accounts[trader] })
// User trades with the lmsrMarketMaker
const tradeValues = [5e17, -1e18, -1e17, 2e18].map(toBN)
const cost = await lmsrMarketMaker.calcNetCost.call(tradeValues)
if(cost.gtn(0)) await etherToken.approve(lmsrMarketMaker.address, cost, { from: accounts[trader] })
await pmSystem.setApprovalForAll(lmsrMarketMaker.address, true, { from: accounts[trader] })
assert.equal(await getParamFromTxEvent(
await lmsrMarketMaker.trade(tradeValues, cost, { from: accounts[trader] }), 'outcomeTokenNetCost'
).then(v => v.toString()), cost.toString())
// All state transitions associated with trade have been performed
for(const [tradeValue, i] of tradeValues.map((v, i) => [v, i])) {
assert.equal(await pmSystem.balanceOf.call(accounts[trader], soliditySha3(
{ t: 'address', v: etherToken.address },
{ t: 'bytes32', v: soliditySha3(
{ t: 'bytes32', v: conditionId },
{ t: 'uint', v: 1 << i },
)}
)).then(v => v.toString()), initialOutcomeTokenCount.add(tradeValue))
}
assert.equal(await etherToken.balanceOf.call(accounts[trader]).then(v => v.toString()), initialWETH9Count.sub(cost).toString())
})
})
contract('LMSRMarketMaker', function (accounts) {
let pmSystem
let etherToken
let conditionId
let lmsrMarketMakerFactory
let centralizedOracle
let questionId = 100
const numOutcomes = 2
before(async () => {
pmSystem = await PredictionMarketSystem.deployed()
etherToken = await WETH9.deployed()
lmsrMarketMakerFactory = await LMSRMarketMakerFactory.deployed()
})
beforeEach(async () => {
// create event
centralizedOracle = accounts[1]
questionId++
conditionId = await getParamFromTxEvent(
await pmSystem.prepareCondition(centralizedOracle, toHex(questionId), toBN(numOutcomes)),
'conditionId')
})
it('can be created and closed', async () => {
// Create lmsrMarketMaker
const buyer = 5
const funding = toBN(100)
const feeFactor = toBN(0)
await etherToken.deposit({ value: funding, from: accounts[buyer] })
await etherToken.approve(lmsrMarketMakerFactory.address, funding, { from: accounts[buyer] })
assert.equal(await etherToken.balanceOf.call(accounts[buyer]).then(v => v.toString()), funding.toString())
const lmsrMarketMaker = await getParamFromTxEvent(
await lmsrMarketMakerFactory.createLMSRMarketMaker(pmSystem.address, etherToken.address, [conditionId], feeFactor, funding, { from: accounts[buyer] }),
'lmsrMarketMaker', LMSRMarketMaker
)
// Close lmsrMarketMaker
await lmsrMarketMaker.close({ from: accounts[buyer] })
// LMSRMarketMaker can only be closed once
await assertRejects(lmsrMarketMaker.close({ from: accounts[buyer] }), 'lmsrMarketMaker closed twice')
// Sell all outcomes
await pmSystem.setApprovalForAll(lmsrMarketMaker.address, true, { from: accounts[buyer] })
await pmSystem.mergePositions(etherToken.address, '0x00', conditionId, [...Array(numOutcomes).keys()].map(i => toBN(1 << i)), funding, { from: accounts[buyer] })
assert.equal(await etherToken.balanceOf.call(accounts[buyer]).then(v => v.toString()), funding.toString())
})
it('should allow buying and selling', async () => {
// create lmsrMarketMaker
const investor = 0
const feeFactor = toBN(5e16) // 5%
const funding = toBN(1e18)
await etherToken.deposit({ value: funding, from: accounts[investor] })
await etherToken.approve(lmsrMarketMakerFactory.address, funding, { from: accounts[investor] })
assert.equal(await etherToken.balanceOf.call(accounts[investor]).then(v => v.toString()), funding.toString())
const lmsrMarketMaker = await getParamFromTxEvent(
await lmsrMarketMakerFactory.createLMSRMarketMaker(pmSystem.address, etherToken.address, [conditionId], feeFactor, funding, { from: accounts[investor] }),
'lmsrMarketMaker', LMSRMarketMaker
)
assert.equal(await etherToken.balanceOf.call(accounts[investor]).then(v => v.toString()), '0')
// Buy outcome tokens
const buyer = 1
const outcome = 0
const positionId = soliditySha3(
{ t: 'address', v: etherToken.address },
{ t: 'bytes32', v: soliditySha3(
{ t: 'bytes32', v: conditionId },
{ t: 'uint', v: 1 << outcome },
)}
)
const tokenCount = toBN(1e15)
let outcomeTokenAmounts = Array.from({length: numOutcomes}, (v, i) => i === outcome ? tokenCount : toBN(0))
const outcomeTokenCost = await lmsrMarketMaker.calcNetCost.call(outcomeTokenAmounts)
let fee = await lmsrMarketMaker.calcMarketFee.call(outcomeTokenCost)
assert.equal(fee.toString(), outcomeTokenCost.muln(5).divn(100).toString())
const cost = fee.add(outcomeTokenCost)
await etherToken.deposit({ value: cost, from: accounts[buyer] })
assert.equal(await etherToken.balanceOf.call(accounts[buyer]).then(v => v.toString()), cost.toString())
await etherToken.approve(lmsrMarketMaker.address, cost, { from: accounts[buyer] })
assert.equal(await getParamFromTxEvent(
await lmsrMarketMaker.trade(outcomeTokenAmounts, cost, { from: accounts[buyer] }), 'outcomeTokenNetCost'
), outcomeTokenCost.toString())
assert.equal(await pmSystem.balanceOf.call(accounts[buyer], positionId).then(v => v.toString()), tokenCount.toString())
assert.equal(await etherToken.balanceOf.call(accounts[buyer]).then(v => v.toString()), 0)
// Sell outcome tokens
outcomeTokenAmounts = Array.from({length: numOutcomes}, (v, i) => i === outcome ? tokenCount.neg() : toBN(0))
const outcomeTokenProfit = (await lmsrMarketMaker.calcNetCost.call(outcomeTokenAmounts)).neg()
fee = await lmsrMarketMaker.calcMarketFee.call(outcomeTokenProfit)
const profit = outcomeTokenProfit.sub(fee)
await pmSystem.setApprovalForAll(lmsrMarketMaker.address, true, { from: accounts[buyer] })
assert.equal(await getParamFromTxEvent(
await lmsrMarketMaker.trade(outcomeTokenAmounts, profit.neg(), { from: accounts[buyer] }), 'outcomeTokenNetCost'
).then(v => v.neg().toString()), outcomeTokenProfit.toString())
assert.equal(await pmSystem.balanceOf.call(accounts[buyer], positionId).then(v => v.toString()), '0')
assert.equal(await etherToken.balanceOf.call(accounts[buyer]).then(v => v.toString()), profit.toString())
})
it('should allow short selling', async () => {
// create lmsrMarketMaker
const investor = 7
const feeFactor = toBN(50000) // 5%
const funding = toBN(1e18)
await etherToken.deposit({ value: funding, from: accounts[investor] })
await etherToken.approve(lmsrMarketMakerFactory.address, funding, { from: accounts[investor] })
const lmsrMarketMaker = await getParamFromTxEvent(
await lmsrMarketMakerFactory.createLMSRMarketMaker(pmSystem.address, etherToken.address, [conditionId], feeFactor, funding, { from: accounts[investor] }),
'lmsrMarketMaker', LMSRMarketMaker
)
assert.equal(await etherToken.balanceOf.call(accounts[investor]).then(v => v.toString()), '0')
// Short sell outcome tokens
const buyer = 7
const outcome = 0
const differentOutcome = 1
const differentPositionId = soliditySha3(
{ t: 'address', v: etherToken.address },
{ t: 'bytes32', v: soliditySha3(
{ t: 'bytes32', v: conditionId },
{ t: 'uint', v: 1 << differentOutcome },
)}
)
const tokenCount = toBN(1e15)
const outcomeTokenAmounts = Array.from({length: numOutcomes}, (v, i) => i !== outcome ? tokenCount : toBN(0))
const outcomeTokenCost = await lmsrMarketMaker.calcNetCost.call(outcomeTokenAmounts)
const fee = await lmsrMarketMaker.calcMarketFee.call(outcomeTokenCost)
const cost = outcomeTokenCost.add(fee)
await etherToken.deposit({ value: cost, from: accounts[buyer] })
assert.equal(await etherToken.balanceOf.call(accounts[buyer]).then(v => v.toString()), cost.toString())
await etherToken.approve(lmsrMarketMaker.address, cost, { from: accounts[buyer] })
assert.equal(
await getParamFromTxEvent(
await lmsrMarketMaker.trade(outcomeTokenAmounts, cost, { from: accounts[buyer] }),
'outcomeTokenNetCost'
).then(v => v.toString()), outcomeTokenCost.toString())
assert.equal(await etherToken.balanceOf.call(accounts[buyer]).then(v => v.toString()), '0')
assert.equal(await pmSystem.balanceOf.call(accounts[buyer], differentPositionId).then(v => v.toString()), tokenCount.toString())
})
it('trading stress testing', async () => {
const MAX_VALUE = toBN(2).pow(toBN(256)).subn(1)
const trader = 9
const feeFactor = toBN(0)
const funding = toBN(1e16)
await etherToken.deposit({ value: funding, from: accounts[trader] })
await etherToken.approve(lmsrMarketMakerFactory.address, funding, { from: accounts[trader] })
const lmsrMarketMaker = await getParamFromTxEvent(
await lmsrMarketMakerFactory.createLMSRMarketMaker(pmSystem.address, etherToken.address, [conditionId], feeFactor, funding, { from: accounts[trader] }),
'lmsrMarketMaker', LMSRMarketMaker
)
const positionIds = [...Array(numOutcomes).keys()].map(i => soliditySha3(
{ t: 'address', v: etherToken.address },
{ t: 'bytes32', v: soliditySha3(
{ t: 'bytes32', v: conditionId },
{ t: 'uint', v: 1 << i },
)}
))
// Get ready for trading
const tradingStipend = toBN(1e19)
await etherToken.deposit({ value: tradingStipend.muln(2), from: accounts[trader] })
await etherToken.approve(pmSystem.address, tradingStipend, { from: accounts[trader] })
await pmSystem.splitPosition(etherToken.address, '0x00', conditionId, [...Array(numOutcomes).keys()].map(i => 1 << i), tradingStipend, { from: accounts[trader] })
// Allow all trading
await etherToken.approve(lmsrMarketMaker.address, MAX_VALUE, { from: accounts[trader] })
await pmSystem.setApprovalForAll(lmsrMarketMaker.address, true, { from: accounts[trader] })
for(let i = 0; i < 10; i++) {
const outcomeTokenAmounts = randnums(-1e16, 1e16, numOutcomes).map(n => toBN(n.valueOf()))
const netCost = await lmsrMarketMaker.calcNetCost.call(outcomeTokenAmounts)
const lmsrMarketMakerOutcomeTokenCounts = await Promise.all(positionIds.map(positionId =>
pmSystem.balanceOf.call(lmsrMarketMaker.address, positionId)))
const lmsrMarketMakerCollateralTokenCount = await etherToken.balanceOf.call(lmsrMarketMaker.address)
let txResult;
try {
txResult = await lmsrMarketMaker.trade(outcomeTokenAmounts, netCost, { from: accounts[trader] })
} catch(e) {
throw new Error(`trade ${ i } with input ${
outcomeTokenAmounts
} and limit ${
netCost
} failed while lmsrMarketMaker has:\n\n${
lmsrMarketMakerOutcomeTokenCounts.map(c => c.toString()).join('\n')
}\n\nand ${
lmsrMarketMakerCollateralTokenCount.toString()
}: ${
e.message
}`)
}
if(txResult)
assert.equal(
(await getParamFromTxEvent(txResult, 'outcomeTokenNetCost')).toString(),
netCost.toString())
}
})
})