Skip to content

Commit

Permalink
Merge pull request #61 from gnosis/acceptable-tx-margin
Browse files Browse the repository at this point in the history
Can buy and sell with a limit margin
  • Loading branch information
cag authored Nov 9, 2017
2 parents 4a024f7 + 76726ff commit dc63783
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 8 deletions.
22 changes: 18 additions & 4 deletions src/markets.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const createMarket = wrapWeb3Function((self, opts) => ({
* @param {(Contract|string)} opts.market - The market to buy tokens from
* @param {(number|string|BigNumber)} opts.outcomeTokenIndex - The index of the outcome
* @param {(number|string|BigNumber)} opts.outcomeTokenCount - Number of outcome tokens to buy
* @param {(number|string|BigNumber)} [opts.limitMargin=0] - Because transactions change prices, there is a chance that the cost limit for the buy, which is set to the cost according to the latest mined block, will prevent the buy transaction from succeeding. This parameter can be used to increase the cost limit by a fixed proportion. For example, specifying `limitMargin: 0.05` will make the cost limit increase by 5%.
* @param {(number|string|BigNumber)} [opts.approvalAmount] - Amount of collateral to allow market to spend. If unsupplied or null, allowance will be reset to the `approvalResetAmount` only if necessary. If set to 0, the approval transaction will be skipped.
* @param {(number|string|BigNumber)} [opts.approvalResetAmount] - Set to this amount when resetting market collateral allowance. If unsupplied or null, will be the cost of this transaction.
* @returns {BigNumber} How much collateral tokens caller paid
Expand All @@ -59,16 +60,22 @@ export async function buyOutcomeTokens() {

const txOpts = _.pick(opts, ['from', 'to', 'value', 'gas', 'gasPrice'])

let { approvalAmount, approvalResetAmount } = opts || {}
let { approvalAmount, approvalResetAmount, limitMargin } = opts || {}

const market = await this.contracts.Market.at(marketAddress)
const collateralToken = await this.contracts.Token.at(
await this.contracts.Event.at(
await market.eventContract(txOpts)
).collateralToken()
)

if(limitMargin == null) {
limitMargin = 0
}

const baseCost = await this.lmsrMarketMaker.calcCost(marketAddress, outcomeTokenIndex, outcomeTokenCount, txOpts)
const cost = baseCost.add(await market.calcMarketFee(baseCost, txOpts))
const baseCostWithFee = baseCost.add(await market.calcMarketFee(baseCost, txOpts))
const cost = baseCostWithFee.mul(this.web3.toBigNumber(1).add(limitMargin)).round()

if(approvalResetAmount == null) {
approvalResetAmount = cost
Expand Down Expand Up @@ -127,6 +134,7 @@ buyOutcomeTokens.estimateGas = async function({ using }) {
* @param {(Contract|string)} opts.market - The market to sell tokens to
* @param {(number|string|BigNumber)} opts.outcomeTokenIndex - The index of the outcome
* @param {(number|string|BigNumber)} opts.outcomeTokenCount - Number of outcome tokens to sell
* @param {(number|string|BigNumber)} [opts.limitMargin=0] - Because transactions change profits, there is a chance that the profit limit for the sell, which is set to the profit according to the latest mined block, will prevent the sell transaction from succeeding. This parameter can be used to decrease the profit limit by a fixed proportion. For example, specifying `limitMargin: 0.05` will make the profit limit decrease by 5%.
* @param {(number|string|BigNumber)} [opts.approvalAmount] - Amount of outcome tokens to allow market to handle. If unsupplied or null, allowance will be reset to the `approvalResetAmount` only if necessary. If set to 0, the approval transaction will be skipped.
* @param {(number|string|BigNumber)} [opts.approvalResetAmount] - Set to this amount when resetting market outcome token allowance. If unsupplied or null, will be the sale amount specified by `outcomeTokenCount`.
* @returns {BigNumber} How much collateral tokens caller received from sale
Expand All @@ -145,16 +153,22 @@ export async function sellOutcomeTokens() {

const txOpts = _.pick(opts, ['from', 'to', 'value', 'gas', 'gasPrice'])

let { approvalAmount, approvalResetAmount } = opts || {}
let { approvalAmount, approvalResetAmount, limitMargin } = opts || {}

const market = await this.contracts.Market.at(marketAddress)
const outcomeToken = await this.contracts.Token.at(
await this.contracts.Event.at(
await market.eventContract(txOpts)
).outcomeTokens(outcomeTokenIndex)
)

if(limitMargin == null) {
limitMargin = 0
}

const baseProfit = await this.lmsrMarketMaker.calcProfit(marketAddress, outcomeTokenIndex, outcomeTokenCount, txOpts)
const minProfit = baseProfit.sub(await market.calcMarketFee(baseProfit, txOpts))
const baseProfitWithFee = baseProfit.sub(await market.calcMarketFee(baseProfit, txOpts))
const minProfit = baseProfitWithFee.mul(this.web3.toBigNumber(1).sub(limitMargin)).round()

if(approvalResetAmount == null) {
approvalResetAmount = outcomeTokenCount
Expand Down
4 changes: 2 additions & 2 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,9 @@ export function requireEventFromTXResult (result, eventName) {
let matchingLogs = _.filter(result.logs, (l) => l.event === eventName)

if (matchingLogs.length < 1) {
throw new Error(`could not find any logs in result ${result} corresponding to event ${eventName}`)
throw new Error(`could not find any logs in transaction ${result.tx} corresponding to event ${eventName}`)
} else if (matchingLogs.length > 1) {
throw new Error(`found too many logs in result ${result} corresponding to event ${eventName}`)
throw new Error(`found too many logs in transaction ${result.tx} corresponding to event ${eventName}`)
}

return matchingLogs[0]
Expand Down
35 changes: 33 additions & 2 deletions test/test_gnosis.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,14 @@ describe('Gnosis', function () {
})

describe('.lmsr', () => {
let gnosis, oracle, event, ipfsHash, market, netOutcomeTokensSold, funding, feeFactor
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({
Expand Down Expand Up @@ -401,7 +402,6 @@ describe('Gnosis', function () {
actualProfit = await gnosis.sellOutcomeTokens({
market, outcomeTokenIndex,
outcomeTokenCount: numOutcomeTokensToSell,
feeFactor,
})
assert(isClose(localCalculatedProfit.valueOf(), actualProfit.valueOf()))
assert(localCalculatedProfit.lte(actualProfit.valueOf()))
Expand Down Expand Up @@ -569,6 +569,37 @@ describe('Gnosis', function () {
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
Expand Down

0 comments on commit dc63783

Please sign in to comment.