@gnosis.pm/conditional-tokens-market-makers
Version:
Collection of Market Makers for Gnosis Conditional Tokens
394 lines (337 loc) • 19.5 kB
JavaScript
const { expectEvent } = require('openzeppelin-test-helpers')
const { getConditionId, getCollectionId, getPositionId } = require('@gnosis.pm/conditional-tokens-contracts/utils/id-helpers')(web3.utils)
const { randomHex, toBN } = web3.utils
const ConditionalTokens = artifacts.require('ConditionalTokens')
const WETH9 = artifacts.require('WETH9')
const FPMMDeterministicFactory = artifacts.require('FPMMDeterministicFactory')
const FixedProductMarketMaker = artifacts.require('FixedProductMarketMaker')
contract('FPMMDeterministicFactory', function([, creator, oracle, trader, investor2, testInvestor]) {
const questionId = randomHex(32)
const numOutcomes = 10
const conditionId = getConditionId(oracle, questionId, numOutcomes)
const collectionIds = Array.from(
{ length: numOutcomes },
(_, i) => getCollectionId(conditionId, toBN(1).shln(i))
);
let conditionalTokens
let collateralToken
let fpmmDeterministicFactory
let positionIds
before(async function() {
conditionalTokens = await ConditionalTokens.deployed();
collateralToken = await WETH9.deployed();
fpmmDeterministicFactory = await FPMMDeterministicFactory.deployed()
positionIds = collectionIds.map(collectionId => getPositionId(collateralToken.address, collectionId))
})
let fixedProductMarketMaker;
const saltNonce = toBN(2020)
const feeFactor = toBN(3e15) // (0.3%)
const initialFunds = toBN(10e18)
const initialDistribution = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
const expectedFundedAmounts = initialDistribution.map(n => toBN(1e18 * n))
step('can be created and funded by factory', async function() {
await collateralToken.deposit({ value: initialFunds, from: creator });
await collateralToken.approve(fpmmDeterministicFactory.address, initialFunds, { from: creator });
await conditionalTokens.prepareCondition(oracle, questionId, numOutcomes);
const createArgs = [
saltNonce,
conditionalTokens.address,
collateralToken.address,
[conditionId],
feeFactor,
initialFunds,
initialDistribution,
{ from: creator }
]
const fixedProductMarketMakerAddress = await fpmmDeterministicFactory.create2FixedProductMarketMaker.call(...createArgs)
// TODO: somehow abstract this deterministic address calculation into a utility function
fixedProductMarketMakerAddress.should.be.equal(
web3.utils.toChecksumAddress(`0x${web3.utils.soliditySha3(
{ t: 'bytes', v: '0xff' },
{ t: 'address', v: fpmmDeterministicFactory.address },
{
t: 'bytes32',
v: web3.utils.keccak256(web3.eth.abi.encodeParameters(
['address', 'uint'],
[creator, saltNonce.toString()]
)),
},
{
t: 'bytes32',
v: web3.utils.keccak256(`0x3d3d606380380380913d393d73${
fpmmDeterministicFactory.address.replace(/^0x/, '')
}5af4602a57600080fd5b602d8060366000396000f3363d3d373d3d3d363d73${
(await fpmmDeterministicFactory.implementationMaster()).replace(/^0x/, '')
}5af43d82803e903d91602b57fd5bf3${
web3.eth.abi.encodeFunctionCall({
name: 'cloneConstructor',
type: 'function',
inputs: [{
type: 'bytes',
name: 'data',
}],
}, [web3.eth.abi.encodeParameters([
'address',
'address',
'bytes32[]',
'uint',
], [
conditionalTokens.address,
collateralToken.address,
[conditionId],
feeFactor.toString(),
])]).replace(/^0x/, '')
}`),
},
).slice(-40)}`)
)
const createTx = await fpmmDeterministicFactory.create2FixedProductMarketMaker(...createArgs);
expectEvent.inLogs(createTx.logs, 'FixedProductMarketMakerCreation', {
creator,
fixedProductMarketMaker: fixedProductMarketMakerAddress,
conditionalTokens: conditionalTokens.address,
collateralToken: collateralToken.address,
// conditionIds: [conditionId],
fee: feeFactor,
});
expectEvent.inLogs(createTx.logs, 'FPMMFundingAdded', {
funder: fpmmDeterministicFactory.address,
// amountsAdded: expectedFundedAmounts,
sharesMinted: initialFunds,
});
fixedProductMarketMaker = await FixedProductMarketMaker.at(fixedProductMarketMakerAddress);
(await collateralToken.balanceOf(creator)).should.be.a.bignumber.equal("0");
(await fixedProductMarketMaker.balanceOf(creator)).should.be.a.bignumber.equal(initialFunds);
for(let i = 0; i < positionIds.length; i++) {
(await conditionalTokens.balanceOf(fixedProductMarketMaker.address, positionIds[i]))
.should.be.a.bignumber.equal(expectedFundedAmounts[i]);
(await conditionalTokens.balanceOf(creator, positionIds[i]))
.should.be.a.bignumber.equal(initialFunds.sub(expectedFundedAmounts[i]));
}
});
const feePoolManipulationAmount = toBN(30e18);
const testAdditionalFunding = toBN(1e18);
const expectedTestEndingAmounts = initialDistribution.map(n => toBN(1.1e18 * n))
step('cannot set fee pool proportion directly with transfer', async function() {
await collateralToken.deposit({
from: creator,
value: feePoolManipulationAmount,
});
await collateralToken.transfer(
fixedProductMarketMaker.address,
feePoolManipulationAmount,
{ from: creator },
);
await collateralToken.deposit({ value: testAdditionalFunding, from: testInvestor });
await collateralToken.approve(fixedProductMarketMaker.address, testAdditionalFunding, { from: testInvestor });
await fixedProductMarketMaker.addFunding(testAdditionalFunding, [], { from: testInvestor });
for(let i = 0; i < positionIds.length; i++) {
(await conditionalTokens.balanceOf(fixedProductMarketMaker.address, positionIds[i]))
.should.be.a.bignumber.equal(expectedTestEndingAmounts[i]);
(await conditionalTokens.balanceOf(testInvestor, positionIds[i]))
.should.be.a.bignumber.equal(
testAdditionalFunding
.add(expectedFundedAmounts[i])
.sub(expectedTestEndingAmounts[i])
);
}
(await fixedProductMarketMaker.balanceOf(testInvestor)).should.be.a.bignumber.equal(testAdditionalFunding);
await fixedProductMarketMaker.removeFunding(testAdditionalFunding, { from: testInvestor });
});
let marketMakerPool;
step('can buy tokens from it', async function() {
const investmentAmount = toBN(1e18)
const buyOutcomeIndex = 1;
await collateralToken.deposit({ value: investmentAmount, from: trader });
await collateralToken.approve(fixedProductMarketMaker.address, investmentAmount, { from: trader });
const outcomeTokensToBuy = await fixedProductMarketMaker.calcBuyAmount(investmentAmount, buyOutcomeIndex);
const feeAmount = investmentAmount.mul(feeFactor).div(toBN(1e18));
const poolProductBefore = (await conditionalTokens.balanceOfBatch(
Array.from(positionIds, () => fixedProductMarketMaker.address),
positionIds,
)).reduce((a, b) => a.mul(b), toBN(1));
const buyTx = await fixedProductMarketMaker.buy(investmentAmount, buyOutcomeIndex, outcomeTokensToBuy, { from: trader });
expectEvent.inLogs(buyTx.logs, 'FPMMBuy', {
buyer: trader,
investmentAmount,
feeAmount,
outcomeIndex: toBN(buyOutcomeIndex),
outcomeTokensBought: outcomeTokensToBuy,
});
const poolProductAfter = (await conditionalTokens.balanceOfBatch(
Array.from(positionIds, () => fixedProductMarketMaker.address),
positionIds,
)).reduce((a, b) => a.mul(b), toBN(1));
poolProductAfter.sub(poolProductBefore)
.should.be.a.bignumber.gte("0")
.and.be.a.bignumber.lte(poolProductBefore.div(toBN(1e18)));
(await collateralToken.balanceOf(trader)).should.be.a.bignumber.equal("0");
(await fixedProductMarketMaker.balanceOf(trader)).should.be.a.bignumber.equal("0");
(await collateralToken.balanceOf(fixedProductMarketMaker.address)).should.be.a.bignumber.equal(feePoolManipulationAmount.add(feeAmount));
marketMakerPool = []
for(let i = 0; i < positionIds.length; i++) {
let newMarketMakerBalance;
if(i === buyOutcomeIndex) {
newMarketMakerBalance = expectedFundedAmounts[i].add(investmentAmount).sub(feeAmount).sub(outcomeTokensToBuy);
(await conditionalTokens.balanceOf(trader, positionIds[i]))
.should.be.a.bignumber.equal(outcomeTokensToBuy);
} else {
newMarketMakerBalance = expectedFundedAmounts[i].add(investmentAmount).sub(feeAmount);
(await conditionalTokens.balanceOf(trader, positionIds[i]))
.should.be.a.bignumber.equal("0");
}
(await conditionalTokens.balanceOf(fixedProductMarketMaker.address, positionIds[i]))
.should.be.a.bignumber.equal(newMarketMakerBalance);
marketMakerPool[i] = newMarketMakerBalance
}
});
step('cannot leech wei by adding and removing funding', async function() {
const collateralBalanceBefore = await collateralToken.balanceOf(fixedProductMarketMaker.address);
const collectedFeesBefore = await fixedProductMarketMaker.collectedFees();
await collateralToken.deposit({ value: testAdditionalFunding, from: testInvestor });
await collateralToken.approve(fixedProductMarketMaker.address, testAdditionalFunding, { from: testInvestor });
await fixedProductMarketMaker.addFunding(testAdditionalFunding, [], { from: testInvestor });
const testSharesMinted = await fixedProductMarketMaker.balanceOf(testInvestor);
await fixedProductMarketMaker.removeFunding(testSharesMinted, { from: testInvestor });
const collateralBalanceAfter = await collateralToken.balanceOf(fixedProductMarketMaker.address);
const collectedFeesAfter = await fixedProductMarketMaker.collectedFees();
collateralBalanceBefore.should.be.a.bignumber.equal(collateralBalanceAfter);
collectedFeesBefore.should.be.a.bignumber.equal(collectedFeesAfter);
});
let postManipulationCreatorPoolShares;
step('cannot raise fee pool ratio by removing funding down to 1 wei', async function() {
const manipulationAmount = initialFunds.subn(1);
const collateralBalanceBefore = await collateralToken.balanceOf(fixedProductMarketMaker.address);
const collectedFeesBefore = await fixedProductMarketMaker.collectedFees();
await fixedProductMarketMaker.removeFunding(manipulationAmount, { from: creator });
await collateralToken.deposit({ from: creator, value: manipulationAmount });
await collateralToken.approve(
fixedProductMarketMaker.address,
manipulationAmount,
{ from: creator }
);
await fixedProductMarketMaker.addFunding(manipulationAmount, [], { from: creator });
(await collateralToken.balanceOf(fixedProductMarketMaker.address))
.should.be.a.bignumber.lte(collateralBalanceBefore);
(await fixedProductMarketMaker.collectedFees())
.should.be.a.bignumber.lte(collectedFeesBefore);
marketMakerPool = await conditionalTokens.balanceOfBatch(
new Array(positionIds.length).fill(fixedProductMarketMaker.address),
positionIds,
)
postManipulationCreatorPoolShares = await fixedProductMarketMaker.balanceOf(creator);
});
step('can sell tokens to it', async function() {
const returnAmount = toBN(1e17)
const sellOutcomeIndex = 1;
await conditionalTokens.setApprovalForAll(fixedProductMarketMaker.address, true, { from: trader });
const outcomeTokensToSell = await fixedProductMarketMaker.calcSellAmount(returnAmount, sellOutcomeIndex);
(await conditionalTokens.balanceOf(trader, positionIds[sellOutcomeIndex]))
.should.be.a.bignumber.gte(outcomeTokensToSell);
const feeAmount = returnAmount.mul(feeFactor).div(toBN(1e18).sub(feeFactor));
const fpmmCollateralBalanceBefore = await collateralToken.balanceOf(fixedProductMarketMaker.address);
const poolProductBefore = (await conditionalTokens.balanceOfBatch(
Array.from(positionIds, () => fixedProductMarketMaker.address),
positionIds,
)).reduce((a, b) => a.mul(b), toBN(1));
const sellTx = await fixedProductMarketMaker.sell(returnAmount, sellOutcomeIndex, outcomeTokensToSell, { from: trader });
expectEvent.inLogs(sellTx.logs, 'FPMMSell', {
seller: trader,
returnAmount,
feeAmount,
outcomeIndex: toBN(sellOutcomeIndex),
outcomeTokensSold: outcomeTokensToSell,
});
const poolProductAfter = (await conditionalTokens.balanceOfBatch(
Array.from(positionIds, () => fixedProductMarketMaker.address),
positionIds,
)).reduce((a, b) => a.mul(b), toBN(1));
poolProductAfter.sub(poolProductBefore)
.should.be.a.bignumber.gte("0")
.and.be.a.bignumber.lte(poolProductBefore.div(toBN(1e18)));
(await collateralToken.balanceOf(trader)).should.be.a.bignumber.equal(returnAmount);
(await fixedProductMarketMaker.balanceOf(trader)).should.be.a.bignumber.equal("0");
const fpmmCollateralBalanceAfter = await collateralToken.balanceOf(fixedProductMarketMaker.address);
fpmmCollateralBalanceAfter.sub(fpmmCollateralBalanceBefore).should.be.a.bignumber.equal(feeAmount);
for(let i = 0; i < positionIds.length; i++) {
let newMarketMakerBalance;
if(i === sellOutcomeIndex) {
newMarketMakerBalance = marketMakerPool[i].sub(returnAmount).sub(feeAmount).add(outcomeTokensToSell)
} else {
newMarketMakerBalance = marketMakerPool[i].sub(returnAmount).sub(feeAmount)
}
(await conditionalTokens.balanceOf(fixedProductMarketMaker.address, positionIds[i]))
.should.be.a.bignumber.equal(newMarketMakerBalance);
marketMakerPool[i] = newMarketMakerBalance
}
})
const addedFunds2 = toBN(5e18)
step('can continue being funded', async function() {
const currentPoolBalances = await conditionalTokens.balanceOfBatch(
new Array(positionIds.length).fill(fixedProductMarketMaker.address),
positionIds
);
const maxPoolBalance = currentPoolBalances.reduce((a, b) => a.gt(b) ? a : b);
const currentPoolShareSupply = await fixedProductMarketMaker.totalSupply();
await collateralToken.deposit({ value: addedFunds2, from: investor2 });
await collateralToken.approve(fixedProductMarketMaker.address, addedFunds2, { from: investor2 });
const collectedFeesBefore = await fixedProductMarketMaker.collectedFees();
const addFundingTx = await fixedProductMarketMaker.addFunding(addedFunds2, [], { from: investor2 });
const collectedFeesAfter = await fixedProductMarketMaker.collectedFees();
collectedFeesBefore.should.be.a.bignumber.equal(collectedFeesAfter);
expectEvent.inLogs(addFundingTx.logs, 'FPMMFundingAdded', {
funder: investor2,
// amountsAdded,
sharesMinted: currentPoolShareSupply.mul(addedFunds2).div(
maxPoolBalance
),
});
(await collateralToken.balanceOf(investor2)).should.be.a.bignumber.equal("0");
(await fixedProductMarketMaker.balanceOf(investor2)).should.be.a.bignumber.gt("0");
for(let i = 0; i < positionIds.length; i++) {
let newMarketMakerBalance = await conditionalTokens.balanceOf(fixedProductMarketMaker.address, positionIds[i])
newMarketMakerBalance.should.be.a.bignumber.gt(marketMakerPool[i]).lte(marketMakerPool[i].add(addedFunds2));
marketMakerPool[i] = newMarketMakerBalance;
(await conditionalTokens.balanceOf(investor2, positionIds[i]))
.should.be.a.bignumber.gte("0").lt(addedFunds2);
}
});
const burnedShares1 = toBN(1e18)
step('can be defunded', async function() {
const fpmmCollateralBalanceBefore = await collateralToken.balanceOf(fixedProductMarketMaker.address);
const creatorCollateralBalanceBefore = await collateralToken.balanceOf(creator);
const shareSupplyBefore = await fixedProductMarketMaker.totalSupply();
const feesWithdrawableByCreatorBefore = await fixedProductMarketMaker.feesWithdrawableBy(creator);
const removeFundingTx = await fixedProductMarketMaker.removeFunding(burnedShares1, { from: creator });
const fpmmCollateralBalanceAfter = await collateralToken.balanceOf(fixedProductMarketMaker.address);
const creatorCollateralBalanceAfter = await collateralToken.balanceOf(creator);
const feesWithdrawableByCreatorAfter = await fixedProductMarketMaker.feesWithdrawableBy(creator);
const collateralRemovedFromFeePool = fpmmCollateralBalanceBefore.sub(fpmmCollateralBalanceAfter);
expectEvent.inLogs(removeFundingTx.logs, 'FPMMFundingRemoved', {
funder: creator,
// amountsRemoved,
sharesBurnt: burnedShares1,
collateralRemovedFromFeePool,
});
creatorCollateralBalanceAfter.sub(creatorCollateralBalanceBefore)
.should.be.a.bignumber.equal(collateralRemovedFromFeePool)
.and.be.a.bignumber.equal(
feesWithdrawableByCreatorBefore.sub(feesWithdrawableByCreatorAfter)
);
(await fixedProductMarketMaker.balanceOf(creator)).should.be.a.bignumber.equal(postManipulationCreatorPoolShares.sub(burnedShares1));
for(let i = 0; i < positionIds.length; i++) {
let newMarketMakerBalance = await conditionalTokens.balanceOf(
fixedProductMarketMaker.address,
positionIds[i],
)
newMarketMakerBalance.should.be.a.bignumber.equal(
marketMakerPool[i].sub(
marketMakerPool[i]
.mul(burnedShares1)
.div(shareSupplyBefore)
)
);
marketMakerPool[i] = newMarketMakerBalance;
}
})
})