Skip to content

Commit

Permalink
some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
hamdiallam committed Oct 31, 2024
1 parent bed82b1 commit 65e2808
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 21 deletions.
43 changes: 28 additions & 15 deletions contracts/src/predictionmarket/PredictionMarket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ struct Market {
uint256 ethBalance;
}

// @notice emitted when a resolver has decided the outcome for a market
error ResolverOutcomeDecided();

// @notice emitted when no value is sent to a payable function
error NoValue();

// @notice emitted when there is insufficient liquidity in the market
error InsufficientLiquidity();

// @notice A very basic implementation of a prediction market.
// 1. We only support markets with a yes or no outcome.
// 2. Once a bet is placed, we do not allow swapping out of a position with the market directly.
Expand Down Expand Up @@ -54,7 +63,7 @@ contract PredictionMarket {
// @notice create and seed a new prediction market with liquidity
// @param _resolver contract identifying the outcome for an open market
function newMarket(IMarketResolver _resolver) public payable {
require(_resolver.outcome() == MarketOutcome.UNDECIDED);
if (_resolver.outcome() != MarketOutcome.UNDECIDED) revert ResolverOutcomeDecided();

Market storage market = markets[_resolver];
market.status = MarketStatus.OPEN;
Expand All @@ -75,7 +84,7 @@ contract PredictionMarket {
// @param _resolver contract identifying the outcome for an open market
function resolveMarket(IMarketResolver _resolver) public {
Market storage market = markets[_resolver];
require(market.status == MarketStatus.OPEN);
if (market.status == MarketStatus.CLOSED) revert ResolverOutcomeDecided();

// Market must be resolvable
MarketOutcome outcome = _resolver.outcome();
Expand All @@ -92,32 +101,30 @@ contract PredictionMarket {
// @notice Entry point for adding liquidity into the specified market
// @param _resolver contract identifying the outcome for an open market
function addLiquidity(IMarketResolver _resolver) public payable {
require(msg.value > 0);
if (msg.value == 0) revert NoValue();
uint256 ethAmount = msg.value;

Market storage market = markets[_resolver];
require(market.status == MarketStatus.OPEN);
if (market.status == MarketStatus.CLOSED) revert ResolverOutcomeDecided();

uint256 lpAmount = ethAmount;

uint256 lpAmount;
uint256 yesAmount;
uint256 noAmount;

if (market.lpToken.totalSupply() == 0) {
// Initial liquidity
lpAmount = ethAmount;
yesAmount = ethAmount;
noAmount = ethAmount;
} else {
// Add liquidity according to the current ratios of the market
uint256 yesBalance = market.yesToken.balanceOf(address(this));
uint256 noBalance = market.noToken.balanceOf(address(this));
uint256 lpSupply = market.lpToken.totalSupply();

// Calculate tokens to mint based on current pool ratios
yesAmount = (ethAmount * yesBalance) / market.ethBalance;
noAmount = (ethAmount * noBalance) / market.ethBalance;

// LP tokens are minted proportionally to ETH contribution
lpAmount = (ethAmount * market.lpToken.totalSupply()) / market.ethBalance;
yesAmount = (ethAmount * yesBalance) / lpSupply;
noAmount = (ethAmount * noBalance) / lpSupply;
}

// Hold pool assets
Expand All @@ -130,6 +137,12 @@ contract PredictionMarket {
emit LiquidityAdded(_resolver, msg.sender, ethAmount);
}

// @notice calculate the amount of outcome tokens to receive for a given amount of ETH
// @param _resolver contract identifying the outcome for an open market
// @param _outcome the outcome to buy
// @param ethAmountIn the amount of ETH to buy
// @dev Held Invariant: `yes_balance * no_balance = K` Where K is the total amount of liquidity. The outcome tokens
// are backed 1:1 with eth which allows eth to always be the input for the swap for either outcome token.
function calcOutcomeOut(IMarketResolver _resolver, MarketOutcome _outcome, uint256 ethAmountIn) public view returns (uint256) {
Market memory market = markets[_resolver];
uint256 yesBalance = market.yesToken.balanceOf(address(this));
Expand All @@ -145,23 +158,23 @@ contract PredictionMarket {
// @param _resolver contract identifying the outcome for an open market
// @param _outcome the outcome to buy
function buyOutcome(IMarketResolver _resolver, MarketOutcome _outcome) public payable {
require(msg.value > 0);
if (msg.value == 0) revert NoValue();
require(_outcome == MarketOutcome.YES || _outcome == MarketOutcome.NO);

uint256 amountIn = msg.value;

// Market must be tradeable & liquid with the amount eth in
Market storage market = markets[_resolver];
require(market.status == MarketStatus.OPEN);
require(market.ethBalance > amountIn);
if (market.status == MarketStatus.CLOSED) revert ResolverOutcomeDecided();

// Compute trade amounts
uint256 amountOut = calcOutcomeOut(_resolver, _outcome, amountIn);
MintableBurnableERC20 outcomeToken = _outcome == MarketOutcome.YES ? market.yesToken : market.noToken;
if (amountOut > outcomeToken.balanceOf(address(this))) revert InsufficientLiquidity();

// Perform swap
market.ethBalance += amountIn;
outcomeToken.transfer(address(this), amountOut);
outcomeToken.transfer(msg.sender, amountOut);
emit BetPlaced(_resolver, msg.sender, _outcome, amountIn, amountOut);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import { TicTacToeGameResolver } from "../resolvers/TicTacToeResolver.sol";
import { TicTacToe } from "../../tictactoe/TicTacToe.sol";

contract TicTacToePredictionMarketFactory {
// @notice TicTacToe contract address
// @notice TicTacToe contract
TicTacToe public game;

// @notice PredictionMarket contract address
// @notice PredictionMarket contract
PredictionMarket public predictionMarket;

// @notice Emitted when a new market for tictactoe is created
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { PredictionMarket } from "../PredictionMarket.sol";
import { TicTacToe } from "../../tictactoe/TicTacToe.sol";

contract TicTacToeGameResolver is IMarketResolver {
// @notice TicTacToe contract address
// @notice TicTacToe contract
TicTacToe public game;

// @notice Current outcome of the game
Expand Down
6 changes: 3 additions & 3 deletions contracts/test/pingpong/CrossChainPingPong.t.sol
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

import {Test} from "forge-std/Test.sol";
import { Test } from "forge-std/Test.sol";

import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";
import { IL2ToL2CrossDomainMessenger } from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
import { Predeploys } from "@contracts-bedrock/libraries/Predeploys.sol";

import {
CrossChainPingPong,
Expand Down
129 changes: 129 additions & 0 deletions contracts/test/predictionmarket/PredictionMarket.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import { Test } from "forge-std/Test.sol";

import { MintableBurnableERC20 } from "../../src/predictionmarket/utils/MintableBurnableERC20.sol";
import { MarketOutcome } from "../../src/predictionmarket/MarketResolver.sol";
import {
Market,
MarketStatus,
NoValue,
ResolverOutcomeDecided,
PredictionMarket
} from "../../src/predictionmarket/PredictionMarket.sol";

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

contract PredictionMarketTest is Test {
PredictionMarket public predictionMarket;

function setUp() public {
predictionMarket = new PredictionMarket();
}

function test_newMarket_succeeds() public {
TestResolver testResolver = new TestResolver();
predictionMarket.newMarket(testResolver);

(MarketStatus status, MarketOutcome outcome, , , , ) = predictionMarket.markets(testResolver);
if (status != MarketStatus.OPEN) fail();
if (outcome != MarketOutcome.UNDECIDED) fail();
}

function test_newMarket_addLiquidityWithValue_succeeds() public {
TestResolver testResolver = new TestResolver();
predictionMarket.newMarket{ value: 1 ether }(testResolver);

(, , MintableBurnableERC20 yesToken, MintableBurnableERC20 noToken, MintableBurnableERC20 lpToken, uint256 liquidity) =
predictionMarket.markets(testResolver);

// eth balance is tracked
assertEq(liquidity, 1 ether);

// lp and pool tokens at fair odds
assertEq(lpToken.balanceOf(address(this)), liquidity);
assertEq(yesToken.balanceOf(address(predictionMarket)), liquidity);
assertEq(noToken.balanceOf(address(predictionMarket)), liquidity);
}

function test_newMarket_decidedOutcome_reverts() public {
TestResolver testResolver = new TestResolver();
testResolver.setOutcome(MarketOutcome.YES);

vm.expectRevert(ResolverOutcomeDecided.selector);
predictionMarket.newMarket(testResolver);
}

function test_buyOutcome_succeeds() public {
// seed some liquidity
TestResolver testResolver = new TestResolver();
predictionMarket.newMarket{ value: 1 ether }(testResolver);

(, , MintableBurnableERC20 yesToken, , ,) = predictionMarket.markets(testResolver);

// buy YES outcome with half of the available liquidity
uint256 expectedAmountOut = predictionMarket.calcOutcomeOut(testResolver, MarketOutcome.YES, 0.5 ether);
predictionMarket.buyOutcome{ value: 0.5 ether }(testResolver, MarketOutcome.YES);

assertEq(yesToken.balanceOf(address(this)), expectedAmountOut);

// Additional ETH is now winnable in this pool
(, , , , , uint256 liquidity) = predictionMarket.markets(testResolver);
assertEq(liquidity, 1.5 ether);
}

function test_addLiquidity_succeeds() public {
TestResolver testResolver = new TestResolver();
predictionMarket.newMarket{ value: 1 ether }(testResolver);

// double the liquidity
predictionMarket.addLiquidity { value: 1 ether }(testResolver);

(, , MintableBurnableERC20 yesToken, MintableBurnableERC20 noToken, MintableBurnableERC20 lpToken, uint256 liquidity) =
predictionMarket.markets(testResolver);

assertEq(liquidity, 2 ether);

// lp and pool tokens at fair odds
assertEq(lpToken.balanceOf(address(this)), liquidity);
assertEq(yesToken.balanceOf(address(predictionMarket)), liquidity);
assertEq(noToken.balanceOf(address(predictionMarket)), liquidity);
}

function test_addLiquidity_noValue_reverts() public {
TestResolver testResolver = new TestResolver();
predictionMarket.newMarket(testResolver);

vm.expectRevert(NoValue.selector);
predictionMarket.addLiquidity(testResolver);
}

function test_addLiquidity_atFairOdds_succeeds() public {
TestResolver testResolver = new TestResolver();
predictionMarket.newMarket{ value: 1 ether }(testResolver);

(, , MintableBurnableERC20 yesToken, MintableBurnableERC20 noToken, MintableBurnableERC20 lpToken, ) =
predictionMarket.markets(testResolver);

// Perform a swap to skew the odds
predictionMarket.buyOutcome{ value: 0.5 ether }(testResolver, MarketOutcome.YES);

uint256 yesBalance = yesToken.balanceOf(address(predictionMarket));
uint256 noBalance = noToken.balanceOf(address(predictionMarket));

// add an additional ETH of liquidity via a new account
vm.prank(address(1)); vm.deal(address(1), 1 ether);
predictionMarket.addLiquidity{ value: 1 ether }(testResolver);

uint256 newYesBalance = yesToken.balanceOf(address(predictionMarket));
uint256 newNoBalance = noToken.balanceOf(address(predictionMarket));

// pool probabilities remain the same relative to lp supply -- added liquidity. (old 1eth, new 2eth)
assertEq(newYesBalance / 2 ether, yesBalance / 1 ether);
assertEq(newNoBalance / 2 ether, noBalance / 1 ether);

// LP Supply should be equal between the two providers
assertEq(lpToken.balanceOf(address(this)), lpToken.balanceOf(address(1)));
}
}
12 changes: 12 additions & 0 deletions contracts/test/predictionmarket/TestResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.25;

import { IMarketResolver, MarketOutcome } from "../../src/predictionmarket/MarketResolver.sol";

contract TestResolver is IMarketResolver {
MarketOutcome public outcome;

function setOutcome(MarketOutcome _outcome) public {
outcome = _outcome;
}
}

0 comments on commit 65e2808

Please sign in to comment.