// File: @openzeppelin/contracts/security/ReentrancyGuard.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be _NOT_ENTERED
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == _ENTERED;
    }
}

// File: @openzeppelin/contracts/token/ERC20/IERC20.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

// File: contracts/FriendBetsVersion.sol



// SPDX-License-Identifier: MIT

/*
🎲 Bet on the key prices of your favorite twitter users

Twitter: https://twitter.com/friendbet_tech
Telegram: https://t.me/FriendBetPortal
Website: https://friendbet.tech/
Docs: https://docs.friendbet.tech/
*/
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {Ownable} from "solady/src/auth/Ownable.sol";

contract FriendTechBetting is ReentrancyGuard, Ownable {
    enum States {
        Open,
        Closed,
        Resolved
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*               STORAGE                                      */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
    IERC20 public xbetsToken;
    address public botAddress;
    uint256 public totalEarned;
    uint256 public totalVolume;
    uint256 public totalBetsPlayed;

    struct BetInfo {
        uint8 prediction;
        uint256 amount;
    }

    struct ReturnIdData {
        uint256 totalBet;
        BetInfo[] bets;
        RoundInfo round;
    }

    struct RoundInfo {
        address targetAddress;
        address otherAddress;
        States state;
        uint256 totalOther;
        uint256 totalTarget;
        uint256 winningOutcome;
        uint256 totalUsersOther;
        uint256 totalUsersTarget;
    }

    struct ReturnInfo {
        bytes32 id;
        States state;
        uint256 totalBet;
        uint8 prediction;
    }

    mapping(bytes32 => bool) public isOpen;
    mapping(bytes32 => bool) public isClosed;
    mapping(bytes32 => uint256) public latestVersion;
    mapping(uint256 => ReturnInfo[]) public openBets;
    mapping(uint256 => mapping(bytes32 => uint256)) public total;
    mapping(uint256 => mapping(bytes32 => RoundInfo)) public rounds;
    mapping(uint256 => mapping(bytes32 => address[])) public betUsers;
    mapping(address => mapping(address => bytes32)) public addressToId;
    mapping(uint256 => mapping(bytes32 => address[])) public roundBettors;
    mapping(uint256 => mapping(bytes32 => mapping(address => BetInfo)))
        public bets;

    bytes32[] public allIds;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*               EVENTS                                       */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
    event BetPlaced(
        bytes32 id,
        address indexed user,
        uint256 prediction,
        uint256 amount,
        uint256 version
    );
    event BetResolved(bytes32 id, uint256 winningOutcome, uint256 version);
    event Claimed(
        bytes32 id,
        address indexed user,
        uint256 amount,
        uint256 version
    );
    event BettingOpened(bytes32 id, uint256 version);
    event BettingClosed(bytes32 id, uint256 version);

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*               ERRORS                                       */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
    error Bet_Closed();
    error Bet_InvalidId();
    error Bet_NotResolved();
    error Bet_InvalidState();
    error Bet_NotClaimable();
    error Bet_AmountToSmall();
    error Bet_InvalidAddress();
    error Bet_InvalidPrediction();
    error Bet_OnlyOnePrediction();
    error Bet_ContractInsufficientTokens();

    constructor(IERC20 _xbetsToken) {
        _initializeOwner(msg.sender);
        xbetsToken = _xbetsToken;
    }

    receive() external payable {}

    modifier onlyOwnerBot() {
        require(
            (msg.sender == owner()) || (msg.sender == botAddress),
            "Only Owner or Bot"
        );
        _;
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*               PUBLIC NON-PAYABLE FUNCTIONS                 */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /**
     * @notice Function to place a bet on whether the price of a specific address will go up or down
     * @param _id The Id of the betting pool
     * @param _prediction The prediction made by the bettor (1 for Target, 2 for Other)
     * @param _amount The bet amount
     */
    function friendBet(
        bytes32 _id,
        uint8 _prediction,
        uint256 _amount
    ) public nonReentrant {
        if (_id == bytes32(0)) {
            revert Bet_InvalidId();
        }
        if (_prediction != 1 && _prediction != 2) {
            revert Bet_InvalidPrediction();
        }
        if (_amount < 1000 * 1e18) {
            revert Bet_AmountToSmall();
        }

        uint256 version = latestVersion[_id];

        RoundInfo storage _rounds = rounds[version][_id];
        BetInfo storage _bets = bets[version][_id][msg.sender];
        if (_rounds.state != States.Open) {
            revert Bet_Closed();
        }

        uint256 prediction = _bets.prediction;
        if (prediction != 0 && prediction != _prediction) {
            revert Bet_OnlyOnePrediction();
        }

        xbetsToken.transferFrom(msg.sender, address(this), _amount);

        totalBetsPlayed += 1;
        totalVolume += _amount;

        betUsers[version][_id].push(msg.sender);

        roundBettors[version][_id].push(msg.sender);

        _rounds.totalUsersTarget += _prediction == 1 ? 1 : 0;
        _rounds.totalUsersOther += _prediction == 2 ? 1 : 0;
        _rounds.totalTarget += _prediction == 1 ? _amount : 0;
        _rounds.totalOther += _prediction == 2 ? _amount : 0;

        _bets.prediction = _prediction;
        _bets.amount += _amount;

        total[version][_id] += _amount;

        emit BetPlaced(_id, msg.sender, _prediction, _amount, version);
    }

    /**
     * @notice Function to claim winnings for a specific bet on an address
     * @param _id The Id of the betting pool
     * @param _version The version of the betting pool to claim (default 0 to get latest)
     */
    function claimRewards(bytes32 _id, uint256 _version) public nonReentrant {
        if (_id == bytes32(0)) {
            revert Bet_InvalidId();
        }
        uint256 version = _version;
        if (version == 0) {
            version = latestVersion[_id];
        }
        if (rounds[version][_id].state != States.Resolved) {
            revert Bet_NotResolved();
        }

        BetInfo storage _bets = bets[version][_id][msg.sender];
        RoundInfo memory _rounds = rounds[version][_id];
        uint256 currentOutcome = _rounds.winningOutcome;
        uint256 totalCurrent = _rounds.winningOutcome == 1
            ? _rounds.totalTarget
            : _rounds.totalOther;

        if (_bets.prediction != currentOutcome) {
            revert Bet_NotClaimable();
        }
        uint256 amount = (_bets.amount * total[version][_id]) / totalCurrent;

        if (xbetsToken.balanceOf(address(this)) < amount) {
            revert Bet_ContractInsufficientTokens();
        }

        xbetsToken.transfer(msg.sender, amount);
        totalEarned += amount;
        _bets.amount = 0;
        _bets.prediction = 0;

        emit Claimed(_id, msg.sender, amount, version);
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                  PUBLIC VIEW FUNCTIONS                     */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /**
     * @notice Function to get the total bet amount for a particular outcome
     * @param _id The Id of the betting pool
     * @param _version The version to check
     * @param _outcome The outcome to check
     * @return The total bet amount for the outcome
     */
    function getTotalBetsForOutcome(
        bytes32 _id,
        uint256 _version,
        uint256 _outcome
    ) public view returns (uint256) {
        uint256 version = _version;
        if (version == 0) {
            version = latestVersion[_id];
        }
        RoundInfo memory _rounds = rounds[version][_id];
        uint256 totalCurrent = _outcome == 1
            ? _rounds.totalTarget
            : _rounds.totalOther;
        return totalCurrent;
    }

    /**
     * @notice Function to get the total bet amount a user has placed for a particular outcome
     * @param _user The user to check
     * @param _id The Id of the betting pool
     * @param _version The version to check
     * @param _outcome The outcome to check
     * @return The total bet amount the user has placed for the outcome
     */
    function getUserPredictionForOutcome(
        address _user,
        bytes32 _id,
        uint256 _version,
        uint8 _outcome
    ) public view returns (uint256) {
        uint256 version = _version;
        if (version == 0) {
            version = latestVersion[_id];
        }
        BetInfo memory _bets = bets[version][_id][_user];
        uint8 outcome = _bets.prediction;
        uint256 amount = _bets.amount;
        return _outcome == outcome ? amount : 0;
    }

    /**
     * @notice Function to get all outcomes a user has bet on
     * @param _user The address of the user
     * @param _id The Id of the betting pool
     * @param _version The version to check
     * @return An array of outcomes the user has bet on
     */
    function getUserOutcomes(
        address _user,
        bytes32 _id,
        uint256 _version
    ) public view returns (uint8) {
        uint256 version = _version;
        if (version == 0) {
            version = latestVersion[_id];
        }
        BetInfo memory _bets = bets[version][_id][_user];
        return _bets.prediction;
    }

    /**
     * @notice Function to get amount to claim for current version
     * @return amount The amount to claim
     * @param _id The Id of the betting pool
     * @param _version The version to check
     */
    function getAmountToClaim(
        bytes32 _id,
        uint256 _version
    ) public view returns (uint256) {
        uint256 version = _version;
        if (version == 0) {
            version = latestVersion[_id];
        }
        BetInfo memory _bets = bets[version][_id][msg.sender];
        RoundInfo memory _rounds = rounds[version][_id];
        uint256 totalCurrent = _rounds.winningOutcome == 1
            ? _rounds.totalTarget
            : _rounds.totalOther;
        if (_bets.prediction == _rounds.winningOutcome) {
            uint256 amount = (_bets.amount * total[version][_id]) /
                totalCurrent;

            return amount;
        }
        return 0;
    }

    /**
     * @notice Function to get amount to claim for current version
     * @return amount The amount to claim
     * @param _userAddress The user Address to check
     * @param _id The Id of the betting pool
     * @param _version The version to check
     */
    function getAmountToClaimUser(
        address _userAddress,
        bytes32 _id,
        uint256 _version
    ) public view returns (uint256) {
        uint256 version = _version;
        if (version == 0) {
            version = latestVersion[_id];
        }
        BetInfo memory _bets = bets[version][_id][_userAddress];
        RoundInfo memory _rounds = rounds[version][_id];
        uint256 totalCurrent = _rounds.winningOutcome == 1
            ? _rounds.totalTarget
            : _rounds.totalOther;
        if (_bets.prediction == _rounds.winningOutcome) {
            uint256 amount = (_bets.amount * total[version][_id]) /
                totalCurrent;

            return amount;
        }
        return 0;
    }

    /**
     * @notice Function to get a list of users by betting pool
     * @return ReturnInfo[] An array of ReturnInfo Structs with user betting information
     * @param _id The Id of the betting pool
     * @param _version The version to check
     */
    function getUsersBetList(
        bytes32 _id,
        uint256 _version
    ) public view returns (ReturnInfo[] memory) {
        uint256 version = _version;
        if (version == 0) {
            version = latestVersion[_id];
        }
        address[] memory bettors = roundBettors[version][_id];
        ReturnInfo[] memory returnInfoList = new ReturnInfo[](bettors.length);

        for (uint i = 0; i < bettors.length; i++) {
            address userAddress = bettors[i];
            BetInfo memory userBet = bets[version][_id][userAddress];
            RoundInfo memory roundInfo = rounds[version][_id];

            returnInfoList[i] = ReturnInfo({
                id: _id,
                state: roundInfo.state,
                totalBet: userBet.amount,
                prediction: userBet.prediction
            });
        }

        return returnInfoList;
    }

    /**
     * @notice Function to get total users for either outcome
     * @param _id The Id of the betting pool
     * @param _version The version to check
     * @return uint256 Total amount the user has bet on the outcome
     */
    function getTotalUserOutcome(
        bytes32 _id,
        uint256 _version,
        uint8 _outcome
    ) public view returns (uint256) {
        if (_outcome != 1 && _outcome != 2) {
            revert Bet_InvalidPrediction();
        }

        uint256 version = _version;
        if (version == 0) {
            version = latestVersion[_id];
        }

        RoundInfo memory _rounds = rounds[version][_id];
        return
            _outcome == 1 ? _rounds.totalUsersTarget : _rounds.totalUsersOther;
    }

    /**
     * @notice Function to get relevent data by id and version
     * @param _id The Id of the betting pool
     * @param _version The version to check
     * @return ReturnIdData Struct with all the data
     */
    function getBetData(
        bytes32 _id,
        uint256 _version
    ) public view returns (ReturnIdData memory) {
        uint256 version = _version;
        if (version == 0) {
            version = latestVersion[_id];
        }
        RoundInfo memory roundInfo = rounds[version][_id];
        address[] memory users = betUsers[version][_id];
        BetInfo[] memory betInfoList = new BetInfo[](users.length);

        for (uint256 i = 0; i < users.length; i++) {
            betInfoList[i] = bets[version][_id][users[i]];
        }

        uint256 totalBet = total[version][_id];

        return
            ReturnIdData({
                totalBet: totalBet,
                bets: betInfoList,
                round: roundInfo
            });
    }

    /**
     * @notice Function to get all open ids
     * @return bytes32[] Array with all ids
     */
    function getAllBets() public view returns (bytes32[] memory) {
        uint256 openCount = 0;
        for (uint256 i = 0; i < allIds.length; i++) {
            if (isOpen[allIds[i]]) {
                openCount++;
            }
        }

        bytes32[] memory openIds = new bytes32[](openCount);
        uint256 index = 0;
        for (uint256 i = 0; i < allIds.length; i++) {
            if (isOpen[allIds[i]]) {
                openIds[index] = allIds[i];
                index++;
            }
        }
        return openIds;
    }

    /**
     * @notice Function to get all closed ids
     * @return bytes32[] Array with all ids
     */
    function getClosedBets() public view returns (bytes32[] memory) {
        uint256 closedCount = 0;
        for (uint256 i = 0; i < allIds.length; i++) {
            if (isClosed[allIds[i]]) {
                closedCount++;
            }
        }

        bytes32[] memory closedIds = new bytes32[](closedCount);
        uint256 index = 0;
        for (uint256 i = 0; i < allIds.length; i++) {
            if (isClosed[allIds[i]]) {
                closedIds[index] = allIds[i];
                index++;
            }
        }
        return closedIds;
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*               EXTERNAL ONLY-OWNER FUNCTIONS                */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    function setBotAddress(address _botAddress) external onlyOwner {
        botAddress = _botAddress;
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*               EXTERNAL ONLY-OWNERBOT FUNCTIONS             */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
    /**
     * @notice Function to open betting
     * @param _targetAddress The target address to initialize
     * @param _otherAddress The target address to initialize
     */
    function openBetting(
        address _targetAddress,
        address _otherAddress
    ) external onlyOwnerBot {
        if (_targetAddress == address(0)) {
            revert Bet_InvalidAddress();
        }
        if (_otherAddress == address(0)) {
            revert Bet_InvalidAddress();
        }
        bytes32 id = addressToId[_targetAddress][_otherAddress];
        if (id == bytes32(0)) {
            id = keccak256(abi.encodePacked(_targetAddress, _otherAddress));
            allIds.push(id);
            addressToId[_targetAddress][_otherAddress] = id;
        }
        isClosed[id] = false;
        latestVersion[id] += 1;
        uint256 version = latestVersion[id];

        RoundInfo storage _rounds = rounds[version][id];
        _rounds.state = States.Open;
        isOpen[id] = true;
        _rounds.targetAddress = _targetAddress;
        _rounds.otherAddress = _otherAddress;
        emit BettingOpened(id, version);
    }

    /**
     * @notice Function to close betting and resolve
     * @param _id The Id of the betting pool
     */
    function closeBetting(bytes32 _id) external onlyOwnerBot {
        uint256 version = latestVersion[_id];
        RoundInfo storage _rounds = rounds[version][_id];
        if (_rounds.state != States.Open) {
            revert Bet_InvalidState();
        }
        _rounds.state = States.Closed;
        isOpen[_id] = false;
        isClosed[_id] = true;
        emit BettingClosed(_id, version);
    }

    /**
     * @notice Function to resolve a bet
     * @param _winningOutcome The winning outcome
     * @param _id The Id of the betting pool
     */
    function resolveBet(
        uint256 _winningOutcome,
        bytes32 _id
    ) external onlyOwnerBot {
        uint256 version = latestVersion[_id];
        RoundInfo storage _rounds = rounds[version][_id];
        if (_rounds.state != States.Closed) {
            revert Bet_InvalidState();
        }
        _rounds.state = States.Resolved;
        _rounds.winningOutcome = _winningOutcome;

        emit BetResolved(_id, _winningOutcome, version);
    }

    /**
     * @notice Function to withdraw Tokens Stuck in CA.
     * @param _recipient Address to receive the withdrawn Tokens
     * @param _amount Amount to withdraw
     */
    function withdrawStuckTokens(
        address _recipient,
        uint256 _amount
    ) external onlyOwner {
        xbetsToken.transfer(_recipient, _amount);
    }

    /**
     * @notice Function to withdraw contract Eth
     * @param _recipient Address to receive the withdrawn Eth
     */
    function withdraw(address _recipient) external onlyOwner {
        (bool success, ) = address(_recipient).call{
            value: address(this).balance
        }("");
        require(success);
    }
}

// File: solady/src/auth/Ownable.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/// @notice Simple single owner authorization mixin.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/auth/Ownable.sol)
///
/// @dev Note:
/// This implementation does NOT auto-initialize the owner to `msg.sender`.
/// You MUST call the `_initializeOwner` in the constructor / initializer.
///
/// While the ownable portion follows
/// [EIP-173](https://eips.ethereum.org/EIPS/eip-173) for compatibility,
/// the nomenclature for the 2-step ownership handover may be unique to this codebase.
abstract contract Ownable {
    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                       CUSTOM ERRORS                        */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The caller is not authorized to call the function.
    error Unauthorized();

    /// @dev The `newOwner` cannot be the zero address.
    error NewOwnerIsZeroAddress();

    /// @dev The `pendingOwner` does not have a valid handover request.
    error NoHandoverRequest();

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                           EVENTS                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The ownership is transferred from `oldOwner` to `newOwner`.
    /// This event is intentionally kept the same as OpenZeppelin's Ownable to be
    /// compatible with indexers and [EIP-173](https://eips.ethereum.org/EIPS/eip-173),
    /// despite it not being as lightweight as a single argument event.
    event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);

    /// @dev An ownership handover to `pendingOwner` has been requested.
    event OwnershipHandoverRequested(address indexed pendingOwner);

    /// @dev The ownership handover to `pendingOwner` has been canceled.
    event OwnershipHandoverCanceled(address indexed pendingOwner);

    /// @dev `keccak256(bytes("OwnershipTransferred(address,address)"))`.
    uint256 private constant _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE =
        0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0;

    /// @dev `keccak256(bytes("OwnershipHandoverRequested(address)"))`.
    uint256 private constant _OWNERSHIP_HANDOVER_REQUESTED_EVENT_SIGNATURE =
        0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d;

    /// @dev `keccak256(bytes("OwnershipHandoverCanceled(address)"))`.
    uint256 private constant _OWNERSHIP_HANDOVER_CANCELED_EVENT_SIGNATURE =
        0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                          STORAGE                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The owner slot is given by: `not(_OWNER_SLOT_NOT)`.
    /// It is intentionally chosen to be a high value
    /// to avoid collision with lower slots.
    /// The choice of manual storage layout is to enable compatibility
    /// with both regular and upgradeable contracts.
    uint256 private constant _OWNER_SLOT_NOT = 0x8b78c6d8;

    /// The ownership handover slot of `newOwner` is given by:
    /// ```
    ///     mstore(0x00, or(shl(96, user), _HANDOVER_SLOT_SEED))
    ///     let handoverSlot := keccak256(0x00, 0x20)
    /// ```
    /// It stores the expiry timestamp of the two-step ownership handover.
    uint256 private constant _HANDOVER_SLOT_SEED = 0x389a75e1;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                     INTERNAL FUNCTIONS                     */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Initializes the owner directly without authorization guard.
    /// This function must be called upon initialization,
    /// regardless of whether the contract is upgradeable or not.
    /// This is to enable generalization to both regular and upgradeable contracts,
    /// and to save gas in case the initial owner is not the caller.
    /// For performance reasons, this function will not check if there
    /// is an existing owner.
    function _initializeOwner(address newOwner) internal virtual {
        /// @solidity memory-safe-assembly
        assembly {
            // Clean the upper 96 bits.
            newOwner := shr(96, shl(96, newOwner))
            // Store the new value.
            sstore(not(_OWNER_SLOT_NOT), newOwner)
            // Emit the {OwnershipTransferred} event.
            log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, 0, newOwner)
        }
    }

    /// @dev Sets the owner directly without authorization guard.
    function _setOwner(address newOwner) internal virtual {
        /// @solidity memory-safe-assembly
        assembly {
            let ownerSlot := not(_OWNER_SLOT_NOT)
            // Clean the upper 96 bits.
            newOwner := shr(96, shl(96, newOwner))
            // Emit the {OwnershipTransferred} event.
            log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, sload(ownerSlot), newOwner)
            // Store the new value.
            sstore(ownerSlot, newOwner)
        }
    }

    /// @dev Throws if the sender is not the owner.
    function _checkOwner() internal view virtual {
        /// @solidity memory-safe-assembly
        assembly {
            // If the caller is not the stored owner, revert.
            if iszero(eq(caller(), sload(not(_OWNER_SLOT_NOT)))) {
                mstore(0x00, 0x82b42900) // `Unauthorized()`.
                revert(0x1c, 0x04)
            }
        }
    }

    /// @dev Returns how long a two-step ownership handover is valid for in seconds.
    /// Override to return a different value if needed.
    /// Made internal to conserve bytecode. Wrap it in a public function if needed.
    function _ownershipHandoverValidFor() internal view virtual returns (uint64) {
        return 48 * 3600;
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                  PUBLIC UPDATE FUNCTIONS                   */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Allows the owner to transfer the ownership to `newOwner`.
    function transferOwnership(address newOwner) public payable virtual onlyOwner {
        /// @solidity memory-safe-assembly
        assembly {
            if iszero(shl(96, newOwner)) {
                mstore(0x00, 0x7448fbae) // `NewOwnerIsZeroAddress()`.
                revert(0x1c, 0x04)
            }
        }
        _setOwner(newOwner);
    }

    /// @dev Allows the owner to renounce their ownership.
    function renounceOwnership() public payable virtual onlyOwner {
        _setOwner(address(0));
    }

    /// @dev Request a two-step ownership handover to the caller.
    /// The request will automatically expire in 48 hours (172800 seconds) by default.
    function requestOwnershipHandover() public payable virtual {
        unchecked {
            uint256 expires = block.timestamp + _ownershipHandoverValidFor();
            /// @solidity memory-safe-assembly
            assembly {
                // Compute and set the handover slot to `expires`.
                mstore(0x0c, _HANDOVER_SLOT_SEED)
                mstore(0x00, caller())
                sstore(keccak256(0x0c, 0x20), expires)
                // Emit the {OwnershipHandoverRequested} event.
                log2(0, 0, _OWNERSHIP_HANDOVER_REQUESTED_EVENT_SIGNATURE, caller())
            }
        }
    }

    /// @dev Cancels the two-step ownership handover to the caller, if any.
    function cancelOwnershipHandover() public payable virtual {
        /// @solidity memory-safe-assembly
        assembly {
            // Compute and set the handover slot to 0.
            mstore(0x0c, _HANDOVER_SLOT_SEED)
            mstore(0x00, caller())
            sstore(keccak256(0x0c, 0x20), 0)
            // Emit the {OwnershipHandoverCanceled} event.
            log2(0, 0, _OWNERSHIP_HANDOVER_CANCELED_EVENT_SIGNATURE, caller())
        }
    }

    /// @dev Allows the owner to complete the two-step ownership handover to `pendingOwner`.
    /// Reverts if there is no existing ownership handover requested by `pendingOwner`.
    function completeOwnershipHandover(address pendingOwner) public payable virtual onlyOwner {
        /// @solidity memory-safe-assembly
        assembly {
            // Compute and set the handover slot to 0.
            mstore(0x0c, _HANDOVER_SLOT_SEED)
            mstore(0x00, pendingOwner)
            let handoverSlot := keccak256(0x0c, 0x20)
            // If the handover does not exist, or has expired.
            if gt(timestamp(), sload(handoverSlot)) {
                mstore(0x00, 0x6f5e8818) // `NoHandoverRequest()`.
                revert(0x1c, 0x04)
            }
            // Set the handover slot to 0.
            sstore(handoverSlot, 0)
        }
        _setOwner(pendingOwner);
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                   PUBLIC READ FUNCTIONS                    */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Returns the owner of the contract.
    function owner() public view virtual returns (address result) {
        /// @solidity memory-safe-assembly
        assembly {
            result := sload(not(_OWNER_SLOT_NOT))
        }
    }

    /// @dev Returns the expiry timestamp for the two-step ownership handover to `pendingOwner`.
    function ownershipHandoverExpiresAt(address pendingOwner)
        public
        view
        virtual
        returns (uint256 result)
    {
        /// @solidity memory-safe-assembly
        assembly {
            // Compute the handover slot.
            mstore(0x0c, _HANDOVER_SLOT_SEED)
            mstore(0x00, pendingOwner)
            // Load the handover slot.
            result := sload(keccak256(0x0c, 0x20))
        }
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                         MODIFIERS                          */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Marks a function as only callable by the owner.
    modifier onlyOwner() virtual {
        _checkOwner();
        _;
    }
}