@gnosis.pm/hg-contracts
Version:
Collection of smart contracts for the Gnosis prediction market platform
1,893 lines (1,771 loc) • 54.2 kB
JavaScript
const ethSigUtil = require("eth-sig-util");
const { assertRejects, getParamFromTxEvent } = require("./utils");
const { toHex, padLeft, keccak256, asciiToHex, toBN, fromWei } = web3.utils;
const PredictionMarketSystem = artifacts.require("PredictionMarketSystem");
const ERC20Mintable = artifacts.require("MockCoin");
const Forwarder = artifacts.require("Forwarder");
const GnosisSafe = artifacts.require("GnosisSafe");
contract("PredictionMarketSystem", function(accounts) {
let collateralToken;
const minter = accounts[0];
let oracle, questionId, outcomeSlotCount, predictionMarketSystem;
let conditionId;
before(async () => {
predictionMarketSystem = await PredictionMarketSystem.deployed();
collateralToken = await ERC20Mintable.new({ from: minter });
// prepare condition
oracle = accounts[1];
questionId =
"0xcafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe";
outcomeSlotCount = 2;
await predictionMarketSystem.prepareCondition(
oracle,
questionId,
outcomeSlotCount
);
conditionId = keccak256(
oracle +
[questionId, outcomeSlotCount]
.map(v => padLeft(toHex(v), 64).slice(2))
.join("")
);
});
it("should not be able to prepare a condition with no outomes slots", async () => {
await assertRejects(
predictionMarketSystem.prepareCondition(oracle, questionId, 0),
"Transaction should have reverted."
);
});
it("should have obtainable conditionIds if in possession of oracle, questionId, and outcomeSlotCount", async () => {
assert.equal(
(await predictionMarketSystem.getOutcomeSlotCount(conditionId)).valueOf(),
outcomeSlotCount
);
assert.equal(
(await predictionMarketSystem.payoutDenominator(conditionId)).valueOf(),
0
);
});
it("should not be able to prepare the same condition more than once", async () => {
await assertRejects(
predictionMarketSystem.prepareCondition(
oracle,
questionId,
outcomeSlotCount
),
"Transaction should have reverted."
);
});
function shouldSplitAndMergePositionsOnOutcomeSlots(trader) {
it("should split and merge positions on outcome slots", async () => {
const collateralTokenCount = toBN(1e19);
await collateralToken.mint(trader.address, collateralTokenCount, {
from: minter
});
assert(
collateralTokenCount.eq(
await collateralToken.balanceOf.call(trader.address)
)
);
await trader.execCall(
collateralToken,
"approve",
predictionMarketSystem.address,
collateralTokenCount
);
for (let i = 0; i < 10; i++) {
await trader.execCall(
predictionMarketSystem,
"splitPosition",
collateralToken.address,
asciiToHex(0),
conditionId,
[0b01, 0b10],
collateralTokenCount.divn(10)
);
}
assert(
collateralTokenCount.eq(
await collateralToken.balanceOf.call(predictionMarketSystem.address)
)
);
assert.equal(await collateralToken.balanceOf.call(trader.address), 0);
assert(
collateralTokenCount.eq(
await predictionMarketSystem.balanceOf.call(
trader.address,
keccak256(
collateralToken.address +
keccak256(
conditionId + padLeft(toHex(0b01), 64).slice(2)
).slice(2)
)
)
)
);
assert(
collateralTokenCount.eq(
await predictionMarketSystem.balanceOf.call(
trader.address,
keccak256(
collateralToken.address +
keccak256(
conditionId + padLeft(toHex(0b10), 64).slice(2)
).slice(2)
)
)
)
);
// Validate getters
assert.equal(
await predictionMarketSystem.getOutcomeSlotCount.call(conditionId),
2
);
await trader.execCall(
predictionMarketSystem,
"mergePositions",
collateralToken.address,
asciiToHex(0),
conditionId,
[0b01, 0b10],
collateralTokenCount
);
assert(
collateralTokenCount.eq(
await collateralToken.balanceOf.call(trader.address)
)
);
assert.equal(
await collateralToken.balanceOf.call(predictionMarketSystem.address),
0
);
assert.equal(
await predictionMarketSystem.balanceOf.call(
trader.address,
keccak256(
collateralToken.address +
keccak256(conditionId + padLeft(toHex(0b01), 64).slice(2)).slice(
2
)
)
),
0
);
assert.equal(
await predictionMarketSystem.balanceOf.call(
trader.address,
keccak256(
collateralToken.address +
keccak256(conditionId + padLeft(toHex(0b10), 64).slice(2)).slice(
2
)
)
),
0
);
});
}
context("with EOAs", () => {
shouldSplitAndMergePositionsOnOutcomeSlots({
address: accounts[0],
async execCall(contract, method, ...args) {
return await contract[method](...args, { from: accounts[0] });
}
});
});
context("with Forwarder", () => {
let trader = {};
before(async () => {
const forwarder = await Forwarder.new();
const executor = accounts[2];
async function forwardCall(contract, method, ...args) {
// ???: why is reformatting the args necessary here?
args = args.map(arg =>
Array.isArray(arg) ? arg.map(a => a.toString()) : arg.toString()
);
return await forwarder.call(
contract.address,
contract.contract.methods[method](...args).encodeABI(),
{ from: executor }
);
}
trader.address = forwarder.address;
trader.execCall = forwardCall;
});
shouldSplitAndMergePositionsOnOutcomeSlots(trader);
});
context("with Gnosis Safes", () => {
let trader = {};
before(async () => {
const zeroAccount = `0x${"0".repeat(40)}`;
const safeOwners = Array.from({ length: 2 }, () =>
web3.eth.accounts.create()
);
safeOwners.sort(({ address: a }, { address: b }) =>
a.toLowerCase() < b.toLowerCase() ? -1 : a === b ? 0 : 1
);
const gnosisSafe = await GnosisSafe.new();
await gnosisSafe.setup(
safeOwners.map(({ address }) => address),
safeOwners.length,
zeroAccount,
"0x",
zeroAccount,
0,
zeroAccount
);
const gnosisSafeTypedDataCommon = {
types: {
EIP712Domain: [{ name: "verifyingContract", type: "address" }],
SafeTx: [
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "data", type: "bytes" },
{ name: "operation", type: "uint8" },
{ name: "safeTxGas", type: "uint256" },
{ name: "baseGas", type: "uint256" },
{ name: "gasPrice", type: "uint256" },
{ name: "gasToken", type: "address" },
{ name: "refundReceiver", type: "address" },
{ name: "nonce", type: "uint256" }
],
SafeMessage: [{ name: "message", type: "bytes" }]
},
domain: {
verifyingContract: gnosisSafe.address
}
};
const safeExecutor = accounts[3];
async function gnosisSafeCall(contract, method, ...args) {
const safeOperations = {
CALL: 0,
DELEGATECALL: 1,
CREATE: 2
};
const nonce = await gnosisSafe.nonce();
// ???: why is reformatting the args necessary here?
args = args.map(arg =>
Array.isArray(arg) ? arg.map(a => a.toString()) : arg.toString()
);
const txData = contract.contract.methods[method](...args).encodeABI();
const signatures = safeOwners.map(safeOwner =>
ethSigUtil.signTypedData(
Buffer.from(safeOwner.privateKey.replace("0x", ""), "hex"),
{
data: Object.assign(
{
primaryType: "SafeTx",
message: {
to: contract.address,
value: 0,
data: txData,
operation: safeOperations.CALL,
safeTxGas: 0,
baseGas: 0,
gasPrice: 0,
gasToken: zeroAccount,
refundReceiver: zeroAccount,
nonce
}
},
gnosisSafeTypedDataCommon
)
}
)
);
const tx = await gnosisSafe.execTransaction(
contract.address,
0,
txData,
safeOperations.CALL,
0,
0,
0,
zeroAccount,
zeroAccount,
`0x${signatures.map(s => s.replace("0x", "")).join("")}`,
{ from: safeExecutor }
);
if (tx.logs[0] && tx.logs[0].event === "ExecutionFailed")
throw new Error(`Safe transaction ${method}(${args}) failed`);
return tx;
}
trader.address = gnosisSafe.address;
trader.execCall = gnosisSafeCall;
});
shouldSplitAndMergePositionsOnOutcomeSlots(trader);
});
it("should split positions, set outcome slot values, and redeem outcome tokens for conditions", async () => {
// Mint outcome slots
const trader = accounts[2];
const recipient = accounts[7];
const collateralTokenCount = 10;
await collateralToken.mint(trader, collateralTokenCount, {
from: minter
});
assert.equal(
await collateralToken.balanceOf.call(trader),
collateralTokenCount
);
await collateralToken.approve(
predictionMarketSystem.address,
collateralTokenCount,
{ from: trader }
);
await predictionMarketSystem.splitPosition(
collateralToken.address,
asciiToHex(0),
conditionId,
[0b01, 0b10],
collateralTokenCount,
{ from: trader }
);
assert.equal(
(await collateralToken.balanceOf.call(
predictionMarketSystem.address
)).valueOf(),
collateralTokenCount
);
assert.equal(await collateralToken.balanceOf.call(trader), 0);
assert.equal(
await predictionMarketSystem.balanceOf.call(
trader,
keccak256(
collateralToken.address +
keccak256(conditionId + padLeft(toHex(0b01), 64).slice(2)).slice(2)
)
),
collateralTokenCount
);
assert.equal(
await predictionMarketSystem.balanceOf.call(
trader,
keccak256(
collateralToken.address +
keccak256(conditionId + padLeft(toHex(0b10), 64).slice(2)).slice(2)
)
),
collateralTokenCount
);
// Set outcome in condition
await predictionMarketSystem.receiveResult(
questionId,
"0x" + [padLeft("3", 64), padLeft("7", 64)].join(""),
{ from: oracle }
);
assert.equal(
await predictionMarketSystem.payoutDenominator.call(conditionId),
10
);
assert.equal(
await predictionMarketSystem.payoutNumerators.call(conditionId, 0),
3
);
assert.equal(
await predictionMarketSystem.payoutNumerators.call(conditionId, 1),
7
);
await predictionMarketSystem.safeTransferFrom(
trader,
recipient,
keccak256(
collateralToken.address +
keccak256(conditionId + padLeft(toHex(0b01), 64).slice(2)).slice(2)
),
collateralTokenCount,
"0x",
{ from: trader }
);
const buyerPayout = getParamFromTxEvent(
await predictionMarketSystem.redeemPositions(
collateralToken.address,
asciiToHex(0),
conditionId,
[0b10],
{ from: trader }
),
"payout",
null,
"PayoutRedemption"
);
assert.equal(buyerPayout.valueOf(), (collateralTokenCount * 7) / 10);
assert.equal(
await predictionMarketSystem.balanceOf.call(
recipient,
keccak256(
collateralToken.address +
keccak256(conditionId + padLeft(toHex(0b01), 64).slice(2)).slice(2)
)
),
collateralTokenCount
);
assert.equal(
await predictionMarketSystem.balanceOf.call(
trader,
keccak256(
collateralToken.address +
keccak256(conditionId + padLeft(toHex(0b10), 64).slice(2)).slice(2)
)
),
0
);
const recipientPayout = getParamFromTxEvent(
await predictionMarketSystem.redeemPositions(
collateralToken.address,
asciiToHex(0),
conditionId,
[0b01],
{ from: recipient }
),
"payout",
null,
"PayoutRedemption"
);
assert.equal(
(await collateralToken.balanceOf.call(recipient)).toNumber(),
recipientPayout.valueOf()
);
assert.equal(
(await collateralToken.balanceOf.call(trader)).toNumber(),
buyerPayout.valueOf()
);
});
it("should redeem outcome tokens in more complex scenarios", async () => {
// Setup a more complex scenario
const _oracle = accounts[1];
const _questionId =
"0x1234567812345678123456781234567812345678123456781234567812345678";
const _outcomeSlotCount = 4;
await predictionMarketSystem.prepareCondition(
_oracle,
_questionId,
_outcomeSlotCount
);
const _conditionId = keccak256(
_oracle +
[_questionId, _outcomeSlotCount]
.map(v => padLeft(toHex(v), 64).slice(2))
.join("")
);
assert.equal(
await predictionMarketSystem.getOutcomeSlotCount(_conditionId),
4
);
for (let i = 0; i < 4; i++) {
assert.equal(
(await predictionMarketSystem.payoutNumerators(
_conditionId,
i
)).valueOf(),
0
);
}
assert.equal(
(await predictionMarketSystem.payoutDenominator(_conditionId)).valueOf(),
0
);
assert.notEqual(conditionId, _conditionId);
// create some buyers and purchase collateralTokens and then some Outcome Slots
const buyers = [3, 4, 5, 6];
const collateralTokenCounts = [
toBN(1e19),
toBN(1e9),
toBN(1e18),
toBN(1000)
];
for (let i = 0; i < buyers.length; i++) {
await collateralToken.mint(
accounts[buyers[i]],
collateralTokenCounts[i],
{
from: minter
}
);
assert.equal(
await collateralToken
.balanceOf(accounts[buyers[i]])
.then(res => res.toString()),
collateralTokenCounts[i]
);
await collateralToken.approve(
predictionMarketSystem.address,
collateralTokenCounts[i],
{ from: accounts[buyers[i]] }
);
await predictionMarketSystem.splitPosition(
collateralToken.address,
asciiToHex(0),
_conditionId,
[0b0001, 0b0010, 0b0100, 0b1000],
collateralTokenCounts[i],
{ from: accounts[buyers[i]] }
);
}
await assertRejects(
predictionMarketSystem.receiveResult(
_questionId,
"0x" +
[
padLeft("14D", 64), // 333
padLeft("29A", 64), // 666
padLeft("1", 64), // 1
padLeft("0", 64)
].join(""),
{ from: accounts[9] }
),
"Transaction should have reverted."
);
// resolve the condition
await predictionMarketSystem.receiveResult(
_questionId,
"0x" +
[
padLeft("14D", 64), // 333
padLeft("29A", 64), // 666
padLeft("1", 64), // 1
padLeft("0", 64)
].join(""),
{ from: _oracle }
);
assert.equal(
await predictionMarketSystem.payoutDenominator
.call(_conditionId)
.then(res => res.toString()),
1000
);
// assert correct payouts for Outcome Slots
const payoutsForOutcomeSlots = [333, 666, 1, 0];
for (let i = 0; i < buyers.length; i++) {
assert(
collateralTokenCounts[i].eq(
await predictionMarketSystem.balanceOf.call(
accounts[buyers[i]],
keccak256(
collateralToken.address +
keccak256(
_conditionId + padLeft(toHex(1 << i), 64).slice(2)
).slice(2)
)
)
)
);
assert.equal(
await predictionMarketSystem.payoutNumerators(_conditionId, i),
payoutsForOutcomeSlots[i]
);
assert.equal(
await predictionMarketSystem.payoutDenominator(_conditionId),
1000
);
}
// assert Outcome Token redemption
for (let i = 0; i < buyers.length; i++) {
await predictionMarketSystem.redeemPositions(
collateralToken.address,
asciiToHex(0),
_conditionId,
[0b0001, 0b0010, 0b0100, 0b1000],
{ from: accounts[buyers[i]] }
);
assert.equal(
await collateralToken
.balanceOf(accounts[buyers[i]])
.then(res => res.toString()),
collateralTokenCounts[i]
);
}
});
});
contract("Complex splitting and merging scenario #1.", function(accounts) {
let predictionMarketSystem,
collateralToken,
minter = accounts[0],
oracle1,
oracle2,
oracle3,
questionId1,
questionId2,
questionId3,
outcomeSlotCount1,
outcomeSlotCount2,
outcomeSlotCount3,
player1,
player2,
player3,
conditionId1,
conditionId2,
conditionId3;
before(async () => {
predictionMarketSystem = await PredictionMarketSystem.deployed();
collateralToken = await ERC20Mintable.new();
// prepare condition
oracle1 = accounts[1];
oracle2 = accounts[2];
oracle3 = accounts[3];
questionId1 =
"0x1234987612349876123498761234987612349876123498761234987612349876";
questionId2 =
"0xcafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe";
questionId3 =
"0xab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12";
outcomeSlotCount1 = 2;
outcomeSlotCount2 = 3;
outcomeSlotCount3 = 4;
player1 = accounts[4];
player2 = accounts[5];
player3 = accounts[6];
await predictionMarketSystem.prepareCondition(
oracle1,
questionId1,
outcomeSlotCount1
);
await predictionMarketSystem.prepareCondition(
oracle2,
questionId2,
outcomeSlotCount2
);
await predictionMarketSystem.prepareCondition(
oracle3,
questionId3,
outcomeSlotCount3
);
conditionId1 = keccak256(
oracle1 +
[questionId1, outcomeSlotCount1]
.map(v => padLeft(toHex(v), 64).slice(2))
.join("")
);
conditionId2 = keccak256(
oracle2 +
[questionId2, outcomeSlotCount2]
.map(v => padLeft(toHex(v), 64).slice(2))
.join("")
);
conditionId3 = keccak256(
oracle3 +
[questionId3, outcomeSlotCount3]
.map(v => padLeft(toHex(v), 64).slice(2))
.join("")
);
await collateralToken.mint(player1, 10000, { from: minter });
await collateralToken.approve(predictionMarketSystem.address, 10000, {
from: player1
});
await collateralToken.mint(player2, 10000, { from: minter });
await collateralToken.approve(predictionMarketSystem.address, 10000, {
from: player2
});
await collateralToken.mint(player3, 10000, { from: minter });
await collateralToken.approve(predictionMarketSystem.address, 10000, {
from: player3
});
});
it("Invalid initial positions should not give any outcome tokens", async () => {
await predictionMarketSystem.splitPosition(
collateralToken.address,
asciiToHex(0),
conditionId1,
[0b01],
toBN(1e19),
{ from: player1 }
);
assert.equal(
await predictionMarketSystem.balanceOf(
player1,
keccak256(
collateralToken.address,
0 +
keccak256(conditionId1, padLeft(toHex(0b01), 64).slice(2)).slice(2)
)
),
0
);
assert.equal(
await collateralToken.balanceOf.call(player1).then(res => res.toString()),
10000
);
await assertRejects(
predictionMarketSystem.splitPosition(
collateralToken.address,
0,
conditionId1,
[0b01, 0b111],
toBN(1e19),
{ from: player1 }
),
"Worked with an invalid indexSet."
);
await assertRejects(
predictionMarketSystem.splitPosition(
collateralToken.address,
0,
conditionId1,
[0b01, 0b11],
toBN(1e19),
{ from: player1 }
),
"Worked with an invalid indexSet."
);
await assertRejects(
predictionMarketSystem.splitPosition(
collateralToken.address,
0,
conditionId1,
[0b01, 0b11, 0b0],
toBN(1e19),
{ from: player1 }
),
"Worked with an invalid indexSet."
);
});
it("should not produce any position changes when split on an incomplete set of base conditions", async () => {
await predictionMarketSystem.splitPosition(
collateralToken.address,
asciiToHex(0),
conditionId1,
[0b10],
1,
{ from: player3 }
);
await predictionMarketSystem.splitPosition(
collateralToken.address,
asciiToHex(0),
conditionId1,
[0b01],
1,
{ from: player3 }
);
const collectionId1 = keccak256(
conditionId1 + padLeft(toHex(0b01), 64).slice(2)
);
const collectionId2 = keccak256(
conditionId1 + padLeft(toHex(0b10), 64).slice(2)
);
const positionId1 = keccak256(
collateralToken.address + collectionId1.slice(2)
);
const positionId2 = keccak256(
collateralToken.address + collectionId2.slice(2)
);
assert.equal(
await predictionMarketSystem
.balanceOf(player3, positionId1)
.then(r => r.toNumber()),
0
);
assert.equal(
await predictionMarketSystem
.balanceOf(player3, positionId2)
.then(r => r.toNumber()),
0
);
});
it("should not be able to merge back into a collateral token from a position without any outcome tokens", async () => {
await assertRejects(
predictionMarketSystem.mergePositions(
collateralToken.address,
asciiToHex(0),
conditionId1,
[0b01, 0b10],
1,
{ from: player3 }
),
"If this didn't fail, the user is somehow able to withdraw ethereum from positions with none in it, or they have already ether in that position"
);
const collectionId1 = keccak256(
conditionId1 + padLeft(toHex(0b01), 64).slice(2)
);
const collectionId2 = keccak256(
conditionId1 + padLeft(toHex(0b10), 64).slice(2)
);
const positionId1 = keccak256(
collateralToken.address + collectionId1.slice(2)
);
const positionId2 = keccak256(
collateralToken.address + collectionId2.slice(2)
);
assert.equal(
await predictionMarketSystem
.balanceOf(player3, positionId1)
.then(r => r.toNumber()),
0
);
assert.equal(
await predictionMarketSystem
.balanceOf(player3, positionId2)
.then(r => r.toNumber()),
0
);
});
it("Should be able to split and merge in more complex scenarios", async () => {
// Split on an initial condition
await predictionMarketSystem.splitPosition(
collateralToken.address,
asciiToHex(0),
conditionId1,
[0b01, 0b10],
1000,
{ from: player1 }
);
const collectionId1 = keccak256(
conditionId1 + padLeft(toHex(0b01), 64).slice(2)
);
const collectionId2 = keccak256(
conditionId1 + padLeft(toHex(0b10), 64).slice(2)
);
const positionId1 = keccak256(
collateralToken.address + collectionId1.slice(2)
);
const positionId2 = keccak256(
collateralToken.address + collectionId2.slice(2)
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId1)
.then(r => r.toNumber()),
1000
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId2)
.then(r => r.toNumber()),
1000
);
assert.equal(
await predictionMarketSystem.getOutcomeSlotCount(conditionId2).valueOf(),
3
);
// Split on a non-root Collection Identifier and Condition
await predictionMarketSystem.splitPosition(
collateralToken.address,
collectionId1,
conditionId2,
[0b10, 0b01, 0b100],
100,
{ from: player1 }
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId1)
.then(r => r.toNumber()),
900
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId2)
.then(r => r.toNumber()),
1000
);
const collectionId3 =
"0x" +
toHex(
toBN(collectionId1).add(
toBN(keccak256(conditionId2 + padLeft(toHex(0b10), 64).slice(2)))
)
).slice(-64);
const collectionId4 =
"0x" +
toHex(
toBN(collectionId1).add(
toBN(keccak256(conditionId2 + padLeft(toHex(0b01), 64).slice(2)))
)
).slice(-64);
const collectionId5 =
"0x" +
toHex(
toBN(collectionId1).add(
toBN(keccak256(conditionId2 + padLeft(toHex(0b100), 64).slice(2)))
)
).slice(-64);
const positionId3 = keccak256(
collateralToken.address + collectionId3.slice(2)
);
const positionId4 = keccak256(
collateralToken.address + collectionId4.slice(2)
);
const positionId5 = keccak256(
collateralToken.address + collectionId5.slice(2)
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId3)
.then(r => r.toNumber()),
100
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId4)
.then(r => r.toNumber()),
100
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId5)
.then(r => r.toNumber()),
100
);
// Split again on a non-root Collection Identifier and Condition
await predictionMarketSystem.splitPosition(
collateralToken.address,
collectionId3,
conditionId3,
[0b10, 0b01, 0b100, 0b1000],
100,
{ from: player1 }
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId3)
.then(r => r.toNumber()),
0
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId2)
.then(r => r.toNumber()),
1000
);
const collectionId6 =
"0x" +
toHex(
toBN(collectionId3).add(
toBN(keccak256(conditionId3 + padLeft(toHex(0b10), 64).slice(2)))
)
).slice(-64);
const collectionId7 =
"0x" +
toHex(
toBN(collectionId3).add(
toBN(keccak256(conditionId3 + padLeft(toHex(0b01), 64).slice(2)))
)
).slice(-64);
const collectionId8 =
"0x" +
toHex(
toBN(collectionId3).add(
toBN(keccak256(conditionId3 + padLeft(toHex(0b100), 64).slice(2)))
)
).slice(-64);
const collectionId9 =
"0x" +
toHex(
toBN(collectionId3).add(
toBN(keccak256(conditionId3 + padLeft(toHex(0b1000), 64).slice(2)))
)
).slice(-64);
const positionId6 = keccak256(
collateralToken.address + collectionId6.slice(2)
);
const positionId7 = keccak256(
collateralToken.address + collectionId7.slice(2)
);
const positionId8 = keccak256(
collateralToken.address + collectionId8.slice(2)
);
const positionId9 = keccak256(
collateralToken.address + collectionId9.slice(2)
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId6)
.then(r => r.toNumber()),
100
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId7)
.then(r => r.toNumber()),
100
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId8)
.then(r => r.toNumber()),
100
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId9)
.then(r => r.toNumber()),
100
);
// Merge a full set of Outcome Slots back into conditionId3
await predictionMarketSystem.mergePositions(
collateralToken.address,
collectionId3,
conditionId3,
[0b10, 0b01, 0b100, 0b1000],
50,
{ from: player1 }
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId6)
.then(r => r.toNumber()),
50
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId7)
.then(r => r.toNumber()),
50
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId8)
.then(r => r.toNumber()),
50
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId9)
.then(r => r.toNumber()),
50
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId3)
.then(r => r.toNumber()),
50
);
// Merge a partial set of Outcome Slots back
await predictionMarketSystem.mergePositions(
collateralToken.address,
collectionId3,
conditionId3,
[0b10, 0b01, 0b1000],
50,
{ from: player1 }
);
const collectionId10 =
"0x" +
toHex(
toBN(collectionId3).add(
toBN(keccak256(conditionId3 + padLeft(toHex(0b1011), 64).slice(2)))
)
).slice(-64);
const positionId10 = keccak256(
collateralToken.address + collectionId10.slice(2)
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId10)
.then(r => r.toNumber()),
50
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId6)
.then(r => r.toNumber()),
0
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId7)
.then(r => r.toNumber()),
0
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId8)
.then(r => r.toNumber()),
50
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId9)
.then(r => r.toNumber()),
0
);
await assertRejects(
predictionMarketSystem.mergePositions(
collateralToken.address,
collectionId3,
conditionId3,
[0b10, 0b01, 0b100, 0b1000],
100,
{ from: player1 }
),
"Invalid merging of more tokens than the positions held did not revent"
);
await assertRejects(
predictionMarketSystem.mergePositions(
collateralToken.address,
collectionId3,
conditionId3,
[0b10, 0b01, 0b1000],
100,
{ from: player1 }
),
"Invalid merging of tokens amounting to more than the positions held happened."
);
await predictionMarketSystem.mergePositions(
collateralToken.address,
collectionId3,
conditionId3,
[0b1011, 0b100],
25,
{ from: player1 }
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId8)
.then(r => r.toNumber()),
25
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId10)
.then(r => r.toNumber()),
25
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId3)
.then(r => r.toNumber()),
75
);
await assertRejects(
predictionMarketSystem.mergePositions(
collateralToken.address,
collectionId1,
conditionId2,
[0b01, 0b10, 0b100],
100,
{ from: player1 }
),
"it didn't revert when only partial positions in the set have enough outcomeTokens."
);
await predictionMarketSystem.mergePositions(
collateralToken.address,
collectionId1,
conditionId2,
[0b01, 0b10, 0b100],
50,
{ from: player1 }
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId1)
.then(r => r.toNumber()),
950
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId3)
.then(r => r.toNumber()),
25
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId4)
.then(r => r.toNumber()),
50
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId5)
.then(r => r.toNumber()),
50
);
await assertRejects(
predictionMarketSystem.mergePositions(
collateralToken.address,
0,
conditionId1,
[0b01],
100,
{ from: player1 }
),
"Should not merge proper positions back into collateralTokens"
);
await assertRejects(
predictionMarketSystem.mergePositions(
collateralToken.address,
0,
conditionId1,
[0b01, 0b10],
1000,
{ from: player1 }
),
"Should not merge positions that dont hold enough value specified back into collateralTokens"
);
await assertRejects(
predictionMarketSystem.mergePositions(
collateralToken.address,
0,
conditionId1,
[0b01, 0b10],
950,
{ from: player3 }
),
"Should not merge positions from the wrong player back into collateralTokens"
);
await predictionMarketSystem.mergePositions(
collateralToken.address,
asciiToHex(0),
conditionId1,
[0b01, 0b10],
950,
{ from: player1 }
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId1)
.then(r => r.toNumber()),
0
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId2)
.then(r => r.toNumber()),
50
);
assert.equal(
await collateralToken.balanceOf(player1).then(r => r.toNumber()),
9950
);
await assertRejects(
predictionMarketSystem.redeemPositions(
collateralToken.address,
asciiToHex(0),
conditionId1,
[0b01, 0b10],
{ from: player1 }
),
"The position is being redeemed before the payouts for the condition have been set."
);
await predictionMarketSystem.receiveResult(
questionId3,
"0x" +
[
padLeft("14D", 64), // 333
padLeft("1", 64), // 1
padLeft("29A", 64), // 666
padLeft("0", 64)
].join(""),
{ from: oracle3 }
);
assert.equal(
await predictionMarketSystem.payoutDenominator(conditionId3).valueOf(),
1000
);
await assertRejects(
predictionMarketSystem.redeemPositions(
collateralToken.address,
asciiToHex(0),
conditionId2,
[0b01, 0b110],
{ from: player1 }
),
"The position is being redeemed before the payouts for the condition have been set."
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId10)
.then(r => r.toNumber()),
25
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId6)
.then(r => r.toNumber()),
0
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId7)
.then(r => r.toNumber()),
0
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId8)
.then(r => r.toNumber()),
25
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId9)
.then(r => r.toNumber()),
0
);
// asserts that if you redeem the wrong indexSets, it won't affect the other indexes.
await predictionMarketSystem.redeemPositions(
collateralToken.address,
collectionId3,
conditionId3,
[0b10, 0b01, 0b1000],
{ from: player1 }
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId8)
.then(r => r.toNumber()),
25
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId3)
.then(r => r.toNumber()),
25
);
await predictionMarketSystem.redeemPositions(
collateralToken.address,
collectionId3,
conditionId3,
[0b10, 0b01, 0b100],
{ from: player1 }
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId8)
.then(r => r.toNumber()),
0
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId3)
.then(r => r.toNumber()),
25 + Math.floor(25 * (666 / 1000))
);
await predictionMarketSystem.redeemPositions(
collateralToken.address,
collectionId3,
conditionId3,
[0b1011],
{ from: player1 }
);
// We have to account for a small fraction of tokens getting stuck in the contract there on payout
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId3)
.then(r => r.toNumber()),
25 + Math.floor(25 * (666 / 1000 + 334 / 1000)) - 1
);
await predictionMarketSystem.receiveResult(
questionId2,
"0x" + [padLeft("FF", 64), padLeft("FF", 64), padLeft("0", 64)].join(""),
{ from: oracle2 }
);
await predictionMarketSystem.redeemPositions(
collateralToken.address,
collectionId1,
conditionId2,
[0b01, 0b10, 0b100],
{ from: player1 }
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId3)
.then(r => r.toNumber()),
0
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId4)
.then(r => r.toNumber()),
0
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId5)
.then(r => r.toNumber()),
0
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId1)
.then(r => r.toNumber()),
49
);
await predictionMarketSystem.receiveResult(
questionId1,
"0x" + [padLeft("1", 64), padLeft("0", 64)].join(""),
{ from: oracle1 }
);
assert.equal(
await predictionMarketSystem.payoutDenominator(conditionId1).valueOf(),
1
);
await predictionMarketSystem.redeemPositions(
collateralToken.address,
asciiToHex(0),
conditionId1,
[0b01],
{ from: player1 }
);
assert.equal(
await predictionMarketSystem
.balanceOf(player1, positionId1)
.then(r => r.toNumber()),
0
);
// Missing 1 for the rounding of different outcomes
assert.equal(
await collateralToken.balanceOf(player1).then(r => r.toNumber()),
9999
);
});
});
contract(
"Should be able to partially split and merge in complex scenarios. #2",
function(accounts) {
let predictionMarketSystem,
collateralToken,
minter = accounts[0],
oracle1,
oracle2,
oracle3,
questionId1,
questionId2,
questionId3,
outcomeSlotCount1,
outcomeSlotCount2,
outcomeSlotCount3,
player1,
player2,
player3,
conditionId1,
conditionId2;
before(async () => {
predictionMarketSystem = await PredictionMarketSystem.deployed();
collateralToken = await ERC20Mintable.new({ from: minter });
// prepare condition
oracle1 = accounts[1];
oracle2 = accounts[2];
oracle3 = accounts[3];
questionId1 =
"0x1234987612349876123498761234987612349876123498761234987612349876";
questionId2 =
"0xcafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe";
questionId3 =
"0xab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12";
outcomeSlotCount1 = 2;
outcomeSlotCount2 = 3;
outcomeSlotCount3 = 4;
player1 = accounts[4];
player2 = accounts[5];
player3 = accounts[6];
await predictionMarketSystem.prepareCondition(
oracle1,
questionId1,
outcomeSlotCount1
);
await predictionMarketSystem.prepareCondition(
oracle2,
questionId2,
outcomeSlotCount2
);
await predictionMarketSystem.prepareCondition(
oracle3,
questionId3,
outcomeSlotCount3
);
conditionId1 = keccak256(
oracle1 +
[questionId1, outcomeSlotCount1]
.map(v => padLeft(toHex(v), 64).slice(2))
.join("")
);
conditionId2 = keccak256(
oracle2 +
[questionId2, outcomeSlotCount2]
.map(v => padLeft(toHex(v), 64).slice(2))
.join("")
);
await collateralToken.mint(player1, toBN(1e19), { from: minter });
await collateralToken.approve(
predictionMarketSystem.address,
toBN(1e19),
{
from: player1
}
);
await collateralToken.mint(player2, toBN(1e19), { from: minter });
await collateralToken.approve(
predictionMarketSystem.address,
toBN(1e19),
{
from: player2
}
);
await collateralToken.mint(player3, toBN(1e19), { from: minter });
await collateralToken.approve(
predictionMarketSystem.address,
toBN(1e19),
{
from: player3
}
);
});
it("Should correctly and safely partially split and merge in complex scnarios.", async () => {
await predictionMarketSystem.splitPosition(
collateralToken.address,
asciiToHex(0),
conditionId1,
[0b01, 0b10],
toBN(1e19),
{ from: player1 }
);
const collectionId1 = keccak256(
conditionId1 + padLeft(toHex(0b01), 64).slice(2)
);
const collectionId2 = keccak256(
conditionId1 + padLeft(toHex(0b10), 64).slice(2)
);
const positionId1 = keccak256(
collateralToken.address + collectionId1.slice(2)
);
const positionId2 = keccak256(
collateralToken.address + collectionId2.slice(2)
);
assert.equal(
fromWei(
await predictionMarketSystem.balanceOf(player1, positionId1),
"ether"
),
10
);
assert.equal(
fromWei(
await predictionMarketSystem.balanceOf(player1, positionId2),
"ether"
),
10
);
assert.equal(
fromWei(await collateralToken.balanceOf(player1), "ether"),
0
);
await assertRejects(
predictionMarketSystem.splitPosition(
collateralToken.address,
collectionId2,
conditionId2,
[0b01, 0b10],
1000,
{ from: player1 }
),
"partial split without having the added positions (3) tokens should be rejected"
);
await assertRejects(
predictionMarketSystem.splitPosition(
collateralToken.address,
collectionId2,
conditionId2,
[0b100, 0b01],
1000,
{ from: player1 }
),
"should be rejected"
);
await predictionMarketSystem.splitPosition(
collateralToken.address,
collectionId2,
conditionId2,
[0b110, 0b01],
toBN(1e19),
{ from: player1 }
);
const collectionId3 =
"0x" +
toHex(
toBN(collectionId2).add(
toBN(keccak256(conditionId2 + padLeft(toHex(0b110), 64).slice(2)))
)
).slice(-64);
const collectionId4 =
"0x" +
toHex(
toBN(collectionId2).add(
toBN(keccak256(conditionId2 + padLeft(toHex(0b01), 64).slice(2)))
)
).slice(-64);
const positionId3 = keccak256(
collateralToken.address + collectionId3.slice(2)
);
const positionId4 = keccak256(
collateralToken.address + collectionId4.slice(2)
);
assert.equal(
fromWei(
await predictionMarketSystem.balanceOf(player1, positionId3),
"ether"
),
10
);
assert.equal(
fromWei(
await predictionMarketSystem.balanceOf(player1, positionId4),
"ether"
),
10
);
await predictionMarketSystem.splitPosition(
collateralToken.address,
collectionId2,
conditionId2,
[0b100, 0b10],
toBN(1e19),
{ from: player1 }
);
assert.equal(
fromWei(
await predictionMarketSystem.balanceOf(player1, positionId3),
"ether"
),
0
);
assert.equal(
fromWei(
await predictionMarketSystem.balanceOf(player1, positionId4),
"ether"
),
10
);
const collectionId5 =
"0x" +
toHex(
toBN(collectionId2).add(
toBN(keccak256(conditionId2 + padLeft(toHex(0b100), 64).slice(2)))
)
).slice(-64);
const collectionId6 =
"0x" +
toHex(
toBN(collectionId2).add(
toBN(keccak256(conditionId2 + padLeft(toHex(0b10), 64).slice(2)))
)
).slice(-64);
const positionId5 = keccak256(
collateralToken.address + collectionId5.slice(2)
);
const positionId6 = keccak256(
collateralToken.address + collectionId6.slice(2)
);
assert.equal(
fromWei(
await predictionMarketSystem.balanceOf(player1, positionId5),
"ether"
),
10
);
assert.equal(
fromWei(
await predictionMarketSystem.balanceOf(player1, positionId6),
"ether"
),
10
);
await predictionMarketSystem.mergePositions(
collateralToken.address,
collectionId2,
conditionId2,
[0b01, 0b10],
toBN(1e19),
{ from: player1 }
);
assert.equal(
fromWei(
await predictionMarketSystem.balanceOf(player1, positionId6),
"ether"
),
0
);
assert.equal(
fromWei(
await predictionMarketSystem.balanceOf(player1, positionId4),
"ether"
),
0
);
const collectionId7 =
"0x" +
toHex(
toBN(collectionId2).add(
toBN(keccak256(conditionId2 + padLeft(toHex(0b11), 64).slice(2)))
)
).slice(-64);
const positionId7 = keccak256(
collateralToken.address + collectionId7.slice(2)
);
assert.equal(
fromWei(
await predictionMarketSystem.balanceOf(player1, positionId7),
"ether"
),
10
);
});
}
);
contract(
"The same positions in different orders should equal each other.",
function(accounts) {
let predictionMarketSystem,
collateralToken,
minter = accounts[0],
oracle1,
oracle2,
oracle3,
questionId1,
questionId2,
questionId3,
outcomeSlotCount1,
outcomeSlotCount2,
outcomeSlotCount3,
player1,
player2,
player3,
conditionId1,
conditionId2;
before(async () => {
predictionMarketSystem = await PredictionMarketSystem.deployed();
collateralToken = await ERC20Mintable.new({ from: minter });
// prepare condition
oracle1 = accounts[1];
oracle2 = accounts[2];
oracle3 = accounts[3];
questionId1 =
"0x1234987612349876123498761234987612349876123498761234987612349876";
questionId2 =
"0xcafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe";
questionId3 =
"0xab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12";
outcomeSlotCount1 = 2;
outcomeSlotCount2 = 3;
outcomeSlotCount3 = 4;
player1 = accounts[4];
player2 = accounts[5];
player3 = accounts[6];
await predictionMarketSystem.prepareCondition(
oracle1,
questionId1,
outcomeSlotCount1
);
await predictionMarketSystem.prepareCondition(
oracle2,
questionId2,
outcomeSlotCount2
);
await predictionMarketSystem.prepareCondition(
oracle3,
questionId3,
outcomeSlotCount3
);
conditionId1 = keccak256(
oracle1 +
[questionId1, outcomeSlotCount1]
.map(v => padLeft(toHex(v), 64).slice(2))
.join("")
);
conditionId2 = keccak256(
oracle2 +
[questionId2, outcomeSlotCount2]
.map(v => padLeft(toHex(v), 64).slice(2))
.join("")
);
await collateralToken.mint(player1, toBN(1e19), { from: minter });
await collateralToken.approve(
pr