// Contract: BetHorde
// Target function: function OwnerWithdraw() external
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.7;

/** @title BetHorde: decentralised bets.
  * @author u/bethorde
  * @notice Truly decentralised bets. No trust. No oracles. Provably random.
  *
  *  Anyone can be a player or a house. Randomness through RSA signatures.
  *
  *  Takes:
  *    If player loses, they lose amount that they bet.
  *    If player wins, they receive:
  *      odds * bet price * (1 - (house take + contract take)).
  *    House take: 0% <= house take <= 10% (0.01% increments, set by house).
  *    Contract take (where pot = odds * bet price):
  *      Pot < 0.1 ETH:         1.0% take
  *      0.1 ETH < Pot < 1 ETH: 0.5% take
  *      1 ETH < Pot < 5 ETH:   0.2% take
  *      Pot > 5 ETH:           0.1% take
  *
  *  Play:
  *    Players call 'CreatePlayer' to create an account and play with 'PlaceBet'.
  *      Funds can be added during calls to 'PlaceBet'.
  *      Withdrawals can be made using 'PlayerWithdraw'.
  *      If house takes over 24 hours to decide a bet, 'ForceBet' can be called.
  *    Houses call 'OpenOrAdjustHouse' to set up a house.
  *      Set min_bet and max_loss parameters to limit incoming bets.
  *      Call 'DecideBet' on incoming bets within 24 hours (or a house loss can be forced).
  *      Call 'FundHouse' to add funds to a house,
  *      Call 'TogglePauseHouse' to pause/unpause a house. While a house is
  *      paused, and if all bets have been decided, house can call
  *      'HouseWithdraw' and 'OpenOrAdjustHouse'. No new bets can be placed while a house is paused.
  *    Anyone can call `ForceBet` if a house has not decided a bet within 24 hours.
  *      Results in player winning.
  *      House take goes to house address that called `ForceBet`.
  *      Funds can only be withdrawn if house exists.
  *      House can be set up after funds arrive without losing funds.
  * 
  *  Randomness:
  *    House provides public part of an RSA key in advance.
  *    Player provides randomness when placing a bet.
  *    Randomness is combined with a nonce and hashed.
  *    House signs hash in bet with house private key.
  *    Unless player knows the key, there is no way to cheat.
  *
  *  This scheme works on four facts/assumptions:
  *    1. House uses a secure 2048 bit RSA key.
  *    2. House cannot control random input from player.
  *    3. Player cannot predict how private key will sign random value.
  *    4. An RSA key can only sign a given message in one way.
  *
  *  A few ways to cheat are dealt with:
  *    1. Players can replay winning 'random' values to keep winning.
  *      - Randomness combined with other data, including a nonce, and hashed.
  *    2. Changing house parameters or withdrawing funds to avoid incoming bets.
  *      - Houses must be paused and have no unresolved bets to take these actions.
  *      - Pauses start 250 blocks after being requested.
  *      - 'PlaceBet' function has a bet_placed_timestamp parameter, preventing
  *        bets from being placed after house parameters have changed.
  *    3. House bets against itself to run out of funds for upcoming losing bets.
  *      - Last_low_balance_timestamp marks recent low funds.
  *    4. Private keys could be insecure/leaked.
  *      - House can change keys and is incentivised not to pick insecure keys.
  *      - 2048 bit RSA keys.
  * 
  *  Signing:
  *    (EMSA-PKCS1-v1_5 in RFC 3447)
  *    1. Generate 2048 bit RSA key.
  *      - d: private exponent.
  *      - n: modulus (public, 2048 bits).
  *      - e: public exponent (must be 17).
  *    2. When creating (or adjusting) a house, supply n as bytes32[8].
  *    3. When signing a bet, pad the 32 byte bet.randomness value:
  *      [0x0001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,
  *       0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,
  *       0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,
  *       0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,
  *       0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,
  *       0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,
  *       0x0001fffffffffffffffffff0013031300d060960864801650304020105000420,
  *       <32 byte bet.randomness>]
  *    5. Sign array (treat it as a large hexadecimal integer):
  *      - (padded ^ d) % n.
  *    6. Supply signed value as bytes32[8] to 'DecideBet'.
  */

contract BetHorde {

  /* --- PLAYER ACTIONS --- */
  /** @notice Create a new player for msg.sender (required before placing bets). */
  function CreatePlayer() external {
    require(players[msg.sender].creation_block == 0, "Player exists");
    players[msg.sender].nonce = 1;
    players[msg.sender].creation_block = block.number;
  }

  /** @notice Place bet. Adds msg.value to player account before deducting bet.
    * @param house House to bet against.
    * @param odds Bet odds: 1 / odds chance of winning odds * amount_gwei.
    *  Winnings are subject to house and contract takes. 2 <= odds <= 1 million.
    * @param amount_gwei GWEI to bet.
    * @param randomness Random 32 byte value.
    * @param nonce A value larger than current nonce, but not by more than 10.
    * @param bet_placed_timestamp When player created the bet (protects against
    *  house changing parameters after bet is sent).
    */
  function PlaceBet(address house, uint256 odds, uint256 amount_gwei,
                    bytes32 randomness, uint256 nonce,
                    uint256 bet_placed_timestamp) payable external {
    uint256 amount = amount_gwei * 1 gwei;
    require(state.bet_counter < type(uint32).max);
    require(houses[house].pause_block > block.number,
            "House unavailable");
    require(players[msg.sender].creation_block > 0,
            "Create player");
    require(odds > 1 && odds <= 1e6, "Invalid odds");
    // House updated after bet was placed.
    require(bet_placed_timestamp > houses[house].last_update_timestamp,
            "House updated");
    require(amount <= players[msg.sender].balance + msg.value,
            "Insufficient funds");
    players[msg.sender].balance = players[msg.sender].balance + msg.value - amount;
    require(nonce > players[msg.sender].nonce
            && nonce <= players[msg.sender].nonce + 10,
            "Nonce");
    require(amount >= houses[house].min_bet, "Bet too low");
    require(amount * odds <= houses[house].max_loss, "Exceeds max loss");
    // Subtract 1 from odds (player pays for bet).
    require(amount * (odds - 1) <= houses[house].balance, "Exceeds house balance");
    state.bet_counter++;
    bets[state.bet_counter] = Bet({
      house: house,
      price_gwei: uint56(amount_gwei),
      timestamp: uint32(block.timestamp),
      player: msg.sender,
      previous_house_bet: houses[house].last_bet,
      next_house_bet: 0,
      odds: uint32(odds),
      randomness: keccak256(abi.encode(players[msg.sender].creation_block, nonce,
                            msg.sender, randomness))
    });
    if(houses[house].first_bet == 0) {
      houses[house].first_bet = state.bet_counter;
    } else {
      bets[houses[house].last_bet].next_house_bet = state.bet_counter;
    }
    houses[house].last_bet = state.bet_counter;
    houses[house].bet_balance += amount * odds;
    houses[house].balance -= (amount * odds) - amount;
    houses[house].active_bets++;
    if(houses[house].balance < houses[house].max_loss) {
      houses[house].last_low_balance_timestamp = block.timestamp;
    }
    
    state.reserved_eth += msg.value;
    state.last_bet_time = block.timestamp;
    players[msg.sender].active_bets++;
    players[msg.sender].nonce = nonce;

    emit BetPlaced(house, state.bet_counter);
  }
  
  /** @notice Withdraw funds from player account.
    * @param amount How much to withdraw (WEI)
    */
  function PlayerWithdraw(uint256 amount) external {
    require(players[msg.sender].balance >= amount, "Insufficient funds");
    state.reserved_eth -= amount;
    players[msg.sender].balance -= amount;
    _VerifiedTransfer(msg.sender, amount);
  }

  /** @notice If house has not decided bet within 24 hours, force player win.
    *  Address calling this function receives house take (into house balance).
    */
  function ForceBet(uint256 bet_id) external {
    require(bets[bet_id].timestamp + 1 days < block.timestamp, "< 24h old");
    _PlayerWin(bet_id);
    _DeleteBet(bet_id);
  }
  /* --- END PLAYER ACTIONS --- */

  /* --- HOUSE ACTIONS --- */
  /** @notice Open a new house for players to bet against or adjust house parameters.
    *  Note on adjusting parameters: house must be paused and this will
    *  update last_update_timestamp.
    * @param modulus bytes32[8] representing modulus of a 2048 bit RSA key.
    * @param max_loss Maximum WEI that house will tolerate losing on a single bet.
    *  Applies to pot value, so small bets with high odds or large bets with smaller odds
    *  can both exceed this value.
    * @param min_bet Minimum bet price that house is willing to accept.
    *  Setting this too low can result in gas prices exceeding earnings.
    * @param take House take. Can be from 0 to 1000 and goes up in 0.01% increments.
    *  Applied only for player wins (except if 'ForceBet' is called).
    */
  function OpenOrAdjustHouse(bytes32[8] calldata modulus, uint256 max_loss,
                             uint256 min_bet, uint256 take)
           PausedOnly payable external {
    // Open house
    if(houses[msg.sender].pause_block == 0) {
      require(msg.value > 1e8 gwei, "Insufficient funds");
      require(msg.sender != address(0), "Invalid address");
      houses[msg.sender].pause_block = type(uint256).max;
      houses[msg.sender].house_address_index = uint128(state.num_houses);
      house_addresses.push(msg.sender);
      state.num_houses++;
    }
    houses[msg.sender].balance += msg.value;
    houses[msg.sender].modulus = modulus;
    houses[msg.sender].max_loss = max_loss;
    houses[msg.sender].min_bet = min_bet;
    houses[msg.sender].take = take;
    houses[msg.sender].last_update_timestamp = block.timestamp;
    // Reserve funds owned by house
    state.reserved_eth += msg.value;
    _ValidateHouseParameters(msg.sender);
  }

  /** @notice Decide outcome of a bet by providing a signature for bet randomness.
    * @param bet_id Identifier of bet to be decided.
    * @param signed_randomness RSA signature (see top of file) for bet.randomness.
    */ 
  function DecideBet(uint256 bet_id, bytes32[8] memory signed_randomness) external {
    require(bets[bet_id].house == msg.sender, "Must be called by house");
    require(bets[bet_id].randomness ==
            _ExtractSigned(signed_randomness, houses[bets[bet_id].house].modulus),
            "Signature mismatch");
    uint256 pot = uint256(bets[bet_id].price_gwei) * bets[bet_id].odds * 1 gwei;
    // 1 / odds chance of winning.
    if(uint256(keccak256(abi.encode(signed_randomness))) % bets[bet_id].odds == 0) {
      _PlayerWin(bet_id);
    } else {
      // If player loses, no house or contract takes are applicable.
      houses[msg.sender].bet_balance -= pot;
      houses[msg.sender].balance += pot;
      emit BetResolved(msg.sender, false, uint88(bets[bet_id].odds),
                       bets[bet_id].player, uint96(pot));
    }
    _DeleteBet(bet_id);
    // Completed bets incremented here to only count decided bets.
    houses[msg.sender].completed_bets++;
    // Neverending bubble sort keeps active houses near top of array.
    uint128 house_idx = houses[msg.sender].house_address_index;
    if(house_idx > 0) {
      // Swap addresses.
      house_addresses[house_idx] = house_addresses[house_idx - 1];
      house_addresses[house_idx - 1] = msg.sender;
      // Update indices.
      houses[msg.sender].house_address_index--;
      houses[house_addresses[house_idx]].house_address_index++;
    }
  }

  /** @notice Withdraw funds from house. House must be paused.
    * @param amount Amount to withdraw (WEI).
    **/
  function HouseWithdraw(uint256 amount) external PausedOnly {
    require(amount <= houses[msg.sender].balance, "Insufficient funds");
    houses[msg.sender].balance -= amount;
    state.reserved_eth -= amount;
    _VerifiedTransfer(msg.sender, amount);
  }
  
  /** @notice Add funds to house. */
  function FundHouse() external payable HouseExists {
    houses[msg.sender].balance += msg.value;
    state.reserved_eth += msg.value;
  }
  
  /** @notice Pause/unpause house.
   *   Pause takes 250 blocks to start and cannot be changed before that.
   *   Unpause takes effect immediately.
   */
  function TogglePauseHouse() external HouseExists {
    // House must either be paused or or have no pending pauses.
    require(houses[msg.sender].pause_block <= block.number ||
            houses[msg.sender].pause_block == type(uint256).max,
            "Pause pending");
    // Pause in future to prevent reactions to an incoming losing bet.
    if(houses[msg.sender].pause_block == type(uint256).max) {
      houses[msg.sender].pause_block = block.number + PAUSE_DELAY_BLOCKS;
    // Unpause.
    } else {
      houses[msg.sender].pause_block = type(uint256).max;
    }
  }
  /* --- END HOUSE ACTIONS --- */

  /* --- CONTRACT ACTIONS --- */
   constructor() {
    state.owner = msg.sender;
    state.sale_price = type(uint256).max;
    // Prevent dismantling right after deployment
    state.last_bet_time = block.timestamp;
  }