diff --git a/contracts/ConditionalTokens.sol b/contracts/ConditionalTokens.sol index 46492c204..6717f4755 100644 --- a/contracts/ConditionalTokens.sol +++ b/contracts/ConditionalTokens.sol @@ -84,8 +84,6 @@ contract ConditionalTokens is ERC1155, ERC1155TokenReceiver { uint payout ); - enum CollateralTypes { ERC20, ERC1155 } - /// Mapping key is an condition ID. Value represents numerators of the payout vector associated with the condition. This array is initialized with a length equal to the outcome slot count. mapping(bytes32 => uint[]) public payoutNumerators; mapping(bytes32 => uint) _payoutDenominator; @@ -119,16 +117,33 @@ contract ConditionalTokens is ERC1155, ERC1155TokenReceiver { require(outcomeSlotCount > 1, "there should be more than one outcome slot"); bytes32 conditionId = getConditionId(msg.sender, questionId, payoutDenominator, outcomeSlotCount); require(payoutNumerators[conditionId].length == outcomeSlotCount && _payoutDenominator[conditionId] == payoutDenominator, "condition not prepared or found"); + + uint emptySlots = 0; uint den = 0; + bool didUpdate = false; for (uint i = 0; i < outcomeSlotCount; i++) { - uint num = payouts[i]; - den = den.add(num); - - require(payoutNumerators[conditionId][i] <= num, "can't lower existing payout numerator"); - payoutNumerators[conditionId][i] = num; + uint oldNum = payoutNumerators[conditionId][i]; + uint newNum = payouts[i]; + if(oldNum == 0) { + if(newNum == 0) + emptySlots++; + else { + payoutNumerators[conditionId][i] = newNum; + den = den.add(newNum); + didUpdate = true; + } + } else { + require(oldNum == newNum, "can't change existing payout"); + den = den.add(newNum); + } } + require(den > 0, "payout is all zeroes"); - require(den <= payoutDenominator, "payouts can't exceed denominator"); + require(didUpdate, "didn't update anything"); + if(emptySlots > 1) + require(den <= payoutDenominator, "payouts can't exceed denominator"); + else + require(den == payoutDenominator, "final report must sum up to denominator"); emit ConditionResolution(conditionId, msg.sender, questionId, outcomeSlotCount, payoutNumerators[conditionId]); } @@ -145,7 +160,24 @@ contract ConditionalTokens is ERC1155, ERC1155TokenReceiver { uint[] calldata partition, uint amount ) external { - (uint fullIndexSet, uint freeIndexSet) = mintSet(msg.sender, CollateralTypes.ERC20, address(collateralToken), 0, parentCollectionId, conditionId, partition, amount); + require(partition.length > 1, "got empty or singleton partition"); + uint outcomeSlotCount = payoutNumerators[conditionId].length; + require(outcomeSlotCount > 0, "condition not prepared yet"); + + uint fullIndexSet = (1 << outcomeSlotCount) - 1; + uint freeIndexSet = fullIndexSet; + for (uint i = 0; i < partition.length; i++) { + uint indexSet = partition[i]; + require(indexSet > 0 && indexSet < fullIndexSet, "got invalid index set"); + require((indexSet & freeIndexSet) == indexSet, "partition not disjoint"); + freeIndexSet ^= indexSet; + _mint( + msg.sender, + getPositionId(collateralToken, getCollectionId(parentCollectionId, conditionId, indexSet)), + amount, + "" + ); + } if (freeIndexSet == 0) { if (parentCollectionId == bytes32(0)) { @@ -177,7 +209,25 @@ contract ConditionalTokens is ERC1155, ERC1155TokenReceiver { uint[] calldata partition, uint amount ) external { - (uint fullIndexSet, uint freeIndexSet) = mintSet(msg.sender, CollateralTypes.ERC1155, address(collateralToken), collateralTokenID, parentCollectionId, conditionId, partition, amount); + require(partition.length > 1, "got empty or singleton partition"); + uint outcomeSlotCount = payoutNumerators[conditionId].length; + require(outcomeSlotCount > 0, "condition not prepared yet"); + + uint fullIndexSet = (1 << outcomeSlotCount) - 1; + uint freeIndexSet = fullIndexSet; + for (uint i = 0; i < partition.length; i++) { + uint indexSet = partition[i]; + require(indexSet > 0 && indexSet < fullIndexSet, "got invalid index set"); + require((indexSet & freeIndexSet) == indexSet, "partition not disjoint"); + freeIndexSet ^= indexSet; + _mint( + msg.sender, + getPositionId(collateralToken, collateralTokenID, + getCollectionId(parentCollectionId, conditionId, indexSet)), + amount, + "" + ); + } if (freeIndexSet == 0) { if (parentCollectionId == bytes32(0)) { @@ -208,6 +258,7 @@ contract ConditionalTokens is ERC1155, ERC1155TokenReceiver { uint[] calldata partition, uint amount ) external { + require(partition.length > 1, "got empty or singleton partition"); uint outcomeSlotCount = payoutNumerators[conditionId].length; require(outcomeSlotCount > 0, "condition not prepared yet"); @@ -257,6 +308,7 @@ contract ConditionalTokens is ERC1155, ERC1155TokenReceiver { uint[] calldata partition, uint amount ) external { + require(partition.length > 1, "got empty or singleton partition"); uint outcomeSlotCount = payoutNumerators[conditionId].length; require(outcomeSlotCount > 0, "condition not prepared yet"); @@ -299,45 +351,20 @@ contract ConditionalTokens is ERC1155, ERC1155TokenReceiver { emit PositionsMerge(msg.sender, collateralToken, collateralTokenID, parentCollectionId, conditionId, partition, amount); } - function mintSet( - address recipient, - CollateralTypes collateralType, - address collateralToken, - uint collateralTokenID, - bytes32 parentCollectionId, - bytes32 conditionId, - uint[] memory partition, - uint amount - ) private returns (uint fullIndexSet, uint freeIndexSet) { - { - uint outcomeSlotCount = payoutNumerators[conditionId].length; - require(outcomeSlotCount > 0, "condition not prepared yet"); - fullIndexSet = (1 << outcomeSlotCount) - 1; - freeIndexSet = fullIndexSet; - } - - for (uint i = 0; i < partition.length; i++) { - uint indexSet = partition[i]; - require(indexSet > 0 && indexSet < fullIndexSet, "got invalid index set"); - require((indexSet & freeIndexSet) == indexSet, "partition not disjoint"); - freeIndexSet ^= indexSet; - _mint( - recipient, - collateralType == CollateralTypes.ERC20 ? - getPositionId(IERC20(collateralToken), getCollectionId(parentCollectionId, conditionId, indexSet)) : - getPositionId(IERC1155(collateralToken), collateralTokenID, getCollectionId(parentCollectionId, conditionId, indexSet)), - amount, - "" - ); - } - } - function redeemPositions(IERC20 collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint[] calldata indexSets) external { uint den = _payoutDenominator[conditionId]; - require(den > 0, "payout denominator for condition not set yet"); uint outcomeSlotCount = payoutNumerators[conditionId].length; require(outcomeSlotCount > 0 && den > 0, "condition not prepared yet"); + bool isCompletelyResolved; + { + uint denSoFar; + for (uint j = 0; j < outcomeSlotCount; j++) { + denSoFar = denSoFar.add(payoutNumerators[conditionId][j]); + } + isCompletelyResolved = (den == denSoFar); + } + uint totalPayout = 0; uint fullIndexSet = (1 << outcomeSlotCount) - 1; @@ -350,6 +377,8 @@ contract ConditionalTokens is ERC1155, ERC1155TokenReceiver { uint payoutNumerator = 0; for (uint j = 0; j < outcomeSlotCount; j++) { if (indexSet & (1 << j) != 0) { + if(!isCompletelyResolved) + require(payoutNumerators[conditionId][j] > 0, "can't redeem zero slots yet"); payoutNumerator = payoutNumerator.add(payoutNumerators[conditionId][j]); } } @@ -379,10 +408,18 @@ contract ConditionalTokens is ERC1155, ERC1155TokenReceiver { uint[] calldata indexSets ) external { uint den = _payoutDenominator[conditionId]; - require(den > 0, "payout denominator for condition not set yet"); uint outcomeSlotCount = payoutNumerators[conditionId].length; require(outcomeSlotCount > 0 && den > 0, "condition not prepared yet"); + bool isCompletelyResolved; + { + uint denSoFar; + for (uint j = 0; j < outcomeSlotCount; j++) { + denSoFar = denSoFar.add(payoutNumerators[conditionId][j]); + } + isCompletelyResolved = (den == denSoFar); + } + uint totalPayout = 0; uint fullIndexSet = (1 << outcomeSlotCount) - 1; @@ -395,6 +432,8 @@ contract ConditionalTokens is ERC1155, ERC1155TokenReceiver { uint payoutNumerator = 0; for (uint j = 0; j < outcomeSlotCount; j++) { if (indexSet & (1 << j) != 0) { + if(!isCompletelyResolved) + require(payoutNumerators[conditionId][j] > 0, "can't redeem zero slots yet"); payoutNumerator = payoutNumerator.add(payoutNumerators[conditionId][j]); } } @@ -428,8 +467,33 @@ contract ConditionalTokens is ERC1155, ERC1155TokenReceiver { { if(operator != address(this)) { (bytes32 conditionId, uint[] memory partition) = abi.decode(data, (bytes32, uint[])); - (uint fullIndexSet, uint freeIndexSet) = mintSet(operator, CollateralTypes.ERC1155, msg.sender, id, bytes32(0), conditionId, partition, value); + + require(partition.length > 1, "got empty or singleton partition"); + uint fullIndexSet; + uint freeIndexSet; + { + uint outcomeSlotCount = payoutNumerators[conditionId].length; + require(outcomeSlotCount > 0, "condition not prepared yet"); + fullIndexSet = (1 << outcomeSlotCount) - 1; + freeIndexSet = fullIndexSet; + } + + for (uint i = 0; i < partition.length; i++) { + uint indexSet = partition[i]; + require(indexSet > 0 && indexSet < fullIndexSet, "got invalid index set"); + require((indexSet & freeIndexSet) == indexSet, "partition not disjoint"); + freeIndexSet ^= indexSet; + _mint( + operator, + getPositionId(IERC1155(msg.sender), id, getCollectionId(bytes32(0), conditionId, indexSet)), + value, + "" + ); + } + require(freeIndexSet == 0, "must partition entire outcome slot set"); + + emit PositionSplit(operator, IERC1155(msg.sender), id, bytes32(0), conditionId, partition, value); } return this.onERC1155Received.selector; @@ -445,7 +509,52 @@ contract ConditionalTokens is ERC1155, ERC1155TokenReceiver { external returns (bytes4) { - revert("operation not supported"); + if(operator != address(this)) { + require(ids.length == values.length, "received values mismatch"); + bytes32[] memory collectionIds; + { + (bytes32 conditionId, uint[] memory partition) = abi.decode(data, (bytes32, uint[])); + require(partition.length > 1, "got empty or singleton partition"); + + collectionIds = new bytes32[](partition.length); + { + uint fullIndexSet; + uint freeIndexSet; + { + uint outcomeSlotCount = payoutNumerators[conditionId].length; + require(outcomeSlotCount > 0, "condition not prepared yet"); + fullIndexSet = (1 << outcomeSlotCount) - 1; + freeIndexSet = fullIndexSet; + } + + for (uint j = 0; j < partition.length; j++) { + require(partition[j] > 0 && partition[j] < fullIndexSet, "got invalid index set"); + require((partition[j] & freeIndexSet) == partition[j], "partition not disjoint"); + freeIndexSet ^= partition[j]; + collectionIds[j] = getCollectionId(bytes32(0), conditionId, partition[j]); + } + + require(freeIndexSet == 0, "must partition entire outcome slot set"); + } + + for(uint i = 0; i < ids.length; i++) { + emit PositionSplit(operator, IERC1155(msg.sender), ids[i], bytes32(0), conditionId, partition, values[i]); + } + } + + for(uint i = 0; i < ids.length; i++) { + for (uint j = 0; j < collectionIds.length; j++) { + _mint( + operator, + getPositionId(IERC1155(msg.sender), ids[i], collectionIds[j]), + values[i], + "" + ); + } + } + } + + return this.onERC1155BatchReceived.selector; } /// @dev Gets the outcome slot count of a condition. diff --git a/test/ERC1155.behavior.js b/test/ERC1155.behavior.js deleted file mode 100644 index 27dbb2ac1..000000000 --- a/test/ERC1155.behavior.js +++ /dev/null @@ -1,145 +0,0 @@ -const { BN, constants, expectRevert } = require("openzeppelin-test-helpers"); -const { ZERO_ADDRESS } = constants; -const { shouldSupportInterfaces } = require("./SupportsInterface.behavior"); - -function shouldBehaveLikeERC1155([minter, firstOwner, secondOwner]) { - const firstTokenId = new BN(1); - const secondTokenId = new BN(2); - const unknownTokenId = new BN(3); - - const firstAmount = new BN(1000); - const secondAmount = new BN(2000); - - describe("like an ERC1155", function() { - describe("balanceOf", function() { - it("reverts when queried about the zero address", async function() { - await expectRevert( - this.token.balanceOf(ZERO_ADDRESS, firstTokenId), - "ERC1155: balance query for the zero address" - ); - }); - - context("when accounts don't own tokens", function() { - it("returns zero for given addresses", async function() { - (await this.token.balanceOf( - firstOwner, - firstTokenId - )).should.be.bignumber.equal("0"); - - (await this.token.balanceOf( - secondOwner, - secondTokenId - )).should.be.bignumber.equal("0"); - - (await this.token.balanceOf( - firstOwner, - unknownTokenId - )).should.be.bignumber.equal("0"); - }); - }); - - context("when accounts own some tokens", function() { - beforeEach(async function() { - await this.token.mint(firstOwner, firstTokenId, firstAmount, "0x", { - from: minter - }); - await this.token.mint( - secondOwner, - secondTokenId, - secondAmount, - "0x", - { - from: minter - } - ); - }); - - it("returns the amount of tokens owned by the given addresses", async function() { - (await this.token.balanceOf( - firstOwner, - firstTokenId - )).should.be.bignumber.equal(firstAmount); - - (await this.token.balanceOf( - secondOwner, - secondTokenId - )).should.be.bignumber.equal(secondAmount); - - (await this.token.balanceOf( - firstOwner, - unknownTokenId - )).should.be.bignumber.equal("0"); - }); - }); - }); - - describe("balanceOfBatch", function() { - it("reverts when input arrays don't match up", async function() { - await expectRevert( - this.token.balanceOfBatch( - [firstOwner, secondOwner, firstOwner, secondOwner], - [firstTokenId, secondTokenId, unknownTokenId] - ), - "ERC1155: owners and IDs must have same lengths" - ); - }); - - it("reverts when one of the addresses is the zero address", async function() { - await expectRevert( - this.token.balanceOfBatch( - [firstOwner, secondOwner, ZERO_ADDRESS], - [firstTokenId, secondTokenId, unknownTokenId] - ), - "ERC1155: some address in batch balance query is zero" - ); - }); - - context("when accounts don't own tokens", function() { - it("returns zeros for each account", async function() { - const result = await this.token.balanceOfBatch( - [firstOwner, secondOwner, firstOwner], - [firstTokenId, secondTokenId, unknownTokenId] - ); - result.should.be.an("array"); - result[0].should.be.a.bignumber.equal("0"); - result[1].should.be.a.bignumber.equal("0"); - result[2].should.be.a.bignumber.equal("0"); - }); - }); - - context("when accounts own some tokens", function() { - beforeEach(async function() { - await this.token.mint(firstOwner, firstTokenId, firstAmount, "0x", { - from: minter - }); - await this.token.mint( - secondOwner, - secondTokenId, - secondAmount, - "0x", - { - from: minter - } - ); - }); - - it("returns amounts owned by each account in order passed", async function() { - const result = await this.token.balanceOfBatch( - [secondOwner, firstOwner, firstOwner], - [secondTokenId, firstTokenId, unknownTokenId] - ); - result.should.be.an("array"); - result[0].should.be.a.bignumber.equal(secondAmount); - result[1].should.be.a.bignumber.equal(firstAmount); - result[2].should.be.a.bignumber.equal("0"); - }); - }); - }); - - shouldSupportInterfaces(["ERC165", "ERC1155"]); - }); -} - -module.exports = { - shouldBehaveLikeERC1155 -}; diff --git a/test/ERC1155.test.js b/test/ERC1155.test.js deleted file mode 100644 index 527a16cdd..000000000 --- a/test/ERC1155.test.js +++ /dev/null @@ -1,101 +0,0 @@ -const { - BN, - constants, - expectEvent, - expectRevert -} = require("openzeppelin-test-helpers"); -const { ZERO_ADDRESS } = constants; - -const { shouldBehaveLikeERC1155 } = require("./ERC1155.behavior"); -const ERC1155Mock = artifacts.require("ERC1155Mock"); - -contract.skip("ERC1155", function([, creator, tokenOwner, ...accounts]) { - beforeEach(async function() { - this.token = await ERC1155Mock.new({ from: creator }); - }); - - shouldBehaveLikeERC1155(accounts); - - describe("internal functions", function() { - const tokenId = new BN(1990); - const mintAmount = new BN(9001); - const burnAmount = new BN(3000); - const data = "0xcafebabe"; - - describe("_mint(address, uint256, uint256, bytes memory)", function() { - it("reverts with a null destination address", async function() { - await expectRevert( - this.token.mint(ZERO_ADDRESS, tokenId, mintAmount, data), - "ERC1155: mint to the zero address" - ); - }); - - context("with minted tokens", function() { - beforeEach(async function() { - ({ logs: this.logs } = await this.token.mint( - tokenOwner, - tokenId, - mintAmount, - data, - { from: creator } - )); - }); - - it("emits a TransferSingle event", function() { - expectEvent.inLogs(this.logs, "TransferSingle", { - operator: creator, - from: ZERO_ADDRESS, - to: tokenOwner, - id: tokenId, - value: mintAmount - }); - }); - - it("credits the minted amount of tokens", async function() { - (await this.token.balanceOf( - tokenOwner, - tokenId - )).should.be.bignumber.equal(mintAmount); - }); - }); - }); - - describe("_burn(address, uint256, uint256)", function() { - it("reverts when burning a non-existent token id", async function() { - await expectRevert( - this.token.burn(tokenOwner, tokenId, mintAmount), - "SafeMath: subtraction overflow" - ); - }); - - context("with minted-then-burnt tokens", function() { - beforeEach(async function() { - await this.token.mint(tokenOwner, tokenId, mintAmount, data); - ({ logs: this.logs } = await this.token.burn( - tokenOwner, - tokenId, - burnAmount, - { from: creator } - )); - }); - - it("emits a TransferSingle event", function() { - expectEvent.inLogs(this.logs, "TransferSingle", { - operator: creator, - from: tokenOwner, - to: ZERO_ADDRESS, - id: tokenId, - value: burnAmount - }); - }); - - it("accounts for both minting and burning", async function() { - (await this.token.balanceOf( - tokenOwner, - tokenId - )).should.be.bignumber.equal(mintAmount.sub(burnAmount)); - }); - }); - }); - }); -}); diff --git a/test/ERC1155Mock.sol b/test/ERC1155Mock.sol index 382bc4d2b..af00c13c4 100644 --- a/test/ERC1155Mock.sol +++ b/test/ERC1155Mock.sol @@ -1,25 +1,13 @@ pragma solidity ^0.5.0; -import "../contracts/ERC1155/ERC1155.sol"; +import { ERC1155 } from "../contracts/ERC1155/ERC1155.sol"; /** * @title ERC1155Mock - * This mock just publicizes internal functions for testing purposes + * This mock just allows minting for testing purposes */ contract ERC1155Mock is ERC1155 { function mint(address to, uint256 id, uint256 value, bytes memory data) public { _mint(to, id, value, data); } - - function burn(address owner, uint256 id, uint256 value) public { - _burn(owner, id, value); - } - - function doSafeTransferAcceptanceCheck(address operator, address from, address to, uint256 id, uint256 value, bytes memory data) public { - _doSafeTransferAcceptanceCheck(operator, from, to, id, value, data); - } - - function doSafeBatchTransferAcceptanceCheck(address operator, address from, address to, uint256[] memory ids, uint256[] memory values, bytes memory data) public { - _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, values, data); - } } diff --git a/test/ERC1155TokenReceiverMock.sol b/test/ERC1155TokenReceiverMock.sol deleted file mode 100644 index 6503ad88e..000000000 --- a/test/ERC1155TokenReceiverMock.sol +++ /dev/null @@ -1,68 +0,0 @@ -pragma solidity ^0.5.0; - -import "../contracts/ERC1155/IERC1155TokenReceiver.sol"; - -contract ERC1155TokenReceiverMock is IERC1155TokenReceiver { - bytes4 private _recRetval; - bool private _recReverts; - bytes4 private _batRetval; - bool private _batReverts; - bytes4 private _isRetval; - bool private _isReverts; - - event Received(address operator, address from, uint256 id, uint256 value, bytes data, uint256 gas); - event BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data, uint256 gas); - - constructor ( - bytes4 recRetval, - bool recReverts, - bytes4 batRetval, - bool batReverts, - bytes4 isRetval, - bool isReverts - ) - public - { - _recRetval = recRetval; - _recReverts = recReverts; - _batRetval = batRetval; - _batReverts = batReverts; - _isRetval = isRetval; - _isReverts = isReverts; - } - - function onERC1155Received( - address operator, - address from, - uint256 id, - uint256 value, - bytes calldata data - ) - external - returns(bytes4) - { - require(!_recReverts, "ERC1155TokenReceiverMock: reverting on receive"); - emit Received(operator, from, id, value, data, gasleft()); - return _recRetval; - } - - function onERC1155BatchReceived( - address operator, - address from, - uint256[] calldata ids, - uint256[] calldata values, - bytes calldata data - ) - external - returns(bytes4) - { - require(!_batReverts, "ERC1155TokenReceiverMock: reverting on batch receive"); - emit BatchReceived(operator, from, ids, values, data, gasleft()); - return _batRetval; - } - - function isERC1155TokenReceiver() external view returns (bytes4) { - require(!_isReverts, "ERC1155TokenReceiverMock: reverting on isERC1155TokenReceiver check"); - return _isRetval; - } -} diff --git a/test/Forwarder.sol b/test/Forwarder.sol index 641d50c14..4fc57bdcd 100644 --- a/test/Forwarder.sol +++ b/test/Forwarder.sol @@ -1,6 +1,8 @@ pragma solidity ^0.5.0; -contract Forwarder { +import { ERC1155TokenReceiver } from "../contracts/ERC1155/ERC1155TokenReceiver.sol"; + +contract Forwarder is ERC1155TokenReceiver { function call(address to, bytes calldata data) external { (bool success, bytes memory retData) = to.call(data); require(success, string(retData)); diff --git a/test/SupportsInterface.behavior.js b/test/SupportsInterface.behavior.js deleted file mode 100644 index 281fc1b0c..000000000 --- a/test/SupportsInterface.behavior.js +++ /dev/null @@ -1,84 +0,0 @@ -// copied here because I can't get it to work with importing it from node_modules - -const { makeInterfaceId } = require("openzeppelin-test-helpers"); - -const INTERFACES = { - ERC165: ["supportsInterface(bytes4)"], - ERC721: [ - "balanceOf(address)", - "ownerOf(uint256)", - "approve(address,uint256)", - "getApproved(uint256)", - "setApprovalForAll(address,bool)", - "isApprovedForAll(address,address)", - "transferFrom(address,address,uint256)", - "safeTransferFrom(address,address,uint256)", - "safeTransferFrom(address,address,uint256,bytes)" - ], - ERC721Enumerable: [ - "totalSupply()", - "tokenOfOwnerByIndex(address,uint256)", - "tokenByIndex(uint256)" - ], - ERC721Metadata: ["name()", "symbol()", "tokenURI(uint256)"], - ERC1155: [ - "balanceOf(address,uint256)", - "balanceOfBatch(address[],uint256[])", - "setApprovalForAll(address,bool)", - "isApprovedForAll(address,address)", - "safeTransferFrom(address,address,uint256,uint256,bytes)", - "safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)" - ] -}; - -const INTERFACE_IDS = {}; -const FN_SIGNATURES = {}; -for (const k of Object.getOwnPropertyNames(INTERFACES)) { - INTERFACE_IDS[k] = makeInterfaceId.ERC165(INTERFACES[k]); - for (const fnName of INTERFACES[k]) { - // the interface id of a single function is equivalent to its function signature - FN_SIGNATURES[fnName] = makeInterfaceId.ERC165([fnName]); - } -} - -function shouldSupportInterfaces(interfaces = []) { - describe("Contract interface", function() { - beforeEach(function() { - this.contractUnderTest = this.mock || this.token; - }); - - for (const k of interfaces) { - const interfaceId = INTERFACE_IDS[k]; - describe(k, function() { - describe("ERC165's supportsInterface(bytes4)", function() { - it("should use less than 30k gas", async function() { - (await this.contractUnderTest.supportsInterface.estimateGas( - interfaceId - )).should.be.lte(30000); - }); - - it("should claim support", async function() { - (await this.contractUnderTest.supportsInterface( - interfaceId - )).should.equal(true); - }); - }); - - for (const fnName of INTERFACES[k]) { - const fnSig = FN_SIGNATURES[fnName]; - describe(fnName, function() { - it("should be implemented", function() { - this.contractUnderTest.abi - .filter(fn => fn.signature === fnSig) - .length.should.equal(1); - }); - }); - } - }); - } - }); -} - -module.exports = { - shouldSupportInterfaces -}; diff --git a/test/test-conditional-tokens.js b/test/test-conditional-tokens.js index 7d935fbf8..7fdc7e9d6 100644 --- a/test/test-conditional-tokens.js +++ b/test/test-conditional-tokens.js @@ -203,7 +203,8 @@ contract("ConditionalTokens", function(accounts) { doRedeem, collateralBalanceOf, getPositionForCollection, - getExpectedEventCollateralProperties + getExpectedEventCollateralProperties, + deeperTests }) { beforeEach(prepareTokens); @@ -253,7 +254,7 @@ contract("ConditionalTokens", function(accounts) { ).should.be.rejected; }); - it.skip("should not split if given an incomplete singleton partition", async function() { + it("should not split if given an incomplete singleton partition", async function() { await doSplit.call( this, conditionId, @@ -274,7 +275,7 @@ contract("ConditionalTokens", function(accounts) { )); }); - it.skip("should emit a PositionSplit event", async function() { + it("should emit a PositionSplit event", async function() { await expectEvent.inTransaction( this.splitTx, ConditionalTokens, @@ -487,7 +488,7 @@ contract("ConditionalTokens", function(accounts) { [3, 8], { from: oracle } ), - "payouts can't exceed denominator" + "final report must sum up to denominator" ); }); @@ -619,6 +620,359 @@ contract("ConditionalTokens", function(accounts) { }); }); }); + + if (deeperTests) + context("with many conditions prepared", async function() { + const conditions = [ + { + oracle, + questionId: randomHex(32), + payoutDenominator: toBN(1000), + outcomeSlotCount: toBN(4) + } + ]; + + conditions.forEach(condition => { + condition.id = getConditionId( + condition.oracle, + condition.questionId, + condition.payoutDenominator, + condition.outcomeSlotCount + ); + }); + + beforeEach(async function() { + for (const { + oracle, + questionId, + payoutDenominator, + outcomeSlotCount + } of conditions) { + await this.conditionalTokens.prepareCondition( + oracle, + questionId, + payoutDenominator, + outcomeSlotCount + ); + } + }); + + context("when trader has collateralized a condition", function() { + const condition = conditions[0]; + const { + oracle, + questionId, + payoutDenominator, + outcomeSlotCount + } = condition; + const conditionId = condition.id; + const finalReport = [0, 33, 289, 678].map(toBN); + const partition = [0b0111, 0b1000]; + const positionIndexSet = partition[0]; + + beforeEach(async function() { + finalReport + .reduce((a, b) => a.add(b)) + .should.be.bignumber.equal(payoutDenominator); + + await doSplit.call( + this, + conditionId, + partition, + collateralTokenCount + ); + await trader.execCall( + this.conditionalTokens, + "safeTransferFrom", + trader.address, + counterparty, + getPositionForCollection.call( + this, + getCollectionId(conditionId, partition[1]) + ), + collateralTokenCount, + "0x" + ); + }); + + it("should not allow full-slots report with payout sum below denominator", async function() { + const lowReport = finalReport.map(x => x.divn(2)); + await expectRevert( + this.conditionalTokens.reportPayouts( + questionId, + payoutDenominator, + lowReport, + { from: oracle } + ), + "final report must sum up to denominator" + ); + }); + + it("should not allow missing-one report with payout sum below denominator", async function() { + const missingOneReport = finalReport.slice(); + missingOneReport[3] = missingOneReport[3].subn(1); + await expectRevert( + this.conditionalTokens.reportPayouts( + questionId, + payoutDenominator, + missingOneReport, + { from: oracle } + ), + "final report must sum up to denominator" + ); + }); + + context("with valid partial report", function() { + const partialReport = finalReport.map((x, i) => + i === 2 ? x : toBN(0) + ); + + beforeEach(async function() { + ({ + logs: this.reportLogs + } = await this.conditionalTokens.reportPayouts( + questionId, + payoutDenominator, + partialReport, + { from: oracle } + )); + }); + + it("should emit ConditionResolution event", function() { + expectEvent.inLogs(this.reportLogs, "ConditionResolution", { + conditionId, + oracle, + questionId, + outcomeSlotCount + }); + }); + + it("should reflect partial report via payoutNumerators", async function() { + for (let i = 0; i < partialReport.length; i++) { + (await this.conditionalTokens.payoutNumerators( + conditionId, + i + )).should.be.bignumber.equal(partialReport[i]); + } + }); + + it("should not allow update report with set payout missing", async function() { + const badUpdateReport = finalReport.map((x, i) => + i === 1 ? x : toBN(0) + ); + await expectRevert( + this.conditionalTokens.reportPayouts( + questionId, + payoutDenominator, + badUpdateReport, + { from: oracle } + ), + "can't change existing payout" + ); + }); + + it("should not allow update report which changes existing payout", async function() { + const badUpdateReport = partialReport.slice(); + badUpdateReport[1] = finalReport[1]; + badUpdateReport[2] = badUpdateReport[2].addn(42); + await expectRevert( + this.conditionalTokens.reportPayouts( + questionId, + payoutDenominator, + badUpdateReport, + { from: oracle } + ), + "can't change existing payout" + ); + }); + + it("should not allow update report identical to previous report", async function() { + await expectRevert( + this.conditionalTokens.reportPayouts( + questionId, + payoutDenominator, + partialReport, + { from: oracle } + ), + "didn't update anything" + ); + }); + + it("should not allow trader to redeem with slots that are unset", async function() { + await doRedeem.call(this, conditionId, [ + positionIndexSet + ]).should.be.rejected; + }); + + context("with valid update report", async function() { + const updateReport = finalReport.map((x, i) => + i === 1 || i === 2 ? x : toBN(0) + ); + + beforeEach(async function() { + ({ + logs: this.updateReportLogs + } = await this.conditionalTokens.reportPayouts( + questionId, + payoutDenominator, + updateReport, + { from: oracle } + )); + }); + + it("should emit ConditionResolution event", function() { + expectEvent.inLogs( + this.updateReportLogs, + "ConditionResolution", + { + conditionId, + oracle, + questionId, + outcomeSlotCount + } + ); + }); + + it("should reflect update report via payoutNumerators", async function() { + for (let i = 0; i < updateReport.length; i++) { + (await this.conditionalTokens.payoutNumerators( + conditionId, + i + )).should.be.bignumber.equal(updateReport[i]); + } + }); + + it("should not allow trader to redeem with zero slots when payout is not final", async function() { + await doRedeem.call(this, conditionId, [ + positionIndexSet + ]).should.be.rejected; + }); + + context( + "redeeming split targeting non-zero slots", + function() { + const targetedPartition = [0b0110, 0b0001]; + const targetedIndexSet = 0b0110; + const payout = collateralTokenCount + .mul( + updateReport.reduce( + (acc, term, i) => + targetedIndexSet & (1 << i) ? acc.add(term) : acc, + toBN(0) + ) + ) + .div(payoutDenominator); + + beforeEach(async function() { + await doSplit.call( + this, + conditionId, + targetedPartition, + collateralTokenCount + ); + ({ tx: this.targetedRedeemTx } = await doRedeem.call( + this, + conditionId, + [targetedIndexSet] + )); + }); + + it("should emit PayoutRedemption event", async function() { + await expectEvent.inTransaction( + this.targetedRedeemTx, + ConditionalTokens, + "PayoutRedemption", + Object.assign( + { + redeemer: trader.address, + parentCollectionId: NULL_BYTES32, + conditionId, + // indexSets: partition, + payout + }, + getExpectedEventCollateralProperties.call(this) + ) + ); + }); + } + ); + + context("with valid final report", async function() { + beforeEach(async function() { + ({ + logs: this.finalReportLogs + } = await this.conditionalTokens.reportPayouts( + questionId, + payoutDenominator, + finalReport, + { from: oracle } + )); + }); + + it("should emit ConditionResolution event", async function() { + expectEvent.inLogs( + this.finalReportLogs, + "ConditionResolution", + { + conditionId, + oracle, + questionId, + outcomeSlotCount + } + ); + }); + + it("should reflect update report via payoutNumerators", async function() { + for (let i = 0; i < finalReport.length; i++) { + (await this.conditionalTokens.payoutNumerators( + conditionId, + i + )).should.be.bignumber.equal(finalReport[i]); + } + }); + + context("with valid redemption", async function() { + const payout = collateralTokenCount + .mul( + finalReport.reduce( + (acc, term, i) => + positionIndexSet & (1 << i) ? acc.add(term) : acc, + toBN(0) + ) + ) + .div(payoutDenominator); + + beforeEach(async function() { + ({ tx: this.redeemTx } = await doRedeem.call( + this, + conditionId, + [positionIndexSet] + )); + }); + + it("should emit PayoutRedemption event", async function() { + await expectEvent.inTransaction( + this.redeemTx, + ConditionalTokens, + "PayoutRedemption", + Object.assign( + { + redeemer: trader.address, + parentCollectionId: NULL_BYTES32, + conditionId, + // indexSets: partition, + payout + }, + getExpectedEventCollateralProperties.call(this) + ) + ); + }); + }); + }); + }); + }); + }); + }); } context("with an ERC-20 collateral allowance", function() { @@ -677,7 +1031,8 @@ contract("ConditionalTokens", function(accounts) { }, getExpectedEventCollateralProperties() { return { collateralToken: this.collateralToken.address }; - } + }, + deeperTests: true }); }); @@ -756,7 +1111,8 @@ contract("ConditionalTokens", function(accounts) { collateralToken: this.collateralMultiToken.address, collateralTokenID }; - } + }, + deeperTests: true }); }); @@ -831,7 +1187,84 @@ contract("ConditionalTokens", function(accounts) { collateralToken: this.collateralMultiToken.address, collateralTokenID }; - } + }, + deeperTests: false + }); + }); + + context("with direct ERC-1155 batch transfers", function() { + const collateralTokenID = toBN(randomHex(32)); + + shouldWorkWithSplittingAndMerging({ + async prepareTokens() { + this.collateralMultiToken = await ERC1155Mock.new({ + from: minter + }); + await this.collateralMultiToken.mint( + trader.address, + collateralTokenID, + collateralTokenCount, + "0x", + { from: minter } + ); + }, + async doSplit(conditionId, partition, amount) { + return await trader.execCall( + this.collateralMultiToken, + "safeBatchTransferFrom", + trader.address, + this.conditionalTokens.address, + [collateralTokenID], + [amount], + web3.eth.abi.encodeParameters( + ["bytes32", "uint256[]"], + [conditionId, partition] + ) + ); + }, + async doMerge(conditionId, partition, amount) { + return await trader.execCall( + this.conditionalTokens, + "merge1155Positions", + this.collateralMultiToken.address, + collateralTokenID, + NULL_BYTES32, + conditionId, + partition, + amount + ); + }, + async doRedeem(conditionId, indexSets) { + return await trader.execCall( + this.conditionalTokens, + "redeem1155Positions", + this.collateralMultiToken.address, + collateralTokenID, + NULL_BYTES32, + conditionId, + indexSets + ); + }, + async collateralBalanceOf(address) { + return await this.collateralMultiToken.balanceOf( + address, + collateralTokenID + ); + }, + getPositionForCollection(collectionId) { + return getPositionId( + this.collateralMultiToken.address, + collateralTokenID, + collectionId + ); + }, + getExpectedEventCollateralProperties() { + return { + collateralToken: this.collateralMultiToken.address, + collateralTokenID + }; + }, + deeperTests: false }); }); } diff --git a/test/utils.js b/test/utils.js deleted file mode 100644 index 4273c735c..000000000 --- a/test/utils.js +++ /dev/null @@ -1,38 +0,0 @@ -async function assertRejects(q, msg) { - let res, - catchFlag = false; - try { - res = await q; - } catch (e) { - catchFlag = true; - } finally { - if (!catchFlag) assert.fail(res, null, msg); - } -} - -function getParamFromTxEvent( - transaction, - paramName, - contractFactory, - eventName -) { - assert.isObject(transaction); - let logs = transaction.logs; - if (eventName != null) { - logs = logs.filter(l => l.event === eventName); - } - assert.equal(logs.length, 1, `expected one log but got ${logs.length} logs`); - let param = logs[0].args[paramName]; - if (contractFactory != null) { - let contract = contractFactory.at(param); - assert.isObject(contract, `getting ${paramName} failed for ${param}`); - return contract; - } else { - return param; - } -} - -Object.assign(exports, { - assertRejects, - getParamFromTxEvent -});