Skip to content

Commit

Permalink
Merge pull request #73 from gnosis/help-extend
Browse files Browse the repository at this point in the history
Exposed ID helper functions even more
  • Loading branch information
cag committed Aug 28, 2019
2 parents e51c3b1 + 563a713 commit f0a24ef
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 186 deletions.
66 changes: 66 additions & 0 deletions contracts/CTHelpers.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
pragma solidity ^0.5.1;

import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol";

library CTHelpers {
/// @dev Constructs a condition ID from an oracle, a question ID, and the outcome slot count for the question.
/// @param oracle The account assigned to report the result for the prepared condition.
/// @param questionId An identifier for the question to be answered by the oracle.
/// @param outcomeSlotCount The number of outcome slots which should be used for this condition. Must not exceed 256.
function getConditionId(address oracle, bytes32 questionId, uint outcomeSlotCount) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(oracle, questionId, outcomeSlotCount));
}

uint constant P = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
uint constant B = 3;
uint constant SQRT_EXP = 5472060717959818805561601436314318772174077789324455915672259473661306552146;

/// @dev Constructs an outcome collection ID from a parent collection and an outcome collection.
/// @param parentCollectionId Collection ID of the parent outcome collection, or bytes32(0) if there's no parent.
/// @param conditionId Condition ID of the outcome collection to combine with the parent outcome collection.
/// @param indexSet Index set of the outcome collection to combine with the parent outcome collection.
function getCollectionId(bytes32 parentCollectionId, bytes32 conditionId, uint indexSet) internal view returns (bytes32) {
uint x1 = uint(keccak256(abi.encodePacked(conditionId, indexSet)));
bool odd = x1 >> 255 != 0;
uint y1;
uint yy;
do {
x1 = addmod(x1, 1, P);
yy = addmod(mulmod(x1, mulmod(x1, x1, P), P), B, P);
(bool success, bytes memory ret) = address(5).staticcall(abi.encode(uint(0x20), uint(0x20), uint(0x20), yy, SQRT_EXP, P));
if(success)
y1 = abi.decode(ret, (uint));
} while(mulmod(y1, y1, P) != yy);
if(odd && y1 % 2 == 0 || !odd && y1 % 2 == 1)
y1 = P - y1;

uint x2 = uint(parentCollectionId);
if(x2 != 0) {
odd = x2 >> 254 != 0;
x2 = (x2 << 2) >> 2;
yy = addmod(mulmod(x2, mulmod(x2, x2, P), P), B, P);
(bool success, bytes memory ret) = address(5).staticcall(abi.encode(uint(0x20), uint(0x20), uint(0x20), yy, SQRT_EXP, P));
require(success, "can't find quadratic residue");
uint y2 = abi.decode(ret, (uint));
if(odd && y2 % 2 == 0 || !odd && y2 % 2 == 1)
y2 = P - y2;
require(mulmod(y2, y2, P) == yy, "invalid parent collection ID");

(success, ret) = address(6).staticcall(abi.encode(x1, y1, x2, y2));
require(success, "ecadd failed");
(x1, y1) = abi.decode(ret, (uint, uint));
}

if(y1 % 2 == 1)
x1 ^= 1 << 254;

return bytes32(x1);
}

/// @dev Constructs a position ID from a collateral token and an outcome collection. These IDs are used as the ERC-1155 ID for this contract.
/// @param collateralToken Collateral token which backs the position.
/// @param collectionId ID of the outcome collection associated with this position.
function getPositionId(IERC20 collateralToken, bytes32 collectionId) internal pure returns (uint) {
return uint(keccak256(abi.encodePacked(collateralToken, collectionId)));
}
}
78 changes: 20 additions & 58 deletions contracts/ConditionalTokens.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
pragma solidity ^0.5.1;
import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol";
import { ERC1155 } from "./ERC1155/ERC1155.sol";

import { CTHelpers } from "./CTHelpers.sol";

contract ConditionalTokens is ERC1155 {

Expand Down Expand Up @@ -66,7 +66,7 @@ contract ConditionalTokens is ERC1155 {
// Limit of 256 because we use a partition array that is a number of 256 bits.
require(outcomeSlotCount <= 256, "too many outcome slots");
require(outcomeSlotCount > 1, "there should be more than one outcome slot");
bytes32 conditionId = getConditionId(oracle, questionId, outcomeSlotCount);
bytes32 conditionId = CTHelpers.getConditionId(oracle, questionId, outcomeSlotCount);
require(payoutNumerators[conditionId].length == 0, "condition already prepared");
payoutNumerators[conditionId] = new uint[](outcomeSlotCount);
emit ConditionPreparation(conditionId, oracle, questionId, outcomeSlotCount);
Expand All @@ -79,7 +79,7 @@ contract ConditionalTokens is ERC1155 {
uint outcomeSlotCount = payouts.length;
require(outcomeSlotCount > 1, "there should be more than one outcome slot");
// IMPORTANT, the oracle is enforced to be the sender because it's part of the hash.
bytes32 conditionId = getConditionId(msg.sender, questionId, outcomeSlotCount);
bytes32 conditionId = CTHelpers.getConditionId(msg.sender, questionId, outcomeSlotCount);
require(payoutNumerators[conditionId].length == outcomeSlotCount, "condition not prepared or found");
require(payoutDenominator[conditionId] == 0, "payout denominator already set");

Expand Down Expand Up @@ -125,7 +125,7 @@ contract ConditionalTokens is ERC1155 {
require(indexSet > 0 && indexSet < fullIndexSet, "got invalid index set");
require((indexSet & freeIndexSet) == indexSet, "partition not disjoint");
freeIndexSet ^= indexSet;
positionIds[i] = getPositionId(collateralToken, getCollectionId(parentCollectionId, conditionId, indexSet));
positionIds[i] = CTHelpers.getPositionId(collateralToken, CTHelpers.getCollectionId(parentCollectionId, conditionId, indexSet));
amounts[i] = amount;
}

Expand All @@ -136,7 +136,7 @@ contract ConditionalTokens is ERC1155 {
} else {
_burn(
msg.sender,
getPositionId(collateralToken, parentCollectionId),
CTHelpers.getPositionId(collateralToken, parentCollectionId),
amount
);
}
Expand All @@ -146,8 +146,8 @@ contract ConditionalTokens is ERC1155 {
// allows the splitting of a position $:(A|C) to positions $:(A) and $:(C).
_burn(
msg.sender,
getPositionId(collateralToken,
getCollectionId(parentCollectionId, conditionId, fullIndexSet ^ freeIndexSet)),
CTHelpers.getPositionId(collateralToken,
CTHelpers.getCollectionId(parentCollectionId, conditionId, fullIndexSet ^ freeIndexSet)),
amount
);
}
Expand Down Expand Up @@ -182,7 +182,7 @@ contract ConditionalTokens is ERC1155 {
require(indexSet > 0 && indexSet < fullIndexSet, "got invalid index set");
require((indexSet & freeIndexSet) == indexSet, "partition not disjoint");
freeIndexSet ^= indexSet;
positionIds[i] = getPositionId(collateralToken, getCollectionId(parentCollectionId, conditionId, indexSet));
positionIds[i] = CTHelpers.getPositionId(collateralToken, CTHelpers.getCollectionId(parentCollectionId, conditionId, indexSet));
amounts[i] = amount;
}
_batchBurn(
Expand All @@ -197,16 +197,16 @@ contract ConditionalTokens is ERC1155 {
} else {
_mint(
msg.sender,
getPositionId(collateralToken, parentCollectionId),
CTHelpers.getPositionId(collateralToken, parentCollectionId),
amount,
""
);
}
} else {
_mint(
msg.sender,
getPositionId(collateralToken,
getCollectionId(parentCollectionId, conditionId, fullIndexSet ^ freeIndexSet)),
CTHelpers.getPositionId(collateralToken,
CTHelpers.getCollectionId(parentCollectionId, conditionId, fullIndexSet ^ freeIndexSet)),
amount,
""
);
Expand All @@ -227,8 +227,8 @@ contract ConditionalTokens is ERC1155 {
for (uint i = 0; i < indexSets.length; i++) {
uint indexSet = indexSets[i];
require(indexSet > 0 && indexSet < fullIndexSet, "got invalid index set");
uint positionId = getPositionId(collateralToken,
getCollectionId(parentCollectionId, conditionId, indexSet));
uint positionId = CTHelpers.getPositionId(collateralToken,
CTHelpers.getCollectionId(parentCollectionId, conditionId, indexSet));

uint payoutNumerator = 0;
for (uint j = 0; j < outcomeSlotCount; j++) {
Expand All @@ -248,7 +248,7 @@ contract ConditionalTokens is ERC1155 {
if (parentCollectionId == bytes32(0)) {
require(collateralToken.transfer(msg.sender, totalPayout), "could not transfer payout to message sender");
} else {
_mint(msg.sender, getPositionId(collateralToken, parentCollectionId), totalPayout, "");
_mint(msg.sender, CTHelpers.getPositionId(collateralToken, parentCollectionId), totalPayout, "");
}
}
emit PayoutRedemption(msg.sender, collateralToken, parentCollectionId, conditionId, indexSets, totalPayout);
Expand All @@ -265,60 +265,22 @@ contract ConditionalTokens is ERC1155 {
/// @param oracle The account assigned to report the result for the prepared condition.
/// @param questionId An identifier for the question to be answered by the oracle.
/// @param outcomeSlotCount The number of outcome slots which should be used for this condition. Must not exceed 256.
function getConditionId(address oracle, bytes32 questionId, uint outcomeSlotCount) public pure returns (bytes32) {
return keccak256(abi.encodePacked(oracle, questionId, outcomeSlotCount));
function getConditionId(address oracle, bytes32 questionId, uint outcomeSlotCount) external pure returns (bytes32) {
return CTHelpers.getConditionId(oracle, questionId, outcomeSlotCount);
}

uint constant P = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
uint constant B = 3;
uint constant SQRT_EXP = 5472060717959818805561601436314318772174077789324455915672259473661306552146;

/// @dev Constructs an outcome collection ID from a parent collection and an outcome collection.
/// @param parentCollectionId Collection ID of the parent outcome collection, or bytes32(0) if there's no parent.
/// @param conditionId Condition ID of the outcome collection to combine with the parent outcome collection.
/// @param indexSet Index set of the outcome collection to combine with the parent outcome collection.
function getCollectionId(bytes32 parentCollectionId, bytes32 conditionId, uint indexSet) public view returns (bytes32) {
uint x1 = uint(keccak256(abi.encodePacked(conditionId, indexSet)));
bool odd = x1 >> 255 != 0;
uint y1;
uint yy;
do {
x1 = addmod(x1, 1, P);
yy = addmod(mulmod(x1, mulmod(x1, x1, P), P), B, P);
(bool success, bytes memory ret) = address(5).staticcall(abi.encode(uint(0x20), uint(0x20), uint(0x20), yy, SQRT_EXP, P));
if(success)
y1 = abi.decode(ret, (uint));
} while(mulmod(y1, y1, P) != yy);
if(odd && y1 % 2 == 0 || !odd && y1 % 2 == 1)
y1 = P - y1;

uint x2 = uint(parentCollectionId);
if(x2 != 0) {
odd = x2 >> 254 != 0;
x2 = (x2 << 2) >> 2;
yy = addmod(mulmod(x2, mulmod(x2, x2, P), P), B, P);
(bool success, bytes memory ret) = address(5).staticcall(abi.encode(uint(0x20), uint(0x20), uint(0x20), yy, SQRT_EXP, P));
require(success, "can't find quadratic residue");
uint y2 = abi.decode(ret, (uint));
if(odd && y2 % 2 == 0 || !odd && y2 % 2 == 1)
y2 = P - y2;
require(mulmod(y2, y2, P) == yy, "invalid parent collection ID");

(success, ret) = address(6).staticcall(abi.encode(x1, y1, x2, y2));
require(success, "ecadd failed");
(x1, y1) = abi.decode(ret, (uint, uint));
}

if(y1 % 2 == 1)
x1 ^= 1 << 254;

return bytes32(x1);
function getCollectionId(bytes32 parentCollectionId, bytes32 conditionId, uint indexSet) external view returns (bytes32) {
return CTHelpers.getCollectionId(parentCollectionId, conditionId, indexSet);
}

/// @dev Constructs a position ID from a collateral token and an outcome collection. These IDs are used as the ERC-1155 ID for this contract.
/// @param collateralToken Collateral token which backs the position.
/// @param collectionId ID of the outcome collection associated with this position.
function getPositionId(IERC20 collateralToken, bytes32 collectionId) public pure returns (uint) {
return uint(keccak256(abi.encodePacked(collateralToken, collectionId)));
function getPositionId(IERC20 collateralToken, bytes32 collectionId) external pure returns (uint) {
return CTHelpers.getPositionId(collateralToken, collectionId);
}
}
135 changes: 7 additions & 128 deletions test/test-conditional-tokens.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
const ethSigUtil = require("eth-sig-util");

const { expectEvent, expectRevert } = require("openzeppelin-test-helpers");
const { BN, toBN, soliditySha3, randomHex } = web3.utils;
const { toBN, randomHex } = web3.utils;
const {
getConditionId,
getCollectionId,
combineCollectionIds,
getPositionId
} = require("./utils");

const ConditionalTokens = artifacts.require("ConditionalTokens");
const ERC20Mintable = artifacts.require("MockCoin");
Expand All @@ -11,133 +17,6 @@ const GnosisSafe = artifacts.require("GnosisSafe");

const NULL_BYTES32 = `0x${"0".repeat(64)}`;

function getConditionId(oracle, questionId, outcomeSlotCount) {
return soliditySha3(
{ t: "address", v: oracle },
{ t: "bytes32", v: questionId },
{ t: "uint", v: outcomeSlotCount }
);
}

const altBN128P = toBN(
"21888242871839275222246405745257275088696311157297823662689037894645226208583"
);
const altBN128PRed = BN.red(altBN128P);
const altBN128B = toBN(3).toRed(altBN128PRed);
const onePRed = toBN(1).toRed(altBN128PRed);
const twoPRed = toBN(2).toRed(altBN128PRed);
const fourPRed = toBN(4).toRed(altBN128PRed);
const oddToggle = toBN(1).ushln(254);

function getCollectionId(conditionId, indexSet) {
const initHash = soliditySha3(
{ t: "bytes32", v: conditionId },
{ t: "uint", v: indexSet }
);
const odd = "89abcdef".includes(initHash[2]);

const x = toBN(initHash).toRed(altBN128PRed);

let y, yy;
do {
x.redIAdd(onePRed);
yy = x.redSqr();
yy.redIMul(x);
yy = yy.mod(altBN128P);
yy.redIAdd(altBN128B);
y = yy.redSqrt();
} while (!y.redSqr().eq(yy));

const ecHash = x.fromRed();
if (odd) ecHash.ixor(oddToggle);
return `0x${ecHash.toString(16, 64)}`;
}

function combineCollectionIds(collectionIds) {
const points = collectionIds.map(id => {
let x = toBN(id);
const odd = x.and(oddToggle).eq(oddToggle);
if (odd) x.ixor(oddToggle);
x = x.toRed(altBN128PRed);
let y, yy;
yy = x.redSqr();
yy.redIMul(x);
yy = yy.mod(altBN128P); // this might be a BN.js bug workaround
yy.redIAdd(altBN128B);
y = yy.redSqrt();
if (!y.redSqr().eq(yy)) throw new Error(`got invalid collection ID ${id}`);
if (odd !== y.isOdd()) y = y.redNeg();
return [x, y];
});

const [X, Y, Z] = points.reduce(([X1, Y1, Z1], [x2, y2]) => {
// https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#addition-madd-2007-bl
if (Z1 == null) {
Z1 = onePRed;
}

// source 2007 Bernstein--Lange
// assume Z2=1

// compute Z1Z1 = Z1^2
const Z1Z1 = Z1.redSqr();
// compute U2 = X2 Z1Z1
const U2 = x2.redMul(Z1Z1);
// compute S2 = Y2 Z1 Z1Z1
const S2 = y2.redMul(Z1).redMul(Z1Z1);
// compute H = U2-X1
const H = U2.redSub(X1);
// compute HH = H^2
const HH = H.redSqr();
// compute I = 4 HH
const I = HH.redMul(fourPRed);
// compute J = H I
const J = H.redMul(I);
// compute r = 2 (S2-Y1)
const r = twoPRed.redMul(S2.redSub(Y1));
// compute V = X1 I
const V = X1.redMul(I);
// compute X3 = r^2-J-2 V
const X3 = r
.redSqr()
.redSub(J)
.redSub(twoPRed.redMul(V));
// compute Y3 = r (V-X3)-2 Y1 J
const Y3 = r.redMul(V.redSub(X3)).redSub(twoPRed.redMul(Y1).redMul(J));
// compute Z3 = (Z1+H)^2-Z1Z1-HH
const Z3 = Z1.redAdd(H)
.redSqr()
.redSub(Z1Z1)
.redSub(HH);

return [X3, Y3, Z3];
});

const invZ = Z.redInvm();
const invZZ = invZ.redSqr();
const invZZZ = invZZ.redMul(invZ);

const x = X.redMul(invZZ);
const y = Y.redMul(invZZZ);

const ecHash = x.fromRed();
if (y.isOdd()) ecHash.ixor(oddToggle);
return `0x${ecHash.toString(16, 64)}`;
}

function getPositionId(collateralToken, collateralTokenID, collectionId) {
if (collectionId == null)
return soliditySha3(
{ t: "address", v: collateralToken },
{ t: "uint", v: collateralTokenID }
);
return soliditySha3(
{ t: "address", v: collateralToken },
{ t: "uint", v: collateralTokenID },
{ t: "uint", v: collectionId }
);
}

contract("ConditionalTokens", function(accounts) {
const [
minter,
Expand Down

0 comments on commit f0a24ef

Please sign in to comment.