UNPKG

@gnosis.pm/pm-js

Version:

A javascript library for building applications on top of Gnosis, the Ethereum prediction market platform

531 lines (417 loc) 21.5 kB
import assert from 'assert' import _ from 'lodash' import Gnosis from '../src/index' import { description, options, assertIsClose, requireRejection, } from './test_utils' const { requireEventFromTXResult, sendTransactionAndGetResult, Decimal } = Gnosis const ONE = Math.pow(2, 64) describe('Gnosis markets', () => { let gnosis, oracle, event, ipfsHash beforeEach(async () => { gnosis = await Gnosis.create(options) ipfsHash = await gnosis.publishEventDescription(description) oracle = await gnosis.createCentralizedOracle(ipfsHash) event = await gnosis.createCategoricalEvent({ collateralToken: gnosis.etherToken, oracle: oracle, outcomeCount: 2 }) }) it('creates markets', async () => { let market = await gnosis.createMarket({ event: event, marketMaker: gnosis.lmsrMarketMaker, fee: 100, }) assert(market) }) }) describe('Gnosis market mechanics', () => { let gnosis, oracle, event, ipfsHash, market, netOutcomeTokensSold, funding, feeFactor, participants beforeEach(async () => { netOutcomeTokensSold = [0, 0] feeFactor = 5000 // 0.5% gnosis = await Gnosis.create(options) participants = gnosis.web3.eth.accounts.slice(0, 4) ipfsHash = await gnosis.publishEventDescription(description) oracle = await gnosis.createCentralizedOracle(ipfsHash) event = await gnosis.createCategoricalEvent({ collateralToken: gnosis.etherToken, oracle: oracle, outcomeCount: netOutcomeTokensSold.length }) market = await gnosis.createMarket({ event: event, marketMaker: gnosis.lmsrMarketMaker, fee: feeFactor, // 0% }) funding = 1e18 requireEventFromTXResult(await gnosis.etherToken.deposit({ value: funding }), 'Deposit') requireEventFromTXResult(await gnosis.etherToken.approve(market.address, funding), 'Approval') requireEventFromTXResult(await market.fund(funding), 'MarketFunding') }) it('can do buying and selling calculations and transactions', async () => { let outcomeTokenIndex = 0 let outcomeTokenCount = 1e18 // Buying shares, long version let localCalculatedCost = Gnosis.calcLMSRCost({ netOutcomeTokensSold, funding, outcomeTokenIndex, outcomeTokenCount, feeFactor, }) let chainCalculatedCost = await gnosis.lmsrMarketMaker.calcCost(market.address, outcomeTokenIndex, outcomeTokenCount) chainCalculatedCost = chainCalculatedCost.add(await market.calcMarketFee(chainCalculatedCost)) assertIsClose(localCalculatedCost.valueOf(), chainCalculatedCost.valueOf()) assert(localCalculatedCost.gte(chainCalculatedCost.valueOf())) requireEventFromTXResult(await gnosis.etherToken.deposit({ value: localCalculatedCost.valueOf() }), 'Deposit') requireEventFromTXResult(await gnosis.etherToken.approve(market.address, localCalculatedCost.valueOf()), 'Approval') let purchaseEvent = requireEventFromTXResult( await market.buy(outcomeTokenIndex, outcomeTokenCount, localCalculatedCost.valueOf()), 'OutcomeTokenPurchase' ) let actualCost = purchaseEvent.args.outcomeTokenCost.plus(purchaseEvent.args.marketFees) assertIsClose(localCalculatedCost.valueOf(), actualCost.valueOf()) assert(localCalculatedCost.gte(actualCost.valueOf())) // Checking inverse calculation let calculatedOutcomeTokenCount = Gnosis.calcLMSROutcomeTokenCount({ netOutcomeTokensSold, funding, outcomeTokenIndex, cost: localCalculatedCost.valueOf(), feeFactor, }) assertIsClose(outcomeTokenCount.valueOf(), calculatedOutcomeTokenCount.valueOf()) netOutcomeTokensSold[outcomeTokenIndex] += outcomeTokenCount // Buying shares, short version outcomeTokenCount = 2.5e17 localCalculatedCost = Gnosis.calcLMSRCost({ netOutcomeTokensSold, funding, outcomeTokenIndex, outcomeTokenCount, feeFactor, }) requireEventFromTXResult(await gnosis.etherToken.deposit({ value: localCalculatedCost.valueOf() }), 'Deposit') actualCost = await gnosis.buyOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount }) assertIsClose(localCalculatedCost.valueOf(), actualCost.valueOf()) assert(localCalculatedCost.gte(actualCost.valueOf())) netOutcomeTokensSold[outcomeTokenIndex] += outcomeTokenCount // Selling shares, long version let numOutcomeTokensToSell = 5e17 let localCalculatedProfit = Gnosis.calcLMSRProfit({ netOutcomeTokensSold, funding, outcomeTokenIndex, outcomeTokenCount: numOutcomeTokensToSell, feeFactor, }) let chainCalculatedProfit = await gnosis.lmsrMarketMaker.calcProfit(market.address, outcomeTokenIndex, numOutcomeTokensToSell) chainCalculatedProfit = chainCalculatedProfit.sub(await market.calcMarketFee(chainCalculatedProfit)) assertIsClose(localCalculatedProfit.valueOf(), chainCalculatedProfit.valueOf()) assert(localCalculatedProfit.lte(chainCalculatedProfit.valueOf())) let outcomeToken = await gnosis.contracts.ERC20.at(await gnosis.contracts.Event.at(await market.eventContract()).outcomeTokens(outcomeTokenIndex)) requireEventFromTXResult(await outcomeToken.approve(market.address, numOutcomeTokensToSell), 'Approval') let saleEvent = requireEventFromTXResult( await market.sell(outcomeTokenIndex, numOutcomeTokensToSell, localCalculatedProfit.valueOf()), 'OutcomeTokenSale' ) let actualProfit = saleEvent.args.outcomeTokenProfit.minus(saleEvent.args.marketFees) assertIsClose(localCalculatedProfit.valueOf(), actualProfit.valueOf()) assert(localCalculatedProfit.lte(actualProfit.valueOf())) netOutcomeTokensSold[outcomeTokenIndex] -= numOutcomeTokensToSell // Selling shares, short version numOutcomeTokensToSell = 2.5e17 localCalculatedProfit = Gnosis.calcLMSRProfit({ netOutcomeTokensSold, funding, outcomeTokenIndex, outcomeTokenCount: numOutcomeTokensToSell, feeFactor, }) actualProfit = await gnosis.sellOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount: numOutcomeTokensToSell, }) assertIsClose(localCalculatedProfit.valueOf(), actualProfit.valueOf()) assert(localCalculatedProfit.lte(actualProfit.valueOf())) netOutcomeTokensSold[outcomeTokenIndex] -= numOutcomeTokensToSell // Setting the outcome and redeeming winnings await gnosis.resolveEvent({ event, outcome: outcomeTokenIndex, }) let balanceBefore = await gnosis.etherToken.balanceOf(gnosis.defaultAccount) let winnings = await sendTransactionAndGetResult({ callerContract: event, methodName: 'redeemWinnings', methodArgs: [], eventName: 'WinningsRedemption', eventArgName: 'winnings', }) let balanceAfter = await gnosis.etherToken.balanceOf(gnosis.defaultAccount) assert.equal(winnings.valueOf(), netOutcomeTokensSold[outcomeTokenIndex]) assert.equal(balanceAfter.sub(balanceBefore).valueOf(), winnings.valueOf()) }) it('lmsr functions return objects with integer string representations', async () => { let localCalculatedCost = Gnosis.calcLMSRCost({ netOutcomeTokensSold: [0, 1.2e25], funding: 1e27, outcomeTokenIndex: 0, outcomeTokenCount: 1e25, feeFactor: 500, }) assert(/^-?(0x[\da-f]+|\d+)$/i.test(localCalculatedCost)) }) it('can override cost and minProfit for buying and selling', async () => { let outcomeTokenIndex = 0 let outcomeTokenCount = 1e18 let localCalculatedCost = Gnosis.calcLMSRCost({ netOutcomeTokensSold, funding, outcomeTokenIndex, outcomeTokenCount, feeFactor, }) requireEventFromTXResult(await gnosis.etherToken.deposit({ value: localCalculatedCost.valueOf() }), 'Deposit') await requireRejection(gnosis.buyOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount, cost: localCalculatedCost.div(10).valueOf() })) let localCalculatedProfit = Gnosis.calcLMSRProfit({ netOutcomeTokensSold, funding, outcomeTokenIndex, outcomeTokenCount, feeFactor, }) await requireRejection(gnosis.sellOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount, minProfit: localCalculatedProfit.mul(10).valueOf() })) }) it('can do buying and selling with custom approve amounts', async () => { const approvalAmount = 2e18 const outcomeTokenIndex = 0 const outcomeTokenCount = 1e18 const outcomeToken = await gnosis.contracts.ERC20.at( await gnosis.contracts.Event.at( await market.eventContract() ).outcomeTokens(outcomeTokenIndex) ) requireEventFromTXResult(await gnosis.etherToken.deposit({ value: approvalAmount }), 'Deposit') const balanceBefore = await gnosis.etherToken.balanceOf(gnosis.defaultAccount) const allowanceBefore = await gnosis.etherToken.allowance(gnosis.defaultAccount, market.address) const actualCost = await gnosis.buyOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount, approvalAmount }) const balanceAfter = await gnosis.etherToken.balanceOf(gnosis.defaultAccount) const allowanceAfter = await gnosis.etherToken.allowance(gnosis.defaultAccount, market.address) assert.equal(balanceBefore.sub(balanceAfter).valueOf(), actualCost.valueOf()) assert.equal(allowanceAfter.sub(allowanceBefore).valueOf(), actualCost.sub(approvalAmount).neg().valueOf()) const actualCost2 = await gnosis.buyOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount }) const balanceAfter2 = await gnosis.etherToken.balanceOf(gnosis.defaultAccount) const allowanceAfter2 = await gnosis.etherToken.allowance(gnosis.defaultAccount, market.address) assert.equal(balanceAfter.sub(balanceAfter2).valueOf(), actualCost2.valueOf()) assert.equal(allowanceAfter.sub(allowanceAfter2).valueOf(), actualCost2.valueOf()) const amountToSell = 3e17 const outcomeBalanceBefore = await outcomeToken.balanceOf(gnosis.defaultAccount) await gnosis.sellOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount: amountToSell, approvalAmount: outcomeTokenCount }) const outcomeBalanceAfter = await outcomeToken.balanceOf(gnosis.defaultAccount) const outcomeAllowanceAfter = await outcomeToken.allowance(gnosis.defaultAccount, market.address) assert.equal(outcomeBalanceBefore.sub(outcomeBalanceAfter).valueOf(), amountToSell) assert.equal(outcomeAllowanceAfter.valueOf(), outcomeTokenCount - amountToSell) await gnosis.sellOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount: amountToSell }) const outcomeBalanceAfter2 = await outcomeToken.balanceOf(gnosis.defaultAccount) const outcomeAllowanceAfter2 = await outcomeToken.allowance(gnosis.defaultAccount, market.address) assert.equal(outcomeBalanceAfter.sub(outcomeBalanceAfter2).valueOf(), amountToSell) assert.equal(outcomeAllowanceAfter2.valueOf(), outcomeTokenCount - 2 * amountToSell) }) it('can do buying and selling while resetting to a custom amount', async () => { const approvalResetAmount = 2e18 const outcomeTokenIndex = 0 const outcomeTokenCount = 1e17 const outcomeToken = await gnosis.contracts.ERC20.at( await gnosis.contracts.Event.at( await market.eventContract() ).outcomeTokens(outcomeTokenIndex) ) requireEventFromTXResult(await gnosis.etherToken.deposit({ value: approvalResetAmount }), 'Deposit') const balanceBefore = await gnosis.etherToken.balanceOf(gnosis.defaultAccount) const allowanceBefore = await gnosis.etherToken.allowance(gnosis.defaultAccount, market.address) const actualCost = await gnosis.buyOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount, approvalResetAmount }) let balanceAfter = await gnosis.etherToken.balanceOf(gnosis.defaultAccount) let allowanceAfter = await gnosis.etherToken.allowance(gnosis.defaultAccount, market.address) assert.equal(balanceBefore.sub(balanceAfter).valueOf(), actualCost.valueOf()) assert.equal(allowanceAfter.sub(allowanceBefore).valueOf(), actualCost.sub(approvalResetAmount).neg().valueOf()) let actualCost2 = await gnosis.buyOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount, approvalResetAmount }) let balanceAfter2, allowanceAfter2 while(allowanceAfter.gte(actualCost2)) { balanceAfter2 = await gnosis.etherToken.balanceOf(gnosis.defaultAccount) allowanceAfter2 = await gnosis.etherToken.allowance(gnosis.defaultAccount, market.address) assert.equal(balanceAfter.sub(balanceAfter2).valueOf(), actualCost2.valueOf()) assert.equal(allowanceAfter.sub(allowanceAfter2).valueOf(), actualCost2.valueOf()) balanceAfter = balanceAfter2 allowanceAfter = allowanceAfter2 actualCost2 = await gnosis.buyOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount, approvalResetAmount }) } balanceAfter2 = await gnosis.etherToken.balanceOf(gnosis.defaultAccount) allowanceAfter2 = await gnosis.etherToken.allowance(gnosis.defaultAccount, market.address) assert.equal(balanceAfter.sub(balanceAfter2).valueOf(), actualCost2.valueOf()) assert.equal(allowanceAfter2.sub(approvalResetAmount).neg().valueOf(), actualCost2.valueOf()) const amountToSell = 3e17 const outcomeBalanceBefore = await outcomeToken.balanceOf(gnosis.defaultAccount) const outcomeAllowanceBefore = await outcomeToken.allowance(gnosis.defaultAccount, market.address) const profit = await gnosis.sellOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount: amountToSell, approvalResetAmount }) const outcomeBalanceAfter = await outcomeToken.balanceOf(gnosis.defaultAccount) const outcomeAllowanceAfter = await outcomeToken.allowance(gnosis.defaultAccount, market.address) assert.equal(outcomeBalanceBefore.sub(outcomeBalanceAfter).valueOf(), amountToSell) assert.equal(outcomeAllowanceAfter.valueOf(), approvalResetAmount - amountToSell) const profit2 = await gnosis.sellOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount: amountToSell, approvalResetAmount }) const outcomeBalanceAfter2 = await outcomeToken.balanceOf(gnosis.defaultAccount) const outcomeAllowanceAfter2 = await outcomeToken.allowance(gnosis.defaultAccount, market.address) assert.equal(outcomeBalanceAfter.sub(outcomeBalanceAfter2).valueOf(), amountToSell) assert.equal(outcomeAllowanceAfter2.valueOf(), approvalResetAmount - 2 * amountToSell) }) it('can buy and sell with a limit margin', async () => { const outcomeTokenIndex = 0 const outcomeTokenCount = 4e16 const amountToStart = 1e18 const limitMargin = 0.05; (await Promise.all(participants.map( (p) => gnosis.etherToken.deposit({ value: amountToStart, from: p }) ))).forEach((res) => requireEventFromTXResult(res, 'Deposit')); (await Promise.all(participants.map( (p) => gnosis.etherToken.balanceOf(p) ))).forEach((b) => assert(b.gte(amountToStart))); (await Promise.all(participants.map( (p) => gnosis.buyOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount, limitMargin, from: p, }) ))); (await Promise.all(participants.map( (p) => gnosis.sellOutcomeTokens({ market, outcomeTokenIndex, outcomeTokenCount, limitMargin, from: p, }) ))) }) it('accepts strings for outcome token index', async () => { let outcomeTokenIndex = 0 let outcomeTokenCount = 1000000000 let calculatedCost1 = Gnosis.calcLMSRCost({ netOutcomeTokensSold, funding, outcomeTokenIndex, outcomeTokenCount, }) let calculatedCost2 = Gnosis.calcLMSRCost({ netOutcomeTokensSold, funding, outcomeTokenIndex: outcomeTokenIndex.toString(), outcomeTokenCount, }) assert.equal(calculatedCost1.valueOf(), calculatedCost2.valueOf()) }) it('calculates marginal price function', async () => { let outcomeTokenIndex = 0 let outcomeTokenCount = 1e18 let chainCalculatedCost = await gnosis.lmsrMarketMaker.calcCost(market.address, outcomeTokenIndex, outcomeTokenCount) chainCalculatedCost = chainCalculatedCost.add(await market.calcMarketFee(chainCalculatedCost)) requireEventFromTXResult(await gnosis.etherToken.deposit({ value: chainCalculatedCost }), 'Deposit') requireEventFromTXResult(await gnosis.etherToken.approve(market.address, chainCalculatedCost), 'Approval') requireEventFromTXResult(await market.buy(outcomeTokenIndex, outcomeTokenCount, chainCalculatedCost), 'OutcomeTokenPurchase') let newNetOutcomeTokensSold = netOutcomeTokensSold.slice() newNetOutcomeTokensSold[outcomeTokenIndex] = outcomeTokenCount let localCalculatedMarginalPrice = Gnosis.calcLMSRMarginalPrice({ netOutcomeTokensSold: newNetOutcomeTokensSold, funding, outcomeTokenIndex }) let chainCalculatedMarginalPrice = await gnosis.lmsrMarketMaker.calcMarginalPrice(market.address, outcomeTokenIndex) chainCalculatedMarginalPrice = Decimal(chainCalculatedMarginalPrice.valueOf()) assertIsClose(localCalculatedMarginalPrice.valueOf(), chainCalculatedMarginalPrice.div(ONE).valueOf()) }) it('can do calculations when market is unfunded', async () => { const outcomeTokenIndex = 0 const outcomeTokenCount = 1e18 const localCalculatedCost = Gnosis.calcLMSRCost({ netOutcomeTokensSold: [0, 0, 0], funding: 0, outcomeTokenIndex, outcomeTokenCount, feeFactor, }) assertIsClose( localCalculatedCost.valueOf(), Decimal(outcomeTokenCount).mul(1e6 + feeFactor).div(1e6).valueOf() ) const localCalculatedProfit = Gnosis.calcLMSRProfit({ netOutcomeTokensSold: [0, 0, 0], funding: 0, outcomeTokenIndex, outcomeTokenCount, feeFactor, }) assert.equal(localCalculatedProfit.valueOf(), 0) const localCalculatedMarginalPrice = Gnosis.calcLMSRMarginalPrice({ netOutcomeTokensSold: [0, 0, 0], funding: 0, outcomeTokenIndex }) assertIsClose(localCalculatedMarginalPrice.valueOf(), 1/3) }) it('does not mutate arguments when performing calculations', () => { let netOutcomeTokensSoldCopy = _.clone(netOutcomeTokensSold) let outcomeTokenIndex = 0 let cost = 1e18 let lmsrOptions = { netOutcomeTokensSold, outcomeTokenIndex, cost, funding } let lmsrOptionsCopy = _.clone(lmsrOptions) assert.deepStrictEqual(netOutcomeTokensSoldCopy, netOutcomeTokensSold) assert.deepStrictEqual(lmsrOptionsCopy, lmsrOptions) const probability = Gnosis.calcLMSRMarginalPrice(lmsrOptions) assert.deepStrictEqual(netOutcomeTokensSoldCopy, netOutcomeTokensSold) assert.deepStrictEqual(lmsrOptionsCopy, lmsrOptions) assert(probability) const maximumWin = Gnosis.calcLMSROutcomeTokenCount(lmsrOptions) assert.deepStrictEqual(netOutcomeTokensSoldCopy, netOutcomeTokensSold) assert.deepStrictEqual(lmsrOptionsCopy, lmsrOptions) assert(maximumWin) }) it('estimates buying and selling gas costs', async () => { let outcomeTokenIndex = 0 let outcomeTokenCount = 1e18 assert(await gnosis.buyOutcomeTokens.estimateGas({ market, outcomeTokenIndex, outcomeTokenCount, using: 'stats' }) > 0) assert(await gnosis.sellOutcomeTokens.estimateGas({ market, outcomeTokenIndex, outcomeTokenCount, using: 'stats' }) > 0) }) })