// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import {ConditionalTokens} from "../src/prediction/ConditionalTokens.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
constructor() ERC20("Mock WETH", "WETH") {}
function mint(address to, uint256 amount) external { _mint(to, amount); }
}
contract ConditionalTokensTest is Test {
ConditionalTokens ct;
MockERC20 weth;
address oracle = address(0xAA);
address alice = address(0xA1);
address bob = address(0xB0);
bytes32 questionId = keccak256("ETH > 3000?");
bytes32 conditionId;
function setUp() public {
ct = new ConditionalTokens();
weth = new MockERC20();
conditionId = ct.getConditionId(oracle, questionId, 2);
weth.mint(alice, 100 ether);
weth.mint(bob, 100 ether);
vm.prank(alice);
weth.approve(address(ct), type(uint256).max);
vm.prank(bob);
weth.approve(address(ct), type(uint256).max);
}
// ─── getConditionId / getPositionId ───
function test_getConditionId_deterministic() public view {
bytes32 expected = keccak256(abi.encodePacked(oracle, questionId, uint256(2)));
assertEq(ct.getConditionId(oracle, questionId, 2), expected);
}
function test_getPositionId_deterministic() public view {
uint256 expected = uint256(keccak256(abi.encodePacked(address(weth), conditionId, uint256(1))));
assertEq(ct.getPositionId(IERC20(address(weth)), conditionId, 1), expected);
}
// ─── prepareCondition ───
function test_prepareCondition_success() public {
ct.prepareCondition(oracle, questionId, 2);
assertEq(ct.getOutcomeSlotCount(conditionId), 2);
}
function test_prepareCondition_revert_lessThan2() public {
vm.expectRevert("need >=2 outcomes");
ct.prepareCondition(oracle, questionId, 1);
}
function test_prepareCondition_revert_duplicate() public {
ct.prepareCondition(oracle, questionId, 2);
vm.expectRevert("condition already prepared");
ct.prepareCondition(oracle, questionId, 2);
}
// ─── splitPosition ───
function test_splitPosition_binary() public {
ct.prepareCondition(oracle, questionId, 2);
uint256[] memory partition = new uint256[](2);
partition[0] = 1; // YES
partition[1] = 2; // NO
vm.prank(alice);
ct.splitPosition(IERC20(address(weth)), conditionId, partition, 10 ether);
uint256 yesId = ct.getPositionId(IERC20(address(weth)), conditionId, 1);
uint256 noId = ct.getPositionId(IERC20(address(weth)), conditionId, 2);
assertEq(ct.balanceOf(alice, yesId), 10 ether);
assertEq(ct.balanceOf(alice, noId), 10 ether);
assertEq(weth.balanceOf(address(ct)), 10 ether);
}
function test_splitPosition_revert_notPrepared() public {
uint256[] memory partition = new uint256[](2);
partition[0] = 1;
partition[1] = 2;
vm.prank(alice);
vm.expectRevert("condition not prepared");
ct.splitPosition(IERC20(address(weth)), conditionId, partition, 10 ether);
}
function test_splitPosition_revert_partialPartition() public {
ct.prepareCondition(oracle, questionId, 2);
uint256[] memory partition = new uint256[](1);
partition[0] = 1;
vm.prank(alice);
vm.expectRevert("singleton partition");
ct.splitPosition(IERC20(address(weth)), conditionId, partition, 10 ether);
}
function test_splitPosition_revert_notFullPartition() public {
ct.prepareCondition(oracle, questionId, 3);
bytes32 cid3 = ct.getConditionId(oracle, questionId, 3);
uint256[] memory partition = new uint256[](2);
partition[0] = 1; // outcome 0
partition[1] = 2; // outcome 1 (missing outcome 2 = 4)
vm.prank(alice);
vm.expectRevert("partition must be full");
ct.splitPosition(IERC20(address(weth)), cid3, partition, 1 ether);
}
// ─── mergePositions ───
function test_mergePositions_fullCycle() public {
ct.prepareCondition(oracle, questionId, 2);
uint256[] memory partition = new uint256[](2);
partition[0] = 1;
partition[1] = 2;
vm.startPrank(alice);
ct.splitPosition(IERC20(address(weth)), conditionId, partition, 10 ether);
ct.mergePositions(IERC20(address(weth)), conditionId, partition, 10 ether);
vm.stopPrank();
uint256 yesId = ct.getPositionId(IERC20(address(weth)), conditionId, 1);
assertEq(ct.balanceOf(alice, yesId), 0);
assertEq(weth.balanceOf(alice), 100 ether);
}
function test_mergePositions_revert_insufficientBalance() public {
ct.prepareCondition(oracle, questionId, 2);
uint256[] memory partition = new uint256[](2);
partition[0] = 1;
partition[1] = 2;
vm.startPrank(alice);
ct.splitPosition(IERC20(address(weth)), conditionId, partition, 5 ether);
vm.expectRevert(); // ERC1155 insufficient balance
ct.mergePositions(IERC20(address(weth)), conditionId, partition, 10 ether);
vm.stopPrank();
}
// ─── reportPayouts ───
function test_reportPayouts_success() public {
ct.prepareCondition(oracle, questionId, 2);
uint256[] memory payouts = new uint256[](2);
payouts[0] = 1;
payouts[1] = 0;
vm.prank(oracle);
ct.reportPayouts(questionId, payouts);
assertEq(ct.payoutDenominator(conditionId), 1);
}
function test_reportPayouts_revert_notOracle() public {
ct.prepareCondition(oracle, questionId, 2);
// conditionId includes msg.sender as oracle, so alice's conditionId won't match
uint256[] memory payouts = new uint256[](2);
payouts[0] = 1;
payouts[1] = 0;
vm.prank(alice);
vm.expectRevert("condition not prepared");
ct.reportPayouts(questionId, payouts);
}
function test_reportPayouts_revert_alreadyResolved() public {
ct.prepareCondition(oracle, questionId, 2);
uint256[] memory payouts = new uint256[](2);
payouts[0] = 1;
payouts[1] = 0;
vm.prank(oracle);
ct.reportPayouts(questionId, payouts);
vm.prank(oracle);
vm.expectRevert("already resolved");
ct.reportPayouts(questionId, payouts);
}
function test_reportPayouts_revert_allZeros() public {
ct.prepareCondition(oracle, questionId, 2);
uint256[] memory payouts = new uint256[](2);
vm.prank(oracle);
vm.expectRevert("payout all zeros");
ct.reportPayouts(questionId, payouts);
}
// ─── redeemPositions ───
function test_redeemPositions_winnerTakesAll() public {
ct.prepareCondition(oracle, questionId, 2);
uint256[] memory partition = new uint256[](2);
partition[0] = 1;
partition[1] = 2;
vm.prank(alice);
ct.splitPosition(IERC20(address(weth)), conditionId, partition, 10 ether);
// Resolve: YES wins
uint256[] memory payouts = new uint256[](2);
payouts[0] = 1;
payouts[1] = 0;
vm.prank(oracle);
ct.reportPayouts(questionId, payouts);
// Redeem YES tokens
uint256[] memory indexSets = new uint256[](1);
indexSets[0] = 1;
vm.prank(alice);
ct.redeemPositions(IERC20(address(weth)), conditionId, indexSets);
assertEq(weth.balanceOf(alice), 100 ether); // got 10 ether back
}
function test_redeemPositions_proportional() public {
ct.prepareCondition(oracle, questionId, 2);
uint256[] memory partition = new uint256[](2);
partition[0] = 1;
partition[1] = 2;
vm.prank(alice);
ct.splitPosition(IERC20(address(weth)), conditionId, partition, 10 ether);
// Resolve: 70/30 split
uint256[] memory payouts = new uint256[](2);
payouts[0] = 7;
payouts[1] = 3;
vm.prank(oracle);
ct.reportPayouts(questionId, payouts);
// Redeem both
uint256[] memory indexSets = new uint256[](2);
indexSets[0] = 1;
indexSets[1] = 2;
vm.prank(alice);
ct.redeemPositions(IERC20(address(weth)), conditionId, indexSets);
assertEq(weth.balanceOf(alice), 100 ether);
}
function test_redeemPositions_revert_notResolved() public {
ct.prepareCondition(oracle, questionId, 2);
uint256[] memory indexSets = new uint256[](1);
indexSets[0] = 1;
vm.prank(alice);
vm.expectRevert("not resolved");
ct.redeemPositions(IERC20(address(weth)), conditionId, indexSets);
}
function test_redeemPositions_loserGetsNothing() public {
ct.prepareCondition(oracle, questionId, 2);
uint256[] memory partition = new uint256[](2);
partition[0] = 1;
partition[1] = 2;
vm.prank(alice);
ct.splitPosition(IERC20(address(weth)), conditionId, partition, 10 ether);
// Transfer YES tokens to bob, alice keeps NO
uint256 yesId = ct.getPositionId(IERC20(address(weth)), conditionId, 1);
vm.prank(alice);
ct.safeTransferFrom(alice, bob, yesId, 10 ether, "");
// YES wins
uint256[] memory payouts = new uint256[](2);
payouts[0] = 1;
payouts[1] = 0;
vm.prank(oracle);
ct.reportPayouts(questionId, payouts);
// Alice redeems NO → gets nothing
uint256[] memory indexSets = new uint256[](1);
indexSets[0] = 2;
vm.prank(alice);
ct.redeemPositions(IERC20(address(weth)), conditionId, indexSets);
assertEq(weth.balanceOf(alice), 90 ether); // no change
// Bob redeems YES → gets 10 ether
indexSets[0] = 1;
vm.prank(bob);
ct.redeemPositions(IERC20(address(weth)), conditionId, indexSets);
assertEq(weth.balanceOf(bob), 110 ether);
}
// ─── 3-outcome market ───
function test_threeOutcome_splitAndRedeem() public {
bytes32 qid3 = keccak256("multi");
ct.prepareCondition(oracle, qid3, 3);
bytes32 cid3 = ct.getConditionId(oracle, qid3, 3);
uint256[] memory partition = new uint256[](3);
partition[0] = 1; // outcome 0
partition[1] = 2; // outcome 1
partition[2] = 4; // outcome 2
vm.prank(alice);
ct.splitPosition(IERC20(address(weth)), cid3, partition, 9 ether);
// outcome 1 wins
uint256[] memory payouts = new uint256[](3);
payouts[0] = 0;
payouts[1] = 1;
payouts[2] = 0;
vm.prank(oracle);
ct.reportPayouts(qid3, payouts);
uint256[] memory indexSets = new uint256[](1);
indexSets[0] = 2; // outcome 1
vm.prank(alice);
ct.redeemPositions(IERC20(address(weth)), cid3, indexSets);
assertEq(weth.balanceOf(alice), 100 ether);
}
}