diff --git a/.travis.yml b/.travis.yml index 4e0996568..a1743b3c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,6 @@ before_install: before_script: greenkeeper-lockfile-update script: - npm run lint -- npm run lint-contracts - npm test - npx codechecks after_script: greenkeeper-lockfile-upload diff --git a/contracts/ConditionalTokens.sol b/contracts/ConditionalTokens.sol index b17b4f7e3..06b98f90a 100644 --- a/contracts/ConditionalTokens.sol +++ b/contracts/ConditionalTokens.sol @@ -1,10 +1,12 @@ pragma solidity ^0.5.1; import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; import { IERC1155TokenReceiver } from "./ERC1155/IERC1155TokenReceiver.sol"; +import { ERC1155TokenReceiver } from "./ERC1155/ERC1155TokenReceiver.sol"; +import { IERC1155 } from "./ERC1155/IERC1155.sol"; import { ERC1155 } from "./ERC1155/ERC1155.sol"; -contract ConditionalTokens is ERC1155 { +contract ConditionalTokens is ERC1155, ERC1155TokenReceiver { /// @dev Emitted upon the successful preparation of a condition. /// @param conditionId The condition's ID. This ID may be derived from the other three parameters via ``keccak256(abi.encodePacked(oracle, questionId, outcomeSlotCount))``. @@ -35,6 +37,15 @@ contract ConditionalTokens is ERC1155 { uint[] partition, uint amount ); + event PositionSplit( + address indexed stakeholder, + IERC1155 collateralToken, + uint collateralTokenID, + bytes32 indexed parentCollectionId, + bytes32 indexed conditionId, + uint[] partition, + uint amount + ); /// @dev Emitted when positions are successfully merged. event PositionsMerge( address indexed stakeholder, @@ -44,6 +55,15 @@ contract ConditionalTokens is ERC1155 { uint[] partition, uint amount ); + event PositionsMerge( + address indexed stakeholder, + IERC1155 collateralToken, + uint collateralTokenID, + bytes32 indexed parentCollectionId, + bytes32 indexed conditionId, + uint[] partition, + uint amount + ); event PayoutRedemption( address indexed redeemer, IERC20 indexed collateralToken, @@ -53,6 +73,8 @@ contract ConditionalTokens is ERC1155 { 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) public payoutDenominator; @@ -104,6 +126,69 @@ contract ConditionalTokens is ERC1155 { bytes32 conditionId, uint[] calldata partition, uint amount + ) external { + (uint fullIndexSet, uint freeIndexSet) = mintSet(msg.sender, CollateralTypes.ERC20, address(collateralToken), 0, parentCollectionId, conditionId, partition, amount); + + if (freeIndexSet == 0) { + if (parentCollectionId == bytes32(0)) { + require(collateralToken.transferFrom(msg.sender, address(this), amount), "could not receive collateral tokens"); + } else { + _burn( + msg.sender, + getPositionId(collateralToken, parentCollectionId), + amount + ); + } + } else { + _burn( + msg.sender, + getPositionId(collateralToken, + getCollectionId(parentCollectionId, conditionId, fullIndexSet ^ freeIndexSet)), + amount + ); + } + + emit PositionSplit(msg.sender, collateralToken, parentCollectionId, conditionId, partition, amount); + } + + function split1155Position( + IERC1155 collateralToken, + uint collateralTokenID, + bytes32 parentCollectionId, + bytes32 conditionId, + uint[] calldata partition, + uint amount + ) external { + (uint fullIndexSet, uint freeIndexSet) = mintSet(msg.sender, CollateralTypes.ERC1155, address(collateralToken), collateralTokenID, parentCollectionId, conditionId, partition, amount); + + if (freeIndexSet == 0) { + if (parentCollectionId == bytes32(0)) { + collateralToken.safeTransferFrom(msg.sender, address(this), collateralTokenID, amount, ""); + } else { + _burn( + msg.sender, + getPositionId(collateralToken, collateralTokenID, parentCollectionId), + amount + ); + } + } else { + _burn( + msg.sender, + getPositionId(collateralToken, collateralTokenID, + getCollectionId(parentCollectionId, conditionId, fullIndexSet ^ freeIndexSet)), + amount + ); + } + + emit PositionSplit(msg.sender, collateralToken, collateralTokenID, parentCollectionId, conditionId, partition, amount); + } + + function mergePositions( + IERC20 collateralToken, + bytes32 parentCollectionId, + bytes32 conditionId, + uint[] calldata partition, + uint amount ) external { uint outcomeSlotCount = payoutNumerators[conditionId].length; require(outcomeSlotCount > 0, "condition not prepared yet"); @@ -115,38 +200,40 @@ contract ConditionalTokens is ERC1155 { require(indexSet > 0 && indexSet < fullIndexSet, "got invalid index set"); require((indexSet & freeIndexSet) == indexSet, "partition not disjoint"); freeIndexSet ^= indexSet; - _mint( + _burn( msg.sender, getPositionId(collateralToken, getCollectionId(parentCollectionId, conditionId, indexSet)), - amount, - "" + amount ); } if (freeIndexSet == 0) { if (parentCollectionId == bytes32(0)) { - require(collateralToken.transferFrom(msg.sender, address(this), amount), "could not receive collateral tokens"); + require(collateralToken.transfer(msg.sender, amount), "could not send collateral tokens"); } else { - _burn( + _mint( msg.sender, getPositionId(collateralToken, parentCollectionId), - amount + amount, + "" ); } } else { - _burn( + _mint( msg.sender, getPositionId(collateralToken, getCollectionId(parentCollectionId, conditionId, fullIndexSet ^ freeIndexSet)), - amount + amount, + "" ); } - emit PositionSplit(msg.sender, collateralToken, parentCollectionId, conditionId, partition, amount); + emit PositionsMerge(msg.sender, collateralToken, parentCollectionId, conditionId, partition, amount); } - function mergePositions( - IERC20 collateralToken, + function merge1155Positions( + IERC1155 collateralToken, + uint collateralTokenID, bytes32 parentCollectionId, bytes32 conditionId, uint[] calldata partition, @@ -164,18 +251,19 @@ contract ConditionalTokens is ERC1155 { freeIndexSet ^= indexSet; _burn( msg.sender, - getPositionId(collateralToken, getCollectionId(parentCollectionId, conditionId, indexSet)), + getPositionId(collateralToken, collateralTokenID, + getCollectionId(parentCollectionId, conditionId, indexSet)), amount ); } if (freeIndexSet == 0) { if (parentCollectionId == bytes32(0)) { - require(collateralToken.transfer(msg.sender, amount), "could not send collateral tokens"); + collateralToken.safeTransferFrom(address(this), msg.sender, collateralTokenID, amount, ""); } else { _mint( msg.sender, - getPositionId(collateralToken, parentCollectionId), + getPositionId(collateralToken, collateralTokenID, parentCollectionId), amount, "" ); @@ -183,14 +271,47 @@ contract ConditionalTokens is ERC1155 { } else { _mint( msg.sender, - getPositionId(collateralToken, + getPositionId(collateralToken, collateralTokenID, getCollectionId(parentCollectionId, conditionId, fullIndexSet ^ freeIndexSet)), amount, "" ); } - emit PositionsMerge(msg.sender, collateralToken, parentCollectionId, conditionId, partition, amount); + 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 { @@ -232,6 +353,37 @@ contract ConditionalTokens is ERC1155 { emit PayoutRedemption(msg.sender, collateralToken, parentCollectionId, conditionId, indexSets, totalPayout); } + function onERC1155Received( + address operator, + address /* from */, + uint256 id, + uint256 value, + bytes calldata data + ) + external + returns (bytes4) + { + if(operator != address(this)) { + (bytes32 conditionId, uint[] memory partition) = abi.decode(data, (bytes32, uint[])); + mintSet(operator, CollateralTypes.ERC1155, msg.sender, id, bytes32(0), conditionId, partition, value); + } + + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address operator, + address /* from */, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) + external + returns (bytes4) + { + revert("operation not supported"); + } + /// @dev Gets the outcome slot count of a condition. /// @param conditionId ID of the condition. /// @return Number of outcome slots associated with a condition, or zero if condition has not been prepared yet. @@ -263,4 +415,9 @@ contract ConditionalTokens is ERC1155 { /// @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(IERC1155 collateralToken, uint collateralTokenID, bytes32 collectionId) public pure returns (uint) { + return uint(keccak256(abi.encodePacked(collateralToken, collateralTokenID, collectionId))); + } +} diff --git a/package.json b/package.json index a4786a79b..3d717cf37 100644 --- a/package.json +++ b/package.json @@ -65,10 +65,6 @@ "*.js": [ "eslint --fix", "git add" - ], - "*.sol": [ - "solium --fix -f", - "git add" ] } } diff --git a/test/test-conditional-tokens.js b/test/test-conditional-tokens.js index e6af96145..cc367f8b5 100644 --- a/test/test-conditional-tokens.js +++ b/test/test-conditional-tokens.js @@ -5,6 +5,7 @@ const { asciiToHex, toBN, fromWei, soliditySha3 } = web3.utils; const ConditionalTokens = artifacts.require("ConditionalTokens"); const ERC20Mintable = artifacts.require("MockCoin"); +const ERC1155Mock = artifacts.require("ERC1155Mock"); const Forwarder = artifacts.require("Forwarder"); const DefaultCallbackHandler = artifacts.require("DefaultCallbackHandler.sol"); const GnosisSafe = artifacts.require("GnosisSafe"); @@ -34,15 +35,21 @@ function combineCollectionIds(collectionIds) { ); } -function getPositionId(collateralToken, collectionId) { +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: "bytes32", v: collectionId } + { t: "uint", v: collateralTokenID }, + { t: "uint", v: collectionId } ); } contract("ConditionalTokens", function(accounts) { - let collateralToken; + let collateralToken, collateralMultiToken; const minter = accounts[0]; let oracle, questionId, outcomeSlotCount, conditionalTokens; let conditionId; @@ -50,6 +57,7 @@ contract("ConditionalTokens", function(accounts) { before(async () => { conditionalTokens = await ConditionalTokens.deployed(); collateralToken = await ERC20Mintable.new({ from: minter }); + collateralMultiToken = await ERC1155Mock.new({ from: minter }); // prepare condition oracle = accounts[1]; @@ -206,6 +214,137 @@ contract("ConditionalTokens", function(accounts) { 0 ); }); + + it("should split and merge positions with ERC-1155 collateral tokens", async () => { + const collateralTokenCount = toBN(1e19); + const collateralTokenID = toBN("432189705"); + await collateralMultiToken.mint( + trader.address, + collateralTokenID, + collateralTokenCount, + "0x", + { + from: minter + } + ); + assert( + collateralTokenCount.eq( + await collateralMultiToken.balanceOf( + trader.address, + collateralTokenID + ) + ) + ); + + for (let i = 0; i < 5; i++) { + await trader.execCall( + collateralMultiToken, + "safeTransferFrom", + trader.address, + conditionalTokens.address, + collateralTokenID, + collateralTokenCount.divn(10), + web3.eth.abi.encodeParameters( + ["bytes32", "uint256[]"], + [conditionId, [0b01, 0b10]] + ) + ); + } + + await trader.execCall( + collateralMultiToken, + "setApprovalForAll", + conditionalTokens.address, + true + ); + + for (let i = 0; i < 5; i++) { + await trader.execCall( + conditionalTokens, + "split1155Position", + collateralMultiToken.address, + collateralTokenID, + asciiToHex(0), + conditionId, + [0b01, 0b10], + collateralTokenCount.divn(10) + ); + } + + assert.equal( + collateralTokenCount.toString(), + (await collateralMultiToken.balanceOf( + conditionalTokens.address, + collateralTokenID + )).toString() + ); + assert.equal( + await collateralMultiToken.balanceOf(trader.address, collateralTokenID), + 0 + ); + + assert( + collateralTokenCount.eq( + await conditionalTokens.balanceOf.call( + trader.address, + getPositionId( + collateralMultiToken.address, + collateralTokenID, + getCollectionId(conditionId, 0b01) + ) + ) + ) + ); + + await trader.execCall( + conditionalTokens, + "merge1155Positions", + collateralMultiToken.address, + collateralTokenID, + asciiToHex(0), + conditionId, + [0b01, 0b10], + collateralTokenCount + ); + assert( + collateralTokenCount.eq( + await collateralMultiToken.balanceOf( + trader.address, + collateralTokenID + ) + ) + ); + assert.equal( + await collateralMultiToken.balanceOf( + conditionalTokens.address, + collateralTokenID + ), + 0 + ); + + assert.equal( + await conditionalTokens.balanceOf.call( + trader.address, + getPositionId( + collateralToken.address, + collateralTokenID, + getCollectionId(conditionId, 0b01) + ) + ), + 0 + ); + assert.equal( + await conditionalTokens.balanceOf.call( + trader.address, + getPositionId( + collateralToken.address, + collateralTokenID, + getCollectionId(conditionId, 0b10) + ) + ), + 0 + ); + }); } context("with EOAs", () => {