// File: @openzeppelin/contracts/access/Ownable.sol



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

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: @openzeppelin/contracts/access/Ownable2Step.sol



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

pragma solidity ^0.8.0;

import "./Ownable.sol";

/**
 * @dev Contract module which provides access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership} and {acceptOwnership}.
 *
 * This module is used through inheritance. It will make available all functions
 * from parent (Ownable).
 */
abstract contract Ownable2Step is Ownable {
    address private _pendingOwner;

    event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Returns the address of the pending owner.
     */
    function pendingOwner() public view virtual returns (address) {
        return _pendingOwner;
    }

    /**
     * @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one.
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual override onlyOwner {
        _pendingOwner = newOwner;
        emit OwnershipTransferStarted(owner(), newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner.
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual override {
        delete _pendingOwner;
        super._transferOwnership(newOwner);
    }

    /**
     * @dev The new owner accepts the ownership transfer.
     */
    function acceptOwnership() public virtual {
        address sender = _msgSender();
        require(pendingOwner() == sender, "Ownable2Step: caller is not the new owner");
        _transferOwnership(sender);
    }
}

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



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

pragma solidity ^0.8.0;

import "./IERC20.sol";
import "./extensions/IERC20Metadata.sol";
import "../../utils/Context.sol";

/**
 * @dev Implementation of the {IERC20} interface.
 *
 * This implementation is agnostic to the way tokens are created. This means
 * that a supply mechanism has to be added in a derived contract using {_mint}.
 * For a generic mechanism see {ERC20PresetMinterPauser}.
 *
 * TIP: For a detailed writeup see our guide
 * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
 * to implement supply mechanisms].
 *
 * The default value of {decimals} is 18. To change this, you should override
 * this function so it returns a different value.
 *
 * We have followed general OpenZeppelin Contracts guidelines: functions revert
 * instead returning `false` on failure. This behavior is nonetheless
 * conventional and does not conflict with the expectations of ERC20
 * applications.
 *
 * Additionally, an {Approval} event is emitted on calls to {transferFrom}.
 * This allows applications to reconstruct the allowance for all accounts just
 * by listening to said events. Other implementations of the EIP may not emit
 * these events, as it isn't required by the specification.
 *
 * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
 * functions have been added to mitigate the well-known issues around setting
 * allowances. See {IERC20-approve}.
 */
contract ERC20 is Context, IERC20, IERC20Metadata {
    mapping(address => uint256) private _balances;

    mapping(address => mapping(address => uint256)) private _allowances;

    uint256 private _totalSupply;

    string private _name;
    string private _symbol;

    /**
     * @dev Sets the values for {name} and {symbol}.
     *
     * All two of these values are immutable: they can only be set once during
     * construction.
     */
    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    /**
     * @dev Returns the name of the token.
     */
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
     * @dev Returns the symbol of the token, usually a shorter version of the
     * name.
     */
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    /**
     * @dev Returns the number of decimals used to get its user representation.
     * For example, if `decimals` equals `2`, a balance of `505` tokens should
     * be displayed to a user as `5.05` (`505 / 10 ** 2`).
     *
     * Tokens usually opt for a value of 18, imitating the relationship between
     * Ether and Wei. This is the default value returned by this function, unless
     * it's overridden.
     *
     * NOTE: This information is only used for _display_ purposes: it in
     * no way affects any of the arithmetic of the contract, including
     * {IERC20-balanceOf} and {IERC20-transfer}.
     */
    function decimals() public view virtual override returns (uint8) {
        return 18;
    }

    /**
     * @dev See {IERC20-totalSupply}.
     */
    function totalSupply() public view virtual override returns (uint256) {
        return _totalSupply;
    }

    /**
     * @dev See {IERC20-balanceOf}.
     */
    function balanceOf(address account) public view virtual override returns (uint256) {
        return _balances[account];
    }

    /**
     * @dev See {IERC20-transfer}.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - the caller must have a balance of at least `amount`.
     */
    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }

    /**
     * @dev See {IERC20-allowance}.
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

    /**
     * @dev See {IERC20-approve}.
     *
     * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
     * `transferFrom`. This is semantically equivalent to an infinite approval.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     */
    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * NOTE: Does not update the allowance if the current allowance
     * is the maximum `uint256`.
     *
     * Requirements:
     *
     * - `from` and `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     * - the caller must have allowance for ``from``'s tokens of at least
     * `amount`.
     */
    function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

    /**
     * @dev Atomically increases the allowance granted to `spender` by the caller.
     *
     * This is an alternative to {approve} that can be used as a mitigation for
     * problems described in {IERC20-approve}.
     *
     * Emits an {Approval} event indicating the updated allowance.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     */
    function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, allowance(owner, spender) + addedValue);
        return true;
    }

    /**
     * @dev Atomically decreases the allowance granted to `spender` by the caller.
     *
     * This is an alternative to {approve} that can be used as a mitigation for
     * problems described in {IERC20-approve}.
     *
     * Emits an {Approval} event indicating the updated allowance.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     * - `spender` must have allowance for the caller of at least
     * `subtractedValue`.
     */
    function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
        address owner = _msgSender();
        uint256 currentAllowance = allowance(owner, spender);
        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
        unchecked {
            _approve(owner, spender, currentAllowance - subtractedValue);
        }

        return true;
    }

    /**
     * @dev Moves `amount` of tokens from `from` to `to`.
     *
     * This internal function is equivalent to {transfer}, and can be used to
     * e.g. implement automatic token fees, slashing mechanisms, etc.
     *
     * Emits a {Transfer} event.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     */
    function _transfer(address from, address to, uint256 amount) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(from, to, amount);

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
            // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
            // decrementing then incrementing.
            _balances[to] += amount;
        }

        emit Transfer(from, to, amount);

        _afterTokenTransfer(from, to, amount);
    }

    /** @dev Creates `amount` tokens and assigns them to `account`, increasing
     * the total supply.
     *
     * Emits a {Transfer} event with `from` set to the zero address.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     */
    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _beforeTokenTransfer(address(0), account, amount);

        _totalSupply += amount;
        unchecked {
            // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
            _balances[account] += amount;
        }
        emit Transfer(address(0), account, amount);

        _afterTokenTransfer(address(0), account, amount);
    }

    /**
     * @dev Destroys `amount` tokens from `account`, reducing the
     * total supply.
     *
     * Emits a {Transfer} event with `to` set to the zero address.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     * - `account` must have at least `amount` tokens.
     */
    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");

        _beforeTokenTransfer(account, address(0), amount);

        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        unchecked {
            _balances[account] = accountBalance - amount;
            // Overflow not possible: amount <= accountBalance <= totalSupply.
            _totalSupply -= amount;
        }

        emit Transfer(account, address(0), amount);

        _afterTokenTransfer(account, address(0), amount);
    }

    /**
     * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
     *
     * This internal function is equivalent to `approve`, and can be used to
     * e.g. set automatic allowances for certain subsystems, etc.
     *
     * Emits an {Approval} event.
     *
     * Requirements:
     *
     * - `owner` cannot be the zero address.
     * - `spender` cannot be the zero address.
     */
    function _approve(address owner, address spender, uint256 amount) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    /**
     * @dev Updates `owner` s allowance for `spender` based on spent `amount`.
     *
     * Does not update the allowance amount in case of infinite allowance.
     * Revert if not enough allowance is available.
     *
     * Might emit an {Approval} event.
     */
    function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            unchecked {
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }

    /**
     * @dev Hook that is called before any transfer of tokens. This includes
     * minting and burning.
     *
     * Calling conditions:
     *
     * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * will be transferred to `to`.
     * - when `from` is zero, `amount` tokens will be minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}

    /**
     * @dev Hook that is called after any transfer of tokens. This includes
     * minting and burning.
     *
     * Calling conditions:
     *
     * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * has been transferred to `to`.
     * - when `from` is zero, `amount` tokens have been minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens have been burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
}

// File: @openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol)

pragma solidity ^0.8.0;

import "../IERC20.sol";

/**
 * @dev Interface for the optional metadata functions from the ERC20 standard.
 *
 * _Available since v4.1._
 */
interface IERC20Metadata is IERC20 {
    /**
     * @dev Returns the name of the token.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the symbol of the token.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the decimals places of the token.
     */
    function decimals() external view returns (uint8);
}

// File: @openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol



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

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in
 * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612].
 *
 * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by
 * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't
 * need to send a transaction, and thus is not required to hold Ether at all.
 */
interface IERC20Permit {
    /**
     * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens,
     * given ``owner``'s signed approval.
     *
     * IMPORTANT: The same issues {IERC20-approve} has related to transaction
     * ordering also apply here.
     *
     * Emits an {Approval} event.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     * - `deadline` must be a timestamp in the future.
     * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`
     * over the EIP712-formatted function arguments.
     * - the signature must use ``owner``'s current nonce (see {nonces}).
     *
     * For more information on the signature format, see the
     * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP
     * section].
     */
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external;

    /**
     * @dev Returns the current nonce for `owner`. This value must be
     * included whenever a signature is generated for {permit}.
     *
     * Every successful call to {permit} increases ``owner``'s nonce by one. This
     * prevents a signature from being used multiple times.
     */
    function nonces(address owner) external view returns (uint256);

    /**
     * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}.
     */
    // solhint-disable-next-line func-name-mixedcase
    function DOMAIN_SEPARATOR() external view returns (bytes32);
}

// 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: @openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.3) (token/ERC20/utils/SafeERC20.sol)

pragma solidity ^0.8.0;

import "../IERC20.sol";
import "../extensions/IERC20Permit.sol";
import "../../../utils/Address.sol";

/**
 * @title SafeERC20
 * @dev Wrappers around ERC20 operations that throw on failure (when the token
 * contract returns false). Tokens that return no value (and instead revert or
 * throw on failure) are also supported, non-reverting calls are assumed to be
 * successful.
 * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
 * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
 */
library SafeERC20 {
    using Address for address;

    /**
     * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     */
    function safeTransfer(IERC20 token, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
    }

    /**
     * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
     * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
     */
    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
    }

    /**
     * @dev Deprecated. This function has issues similar to the ones found in
     * {IERC20-approve}, and its usage is discouraged.
     *
     * Whenever possible, use {safeIncreaseAllowance} and
     * {safeDecreaseAllowance} instead.
     */
    function safeApprove(IERC20 token, address spender, uint256 value) internal {
        // safeApprove should only be called when setting an initial allowance,
        // or when resetting it to zero. To increase and decrease it, use
        // 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
        require(
            (value == 0) || (token.allowance(address(this), spender) == 0),
            "SafeERC20: approve from non-zero to non-zero allowance"
        );
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
    }

    /**
     * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     */
    function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
        uint256 oldAllowance = token.allowance(address(this), spender);
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance + value));
    }

    /**
     * @dev Decrease the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     */
    function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal {
        unchecked {
            uint256 oldAllowance = token.allowance(address(this), spender);
            require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
            _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance - value));
        }
    }

    /**
     * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
     * to be set to zero before setting it to a non-zero value, such as USDT.
     */
    function forceApprove(IERC20 token, address spender, uint256 value) internal {
        bytes memory approvalCall = abi.encodeWithSelector(token.approve.selector, spender, value);

        if (!_callOptionalReturnBool(token, approvalCall)) {
            _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, 0));
            _callOptionalReturn(token, approvalCall);
        }
    }

    /**
     * @dev Use a ERC-2612 signature to set the `owner` approval toward `spender` on `token`.
     * Revert on invalid signature.
     */
    function safePermit(
        IERC20Permit token,
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal {
        uint256 nonceBefore = token.nonces(owner);
        token.permit(owner, spender, value, deadline, v, r, s);
        uint256 nonceAfter = token.nonces(owner);
        require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed");
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     */
    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
        // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
        // the target address contains contract code and also asserts for success in the low-level call.

        bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
        require(returndata.length == 0 || abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     *
     * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead.
     */
    function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
        // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
        // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false
        // and not revert is the subcall reverts.

        (bool success, bytes memory returndata) = address(token).call(data);
        return
            success && (returndata.length == 0 || abi.decode(returndata, (bool))) && Address.isContract(address(token));
    }
}

// File: @openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC721/extensions/IERC721Enumerable.sol)

pragma solidity ^0.8.0;

import "../IERC721.sol";

/**
 * @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
 * @dev See https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721Enumerable is IERC721 {
    /**
     * @dev Returns the total amount of tokens stored by the contract.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns a token ID owned by `owner` at a given `index` of its token list.
     * Use along with {balanceOf} to enumerate all of ``owner``'s tokens.
     */
    function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256);

    /**
     * @dev Returns a token ID at a given `index` of all the tokens stored by the contract.
     * Use along with {totalSupply} to enumerate all tokens.
     */
    function tokenByIndex(uint256 index) external view returns (uint256);
}

// File: @openzeppelin/contracts/token/ERC721/IERC721.sol



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

pragma solidity ^0.8.0;

import "../../utils/introspection/IERC165.sol";

/**
 * @dev Required interface of an ERC721 compliant contract.
 */
interface IERC721 is IERC165 {
    /**
     * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
     */
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
     */
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
     */
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    /**
     * @dev Returns the number of tokens in ``owner``'s account.
     */
    function balanceOf(address owner) external view returns (uint256 balance);

    /**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function ownerOf(uint256 tokenId) external view returns (address owner);

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
     * are aware of the ERC721 protocol to prevent tokens from being forever locked.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId) external;

    /**
     * @dev Transfers `tokenId` token from `from` to `to`.
     *
     * WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
     * or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
     * understand this adds an external call which potentially creates a reentrancy vulnerability.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 tokenId) external;

    /**
     * @dev Gives permission to `to` to transfer `tokenId` token to another account.
     * The approval is cleared when the token is transferred.
     *
     * Only a single account can be approved at a time, so approving the zero address clears previous approvals.
     *
     * Requirements:
     *
     * - The caller must own the token or be an approved operator.
     * - `tokenId` must exist.
     *
     * Emits an {Approval} event.
     */
    function approve(address to, uint256 tokenId) external;

    /**
     * @dev Approve or remove `operator` as an operator for the caller.
     * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
     *
     * Requirements:
     *
     * - The `operator` cannot be the caller.
     *
     * Emits an {ApprovalForAll} event.
     */
    function setApprovalForAll(address operator, bool approved) external;

    /**
     * @dev Returns the account approved for `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function getApproved(uint256 tokenId) external view returns (address operator);

    /**
     * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
     *
     * See {setApprovalForAll}
     */
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

// File: @openzeppelin/contracts/utils/Address.sol



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

pragma solidity ^0.8.1;

/**
 * @dev Collection of functions related to the address type
 */
library Address {
    /**
     * @dev Returns true if `account` is a contract.
     *
     * [IMPORTANT]
     * ====
     * It is unsafe to assume that an address for which this function returns
     * false is an externally-owned account (EOA) and not a contract.
     *
     * Among others, `isContract` will return false for the following
     * types of addresses:
     *
     *  - an externally-owned account
     *  - a contract in construction
     *  - an address where a contract will be created
     *  - an address where a contract lived, but was destroyed
     *
     * Furthermore, `isContract` will also return true if the target contract within
     * the same transaction is already scheduled for destruction by `SELFDESTRUCT`,
     * which only has an effect at the end of a transaction.
     * ====
     *
     * [IMPORTANT]
     * ====
     * You shouldn't rely on `isContract` to protect against flash loan attacks!
     *
     * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
     * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
     * constructor.
     * ====
     */
    function isContract(address account) internal view returns (bool) {
        // This method relies on extcodesize/address.code.length, which returns 0
        // for contracts in construction, since the code is only stored at the end
        // of the constructor execution.

        return account.code.length > 0;
    }

    /**
     * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
     * `recipient`, forwarding all available gas and reverting on errors.
     *
     * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
     * of certain opcodes, possibly making contracts go over the 2300 gas limit
     * imposed by `transfer`, making them unable to receive funds via
     * `transfer`. {sendValue} removes this limitation.
     *
     * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].
     *
     * IMPORTANT: because control is transferred to `recipient`, care must be
     * taken to not create reentrancy vulnerabilities. Consider using
     * {ReentrancyGuard} or the
     * https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
     */
    function sendValue(address payable recipient, uint256 amount) internal {
        require(address(this).balance >= amount, "Address: insufficient balance");

        (bool success, ) = recipient.call{value: amount}("");
        require(success, "Address: unable to send value, recipient may have reverted");
    }

    /**
     * @dev Performs a Solidity function call using a low level `call`. A
     * plain `call` is an unsafe replacement for a function call: use this
     * function instead.
     *
     * If `target` reverts with a revert reason, it is bubbled up by this
     * function (like regular Solidity function calls).
     *
     * Returns the raw returned data. To convert to the expected return value,
     * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
     *
     * Requirements:
     *
     * - `target` must be a contract.
     * - calling `target` with `data` must not revert.
     *
     * _Available since v3.1._
     */
    function functionCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, "Address: low-level call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
     * `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but also transferring `value` wei to `target`.
     *
     * Requirements:
     *
     * - the calling contract must have an ETH balance of at least `value`.
     * - the called Solidity function must be `payable`.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
        return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
    }

    /**
     * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
     * with `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value,
        string memory errorMessage
    ) internal returns (bytes memory) {
        require(address(this).balance >= value, "Address: insufficient balance for call");
        (bool success, bytes memory returndata) = target.call{value: value}(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a static call.
     *
     * _Available since v3.3._
     */
    function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
        return functionStaticCall(target, data, "Address: low-level static call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but performing a static call.
     *
     * _Available since v3.3._
     */
    function functionStaticCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        (bool success, bytes memory returndata) = target.staticcall(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionDelegateCall(target, data, "Address: low-level delegate call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function functionDelegateCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        (bool success, bytes memory returndata) = target.delegatecall(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    /**
     * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling
     * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract.
     *
     * _Available since v4.8._
     */
    function verifyCallResultFromTarget(
        address target,
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        if (success) {
            if (returndata.length == 0) {
                // only check isContract if the call was successful and the return data is empty
                // otherwise we already know that it was a contract
                require(isContract(target), "Address: call to non-contract");
            }
            return returndata;
        } else {
            _revert(returndata, errorMessage);
        }
    }

    /**
     * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the
     * revert reason or using the provided one.
     *
     * _Available since v4.3._
     */
    function verifyCallResult(
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal pure returns (bytes memory) {
        if (success) {
            return returndata;
        } else {
            _revert(returndata, errorMessage);
        }
    }

    function _revert(bytes memory returndata, string memory errorMessage) private pure {
        // Look for revert reason and bubble it up if present
        if (returndata.length > 0) {
            // The easiest way to bubble the revert reason is using memory via assembly
            /// @solidity memory-safe-assembly
            assembly {
                let returndata_size := mload(returndata)
                revert(add(32, returndata), returndata_size)
            }
        } else {
            revert(errorMessage);
        }
    }
}

// File: @openzeppelin/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

// File: @openzeppelin/contracts/utils/introspection/IERC165.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[EIP].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

// File: contracts/AuthorizationManager.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

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

/**
 * @title A module for managing access control using hashed inputs
 * @notice This module is used to hash inputs pertaining to access control around various
 * aspects that a developer may care about. For instance, access on a global scope
 * vs a scope that has a reuired input may have different permission
 */
abstract contract AuthorizationManager is UnderlyingStakeable {
  /**
   * tracks which keys are provided which authorization permissions
   * @dev most of the time the keys will be addresses
   * so you will often have to encode the addresses as byte32
   */
  mapping(bytes32 key => uint256 settings) public authorization;
  /**
   * emitted after settings are updated to allow various
   * addresses and key combinations to act on owners behalf
   * @param key the key, usually an address, that is authorized to perform new actions
   * @param settings the settings number - used as binary
   */
  event UpdateAuthorization(bytes32 indexed key, uint256 settings);
  /**
   * the maximum authorization value that a setting can hold
   * @notice - this is enforced during _setAuthorization only so it
   * could be set elsewhere if the contract decides to
   */
  uint256 public immutable MAX_AUTHORIZATION;
  /**
   * Sets up the contract by accepting a value limit during construction.
   * Usually this is type(uint8).max or other derived value
   * @param maxAuthorization the maximum uint that can be set
   * on the authorization manager as a value.
   */
  constructor(uint256 maxAuthorization) {
    MAX_AUTHORIZATION = maxAuthorization;
  }
  /**
   * set the authorization status of an address
   * @param key the address to set the authorization flag of
   * @param settings allowed to start / end / early end stakes
   */
  function _setAuthorization(bytes32 key, uint256 settings) internal {
    if (settings > MAX_AUTHORIZATION) {
      revert NotAllowed();
    }
    authorization[key] = settings;
    emit UpdateAuthorization({
      key: key,
      settings: settings
    });
  }
  /**
   * sets an authorization level for an address
   * @param account the address to scope an authorization value
   * @param settings the settings configuration in uint256 form
   */
  function _setAddressAuthorization(address account, uint256 settings) internal {
    _setAuthorization({
      key: bytes32(uint256(uint160(account))),
      settings: settings
    });
  }
  /**
   * check if an address is authorized to perform an action
   * this index will be different for each implementation
   * @param account the address to verify is authorized to do an action
   * @param index the index of the bit to check
   * @dev the index is an index of the bits as in binary (1/0)
   * @return whether or not the address authorization value has a 1/0 at the provided index
   */
  function isAddressAuthorized(address account, uint256 index) view external returns(bool) {
    return _isAddressAuthorized({
      account: account,
      index: index
    });
  }
  /**
   * check if the provided address is authorized to perform an action
   * @param account the address to check authorization against
   * @param index the index of the setting boolean to check
   * @return whether or not the address authorization value has a 1/0 at the provided index
   */
  function _isAddressAuthorized(address account, uint256 index) view internal returns(bool) {
    return _isAuthorized({
      key: bytes32(uint256(uint160(account))),
      index: index
    });
  }
  /**
   * check the index of the setting for the provided key
   * return true if flag is true
   * @param key the key to check against the authorization mapping
   * @param index the index of the setting flag to check
   * @return whether or not the authorization value has a 1 or a 0 at the provided index
   */
  function _isAuthorized(bytes32 key, uint256 index) view internal returns(bool) {
    return _isOneAtIndex({
      settings: authorization[key],
      index: index
    });
  }
  /**
   * access settings scoped under an account (address) only
   * @param account the account whose settings you wish to access
   * @return arbitrary authorization value
   */
  function _getAddressSettings(address account) view internal returns(uint256) {
    return authorization[bytes32(uint256(uint160(account)))];
  }
}

// File: contracts/Bank.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { IERC20 } from  "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Utils } from "./Utils.sol";

/**
 * @title A subcontract to track balances of deposited tokens
 */
contract Bank is Utils {
  using Address for address payable;
  using SafeERC20 for IERC20;

  /**
   * notes that a previously unattributed token has been
   * collected and attributed to an address
   * @param token the token that is being collected by the caller
   * @param to the address that the tokens are being attributed to
   * @param amount the number of tokens being collected for the to address
   */
  event CollectUnattributedToken(address indexed token, address indexed to, uint256 amount);
  /**
   * @notice keeps a global mapping of attributed funds that the contract is custodying
   */
  mapping(address token => uint256 balance) public attributed;
  /**
   * @notice keeps a mapping of the withdrawable funds that the contract is custodying
   * the contract may also be custodying tips, but an amount held within
   * a tip is not withdrawable so it cannot be held in this mapping
   */
  mapping(address token => mapping(address account => uint256 balance)) public withdrawableBalanceOf;
  /**
   * gets unattributed tokens floating in the contract
   * @param token the address of the token that you wish to get the unattributed value of
   * @return amount representing the amount of tokens that have been
   * deposited into the contract, which are not attributed to any address
   */
  function _getUnattributed(address token) internal view returns(uint256 amount) {
    return _getBalance({
      token: token,
      owner: address(this)
    }) - attributed[token];
  }
  /**
   * get the balance and ownership of any token
   * @param token the token address that you wish to get the balance of (including native)
   * @param owner the owner address to get the balance of
   * @return amount of a balance custodied by this contract
   */
  function _getBalance(address token, address owner) internal view returns(uint256 amount) {
    return token == address(0) ? owner.balance : IERC20(token).balanceOf(owner);
  }
  /**
   * gets the amount of unattributed tokens
   * @param token the token to get the unattributed balance of
   * @return amount of a token that can be withdrawn
   */
  function getUnattributed(address token) external view returns(uint256 amount) {
    return _getUnattributed({
      token: token
    });
  }
  /**
   * given a provided input amount, clamp the input to a maximum, using maximum if 0 provided
   * @param amount the requested or input amount
   * @param max the maximum amount that the value can be
   * @return clamped the clamped value that is set to the limit if
   * 0 or a number above the limit is passed
   */
  function clamp(uint256 amount, uint256 max) external pure returns(uint256 clamped) {
    return _clamp({
      amount: amount,
      max: max
    });
  }
  /**
   * clamp a given amount to the maximum amount
   * use the maximum amount if no amount is requested
   * @param amount the amount requested by another function
   * @param max the limit that the value can be
   * @return clamped the clamped value that is set to the limit if
   * 0 or a number above the limit is passed
   */
  function _clamp(uint256 amount, uint256 max) internal pure returns(uint256 clamped) {
    unchecked {
      return amount == ZERO || amount > max ? max : amount;
    }
  }
  /**
   * transfer a given number of tokens to the contract to be used by the contract's methods
   * @param amount the number of tokens to transfer to the contract
   * @notice an extra layer of protection is provided by this method
   * and can be refused by calling the dangerous version
   */
  function depositToken(address token, uint256 amount) external payable returns(uint256) {
    return _depositTokenTo({
      token: token,
      to: msg.sender,
      amount: amount
    });
  }
  /**
   * deposit an amount of tokens to the contract and attribute
   * them to the provided address
   * @param to the account to give ownership over tokens
   * @param amount the amount of tokens
   */
  function depositTokenTo(address token, address to, uint256 amount) external payable returns(uint256) {
    return _depositTokenTo({
      token: token,
      to: to,
      amount: amount
    });
  }
  function _depositTokenTo(address token, address to, uint256 amount) internal returns(uint256) {
    amount = _depositTokenFrom({
      token: token,
      depositor: msg.sender,
      amount: amount
    });
    _addToTokenWithdrawable({
      token: token,
      to: to,
      amount: amount
    });
    return amount;
  }
  /**
   * collect unattributed tokens and send to recipient of choice
   * @param transferOut transfers tokens to the provided address
   * @param to the address to receive or have tokens attributed to
   * @param amount the requested amount - clamped to the amount unattributed
   * @notice when 0 is passed, withdraw maximum available
   * or in other words, all unattributed tokens
   */
  function collectUnattributed(
    address token, bool transferOut,
    address payable to,
    uint256 amount
  ) external payable returns(uint256) {
    return _collectUnattributed({
      token: token,
      transferOut: transferOut,
      to: to,
      amount: amount,
      max: _getUnattributed(token)
    });
  }
  function _collectUnattributed(
    address token, bool transferOut, address payable to,
    uint256 amount, uint256 max
  ) internal returns(uint256 withdrawable) {
    withdrawable = _clamp(amount, max);
    if (withdrawable > ZERO) {
      if (transferOut) {
        _withdrawTokenTo({
          token: token,
          to: to,
          amount: withdrawable
        });
      } else {
        _addToTokenWithdrawable({
          token: token,
          to: to,
          amount: withdrawable
        });
      }
      emit CollectUnattributedToken({
        token: token,
        to: to,
        amount: amount
      });
    }
  }
  /**
   * collect a number of unattributed tokens as basis points
   * @param token the token that you wish to collect
   * @param transferOut whether to transfer token out
   * @param recipient the recipient of the tokens
   * @param basisPoints the number of basis points (100% = 10_000)
   * @notice collecting unattributed percentages should
   * be used before a blanket collection
   * in order to reduce rounding errors
   * @dev please be sure to run blanket collect unattributed
   * calls to collect any remaining tokens
   */
  function collectUnattributedPercent(
    address token, bool transferOut, address payable recipient,
    uint256 basisPoints
  ) external returns(uint256 amount) {
    uint256 unattributed = _getUnattributed(token);
    amount = (unattributed * basisPoints) / TEN_K;
    _collectUnattributed(token, transferOut, recipient, amount, unattributed);
  }
  /**
   * transfer an amount of tokens currently attributed to the withdrawable balance of the sender
   * @param token the token to transfer - uses address(0) for native
   * @param to the to of the funds
   * @param amount the amount that should be deducted from the sender's balance
   */
  function withdrawTokenTo(address token, address payable to, uint256 amount) external payable returns(uint256) {
    return _withdrawTokenTo({
      token: token,
      to: to,
      amount: _deductWithdrawable({
        token: token,
        account: msg.sender,
        amount: amount
      })
    });
  }
  function _getTokenBalance(address token) internal view returns(uint256) {
    return token == address(0)
      ? address(this).balance
      : IERC20(token).balanceOf(address(this));
  }

  /**
   * adds a balance to the provided staker of the magnitude given in amount
   * @param token the token being accounted for
   * @param to the account to add a withdrawable balance to
   * @param amount the amount to add to the staker's withdrawable balance as well as the attributed tokens
   */
  function _addToTokenWithdrawable(address token, address to, uint256 amount) internal {
    unchecked {
      withdrawableBalanceOf[token][to] = withdrawableBalanceOf[token][to] + amount;
      attributed[token] = attributed[token] + amount;
    }
  }
  /**
   * deduce an amount from the provided account
   * @param account the account to deduct funds from
   * @param amount the amount of funds to deduct
   * @notice after a deduction, funds could be considered "unattributed"
   * and if they are left in such a state they could be picked up by anyone else
   */
  function _deductWithdrawable(address token, address account, uint256 amount) internal returns(uint256) {
    uint256 withdrawable = withdrawableBalanceOf[token][account];
    amount = _clamp({
      amount: amount,
      max: withdrawable
    });
    unchecked {
      withdrawableBalanceOf[token][account] = withdrawable - amount;
      attributed[token] = attributed[token] - amount;
    }
    return amount;
  }
  /** deposits tokens from a staker and marks them for that staker */
  function _depositTokenFrom(address token, address depositor, uint256 amount) internal returns(uint256 amnt) {
    if (token != address(0)) {
      if (amount > ZERO) {
        amnt = IERC20(token).balanceOf(address(this));
        IERC20(token).safeTransferFrom(depositor, address(this), amount);
        amnt = IERC20(token).balanceOf(address(this)) - amnt;
      }
    } else {
      // transfer in already occurred
      // make sure that multicall is not payable (it isn't)
      amnt = msg.value;
    }
  }
  /**
   * deposit a number of tokens to the contract
   * @param amount the number of tokens to deposit
   */
  function depositTokenUnattributed(address token, uint256 amount) external {
    _depositTokenFrom({
      token: token,
      depositor: msg.sender,
      amount: amount
    });
  }
  /**
   * transfers tokens to a recipient
   * @param to where to send the tokens
   * @param amount the number of tokens to send
   */
  function _withdrawTokenTo(address token, address payable to, uint256 amount) internal returns(uint256) {
    if (token == address(0)) {
      to.sendValue(amount);
    } else {
      IERC20(token).safeTransfer(to, amount);
    }
    return amount;
  }
  function _attributeFunds(uint256 settings, address token, address staker, uint256 amount) internal {
    if (_isOneAtIndex({
      settings: settings,
      index: FOUR
    })) {
      _withdrawTokenTo({
        token: token,
        to: payable(staker),
        amount: amount
      });
    } else {
      _addToTokenWithdrawable({
        token: token,
        to: staker,
        amount: amount
      });
    }
  }
}

// File: contracts/CurrencyList.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { Utils } from "./Utils.sol";

contract CurrencyList is Utils {
  using Address for address;

  event AddCurrency(
    address indexed token,
    uint256 indexed index
  );

  error MustBeHolder();

  address[] public indexToToken;
  mapping(address token => uint256 index) public currencyToIndex;
  /**
   * creates a registry of tokens to map addresses that stakes will tip in
   * to numbers so that they can fit in a single byteword,
   * reducing costs when tips in the same currency occur
   * @param token the token to add to the list of tippable tokens
   */
  function addCurrencyToList(address token) external returns(uint256) {
    if (currencyToIndex[token] > ZERO || token == address(0)) {
      return currencyToIndex[token];
    }
    // token must already exist - helps reduce grief attacks
    if (!token.isContract()) {
      revert NotAllowed();
    }
    // reduces griefing
    if (IERC20(token).balanceOf(msg.sender) == ZERO) {
      revert MustBeHolder();
    }
    // add token to list
    return _addCurrencyToList({
      token: token
    });
  }
  /**
   * adds a hash to a list and mapping to fit them in smaller sload counts
   * @param token the token to add to the internally tracked list and mapping
   */
  function _addCurrencyToList(address token) internal returns(uint256) {
    uint256 index = indexToToken.length;
    currencyToIndex[token] = index;
    indexToToken.push(token);
    emit AddCurrency(token, index);
    return index;
  }
  function currencyListSize() external view returns(uint256) {
    return indexToToken.length;
  }
}

// File: contracts/EarningsOracle.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { IHEX } from './interfaces/IHEX.sol';
import { Utils } from './Utils.sol';

contract EarningsOracle is Utils {
  uint256 public immutable lastZeroDay;
  /**
   * @dev this max constraint is very generous given that
   * the sstore opcode costs ~20k gas at the time of writing
   */
  uint256 public constant MAX_CATCH_UP_DAYS = 1_000;
  uint256 public constant MAX_UINT_128 = type(uint128).max;
  uint256 public constant SHARE_SCALE = 1e5;
  TotalStore[] public totals;
  struct TotalStore {
    uint128 payout;
    uint128 shares;
  }
  struct Total {
    uint256 payout;
    uint256 shares;
  }
  /**
   * deploy contract and start collecting data immediately.
   * pass 0 for untilDay arg to skip collection and start with nothing in payoutTotal array
   * @param _lastZeroDay the final day to allow zero value (used to filter out empty values)
   * @param untilDay the day to end collection
   */
  constructor(uint256 _lastZeroDay, uint256 untilDay) {
    lastZeroDay = _lastZeroDay;
    if (untilDay > ZERO) {
      _storeDays({
        startDay: 0,
        untilDay: untilDay
      });
    }
  }
  /**
   * the size of the payoutTotal array - correlates to days stored
   */
  function totalsCount() external view returns(uint256 count) {
    return totals.length;
  }
  /**
   * the delta between two days. untilDay argument must be greater
   * than startDay argument otherwise call may fail
   * @param startDay the day to start counting from
   * @param untilDay the day to end with (inclusive)
   */
  function payoutDelta(uint256 startDay, uint256 untilDay) external view returns(uint256 payout, uint256 shares) {
    if (startDay >= untilDay) {
      revert NotAllowed();
    }
    TotalStore memory start = totals[startDay];
    TotalStore memory until = totals[untilDay];
    unchecked {
      return (
        until.payout - start.payout,
        until.shares - start.shares
      );
    }
  }
  /**
   * multiply the difference of the payout by a constant and divide that result by the denominator
   * subtract half of the difference between the two days to find the possible lower bound
   * @param lockedDay the day that the stake was locked
   * @param stakedDays the number of days that the stake was locked
   * @param shares a number to multiply by the difference of the payout
   */
  function payoutDeltaTruncated(
    uint256 lockedDay,
    uint256 stakedDays,
    uint256 shares
  ) external view returns(uint256 payout) {
    // we have to have a data point that can be used as our zero point
    uint256 zeroDay = lockedDay - ONE;
    uint256 untilDay = zeroDay + stakedDays;
    return ((
      (totals[untilDay].payout - totals[zeroDay].payout) * shares * (untilDay - zeroDay)
    ) / (
      totals[untilDay].shares - totals[zeroDay].shares
    )) - (
      // for a 1 day span, the amount is actually known
      untilDay - lockedDay
    );
  }
  /**
   * store the payout total for a given day. day must be the next day in the sequence (start with 0)
   * day must have data available to read from the hex contract
   * @param day the day being targeted
   * @param total the total payout logged in the previous day
   * @dev the _total arg must be handled internally - cannot be passed from external
   */
  function _storeDay(uint256 day, Total memory _total) internal returns(Total memory total) {
    (
      uint256 dayPayoutTotal,
      uint256 dayStakeSharesTotal,
      // uint256 dayUnclaimedSatoshisTotal
    ) = IHEX(TARGET).dailyData({
      day: day
    });
    if (day > lastZeroDay) {
      if (dayStakeSharesTotal == ZERO) {
        // stake data is not yet set
        revert NotAllowed();
      }
    }
    (uint256 payout, uint256 shares) = _readTotals(day, _total);
    return _saveDay(dayPayoutTotal + payout, dayStakeSharesTotal + shares);
  }
  function _readTotals(uint256 day, Total memory _total) internal view returns(uint256 payout, uint256 shares) {
    (payout, shares) = (_total.payout, _total.shares);
    if (payout == ZERO) {
      if (shares == ZERO) {
        if (day > ZERO) {
          TotalStore memory prev = totals[day - ONE];
          payout = prev.payout;
          shares = prev.shares;
        }
      }
    }
  }
  function _saveDay(uint256 payout, uint256 shares) internal returns(Total memory total) {
    total.payout = payout;
    total.shares = shares;
    // coveralls-ignore-start
    _validateTotals(payout, shares);
    // coveralls-ignore-stop
    totals.push(TotalStore({
      payout: uint128(payout),
      shares: uint128(shares)
    }));
  }
  /**
   * store a singular day, only the next day in the sequence is allowed
   * @param day the day to store
   */
  function storeDay(uint256 day) external returns(Total memory total) {
    if (totals.length != day) {
      revert NotAllowed();
    }
    return _storeDay({
      day: day,
      _total: Total(ZERO, ZERO)
    });
  }
  /**
   * checks the current day and increments the stored days if not yet covered
   */
  function incrementDay() external returns(Total memory total, uint256 day) {
    uint256 size = totals.length;
    if (size >= IHEX(TARGET).currentDay()) {
      // no need to increment
      return (total, ZERO);
    }
    return (_storeDay({
      day: size,
      _total: Total(ZERO, ZERO)
    }), size);
  }
  /**
   * store a range of day payout information. untilDay is exclusive unless startDay and untilDay match
   * @param startDay the day to start storing day information
   * @param untilDay the day to stop storing day information
   */
  function _storeDays(uint256 startDay, uint256 untilDay) internal returns(Total memory total, uint256 day) {
    uint256[] memory range = IHEX(TARGET).dailyDataRange(startDay, untilDay);
    uint256 len = range.length;
    uint256 payout;
    uint256 shares;
    uint256 i;
    do {
      (payout, shares) = _readTotals(startDay, total);
      unchecked {
        payout += uint72(range[i]);
        shares += uint72(range[i] >> SEVENTY_TWO);
        total = _saveDay(payout, shares);
        ++i;
        ++startDay;
      }
    } while (i < len);
    return (Total(payout, shares), startDay);
  }
  /**
   * store a range of day payout information. range is not constrained by max catch up days constant
   * nor is it constrained to the current day so if it goes beyond the current day or has not yet been stored
   * then it is subject to failure
   * @param startDay the day to start storing day information
   * @param untilDay the day to stop storing day information. Until day is inclusive
   */
  function storeDays(uint256 startDay, uint256 untilDay) external returns(Total memory total, uint256 day) {
    uint256 size = totals.length;
    if (startDay != size) {
      startDay = size;
    }
    if (untilDay <= size) {
      return (Total(totals[untilDay].payout, totals[untilDay].shares), untilDay);
    }
    return _storeDays({
      startDay: startDay,
      untilDay: untilDay
    });
  }
  /**
   * catch up the contract by reading up to 1_000 days of payout information at a time
   * @param iterations the maximum number of days to iterate over - capped at 1_000 due to sload constraints
   */
  function catchUpDays(uint256 iterations) external returns(Total memory total, uint256 day) {
    // constrain by gas costs
    iterations = iterations == ZERO || iterations > MAX_CATCH_UP_DAYS ? MAX_CATCH_UP_DAYS : iterations;
    uint256 startDay = totals.length;
    // constrain by size
    uint256 limit = IHEX(TARGET).currentDay();
    uint256 untilDay = startDay + iterations;
    if (untilDay > limit) {
      untilDay = limit;
    }
    return _storeDays({
      startDay: startDay,
      // iterations is used as untilDay to reduce number of variables in stack
      untilDay: untilDay
    });
  }
  function _validateTotals(uint256 payout, uint256 shares) internal pure virtual {
    // this line is very difficult to test, so it is going to be skipped
    // until an easy way to test it can be devised for low effort
    // basically hex would have to break for the line to be hit
    // total supply: 59004373824667548121*20% - for the staked hex
    // total shares on any given day: 9828590775299543795 (<2^72-1 as maximum)
    // inflation rate: 3.69% maximum
    // assume 200 years
    // 59004373824667548121*(1.0369^200) << 2^128-1
    // ((2^72)-1)*365*200 << 2^128-1
    // if (payout > MAX_UINT_128 || shares > MAX_UINT_128) revert NotAllowed();
  }
}

// File: contracts/EncodableSettings.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

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

abstract contract EncodableSettings is StakeInfo {
  // the index of the first bit of targeted information
  uint256 internal constant UNUSED_SPACE_RIGHT_UINT8 = SLOTS - EIGHT; // 256-8=248
  uint256 internal constant UNUSED_SPACE_RIGHT_UINT16 = SLOTS - SIXTEEN; // 256-16=240
  uint256 internal constant UNUSED_SPACE_RIGHT_UINT64 = SLOTS - SIXTY_FOUR; // 256-64=192
  uint256 internal constant INDEX_RIGHT_HEDRON_TIP = SLOTS - SEVENTY_TWO; // 256-72=184
  uint256 internal constant INDEX_RIGHT_TARGET_TIP = INDEX_RIGHT_HEDRON_TIP - SEVENTY_TWO; // 184-72=112
  uint256 internal constant INDEX_LEFT_TARGET_TIP = SLOTS - 144; // 256-144=112
  uint256 internal constant INDEX_RIGHT_NEW_STAKE = INDEX_RIGHT_TARGET_TIP - SEVENTY_TWO;
  uint256 internal constant INDEX_LEFT_NEW_STAKE = SLOTS - INDEX_RIGHT_NEW_STAKE;
  uint256 internal constant INDEX_RIGHT_NEW_STAKE_DAYS_METHOD = THIRTY_TWO;
  uint256 internal constant INDEX_RIGHT_NEW_STAKE_DAYS_MAGNITUDE = SIXTEEN;
  uint256 internal constant INDEX_RIGHT_COPY_ITERATIONS = EIGHT;
  uint256 internal constant INDEX_RIGHT_HAS_EXTERNAL_TIPS = 7;
  uint256 internal constant INDEX_RIGHT_COPY_EXTERNAL_TIPS = 6;
  uint256 internal constant INDEX_RIGHT_STAKE_IS_TRANSFERABLE = 5;
  uint256 internal constant INDEX_LEFT_STAKE_IS_TRANSFERABLE = SLOTS - INDEX_RIGHT_STAKE_IS_TRANSFERABLE;
  uint256 internal constant INDEX_RIGHT_SHOULD_SEND_TOKENS_TO_STAKER = FOUR;
  uint256 internal constant INDEX_RIGHT_CAN_MINT_HEDRON_AT_END = THREE;
  uint256 internal constant INDEX_RIGHT_CAN_MINT_HEDRON = TWO;
  uint256 internal constant INDEX_RIGHT_CAN_EARLY_STAKE_END = ONE;
  uint256 internal constant INDEX_RIGHT_CAN_STAKE_END = ZERO;

  /**
   * @notice this struct holds a series of flags to allow clients
   * to easily access and understand 1/0 mappings
   */
  struct ConsentAbilities {
    bool canStakeEnd;
    bool canEarlyStakeEnd;
    bool canMintHedron;
    bool canMintHedronAtEnd;
    bool shouldSendTokensToStaker;
    bool stakeIsTransferable;
    bool copyExternalTips;
    bool hasExternalTips;
  }
  /**
   * @notice this struct holds information that can be encoded into a uint256
   */
  struct Settings {
    Linear hedronTip;
    // starts with full amount of end stake
    Linear targetTip;
    // the rest goes into a new stake if the number of days are set
    Linear newStake;
    // useful to use methods 6+7 for stake days
    uint256 newStakeDaysMethod;
    uint256 newStakeDaysMagnitude;
    uint256 copyIterations; // 0 for do not restart, 1-254 as countdown, 255 as restart indefinitely
    /**
     * 00000001(0): can stake end
     * 00000010(1): can early stake end
     * 00000100(2): can mint hedron (any time)
     * 00001000(3): can mint hedron during end stake - future should be 0
     * 00010000(4): should send tokens to staker
     * 00100000(5): stake is transferable
     * 01000000(6): copy external tips to next stake
     * 10000000(7): has external tips (contract controlled)
     */
    ConsentAbilities consentAbilities;
  }
  mapping(uint256 stakeId => uint256 settings) public stakeIdToSettings;
  /**
   * an event to signal that settings to direct funds
   * at the end of a stake have been updated
   * @param stakeId the stake id that was updated
   * @param settings the newly updated settings
   */
  event UpdateSettings(uint256 indexed stakeId, uint256 settings);
  uint256 private constant DEFAULT_ENCODED_SETTINGS
    = uint256(0x000000000000000000000000000000000000000000000000000002020000ff01);
  /**
   * @return the default encoded settings used by end stakers to tip and end stakes
   */
  function defaultEncodedSettings() external virtual pure returns(uint256) {
    return DEFAULT_ENCODED_SETTINGS;
  }
  /**
   * access settings of a stake id and decode it, returning the decoded settings struct
   * @param stakeId the stake id to access and decode
   * @return decoded settings struct that holds all configuration by owner
   */
  function stakeIdSettings(uint256 stakeId) external view returns (Settings memory) {
    return _decodeSettings({
      encoded: stakeIdToSettings[stakeId]
    });
  }
  /**
   * decode a uint's first byte as consent abilities struct
   * @param abilities encoded consent abilities to decode
   * @return a ConsentAbilities struct with flags appropriately set
   */
  function decodeConsentAbilities(uint256 abilities) external pure returns(ConsentAbilities memory) {
    return _decodeConsentAbilities({
      abilities: abilities
    });
  }
  /**
   * decode a uint's first byte as consent abilities struct
   * @param abilities encoded consent abilities to decode
   * @return a ConsentAbilities struct with flags appropriately set
   */
  function _decodeConsentAbilities(uint256 abilities) internal pure returns(ConsentAbilities memory) {
    unchecked {
      return ConsentAbilities({
        hasExternalTips: (abilities >> INDEX_RIGHT_HAS_EXTERNAL_TIPS) % TWO == ONE,
        copyExternalTips: (abilities >> INDEX_RIGHT_COPY_EXTERNAL_TIPS) % TWO == ONE,
        stakeIsTransferable: (abilities >> INDEX_RIGHT_STAKE_IS_TRANSFERABLE) % TWO == ONE,
        shouldSendTokensToStaker: (abilities >> INDEX_RIGHT_SHOULD_SEND_TOKENS_TO_STAKER) % TWO == ONE,
        canMintHedronAtEnd: (abilities >> INDEX_RIGHT_CAN_MINT_HEDRON_AT_END) % TWO == ONE,
        canMintHedron: (abilities >> INDEX_RIGHT_CAN_MINT_HEDRON) % TWO == ONE,
        canEarlyStakeEnd: (abilities >> INDEX_RIGHT_CAN_EARLY_STAKE_END) % TWO == ONE,
        canStakeEnd: abilities % TWO == ONE
      });
    }
  }
  /**
   * updates settings under a stake id to the provided settings struct
   * @param stakeId the stake id to update
   * @param settings the settings to update the stake id to
   * @notice payable is only available to reduce costs, any native token
   * sent to this method will be unattributed and claimable by anyone
   */
  function updateSettings(uint256 stakeId, Settings calldata settings) external virtual payable {
    _updateSettingsEncoded({
      stakeId: stakeId,
      settings: _encodeSettings(settings)
    });
  }
  /**
   * update a stake's settings by providing a new, encoded value
   * @param stakeId the stake id to update settings for
   * @param settings the settings value to update settings for
   */
  function updateSettingsEncoded(uint256 stakeId, uint256 settings) external virtual payable {
    _updateSettingsEncoded({
      stakeId: stakeId,
      settings: settings
    });
  }
  /**
   * update a stake's setting by providing a uint256 encoded settings
   * @param stakeId the stake id to update settings for
   * @param settings the encoded settings to update to (7th index is maintained)
   * @notice This method will validate that the msg.sender owns the stake
   */
  function _updateSettingsEncoded(uint256 stakeId, uint256 settings) internal {
    _verifyStakeOwnership({
      owner: msg.sender,
      stakeId: stakeId
    });
    _logPreservedSettingsUpdate({
      stakeId: stakeId,
      settings: settings
    });
  }
  /**
   * updates a stake id's settings
   * @param stakeId the stake id to update settings for
   * @param settings the settings to update against a provided stakeId.
   * 7th index will be ignored as it is controlled by the contract
   */
  function _logPreservedSettingsUpdate(
    uint256 stakeId,
    uint256 settings
  ) internal {
    // preserve the 7th index which contract controls
    unchecked {
      _logSettingsUpdate({
        stakeId: stakeId,
        settings: (
          (settings >> INDEX_RIGHT_COPY_ITERATIONS << INDEX_RIGHT_COPY_ITERATIONS)
          | uint8(stakeIdToSettings[stakeId] >> INDEX_RIGHT_HAS_EXTERNAL_TIPS << INDEX_RIGHT_HAS_EXTERNAL_TIPS)
          | uint256(uint8(settings << ONE) >> ONE)
        )
      });
    }
  }
  /**
   * update the settings for a stake id
   * @param stakeId the stake id to update settings for
   * @param settings an object that holds settings values
   * to inform end stakers how to handle the stake
   */
  function _logSettingsUpdate(
    uint256 stakeId,
    uint256 settings
  ) internal {
    stakeIdToSettings[stakeId] = settings;
    emit UpdateSettings({
      stakeId: stakeId,
      settings: settings
    });
  }
  /**
   * read a single property from encoded settings
   * @notice most useful for other contracts to pull out 1 property without
   * needing logic for parsing
   * @param settings the settings number to read 1 property from
   * @param fromEnd the index from the end to start at
   * @param length the number of bits to read
   */
  function readEncodedSettings(
    uint256 settings,
    uint256 fromEnd,
    uint256 length
  ) external pure returns(uint256) {
    return _readEncodedSettings({
      settings: settings,
      fromEnd: fromEnd,
      length: length
    });
  }
  /**
   * parse out a single value from an encoded settings uint Only useful
   * if you do not want the whole settings struct to be decoded
   * @param settings the settings value to parse out
   * @param fromEnd the index (from left) to start at. Left most is 0
   * @param length the number of bits to retain after the fromEnd param
   * @return the uint retained by the fromEnd and length arguments of settings
   */
  function _readEncodedSettings(
    uint256 settings,
    uint256 fromEnd,
    uint256 length
  ) internal pure returns(uint256) {
    unchecked {
      return settings << fromEnd >> (SLOTS - length);
    }
  }
  /**
   * encode a settings struct into it's number
   * @param settings the settings struct to be encoded into a number
   * @return encoded a uint256 expression of settings struct
   */
  function encodeSettings(Settings memory settings) external pure returns(uint256 encoded) {
    return _encodeSettings({
      settings: settings
    });
  }
  /**
   * encode a settings struct as a uint value to fit it within 1 word
   * @param settings the settings struct to encode as a uint
   * @return encoded a uint256 expression of settings struct
   */
  function _encodeSettings(Settings memory settings) internal pure returns(uint256 encoded) {
    unchecked {
      return _encodeLinear(settings.hedronTip) << INDEX_RIGHT_HEDRON_TIP
        | _encodeLinear(settings.targetTip) << INDEX_RIGHT_TARGET_TIP
        | _encodeLinear(settings.newStake) << INDEX_RIGHT_NEW_STAKE
        | uint256(uint8(settings.newStakeDaysMethod)) << INDEX_RIGHT_NEW_STAKE_DAYS_METHOD
        | uint256(uint16(settings.newStakeDaysMagnitude)) << INDEX_RIGHT_NEW_STAKE_DAYS_MAGNITUDE
        | uint256(uint8(settings.copyIterations)) << INDEX_RIGHT_COPY_ITERATIONS
        | _encodeConsentAbilities(settings.consentAbilities);
    }
  }
  /**
   * decode an encoded setting into it's settings struct
   * @param encoded the encoded setting to decode
   * @return settings the decoded settings struct
   */
  function decodeSettings(uint256 encoded) external pure returns(Settings memory settings) {
    return _decodeSettings({
      encoded: encoded
    });
  }
  /**
   * decode a settings struct (2 words minimum) from a single uint256
   * @param encoded a number that represents all data needed for an encoded settings struct
   */
  function _decodeSettings(uint256 encoded) internal pure returns(Settings memory settings) {
    unchecked {
      return Settings(
        _decodeLinear(uint72(encoded >> INDEX_RIGHT_HEDRON_TIP)),
        _decodeLinear(uint72(encoded >> INDEX_RIGHT_TARGET_TIP)),
        _decodeLinear(uint72(encoded >> INDEX_RIGHT_NEW_STAKE)),
        uint8( encoded >> INDEX_RIGHT_NEW_STAKE_DAYS_METHOD),
        uint16(encoded >> INDEX_RIGHT_NEW_STAKE_DAYS_MAGNITUDE),
        uint8( encoded >> INDEX_RIGHT_COPY_ITERATIONS),
        _decodeConsentAbilities({
          abilities: uint8(encoded)
        })
      );
    }
  }
  /**
   * encode a ConsentAbilities struct to fit in 1 byte
   * @param consentAbilities the consent abilities struct to encode as a uint
   * @return the encoded list of consetn abilities as a uint
   */
  function encodeConsentAbilities(ConsentAbilities calldata consentAbilities) external pure returns(uint256) {
    return _encodeConsentAbilities({
      consentAbilities: consentAbilities
    });
  }
  /**
   * encode a struct of consent abilities to fit in 1 byte
   * @param consentAbilities encodes a struct of 8 booleans as a uint to fit in 1 byte
   * @return the encoded list of consent abilities as a uint
   */
  function _encodeConsentAbilities(ConsentAbilities memory consentAbilities) internal pure returns(uint256) {
    unchecked {
      return (
        ((consentAbilities.hasExternalTips ? ONE : ZERO) << INDEX_RIGHT_HAS_EXTERNAL_TIPS )
        | ((consentAbilities.copyExternalTips ? ONE : ZERO) << INDEX_RIGHT_COPY_EXTERNAL_TIPS)
        | ((consentAbilities.stakeIsTransferable ? ONE : ZERO) << INDEX_RIGHT_STAKE_IS_TRANSFERABLE)
        | ((consentAbilities.shouldSendTokensToStaker ? ONE : ZERO) << INDEX_RIGHT_SHOULD_SEND_TOKENS_TO_STAKER)
        | ((consentAbilities.canMintHedronAtEnd ? ONE : ZERO) << INDEX_RIGHT_CAN_MINT_HEDRON_AT_END)
        | ((consentAbilities.canMintHedron ? ONE : ZERO) << INDEX_RIGHT_CAN_MINT_HEDRON)
        | ((consentAbilities.canEarlyStakeEnd ? ONE : ZERO) << INDEX_RIGHT_CAN_EARLY_STAKE_END)
        | (consentAbilities.canStakeEnd ? ONE : ZERO)
      );
    }
  }
  /**
   * gets default settings struct
   * @return settings struct with default settings
   */
  function _defaultSettings() internal virtual pure returns(Settings memory settings) {
    // 0x00000000000000000000000000000000000000000000020000000000000000020000ff01
    unchecked {
      return Settings(
        /*
        * by default, there is no hedron tip
        * assume that stakers will manage their own stakes at bare minimum
        */
        Linear({
          method: ZERO,
          xFactor: ZERO,
          x: 0,
          yFactor: ZERO,
          y: ZERO,
          bFactor: ZERO,
          b: 0
        }),
        /*
        * by default, there is no target (hex) tip
        * assume that stakers will manage their own stakes at bare minimum
        */
        Linear({
          method: ZERO,
          xFactor: ZERO,
          x: 0,
          yFactor: ZERO,
          y: ZERO,
          bFactor: ZERO,
          b: 0
        }),
        /*
        * by default, assume that all tokens minted from an end stake
        * should go directly into a new stake
        */
        Linear({
          method: TWO,
          xFactor: ZERO,
          x: 0,
          yFactor: ZERO,
          y: ZERO,
          bFactor: ZERO,
          b: 0
        }),
        /*
        * by default, assume that by using this contract, users want efficiency gains
        * so by default, restarting their stakes are the most efficient means of managing tokens
        */
        uint8(TWO), uint16(ZERO),
        uint8(MAX_UINT_8), // restart forever
        /*
        * by index: 00000001
        * 7: signal to ender that tips exist to be collected (allows contract to avoid an SLOAD) (0)
        * 6: should recreate external tips
        * 5: give dominion over hedron after tip to staker (0)
        * 4: give dominion over target after tip to staker (0)
        * 3: do not allow end hedron mint (0)
        * 2: do not allow continuous hedron mint (0)
        * 1: do not allow early end (0)
        * 0: allow end stake once days have been served (1)
        *
        * restarting is signalled by using settings above
        * no funds are ever pulled from external address
        * is ever allowed except by sender
        *
        * the reason why the hedron flags are 0 by default on the contract level is because
        * it may be worthwhile for hedron developers to build on top of this contract
        * and it is poor form to force people in the future to have to cancel out the past
        * front ends may choose to send a different default (non 0) during stake start
        */
        ConsentAbilities({
          canStakeEnd: true,
          canEarlyStakeEnd: false,
          canMintHedron: false,
          canMintHedronAtEnd: false,
          shouldSendTokensToStaker: false,
          stakeIsTransferable: false,
          copyExternalTips: false,
          hasExternalTips: false
        })
      );
    }
  }
  /**
   * modify the second byteword from the right to appropriately decrement
   * the number of times that these settings should be copied
   * @param settings the settings to start with - only the 2nd byte from the right is modified
   */
  function decrementCopyIterations(uint256 settings) external pure returns(uint256) {
    return _decrementCopyIterations({
      settings: settings
    });
  }
  /**
   * decrement the 2nd byte from the right if the value is < 255
   * @param settings the settings to start with - only the 2nd byte from the right is modified
   * @return updated encoded settings with appropriately decremented value
   */
  function _decrementCopyIterations(uint256 settings) internal pure returns(uint256) {
    unchecked {
      uint256 copyIterations = uint8(settings >> INDEX_RIGHT_COPY_ITERATIONS);
      if (copyIterations == ZERO) {
        return uint8(settings);
      }
      if (copyIterations == MAX_UINT_8) {
        return settings;
      }
      --copyIterations;
      return (
        (settings >> INDEX_RIGHT_NEW_STAKE_DAYS_MAGNITUDE << INDEX_RIGHT_NEW_STAKE_DAYS_MAGNITUDE)
        | (copyIterations << INDEX_RIGHT_COPY_ITERATIONS)
        | uint8(settings)
      );
    }
  }
  /**
   * exposes the default settings to external for ease of access
   * @return a settings struct with default values
   */
  function defaultSettings() external virtual pure returns(Settings memory) {
    return _defaultSettings();
  }
}

// File: contracts/ExistingStakeManager.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

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

// in alphabetical order
contract ExistingStakeManager is MaximusStakeManager {}

// File: contracts/GoodAccounting.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { IHEX } from "./interfaces/IHEX.sol";
import { UnderlyingStakeable } from "./UnderlyingStakeable.sol";
import { Bank } from "./Bank.sol";
import { Tipper } from "./Tipper.sol";

abstract contract GoodAccounting is Tipper {
  /**
   * check that the provided stake can be ended and end it
   * @param stakeId the stake id to end as custodied by this contract
   */
  function checkAndDoStakeGoodAccounting(uint256 stakeId) external {
    _checkAndDoStakeGoodAccounting({
      staker: address(this),
      index: _stakeIdToIndex(stakeId),
      stakeId: stakeId
    });
  }
  /**
   * check that the stake can be good accounted, and execute the method if it will not fail
   * @param staker the custodian of the provided stake
   * @param index the index of the stake
   * @param stakeId the stake id of the stake
   */
  function checkAndDoStakeGoodAccountingFor(address staker, uint256 index, uint256 stakeId) external {
    _checkAndDoStakeGoodAccounting({
      staker: staker,
      index: index,
      stakeId: stakeId
    });
  }
  /**
   * run the appropriate checks if the stake is good accountable.
   * return 0 if it can be good accounted
   * return other numbers for those failed conditions
   * @param staker the custodian of the provided stake
   * @param index the index of the stake
   * @param stakeId the stake id of the stake
   */
  function isGoodAccountable(
    address staker,
    uint256 index,
    uint256 stakeId
  ) external view returns(GoodAccountingStatus) {
    return _isGoodAccountable({
      staker: staker,
      index: index,
      stakeId: stakeId
    });
  }
  function isStakeIdGoodAccountable(uint256 stakeId) external view returns(GoodAccountingStatus) {
    return _isGoodAccountable({
      staker: address(this),
      index: _stakeIdToIndex({
        stakeId: stakeId
      }),
      stakeId: stakeId
    });
  }
  enum GoodAccountingStatus {
    READY,
    ENDED,
    EARLY,
    MISMATCH,
    MISCOUNT
  }
  function _isGoodAccountable(
    address staker,
    uint256 index,
    uint256 stakeId
  ) internal view returns(GoodAccountingStatus) {
    uint256 count = IHEX(TARGET).stakeCount(staker);
    if (index >= count) {
      return GoodAccountingStatus.MISCOUNT;
    }
    StakeStore memory stake = _getStake({
      custodian: staker,
      index: index
    });
    if (stake.stakeId != stakeId) {
      // the stake id does not match
      return GoodAccountingStatus.MISMATCH;
    }
    if (_isEarlyEnding({
      lockedDay: stake.lockedDay,
      stakedDays: stake.stakedDays,
      targetDay: IHEX(TARGET).currentDay()
    })) {
      // return if it is too early to run good accounting
      return GoodAccountingStatus.EARLY;
    }
    if (stake.unlockedDay > ZERO) {
      // the stake has already been ended
      return GoodAccountingStatus.ENDED;
    }
    return GoodAccountingStatus.READY;
  }
  function _checkAndDoStakeGoodAccounting(address staker, uint256 index, uint256 stakeId) internal {
    if (_isGoodAccountable({
      staker: staker,
      index: index,
      stakeId: stakeId
    }) == GoodAccountingStatus.READY) {
      _stakeGoodAccounting({
        stakerAddr: staker,
        stakeIndex: index,
        stakeIdParam: stakeId
      });
    }
  }
  /**
   * freeze the progression of a stake to avoid penalties and preserve payout
   * @param stakerAddr the originating stake address
   * @param stakeIndex the index of the stake on the address
   * @param stakeIdParam the stake id to verify the same stake is being targeted
   */
  function stakeGoodAccounting(address stakerAddr, uint256 stakeIndex, uint40 stakeIdParam) external override {
    _stakeGoodAccounting({
      stakerAddr: stakerAddr,
      stakeIndex: stakeIndex,
      stakeIdParam: stakeIdParam
    });
  }
  /**
   * freeze the progression of a stake to avoid penalties and preserve payout
   * @param stakerAddr the originating stake address
   * @param stakeIndex the index of the stake on the address
   * @param stakeIdParam the stake id to verify the same stake is being targeted
   */
  function _stakeGoodAccounting(address stakerAddr, uint256 stakeIndex, uint256 stakeIdParam) internal {
    // no data is marked during good accounting, only computed and placed into logs
    // so we cannot return anything useful to the caller of this method
    UnderlyingStakeable(TARGET).stakeGoodAccounting(stakerAddr, stakeIndex, uint40(stakeIdParam));
  }
}

// File: contracts/HSIStakeManager.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IHedron } from "./interfaces/IHedron.sol";
import { IHEXStakeInstanceManager } from "./interfaces/IHEXStakeInstanceManager.sol";
import { IHEX } from "./interfaces/IHEX.sol";
import { IUnderlyingStakeable } from "./interfaces/IUnderlyingStakeable.sol";
import { StakeEnder } from "./StakeEnder.sol";

contract HSIStakeManager is StakeEnder {
  /**
   * a mapping of hsi addresses to the address that deposited the hsi into this contract
   */
  uint256 private constant DEFAULT_ENCODED_SETTINGS
    = 0x000000000000000000000000000000000000000000000000000000000000000d;
  function defaultEncodedSettings() external override pure returns(uint256) {
    return DEFAULT_ENCODED_SETTINGS;
  }
  function _defaultSettings() internal override pure returns(Settings memory) {
    return _decodeSettings({
      encoded: DEFAULT_ENCODED_SETTINGS
    });
  }
  /**
   * transfer stakes by their token ids
   * @param tokenId the token id to move to this contract
   * @dev requires approval to transfer hsi to this contract
   */
  function depositHsi(uint256 tokenId, uint256 encodedSettings) external returns(address hsiAddress) {
    address owner = _deposit721({
      token: HSIM,
      tokenId: tokenId
    });
    uint256 index = _hsiCount({
      staker: address(this)
    });
    hsiAddress = IHEXStakeInstanceManager(HSIM).hexStakeDetokenize(tokenId);
    uint256 stakeId = uint256(uint160(hsiAddress));
    // erc721 is burned - no owner - only hsi address remains
    stakeIdInfo[stakeId] = _encodeInfo({
      index: index,
      owner: owner
    });
    _logSettingsUpdate({
      stakeId: stakeId,
      settings: encodedSettings
    });
  }
  /**
   * deposit a tokenized hsi into this contract
   * @param token the address of the token (HSIM)
   * @param tokenId the token id to deposit into this contract
   */
  function _deposit721(address token, uint256 tokenId) internal returns(address owner) {
    owner = IERC721(token).ownerOf(tokenId);
    IERC721(token).transferFrom(msg.sender, address(this), tokenId);
  }
  /**
   * a convenience method to retrieve a stake id from an hsi address
   * @param hsiAddress the hsi address to target
   */
  function hsiAddressToId(address hsiAddress) external view returns(uint256) {
    return _hsiAddressToId({
      hsiAddress: hsiAddress
    });
  }
  /**
   * an internal convenience method to retreive a stake id from an hsi address
   * @param hsiAddress the hsi address to target
   */
  function _hsiAddressToId(address hsiAddress) internal view returns(uint256) {
    return _getStake({
      custodian: hsiAddress,
      index: ZERO
    }).stakeId;
  }
  /**
   * withdraw an hsi from this contract's custody
   * @param hsiAddress the hsi address to withdraw from this contract
   * @dev caller must be logged as owner of hsi
   */
  function withdrawHsi(address hsiAddress) external returns(uint256 tokenId) {
    uint256 stakeId = uint256(uint160(hsiAddress));
    _verifyStakeOwnership({
      owner: msg.sender,
      stakeId: stakeId
    });
    (uint256 index, address owner) = _stakeIdToInfo({
      stakeId: stakeId
    });
    stakeIdInfo[stakeId] = ZERO;
    // this will be used later to determine whether or not to send tips back to staker
    uint256 settings = stakeIdToSettings[stakeId];
    _logSettingsUpdate({
      stakeId: stakeId,
      settings: ZERO
    });
    tokenId = _withdraw721({
      index: index,
      owner: owner,
      hsiAddress: hsiAddress
    });
    if (_hsiCount({
      staker: address(this)
    }) > index) {
      _rewriteIndex({
        index: index
      });
    }
    // because an unbounded range of tokens can exist in the tips list,
    // we send those back last
    _removeAllTips({
      stakeId: stakeId,
      settings: settings
    });
  }
  /**
   * the count or length of hsi's attributed to this contract
   */
  function hsiCount(address staker) external view returns(uint256 count) {
    return _hsiCount({
      staker: staker
    });
  }
  function _hsiCount(address staker) internal view returns(uint256 count) {
    return IHEXStakeInstanceManager(HSIM).hsiCount(staker);
  }
  function _getStakeCount(address staker) internal view override returns(uint256 count) {
    return _hsiCount({
      staker: staker
    });
  }
  /**
   * tokenize/mint a stake's erc721 token to transfer ownership of it
   * @param index the index of the stake to tokenize
   * @param owner the owner of the stake
   * @param hsiAddress the hsi address (contract) that the stake is being custodied by
   */
  function _withdraw721(uint256 index, address owner, address hsiAddress) internal returns(uint256 tokenId) {
    tokenId = IHEXStakeInstanceManager(HSIM).hexStakeTokenize(index, hsiAddress);
    IERC721(HSIM).transferFrom(address(this), owner, tokenId);
  }
  function hsiStakeEndMany(address[] calldata hsiAddresses) external payable {
    _hsiStakeEndMany({
      hsiAddresses: hsiAddresses,
      tipTo: address(0)
    });
  }
  function hsiStakeEndManyWithTipTo(address[] calldata hsiAddresses, address tipTo) external payable {
    _hsiStakeEndMany({
      hsiAddresses: hsiAddresses,
      tipTo: tipTo
    });
  }
  /**
   * provide a list of hsi addresses to end the stake of
   * @param hsiAddresses a list of hsi addresses (known in this contract as stake ids)
   */
  function _hsiStakeEndMany(address[] calldata hsiAddresses, address tipTo) internal {
    uint256 len = hsiAddresses.length;
    uint256 i;
    unchecked {
      uint256 count = (_currentDay() << INDEX_RIGHT_TODAY) | _hsiCount({
        staker: address(this)
      });
      do {
        (, count) = _stakeEndByConsent({
          stakeId: uint160(hsiAddresses[i]),
          tipTo: tipTo,
          count: count
        });
        ++i;
      } while (i < len);
    }
  }
  /**
   * retrieve a stake id's (hsi address's) singular stake
   * @param stakeId the stake id or hsi address to retrieve a stake from its list
   */
  function _getStakeInfo(uint256 stakeId) internal view override returns(
    bool valid,
    address staker,
    uint256 stakeIndex,
    IUnderlyingStakeable.StakeStore memory stake
  ) {
    // we are only testing existance because the index
    // is always 0 for the custodian
    address hsiAddress = address(uint160(stakeId));
    (stakeIndex, staker) = _stakeIdToInfo(stakeId);
    if (_stakeCount({ staker: hsiAddress }) == ONE) {
      stake = _getStake({
        custodian: hsiAddress,
        index: ZERO
      });
      valid = staker != address(0);
    }
  }
  /**
   * end a hsi's stake and return the amount of
   * unattributed tokens sent to this contract
   * @param index the hsim index of the stake to end
   * @param stakeId the stake id or hsi address
   * @param hsiCountAfter the length of stakes that will exist
   * under the hsim after this end operation is complete
   */
  function _stakeEnd(
    uint256 index,
    uint256 stakeId,
    uint256 hsiCountAfter
  ) internal override returns(uint256 targetReward) {
    targetReward = IHEXStakeInstanceManager(HSIM)
      .hexStakeEnd(index, address(uint160(stakeId)));
    // move around the indexes for future stake ends
    if (hsiCountAfter > index) {
      _rewriteIndex({
        index: index
      });
    }
  }
  function _rewriteIndex(uint256 index) internal {
    address movedHsiAddress = IHEXStakeInstanceManager(HSIM)
      .hsiLists(address(this), index);
    (, address movedOwner) = _stakeIdToInfo({
      stakeId: uint256(uint160(movedHsiAddress))
    });
    stakeIdInfo[uint256(uint160(movedHsiAddress))] = _encodeInfo({
      index: index,
      owner: movedOwner
    });
  }
  /**
   * starts an hsi for the provided staker and saves its data appropriately
   * @param staker the staker that will own this stake
   * @param newStakeAmount the number of tokens to add to the newly formed stake
   * @param newStakeDays the number of days to stake said tokens for
   * @param index the index of the stake in the list of all stakes
   */
  function _stakeStartFor(
    address staker,
    uint256 newStakeAmount,
    uint256 newStakeDays,
    uint256 index
  ) internal override returns(uint256 stakeId) {
    IERC20(TARGET).approve(HSIM, newStakeAmount);
    address hsiAddress = IHEXStakeInstanceManager(HSIM)
      .hexStakeStart(newStakeAmount, newStakeDays);
    stakeId = uint160(hsiAddress);
    stakeIdInfo[stakeId] = _encodeInfo({
      index: index,
      owner: staker
    });
  }
  /**
   * mint hedron from an hsi
   * @param index the index of the stake on hsim to mint
   * @param stakeId the stake id or in this case, hsi address
   */
  function _mintHedron(uint256 index, uint256 stakeId) internal override returns(uint256) {
    return IHedron(HEDRON).mintInstanced(index, address(uint160(stakeId)));
  }
  /**
   * check that this contract is the custodian of this hsi (nft was depostied and detokenized)
   * @param stakeId the stake id to check ownership over
   */
  function _checkStakeCustodian(uint256 stakeId) internal override view {
    _verifyCustodian({
      stakeId: stakeId
    });
  }
}

// File: contracts/interfaces/IExternalPerpetualFilter.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

interface IExternalPerpetualFilter {
  function verifyPerpetual(address perpetual) external view returns(bool isPerpetual);
}

// File: contracts/interfaces/IGasReimberser.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

interface IGasReimberser {
  function flush() external;
  function flush_erc20(address token) external;
}

// File: contracts/interfaces/IHedron.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IHedron is IERC20 {
  event Claim(uint256 data, address indexed claimant, uint40 indexed stakeId);
  event LoanEnd(
      uint256 data,
      address indexed borrower,
      uint40 indexed stakeId
  );
  event LoanLiquidateBid(
      uint256 data,
      address indexed bidder,
      uint40 indexed stakeId,
      uint40 indexed liquidationId
  );
  event LoanLiquidateExit(
      uint256 data,
      address indexed liquidator,
      uint40 indexed stakeId,
      uint40 indexed liquidationId
  );
  event LoanLiquidateStart(
      uint256 data,
      address indexed borrower,
      uint40 indexed stakeId,
      uint40 indexed liquidationId
  );
  event LoanPayment(
      uint256 data,
      address indexed borrower,
      uint40 indexed stakeId
  );
  event LoanStart(
      uint256 data,
      address indexed borrower,
      uint40 indexed stakeId
  );
  event Mint(uint256 data, address indexed minter, uint40 indexed stakeId);

  function hsim() external view returns(address);
  function mintInstanced(uint256 hsiIndex, address hsiAddress) external returns (uint256);
  function mintNative(uint256 stakeIndex, uint40 stakeId) external returns (uint256);
}

// File: contracts/interfaces/IHEX.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { IUnderlyingStakeable } from "./IUnderlyingStakeable.sol";

interface IHEX is IERC20, IERC20Metadata, IUnderlyingStakeable {
  /*  XfLobbyEnter      (auto-generated event)

      uint40            timestamp       -->  data0 [ 39:  0]
      address  indexed  memberAddr
      uint256  indexed  entryId
      uint96            rawAmount       -->  data0 [135: 40]
      address  indexed  referrerAddr
  */
  event XfLobbyEnter(
    uint256 data0,
    address indexed memberAddr,
    uint256 indexed entryId,
    address indexed referrerAddr
  );

  /*  XfLobbyExit       (auto-generated event)

      uint40            timestamp       -->  data0 [ 39:  0]
      address  indexed  memberAddr
      uint256  indexed  entryId
      uint72            xfAmount        -->  data0 [111: 40]
      address  indexed  referrerAddr
  */
  event XfLobbyExit(
    uint256 data0,
    address indexed memberAddr,
    uint256 indexed entryId,
    address indexed referrerAddr
  );

  /*  DailyDataUpdate   (auto-generated event)

      uint40            timestamp       -->  data0 [ 39:  0]
      uint16            beginDay        -->  data0 [ 55: 40]
      uint16            endDay          -->  data0 [ 71: 56]
      bool              isAutoUpdate    -->  data0 [ 79: 72]
      address  indexed  updaterAddr
  */
  event DailyDataUpdate(
      uint256 data0,
      address indexed updaterAddr
  );

  /*  Claim             (auto-generated event)

      uint40            timestamp       -->  data0 [ 39:  0]
      bytes20  indexed  btcAddr
      uint56            rawSatoshis     -->  data0 [ 95: 40]
      uint56            adjSatoshis     -->  data0 [151: 96]
      address  indexed  claimToAddr
      uint8             claimFlags      -->  data0 [159:152]
      uint72            claimedHearts   -->  data0 [231:160]
      address  indexed  referrerAddr
      address           senderAddr      -->  data1 [159:  0]
  */
  event Claim(
    uint256 data0,
    uint256 data1,
    bytes20 indexed btcAddr,
    address indexed claimToAddr,
    address indexed referrerAddr
  );

  /*  ClaimAssist       (auto-generated event)

      uint40            timestamp       -->  data0 [ 39:  0]
      bytes20           btcAddr         -->  data0 [199: 40]
      uint56            rawSatoshis     -->  data0 [255:200]
      uint56            adjSatoshis     -->  data1 [ 55:  0]
      address           claimToAddr     -->  data1 [215: 56]
      uint8             claimFlags      -->  data1 [223:216]
      uint72            claimedHearts   -->  data2 [ 71:  0]
      address           referrerAddr    -->  data2 [231: 72]
      address  indexed  senderAddr
  */
  event ClaimAssist(
    uint256 data0,
    uint256 data1,
    uint256 data2,
    address indexed senderAddr
  );

  /*  StakeStart        (auto-generated event)

      uint40            timestamp       -->  data0 [ 39:  0]
      address  indexed  stakerAddr
      uint40   indexed  stakeId
      uint72            stakedHearts    -->  data0 [111: 40]
      uint72            stakeShares     -->  data0 [183:112]
      uint16            stakedDays      -->  data0 [199:184]
      bool              isAutoStake     -->  data0 [207:200]
  */
  event StakeStart(
    uint256 data0,
    address indexed stakerAddr,
    uint40 indexed stakeId
  );

  /*  StakeGoodAccounting(auto-generated event)

      uint40            timestamp       -->  data0 [ 39:  0]
      address  indexed  stakerAddr
      uint40   indexed  stakeId
      uint72            stakedHearts    -->  data0 [111: 40]
      uint72            stakeShares     -->  data0 [183:112]
      uint72            payout          -->  data0 [255:184]
      uint72            penalty         -->  data1 [ 71:  0]
      address  indexed  senderAddr
  */
  event StakeGoodAccounting(
    uint256 data0,
    uint256 data1,
    address indexed stakerAddr,
    uint40 indexed stakeId,
    address indexed senderAddr
  );

  /*  StakeEnd          (auto-generated event)

      uint40            timestamp       -->  data0 [ 39:  0]
      address  indexed  stakerAddr
      uint40   indexed  stakeId
      uint72            stakedHearts    -->  data0 [111: 40]
      uint72            stakeShares     -->  data0 [183:112]
      uint72            payout          -->  data0 [255:184]
      uint72            penalty         -->  data1 [ 71:  0]
      uint16            servedDays      -->  data1 [ 87: 72]
      bool              prevUnlocked    -->  data1 [ 95: 88]
  */
  event StakeEnd(
    uint256 data0,
    uint256 data1,
    address indexed stakerAddr,
    uint40 indexed stakeId
  );

  /*  ShareRateChange   (auto-generated event)

      uint40            timestamp       -->  data0 [ 39:  0]
      uint40            shareRate       -->  data0 [ 79: 40]
      uint40   indexed  stakeId
  */
  event ShareRateChange(
    uint256 data0,
    uint40 indexed stakeId
  );
  function stakeLists(address staker, uint256 index) view external returns(StakeStore memory);
  function currentDay() external view returns (uint256);
  function globalInfo() external view returns(uint256[13] memory);

  function dailyData(uint256 day) external view returns(
    uint72 dayPayoutTotal,
    uint72 dayStakeSharesTotal,
    uint56 dayUnclaimedSatoshisTotal
  );
  function dailyDataRange(uint256 beginDay, uint256 endDay)
    external
    view
    returns (uint256[] memory list);
}

// File: contracts/interfaces/IHEXStakeInstanceManager.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { IERC721Enumerable } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol";

interface IHEXStakeInstanceManager is IERC721, IERC721Enumerable {
  event HSIStart(
    uint256         timestamp,
    address indexed hsiAddress,
    address indexed staker
  );

  event HSIEnd(
    uint256         timestamp,
    address indexed hsiAddress,
    address indexed staker
  );

  event HSITransfer(
    uint256         timestamp,
    address indexed hsiAddress,
    address indexed oldStaker,
    address indexed newStaker
  );

  event HSITokenize(
    uint256         timestamp,
    uint256 indexed hsiTokenId,
    address indexed hsiAddress,
    address indexed staker
  );

  event HSIDetokenize(
    uint256         timestamp,
    uint256 indexed hsiTokenId,
    address indexed hsiAddress,
    address indexed staker
  );
  function hsiLists(address generator, uint256 index) external view returns(address);
  function hsiCount(address originator) external view returns(uint256);
  function hexStakeDetokenize (uint256 tokenId) external returns (address);
  function hexStakeTokenize (uint256 hsiIndex, address hsiAddress) external returns (uint256);
  function hexStakeEnd (uint256 hsiIndex, address hsiAddress) external returns (uint256);
  function hexStakeStart (uint256 amount, uint256 length) external returns (address);
  function hsiToken(uint256 tokenId) external view returns(address);
  function setApprovalForall(address operator, bool approved) external;
}

// File: contracts/interfaces/IPoolContract.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

interface IPoolContract {
  function getEndStaker() external view returns(address end_staker_address);
}

// File: contracts/interfaces/IPublicEndStakeable.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

interface IPublicEndStakeable {
  function STAKE_END_DAY() external view returns(uint256);
  function STAKE_IS_ACTIVE() external view returns(bool);
  function mintHedron(uint256 stakeIndex, uint40 stakeIdParam) external;
  function endStakeHEX(uint256 stakeIndex, uint40 stakeIdParam) external;
  function getCurrentPeriod() external view returns (uint256);
}

// File: contracts/interfaces/IStakeReceiver.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

interface IStakeReceiver {
  function onStakeReceived(address from, uint256 stakeId) external;
}

// File: contracts/interfaces/IUnderlyingStakeable.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

/**
 * @title IUnderlyingStakeable
 * @notice this is the minimum interface needed to start and end stakes appropriately on hex
 */
interface IUnderlyingStakeable {
  /** the stake store that holds data about the stake */
  struct StakeStore {
    uint40 stakeId;
    uint72 stakedHearts;
    uint72 stakeShares;
    uint16 lockedDay;
    uint16 stakedDays;
    uint16 unlockedDay;
    bool isAutoStake;
  }
  /**
   * starts a stake from the provided amount
   * @param newStakedHearts amount of tokens to stake
   * @param newStakedDays the number of days for this new stake
   * @dev this method interface matches the original underlying token contract
   */
  function stakeStart(uint256 newStakedHearts, uint256 newStakedDays) external;
  /**
   * end your own stake which is custodied by the stake manager. skips tip computing
   * @param stakeIndex the index on the underlying contract to end stake
   * @param stakeId the stake id from the underlying contract to end stake
   * @notice this is not payable to match the underlying contract
   * @notice this moves funds back to the sender to make behavior match underlying token
   * @notice this method only checks that the sender owns the stake it does not care
   * if it is managed in a created contract and externally endable by this contract (1)
   * or requires that the staker send start and end methods (0)
   */
  function stakeEnd(uint256 stakeIndex, uint40 stakeId) external;
  /**
   * freeze the progression of a stake to avoid penalties and preserve payout
   * @param stakerAddr the custoidan of the stake
   * @param stakeIndex the index of the stake in question
   * @param stakeIdParam the id of the stake
   */
  function stakeGoodAccounting(address stakerAddr, uint256 stakeIndex, uint40 stakeIdParam) external;
  /**
   * the count of stakes for a given custodian / staker
   * @param staker the custodian in question
   * @return count of the stakes under a given custodian / staker
   */
  function stakeCount(address staker) external view returns (uint256 count);
  /**
   * retrieve the global info from the target contract (hex)
   * updated at the first start or end stake on any given day
   */
  function globalInfo() external view returns(uint256[13] memory);
  /**
   * retrieve a stake at a staker's index given a staker address and an index
   * @param staker the staker in question
   * @param index the index to focus on
   * @return stake the stake custodied by a given staker at a given index
   */
  function stakeLists(address staker, uint256 index) view external returns(StakeStore memory);
  /**
   * retrieve the current day from the target contract
   * @return day the current day according to the hex contract
   */
  function currentDay() external view returns (uint256);
}

// File: contracts/IsolatedStakeManager.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol";
import { IUnderlyingStakeable } from "./interfaces/IUnderlyingStakeable.sol";
import { AuthorizationManager } from "./AuthorizationManager.sol";
import { GoodAccounting } from "./GoodAccounting.sol";

contract IsolatedStakeManager is Ownable2Step, AuthorizationManager, GoodAccounting {
  using SafeERC20 for IERC20;
  constructor(address account) AuthorizationManager(31) {
    /*
     * by index:
     * 0: can start stakes
     * 1: can end stakes
     * 2: can early end stakes
     * 3: can transfer balance to owner
     * 4: can transfer from owner
     */
    _setAddressAuthorization({
      account: account,
      settings: MAX_AUTHORIZATION
    });
    _transferOwnership(account);
  }
  /**
   * set authorization flags for a provided target
   * @param account the address to change settings for
   * @param settings the encoded setting (binary) to apply to the target address
   */
  function setAuthorization(address account, uint256 settings) external onlyOwner {
    _setAddressAuthorization({
      account: account,
      settings: settings
    });
  }
  /**
   * allow addresses to start stakes from tokens already in the contract
   * @param runner the anticipated address(es) that will be running the following method
   * @param stakeDays the number of days that can be passed for the address (to constrain griefing)
   * @param settings the settings to provide (only index 0 is relevant)
   */
  function setStartAuthorization(address runner, uint16 stakeDays, uint256 settings) external onlyOwner {
    _setAuthorization({
      key: _startAuthorizationKey({
        runner: runner,
        stakeDays: stakeDays
      }),
      settings: settings
    });
  }
  /**
   * gets the start authorization key given a runner and stake days
   * @param runner the anticipated address(es) that will be running the following method
   * @param stakeDays the number of days that can be passed for the address (to constrain griefing)
   */
  function startAuthorizationKey(address runner, uint256 stakeDays) external pure returns(bytes32) {
    return _startAuthorizationKey({
      runner: runner,
      stakeDays: stakeDays
    });
  }
  /**
   * stake a given amount of tokens for a given number of days
   * @param newStakedHearts the number of hearts to stake
   * @param newStakedDays the number of days to stake said hearts
   * @notice if 0 is provided then the balance of the contract will be utilized
   * this should generally only be used if tokens are sent to the contract
   * and end stakes are not occuring for a number of days
   * @notice if you do not have global start abilities, but do have scoped abilities
   * it is not rational to pass anything but zero for this method
   */
  function stakeStart(uint256 newStakedHearts, uint256 newStakedDays) external override {
    uint256 settings = _getAddressSettings({
      account: msg.sender
    });
    // blanket start authorization
    if (!_isOneAtIndex({
      settings: settings,
      index: ZERO
    })) {
      revert NotAllowed();
    }
    if (newStakedHearts > ZERO) {
      _transferFromOwner({
        amount: newStakedHearts
      });
    }
    _stakeStart({
      newStakedDays: newStakedDays
    });
  }
  /**
   * start a stakes, so long as sender has the authorization to do so from owner
   * @param newStakedDays the number of days to start a stake
   */
  function stakeStartWithAuthorization(uint256 newStakedDays) external {
    // scoped authorization - to keep non-permitted contracts from griefing users
    if (!_isOneAtIndex({
      settings: authorization[_startAuthorizationKey(msg.sender, newStakedDays)],
      index: ZERO
    })) {
      revert NotAllowed();
    }
    _stakeStart({
      newStakedDays: newStakedDays
    });
  }
  /**
   * transfer a number of hearts from the owner into the contract
   * @param newStakedHearts number of hearts to deposit into contract
   * @notice authorization occurs inside of the internal method
   */
  function transferFromOwner(uint256 newStakedHearts) external {
    _transferFromOwner(newStakedHearts);
  }
  /**
   * ends the stake on the underlying target contract (HEX)
   * and transfers tokens to the owner
   * @param stakeIndex the index of the stake in ownership list
   * @param stakeId the id held on the stake
   * @notice this method fails if the stake at the provided index does not match the stakeId
   */
  function stakeEnd(uint256 stakeIndex, uint40 stakeId) external override {
    StakeStore memory stake = _getStake({
      custodian: address(this),
      index: stakeIndex
    });
    if (!_settingsCheck({
      stake: stake
    })) {
      revert NotAllowed();
    }
    _endStake({
      stakeIndex: stakeIndex,
      stakeId: stakeId
    });
  }
  /**
   * transfers tokens to the owner of the contract
   */
  function transferToOwner() external payable {
    _transferToOwner();
  }
  /**
   * ends the stake on the underlying target contract (HEX)
   * and transfers tokens to the owner
   * @param stakeIndex the index of the stake in ownership list
   * @param stakeId the id held on the stake
   * @notice this method does not fail if the stake at the
   * provided index does not have the provided stake id
   * @notice this method does not fail if authorization
   * is not provided to the runner of this method
   * this is to give every opportunity for strangers (who are authorized)
   * to end stakes without risk of losing too much gas money
   */
  function checkAndStakeEnd(uint256 stakeIndex, uint40 stakeId) external {
    StakeStore memory stake = _getStake({
      custodian: address(this),
      index: stakeIndex
    });
    if (stake.stakeId != stakeId || !_settingsCheck({
      stake: stake
    })) {
      return;
    }
    _endStake({
      stakeIndex: stakeIndex,
      stakeId: stakeId
    });
  }
  /**
   * ends a stake on the underlying contract
   * @param stakeIndex stake index to end
   * @param stakeId stake id to end
   * @notice this will fail on the underlying if
   * the stakeIndex and stakeId does not match
   */
  function _endStake(uint256 stakeIndex, uint40 stakeId) internal {
    IUnderlyingStakeable(TARGET).stakeEnd(stakeIndex, stakeId);
  }
  /**
   * transfer balance to the owner of this contract
   */
  function _transferToOwner() internal {
    if (!_isOneAtIndex({
      settings: _getAddressSettings(msg.sender),
      index: THREE
    })) {
      revert NotAllowed();
    }
    IERC20(TARGET).safeTransfer(owner(), _balanceOf({
      owner: address(this)
    }));
  }
  /**
   * check the settings of the running address
   * @param stake the stake to check authorization over
   */
  function _settingsCheck(IUnderlyingStakeable.StakeStore memory stake) internal view returns(bool) {
    uint256 settings = _getAddressSettings(msg.sender);
    if (_isEarlyEnding(stake.lockedDay, stake.stakedDays, _currentDay())) {
      // can early end stake
      return _isOneAtIndex({
        settings: settings,
        index: TWO
      });
    } else {
      // can end stake
      return _isOneAtIndex({
        settings: settings,
        index: ONE
      });
    }
  }
  /**
   * get the start authorization key for an address and number of stake days
   * @param runner the address that will run the method
   * @param stakeDays the number of days to stake
   */
  function _startAuthorizationKey(address runner, uint256 stakeDays) internal pure returns(bytes32) {
    return bytes32(uint256(uint160(runner)) << SIXTEEN | uint16(stakeDays));
  }
  /**
   * starts a stake on the underlying contract for a given number of days
   * @param newStakedDays a number of days to start a stake for
   */
  function _stakeStart(uint256 newStakedDays) internal {
    uint256 stakedHearts = _balanceOf({
      owner: address(this)
    });
    if (stakedHearts > ZERO) {
      IUnderlyingStakeable(TARGET).stakeStart({
        newStakedHearts: stakedHearts,
        newStakedDays: newStakedDays
      });
    }
  }
  /**
   * transfer a number of hearts from the owner to this contract
   * @param amount number of hearts to transfer from owner
   */
  function _transferFromOwner(uint256 amount) internal {
    if (!_isOneAtIndex({
      settings: _getAddressSettings(msg.sender),
      index: FOUR
    })) {
      revert NotAllowed();
    }
    IERC20(TARGET).safeTransferFrom(owner(), address(this), amount);
  }
}

// File: contracts/IsolatedStakeManagerFactory.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

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

contract IsolatedStakeManagerFactory {
  event CreateIsolatedStakeManager(address owner, address instance);
  /**
   * @notice a mapping of a key that contains a modifier and the owning address
   * pointing to the address of the contract created by the stake manager
   */
  mapping(address originalOwner => address manager) public isolatedStakeManagers;
  function createIsolatedManager(address staker) external returns(address existing) {
    existing = isolatedStakeManagers[staker];
    if (existing != address(0)) {
      return existing;
    }
    // this scopes up to 2 stake managers to a single address
    // one that can only be ended by the staker one that can be ended by the stake manager
    existing = address(new IsolatedStakeManager{salt: keccak256(abi.encode(staker))}(staker));
    emit CreateIsolatedStakeManager({
      owner: staker,
      instance: existing
    });
    isolatedStakeManagers[staker] = existing;
  }
}

// File: contracts/Magnitude.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

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

contract Magnitude is Utils {
  uint256 constant internal X_OPTIONS = THREE;
  function _computeDayMagnitude(
    uint256 limit, uint256 method, uint256 x,
    uint256 today, // today
    uint256 lockedDay, // lockedDay
    uint256 stakedDays
  ) internal pure returns(uint256 newMethod, uint256 numDays) {
    newMethod = method;
    unchecked {
      if (method < THREE) {
        if (method == ONE) {
          // useful when you want the next stake start to be
          // entirely different and simply repeat after that
          numDays = x; // 1
        } else {
          numDays = stakedDays; // 2 - repeat number of days
        }
      } else {
        // 4 is a flag to front end that this stake may no longer match its history
        // the reason this must cut off contract controlled days is because
        // we do not have enough bits to store locked day and staked days from
        // previous iterations. the code below either does a shorter or a longer stake
        // depending on the x (bump min) value which helps get around the 0 issue
        // using this feature and not paying attention
        // to your stake could lead to it not being restarted
        // with this constraint, a stake will never be restarted for longer than 2x
        // the original stake - otherwise the stakes could indefinitely elongate
        if (newMethod == FOUR) {
          return (newMethod, ZERO);
        }
        // 3 - start an equally spaced ladder, even if end stake happens late
        uint256 nextLockedDay = today + ONE;
        uint256 lockedDaysDelta = nextLockedDay - lockedDay;
        uint256 stakeDaysConsumption = stakedDays + ONE;
        uint256 stakedDaysModifier = lockedDaysDelta % stakeDaysConsumption;
        numDays = stakedDays - stakedDaysModifier;
        // x is the equivalent to "bump to next stake ladder" if under this value
        if (numDays < x) {
          numDays = stakedDays + numDays + ONE;
        }
        if (numDays != stakedDays) {
          // unless the ladder is being restarted on the end day
          // disallow new stakes until the user updates the setting (reset to 3)
          ++newMethod;
        }
      }
      numDays = numDays > limit ? limit : numDays;
    }
  }
  /**
   * compute a useful value from 2 inputs
   * @param linear holds the linear data to describe how to plot a provied y value
   * @param v2 a secondary magnitude to use - generally the amount of the end stake
   * @param v1 the starting point of v2 used for deltas
   * @notice funds may never be linked to x variable. X should only hold data that we can plug into
   * an expression to tell us where to land on the plot. Result is never less than 0, nor greater than limit
   */
  function _computeMagnitude(
    uint256 limit, uint256 linear,
    uint256 v2, uint256 v1
  ) internal pure returns(uint256 amount) {
    // we can use unchecked here because all minuses (-)
    // are checked before they are run
    unchecked {
      uint256 method = uint8(linear);
      if (method < X_OPTIONS) {
        if (method == ONE) {
          amount = (uint256(uint56(linear >> EIGHT)) << uint256(uint8(linear >> SIXTY_FOUR))); // 1
        } else {
          amount = v2; // 2
        }
      } else {
        Linear memory line = _decodeLinear(linear);
        uint256 delta = _getDelta(line.method, v2, v1);
        if (delta == ZERO) return ZERO;
        // even with uint16 (max: 65535), we can still get down to 0.01% increments
        // with scaling we can go even further (though it is choppier)
        // x has an embedded 1 offset
        int256 x = line.x << (line.xFactor - ONE);
        uint256 y = line.y << line.yFactor;
        int256 b = line.b << line.bFactor;
        int256 amnt = ((x * int256(delta)) / int256(y)) + b;
        amount = amnt < 0 ? ZERO : uint256(amnt);
      }
      amount = amount > limit ? limit : amount;
    }
  }
  function _getDelta(uint256 method, uint256 v2, uint256 v1) internal pure returns(uint256 y) {
    unchecked {
      y = v2;
      if (method == ONE) {
        // v1 only
        y = v1;
      } else if (method == TWO) {
        // yield only
        if (v2 > v1) {
          y = v2 - v1;
        } else {
          y = ZERO;
        }
      }
    }
  }
  struct Linear {
    uint256 method;
    uint256 xFactor;
    int256 x;
    uint256 yFactor;
    uint256 y;
    uint256 bFactor;
    int256 b;
  }
  function encodeLinear(Linear calldata linear) external pure returns(uint256 encoded) {
    return _encodeLinear(linear);
  }
  /**
   * convert an x/y+b linear struct into a number held in under 72 total bits
   * @param linear the struct with all relevant linear data in it
   * @return encoded the encoded numbers describing (x/y)+b
   */
  function _encodeLinear(Linear memory linear) internal pure returns(uint256 encoded) {
    if (linear.method >= X_OPTIONS) revert NotAllowed();
    if (linear.xFactor == ZERO) {
      return uint72(
        uint256(linear.yFactor << SIXTY_FOUR)
        | uint256(uint56(linear.y << EIGHT))
        | uint256(uint8(linear.method))
      );
    }
    // xFactor must be > 0
    unchecked {
      return uint256(
        (uint256(uint16(int16(linear.x)) - uint16(int16(MIN_INT_16))) << FIFTY_SIX)
        | (uint256(uint8(linear.yFactor)) << FOURTY_EIGHT)
        | (uint256(uint16(linear.y)) << THIRTY_TWO)
        | (uint256(uint8(linear.bFactor)) << TWENTY_FOUR)
        | (uint256(uint16(int16(linear.b)) - uint16(int16(MIN_INT_16))) << EIGHT)
        | uint256(uint8((linear.xFactor * X_OPTIONS) + linear.method))
      );
    }
  }
  /**
   * decode an b+(x/y) slope from a number and scale it to your preference
   * @param encodedLinear holds all relevant data for filling out a Linear struct
   * @return linear the full set of parameters to describe a (x/y)+b pattern
   * @notice this limits the bFactor from scaling beyond 2^84, which should be enough for most use cases
   */
  function decodeLinear(uint256 encodedLinear) external pure returns (Linear memory linear) {
    return _decodeLinear({
      encodedLinear: encodedLinear
    });
  }
  function _decodeLinear(uint256 encodedLinear) internal pure returns (Linear memory linear) {
    // only first 72 bits of magnitude are read / relevant for our purposes
    unchecked {
      uint256 method = uint8(encodedLinear);
      linear.xFactor = method / X_OPTIONS;
      linear.method = method % X_OPTIONS;
      // when xFactor is 0, nothing below makes a difference except y
      if (linear.xFactor == ZERO) {
        // y is being used because it is the only uint
        linear.y = (uint256(uint56(encodedLinear >> EIGHT)) << uint8(encodedLinear >> SIXTY_FOUR));
        return linear;
      }
      // numerator
      linear.x = int16(uint16(encodedLinear >> FIFTY_SIX)) + int16(-MIN_INT_16);
      // denominator - uint
      linear.yFactor = uint256(uint8(encodedLinear >> FOURTY_EIGHT)); // udd*(4^sfd)=d
      linear.y = uint16(encodedLinear >> THIRTY_TWO);
      // offset
      linear.bFactor = uint256(uint8(encodedLinear >> TWENTY_FOUR)); // b*(2^sfn)=b
      linear.b = int16(uint16(encodedLinear >> EIGHT)) + int16(-MIN_INT_16);
    }
  }
  /**
   * compute a magnitude given an x and y
   * @param limit a limit that the uint result can not be greater than
   * @param linear the linear data to describe an (x/y)+b relationship
   * @param v2 the second value as input
   * @param v1 the stake to use as an input for the second value
   */
  function computeMagnitude(
    uint256 limit, Linear calldata linear,
    uint256 v2, uint256 v1
  ) external pure returns(uint256 result) {
    // the reason that this condition is necessary is because "method" of a decoded linear struct
    // will range between 0-2 with the first two existing as special cases.
    // after the range 0-2, the linear treatment turns into something actually linear (x/y)+b
    // the flag of xFactor == 0 in combination with method = 0 is used to get around this,
    // or act as a boolean to signal special treatment of 1 and 2
    if (limit == ZERO) {
      return ZERO;
    }
    if (linear.method == ZERO) {
      if (linear.xFactor == ZERO) {
        return ZERO;
      }
    }
    return _computeMagnitude({
      limit: limit,
      linear: _encodeLinear(linear),
      v2: v2,
      v1: v1
    });
  }
  /**
   * compute a day magnitude given an x and y
   * @param limit a limit that the uint result can not be greater than
   * @param method the method to use to compute the result
   * @param x the first value as input
   * @param today the hex day value
   * @param lockedDay the day that the stake was locked
   * @param stakedDays the number of full days that the stake was locked
   */
  function computeDayMagnitude(
    uint256 limit, uint256 method, uint256 x,
    uint256 today,
    uint256 lockedDay,
    uint256 stakedDays
  ) external pure returns(uint256 newMethod, uint256 numDays) {
    if (limit == ZERO || method == ZERO) {
      return (method, ZERO);
    }
    return _computeDayMagnitude({
      limit: limit,
      method: method,
      x: x,
      today: today,
      lockedDay: lockedDay,
      stakedDays: stakedDays
    });
  }
}

// File: contracts/MaximusStakeManager.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { IPublicEndStakeable } from "./interfaces/IPublicEndStakeable.sol";
import { IExternalPerpetualFilter } from "./interfaces/IExternalPerpetualFilter.sol";
import { HSIStakeManager } from "./HSIStakeManager.sol";
import { IGasReimberser } from './interfaces/IGasReimberser.sol';

contract MaximusStakeManager is HSIStakeManager {
  using Address for address payable;
  address public externalPerpetualSetter;
  address public externalPerpetualFilter;
  mapping(address perpetual => bool isPerpetual) public perpetualWhitelist;
  /**
   * bytes32 is a key made up of the perpetual whitelist address + the iteration of the stake found at
   */
  mapping(address perpetual => mapping(uint256 period => address to)) public rewardsTo;
  /**
   * emitted when a contract is added to the whitelist
   * @param perpetual the perpetual contract added to the whitelist
   */
  event AddPerpetual(address indexed perpetual);
  /**
   * collect a reward from a given perpetual within a period
   * @param perpetual the perpetual contract being targeted
   * @param period the period, managed internally by the perpetual
   * @param token the token being rewarded
   * @param amount the amount of a token being rewarded
   */
  event CollectReward(
    address indexed perpetual,
    uint256 indexed period,
    address indexed token,
    uint256 amount
  );
  /**
   * a list of known perpetual contracts is set during constructor
   */
  constructor() {
    _addPerpetual(0x0d86EB9f43C57f6FF3BC9E23D8F9d82503f0e84b); // maxi
    _addPerpetual(0x6b32022693210cD2Cfc466b9Ac0085DE8fC34eA6); // deci
    _addPerpetual(0x6B0956258fF7bd7645aa35369B55B61b8e6d6140); // lucky
    _addPerpetual(0xF55cD1e399e1cc3D95303048897a680be3313308); // trio
    _addPerpetual(0xe9f84d418B008888A992Ff8c6D22389C2C3504e0); // base
    externalPerpetualSetter = msg.sender;
  }
  /**
   * sets the extended perpetual filter to allow for other perpetual contracts
   * to pass through the filter and added at a later date
   * @param _externalPerpetualFilter the extended perpetual filter set by the creator of this contract
   */
  function setExternalPerpetualFilter(address _externalPerpetualFilter) external {
    if (msg.sender != externalPerpetualSetter) revert NotAllowed();
    externalPerpetualSetter = address(0);
    externalPerpetualFilter = _externalPerpetualFilter;
  }
  /**
   * check if a given contract can pass through the perpetual filter
   * @param perpetual the perpetual contract to check
   * @return isPerpetual when address has passed through the filter or extended filter
   */
  function checkPerpetual(address perpetual) external returns(bool isPerpetual) {
    return _checkPerpetual({
      perpetual: perpetual
    });
  }
  /**
   * check if a given contract can pass through the perpetual filter
   * @param perpetual the perpetual contract to check
   * @return isPerpetual when address has passed through the filter or extended filter
   * after passing through extended filter, the result is cached
   */
  function _checkPerpetual(address perpetual) internal returns(bool isPerpetual) {
    if (perpetualWhitelist[perpetual]) return true;
    address _externalPerpetualFilter = externalPerpetualFilter;
    if (_externalPerpetualFilter == address(0)) return false;
    bool result = IExternalPerpetualFilter(_externalPerpetualFilter).verifyPerpetual({
      perpetual: perpetual
    });
    if (result) {
      _addPerpetual(perpetual);
      return true;
    }
    return false;
  }
  /**
   * adds new perpetual contract to the whitelist
   * Once a perpetual is whitelisted it cannot be removed
   * @param perpetual the perpetual address to add to the persistent mapping
   */
  function _addPerpetual(address perpetual) internal {
    perpetualWhitelist[perpetual] = true;
    emit AddPerpetual(perpetual);
  }
  /**
   * end a stake on a known perpetual
   * @param rewarded the address to reward with tokens
   * @param perpetual the perpetual to end a stake on
   * @param stakeId the stake id to end
   */
  function stakeEndAs(address rewarded, address perpetual, uint256 stakeId) external {
    if (!_checkPerpetual(perpetual)) revert NotAllowed();
    IPublicEndStakeable endable = IPublicEndStakeable(perpetual);
    // STAKE_END_DAY is locked + staked days - 1 so > is correct in this case
    if (_checkEndable(endable)) {
      endable.mintHedron(ZERO, uint40(stakeId));
      endable.endStakeHEX(ZERO, uint40(stakeId));
      // by now we have incremented by 1 since the start of this function
      uint256 currentPeriod = endable.getCurrentPeriod();
      rewardsTo[perpetual][currentPeriod] = rewarded;
      // add 1 because the period will increment at the next stake start (1 week's time)
      // so this contract should also recognize that range until the end
      rewardsTo[perpetual][currentPeriod + 1] = rewarded;
    }
  }
  /**
   * checks if a given perpetual is endable
   * @param endable the endable perpetual contract
   * @return isEndable denotes whether or not the stake is endable
   */
  function _checkEndable(IPublicEndStakeable endable) internal view returns(bool isEndable) {
    if (_currentDay() > endable.STAKE_END_DAY()) {
      return endable.STAKE_IS_ACTIVE();
    }
  }
  /**
   * checks if a given perpetual is endable
   * @param endable the endable perpetual contract
   * @return isEndable verifies that the provided address is endable
   */
  function checkEndable(address endable) external view returns(bool isEndable) {
    return _checkEndable(IPublicEndStakeable(endable));
  }
  /**
   * flush erc20 tokens into this contract
   * @param gasReimberser the address to collect gas reimbursement from
   * @param perpetual the perpetual pool to call flush on
   * @param period the period that the token collects against
   * @param tokens the token addresses to flush into this contract
   * @notice this assumes that only one token is flushed at a time
   * accounting will be lost if this patterns is broken by distribution tokens
   * or perpetual sending more than one token at a time
   * @dev this method should not be chained to a stake end - it should be done in a separate transaction
   */
  function flush(
    address gasReimberser,
    address perpetual,
    uint256 period,
    address[] calldata tokens
  ) external {
    if (!perpetualWhitelist[perpetual]) revert NotAllowed();
    if (IPublicEndStakeable(perpetual).getCurrentPeriod() != period) {
      return;
    }
    uint256 len = tokens.length;
    uint256 i;
    do {
      address token = tokens[i];
      uint256 bal = _getTokenBalance({
        token: token
      });
      if (token == address(0)) {
        IGasReimberser(payable(gasReimberser)).flush();
      } else {
        IGasReimberser(payable(gasReimberser)).flush_erc20(token);
      }
      // bal now represents delta
      bal = _getTokenBalance({
        token: token
      }) - bal;
      address to = rewardsTo[perpetual][period];
      emit CollectReward(perpetual, period, token, bal);
      unchecked {
        withdrawableBalanceOf[token][to] += bal;
        attributed[token] += bal;
        ++i;
      }
    } while (i < len);
  }
}

// File: contracts/MulticallExtension.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

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

/**
 * this multicall extension is useful for chaining permissioned calls
 * in other words, calls that operate on the senders funds or settings
 */
contract MulticallExtension is Utils {
  error BlockHash(bytes32 expected, bytes32 actual);
  error Deadline(uint256 deadline, uint256 currentTime);
  event TxFailed(uint256 indexed index, bytes result);
  /**
   * call a series of functions on a contract that inherits this method
   * @param calls the calls to perform on this contract
   * @param allowFailures whether to allow failures or to error out
   */
  function multicall(
    bytes[] calldata calls,
    bool allowFailures
  ) external {
    _multicall({
      calls: calls,
      allowFailures: allowFailures
    });
  }
  /**
   * call multiple methods and pass a deadline, after which the transaction should fail
   * @param deadline the timestamp, in seconds
   * @param calls the calldata to run on the external method
   * @param allowFailures allows failures when true
   */
  function multicallWithDeadline(
    uint256 deadline,
    bytes[] calldata calls,
    bool allowFailures
  ) external {
    if (block.timestamp > deadline) {
      revert Deadline({
        deadline: deadline,
        currentTime: block.timestamp
      });
    }
    _multicall({
      calls: calls,
      allowFailures: allowFailures
    });
  }
  /**
   * pass the previous block hash to enable mev uncle bandit protection
   * @param previousBlockhash the previously mined block - useful for mev protected uncle bandit risks
   * @param calls the calldata to run on the external method
   * @param allowFailures allows failures when true
   */
  function multicallWithPreviousBlockHash(
    bytes32 previousBlockhash,
    bytes[] calldata calls,
    bool allowFailures
  ) external {
    if (blockhash(block.number - 1) != previousBlockhash) {
      revert BlockHash({
        expected: previousBlockhash,
        actual: blockhash(block.number - 1)
      });
    }
    _multicall({
      calls: calls,
      allowFailures: allowFailures
    });
  }
  /**
   * call multiple / arbitrary steps allowing each to fail independently or requiring all to succeed
   * @param calls the sequence of calls that is requested
   * @param allowFailures allows the calls to fail separately or requires all to succeed or fail
   */
  function _multicall(bytes[] calldata calls, bool allowFailures) internal {
    uint256 len = calls.length;
    uint256 i;
    bool success;
    bytes memory result;
    do {
      (success, result) = address(this).delegatecall(calls[i]);
      if (!success) {
        if (allowFailures) {
          emit TxFailed({
            index: i,
            result: result
          });
        } else {
          _bubbleRevert(result);
        }
      }
      unchecked {
        ++i;
      }
    } while (i < len);
  }
}

// File: contracts/SingletonHedronManager.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { IHedron } from "./interfaces/IHedron.sol";
import { UnderlyingStakeManager } from "./UnderlyingStakeManager.sol";

contract SingletonHedronManager is UnderlyingStakeManager {
  /**
   * combine a boolean from a settings value and the owner address to
   * reduce the number of transfers / writes that occur during a loop
   * @param settings the settings to determine if a withdrawal should occur
   * @param owner the owner of the underlying stake
   * @return to the uint256 representation of a single bit in settings and an owner address
   */
  function createTo(uint256 settings, address owner) external pure returns(uint256 to) {
    return _createTo({
      settings: settings,
      owner: owner
    });
  }
  function _createTo(uint256 settings, address owner) internal pure returns(uint256 to) {
    unchecked {
      return ((_isOneAtIndex({
        settings: settings,
        index: INDEX_RIGHT_SHOULD_SEND_TOKENS_TO_STAKER
      }) ? ONE : ZERO) << ADDRESS_BIT_LENGTH) | uint160(owner);
    }
  }
  /**
   * mint rewards and transfer them to a provided address
   * @param stakeIds list of stake ids to mint
   * @notice any combination of owners can be passed, however, it is most efficient to order the hsi address by owner
   */
  function mintHedronRewards(uint256[] calldata stakeIds) external {
    uint256 len = stakeIds.length;
    uint256 i;
    uint256 hedronTokens;
    address currentOwner;
    uint256 stakeIndex;
    uint256 stakeId;
    uint256 settings = stakeIdToSettings[stakeId];
    uint256 to = _createTo(settings, _stakeIdToOwner({
      stakeId: stakeIds[ZERO]
    }));
    unchecked {
      do {
        stakeId = stakeIds[i];
        (stakeIndex, currentOwner) = _stakeIdToInfo({
          stakeId: stakeId
        });
        settings = stakeIdToSettings[stakeId];
        if (msg.sender == currentOwner || _isOneAtIndex({
          settings: settings,
          index: INDEX_RIGHT_CAN_MINT_HEDRON
        })) {
          uint256 currentTo = _createTo(settings, currentOwner);
          if (currentTo != to) {
            _attributeFunds({
              settings: settings,
              token: HEDRON,
              staker: address(uint160(to)),
              amount: hedronTokens
            });
            hedronTokens = ZERO;
          }
          to = currentTo;
          hedronTokens += _mintHedron({
            index: stakeIndex,
            stakeId: stakeId
          });
        }
        ++i;
      } while (i < len);
      if (hedronTokens > ZERO) {
        _attributeFunds({
          settings: settings,
          token: HEDRON,
          staker: address(uint160(to)),
          amount: hedronTokens
        });
      }
    }
  }
  function _mintHedron(uint256 index, uint256 stakeId) internal virtual returns(uint256 amount) {
    return IHedron(HEDRON).mintNative(index, uint40(stakeId));
  }
}

// File: contracts/StakeEnder.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { UnderlyingStakeable } from "./UnderlyingStakeable.sol";
import { SingletonHedronManager } from "./SingletonHedronManager.sol";
import { Magnitude } from "./Magnitude.sol";

contract StakeEnder is Magnitude, SingletonHedronManager {
  uint8 public constant INDEX_RIGHT_TODAY = 128;

  /**
   * end a stake for someone other than the sender of the transaction
   * @param stakeId the stake id on the underlying contract to end
   */
  function stakeEndByConsent(uint256 stakeId) external payable returns(
    uint256 delta, uint256 count
  ) {
    return _stakeEndByConsentWithTipTo({
      stakeId: stakeId,
      tipTo: address(0)
    });
  }
  function stakeEndByConsentWithTipTo(
    uint256 stakeId, address tipTo
  ) external payable returns(uint256 delta, uint256 count) {
    return _stakeEndByConsentWithTipTo({
      stakeId: stakeId,
      tipTo: tipTo
    });
  }
  function _stakeEndByConsentWithTipTo(
    uint256 stakeId, address tipTo
  ) internal returns(uint256 delta, uint256 count) {
    unchecked {
      return _stakeEndByConsent({
        stakeId: stakeId,
        tipTo: tipTo,
        count: (_currentDay() << INDEX_RIGHT_TODAY) | _stakeCount({
          staker: address(this)
        })
      });
    }
  }
  /**
   * end a stake with the consent of the underlying staker's settings
   * @param stakeId the stake id to end
   * @return delta the amount of hex at the end of the stake
   * @notice hedron minting happens as last step before end stake
   * @dev the stake count is today | stake count because
   * if there were 2 variables, the contract ended up too large
   */
  function _stakeEndByConsent(
    uint256 stakeId, address tipTo, uint256 count
  ) internal returns(uint256 delta, uint256) {
    unchecked {
      address staker;
      uint256 idx;
      UnderlyingStakeable.StakeStore memory stake;
      {
        bool valid;
        (valid, staker, idx, stake) = _getStakeInfo(stakeId);
        if (!valid) {
          return (ZERO, count);
        }
      }
      uint256 settings = stakeIdToSettings[stakeId];
      if (!_isOneAtIndex({
        settings: settings,
        index: INDEX_RIGHT_CAN_STAKE_END
      })) {
        return (ZERO, count);
      }
      if (_isEarlyEnding({
        lockedDay: stake.lockedDay,
        stakedDays: stake.stakedDays,
        targetDay: count >> INDEX_RIGHT_TODAY
      })) {
        if (!_isOneAtIndex({
          settings: settings,
          index: INDEX_RIGHT_CAN_EARLY_STAKE_END
        })) return (ZERO, count);
      }
      if (_isOneAtIndex({
        settings: settings,
        index: INDEX_RIGHT_CAN_MINT_HEDRON_AT_END
      })) {
        // consent has been confirmed
        uint256 hedronAmount = _mintHedron(idx, stakeId);
        if (uint8(settings >> INDEX_RIGHT_HEDRON_TIP) > ZERO) {
          uint256 hedronTip = _computeMagnitude({
            limit: hedronAmount,
            linear: uint72(settings >> INDEX_RIGHT_HEDRON_TIP),
            v2: hedronAmount,
            v1: ZERO
          });
          if (hedronTip > ZERO) {
            hedronAmount = hedronAmount - hedronTip;
            if (tipTo != address(0)) {
              _addToTokenWithdrawable({
                token: HEDRON,
                to: tipTo,
                amount: hedronTip
              });
            }
            emit Tip({
              stakeId: stakeId,
              token: HEDRON,
              to: tipTo,
              amount: hedronTip
            });
          }
        }
        if (hedronAmount > ZERO) {
          _attributeFunds({
            settings: settings,
            token: HEDRON,
            staker: staker,
            amount: hedronAmount
          });
        }
      }
      // if this were to ever overflow then it will fail
      // in the subsequent stake end method since
      // hex can only hold 2^40-1 stakes
      --count;
      delta = _stakeEnd({
        stakeIndex: idx,
        stakeId: stakeId,
        stakeCountAfter: uint128(count)
      });
      // direct funds after end stake
      // only place the stake struct exists is in memory in this method
      if (uint8(settings >> INDEX_RIGHT_TARGET_TIP) > ZERO) {
        uint256 targetTip = _computeMagnitude({
          limit: delta,
          linear: uint72(settings >> INDEX_RIGHT_TARGET_TIP),
          v2: delta,
          v1: stake.stakedHearts
        });
        if (targetTip > ZERO) {
          delta -= targetTip;
          if (tipTo != address(0)) {
            _addToTokenWithdrawable({
              token: TARGET,
              to: tipTo,
              amount: targetTip
            });
          }
          emit Tip({
            stakeId: stakeId,
            token: TARGET,
            to: tipTo,
            amount: targetTip
          });
        }
      }
      uint256 nextStakeId;
      if (delta > ZERO) {
        if (uint8(settings >> INDEX_RIGHT_NEW_STAKE) > ZERO) {
          uint256 newStakeAmount = _computeMagnitude({
            limit: delta,
            linear: uint72(settings >> INDEX_RIGHT_NEW_STAKE),
            v2: delta,
            v1: stake.stakedHearts
          });
          if (newStakeAmount > ZERO) {
            // this does not account for minimum newStakeAmount to have at least 1 share
            // it is too expensive to check in the middle of this tight loop without
            // crazy high costs - for the next 15 years, it should be done off chain
            // stakes small enough to be anywhere near this
            // threshold should simply be ignored
            uint256 newStakeDaysMethod = uint8(settings >> INDEX_RIGHT_NEW_STAKE_DAYS_METHOD);
            if (newStakeDaysMethod > ZERO) {
              uint256 newStakeDays;
              (newStakeDaysMethod, newStakeDays) = _computeDayMagnitude({
                limit: MAX_DAYS,
                method: newStakeDaysMethod,
                x: uint16(settings >> INDEX_RIGHT_NEW_STAKE_DAYS_MAGNITUDE),
                today: count >> INDEX_RIGHT_TODAY,
                lockedDay: stake.lockedDay,
                stakedDays: stake.stakedDays
              });
              settings = (
                (settings >> INDEX_RIGHT_NEW_STAKE) << INDEX_RIGHT_NEW_STAKE
                | (newStakeDaysMethod << THIRTY_TWO) // only 0-4
                | uint32(settings)
              );
              delta -= newStakeAmount; // already checked for underflow
              nextStakeId = _stakeStartFor({
                owner: staker,
                amount: newStakeAmount,
                newStakedDays: newStakeDays,
                index: uint128(count)
              });
              ++count;
            }
          }
        }
      }
      if (delta > ZERO) {
        _attributeFunds({
          settings: settings,
          token: TARGET,
          staker: staker,
          amount: delta
        });
      }
      if (_isOneAtIndex({
        settings: settings,
        index: INDEX_RIGHT_HAS_EXTERNAL_TIPS
      })) {
        uint256 nextStakeTipsLength = _executeTipList({
          stakeId: stakeId,
          staker: staker,
          tipTo: tipTo,
          nextStakeId: _isOneAtIndex({
            settings: settings,
            index: INDEX_RIGHT_COPY_EXTERNAL_TIPS
          }) ? nextStakeId : ZERO
        });
        if (nextStakeTipsLength > ZERO) {
          // add settings to flag tips as existing in new settings
          settings = settings | (ONE << INDEX_RIGHT_HAS_EXTERNAL_TIPS);
        }
      }
      if (nextStakeId > ZERO) {
        // settings will be maintained for the new stake
        // note, because 0 is used, one often needs to use x-1
        // for the number of times you want to copy
        // but because permissions are maintained, it may end up
        // being easier to think about it as x-2
        settings = (_decrementCopyIterations({
          settings: settings
        }) >> INDEX_RIGHT_CAN_MINT_HEDRON << INDEX_RIGHT_CAN_MINT_HEDRON) | ONE;
        _logSettingsUpdate({
          stakeId: nextStakeId,
          settings: settings
        });
      }
      return (delta, count);
    }
  }
  function stakeEndByConsentForMany(uint256[] calldata stakeIds) external payable {
    _stakeEndByConsentForManyWithTipTo({
      stakeIds: stakeIds,
      tipTo: address(0)
    });
  }
  function stakeEndByConsentForManyWithTipTo(uint256[] calldata stakeIds, address tipTo) external payable {
    _stakeEndByConsentForManyWithTipTo({
      stakeIds: stakeIds,
      tipTo: tipTo
    });
  }
  /**
   * end many stakes at the same time
   * provides an optimized path for all stake ends
   * and assumes that detectable failures should be skipped
   * @param stakeIds stake ids to end
   * @notice this method should, generally, only be called when multiple enders
   * are attempting to end stake the same stakes
   */
  function _stakeEndByConsentForManyWithTipTo(uint256[] calldata stakeIds, address tipTo) internal {
    uint256 i;
    uint256 len = stakeIds.length;
    uint256 count;
    unchecked {
      count = (_currentDay() << INDEX_RIGHT_TODAY) | _stakeCount({
        staker: address(this)
      });
      do {
        (, count) = _stakeEndByConsent({
          stakeId: stakeIds[i],
          tipTo: tipTo,
          count: count
        });
        ++i;
      } while(i < len);
    }
  }
  function _stakeEnd(
    uint256 stakeIndex, uint256 stakeId, uint256 stakeCountAfter
  ) internal virtual override returns(uint256) {
    if (stakeIdToSettings[stakeId] > ZERO) {
      stakeIdToSettings[stakeId] = ZERO;
    }
    return super._stakeEnd(stakeIndex, stakeId, stakeCountAfter);
  }
}

// File: contracts/StakeInfo.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

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

contract StakeInfo is Magnitude {
  /**
   * @notice the owner of a stake indexed by the stake id
   * index + 160(owner)
   */
  mapping(uint256 stakeId => uint256 info) public stakeIdInfo;
  /**
   * @notice this error is thrown when the stake in question
   * is not owned by the expected address
   */
  error StakeNotOwned(address provided, address expected);
  error StakeNotCustodied(uint256 stakeId);
  /**
   * verify the ownership of a stake given its id
   * @param owner the supposed owner of the stake
   * @param stakeId the id of the stake in question
   * @notice error occurs if owner does not match
   */
  function verifyStakeOwnership(address owner, uint256 stakeId) external view {
    _verifyStakeOwnership(owner, stakeId);
  }
  /**
   * verify the ownership of a stake given its id
   * @param owner the supposed owner of the stake
   * @param stakeId the id of the stake in question
   * @notice StakeNotOwned error occurs if owner does not match
   */
  function _verifyStakeOwnership(address owner, uint256 stakeId) internal view {
    if (_stakeIdToOwner(stakeId) != owner) {
      revert StakeNotOwned(owner, _stakeIdToOwner(stakeId));
    }
  }
  /**
   * verify that this contract knows the owner of a given stake id
   * and is acting as custodian for said owner
   * @param stakeId the stake id to verify custodialship over
   * @notice StakeNotCustodied error occurs if owner is not known
   */
  function verifyCustodian(uint256 stakeId) external view {
    _verifyCustodian(stakeId);
  }
  /**
   * verify that this contract knows the owner of a given stake id
   * and is acting as custodian for said owner
   * @param stakeId the stake id to verify custodialship over
   * @notice StakeNotCustodied error occurs if owner is not known
   */
  function _verifyCustodian(uint256 stakeId) internal view {
    if (_stakeIdToOwner(stakeId) == address(0)) {
      revert StakeNotCustodied(stakeId);
    }
  }
  /**
   * get the owner of the stake id - the account that has rights over
   * the stake's settings and ability to end it outright
   * @param stakeId the stake id in question
   * @return owner of the stake at the provided id
   * @notice value will be address(0) for unknown
   */
  function stakeIdToOwner(uint256 stakeId) external view returns(address owner) {
    return _stakeIdToOwner(stakeId);
  }
  /**
   * access the owner of a given stake id
   * @param stakeId the stake id in question
   * @return owner of a given stake id
   * @notice value will be address(0) for unknown
   */
  function _stakeIdToOwner(uint256 stakeId) internal view returns(address owner) {
    unchecked {
      return address(uint160(stakeIdInfo[stakeId]));
    }
  }
  /**
   * get the info of a stake given it's id. The index must match
   * the index of the stake in the hex/hedron contract
   * @param stakeId the stake id to get info for
   * @return index of the stake id in the hex list
   * @return owner of the stake
   */
  function stakeIdToInfo(uint256 stakeId) external view returns(uint256 index, address owner) {
    return _stakeIdToInfo(stakeId);
  }
  /**
   * retrieve the index and owner of a stake id
   * @param stakeId the id of the stake in question
   * @return index the index of the stake in the hex list or the hsim list
   * @return owner the owner of the stake
   * @notice for a non custodied stake, the index is 0 and the owner is address(0)
   */
  function _stakeIdToInfo(uint256 stakeId) internal view returns(uint256 index, address owner) {
    uint256 info = stakeIdInfo[stakeId];
    unchecked {
      return (info >> ADDRESS_BIT_LENGTH, address(uint160(info)));
    }
  }
  /**
   * the index of the stake id - useful when indexes are moving around
   * and could be moved by other people
   * @param stakeId the stake id to target
   * @return index of the stake in the targeted list
   */
  function stakeIdToIndex(uint256 stakeId) external view returns(uint256 index) {
    return _stakeIdToIndex(stakeId);
  }
  /**
   * the index of the stake id - useful when indexes are moving around
   * and could be moved by other people
   * @param stakeId the stake id to target
   * @return index of the stake in the targeted list
   */
  function _stakeIdToIndex(uint256 stakeId) internal view returns(uint256 index) {
    unchecked {
      return stakeIdInfo[stakeId] >> ADDRESS_BIT_LENGTH;
    }
  }
  /**
   * encode an index and owner pair to track under a single sload
   * @param index index of a stake
   * @param owner the owner of a stake
   * @return info the encoded uint256 that can be decoded to the index and owner
   */
  function encodeInfo(uint256 index, address owner) external pure returns(uint256 info) {
    return _encodeInfo({
      index: index,
      owner: owner
    });
  }
  /**
   * encode an index and owner pair to track under a single sload
   * @param index index of a stake
   * @param owner the owner of a stake
   * @return info the encoded uint256 that can be decoded to the index and owner
   */
  function _encodeInfo(uint256 index, address owner) internal pure returns(uint256 info) {
    unchecked {
      return (index << ADDRESS_BIT_LENGTH) | uint160(owner);
    }
  }
}

// File: contracts/StakeManager.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

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

contract StakeManager is TransferableStakeManager {}

// File: contracts/StakeStarter.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

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

contract StakeStarter is StakeEnder {
  /**
   * stake a given number of tokens for a given number of days
   * @param to the address that will own the staker
   * @param amount the number of tokens to stake
   * @param newStakedDays the number of days to stake for
   */
  function stakeStartFromBalanceFor(
    address to,
    uint256 amount,
    uint256 newStakedDays,
    uint256 settings
  ) external payable returns(uint256 stakeId) {
    _depositTokenFrom({
      token: TARGET,
      depositor: msg.sender,
      amount: amount
    });
    // tokens are essentially unattributed at this point
    stakeId = _stakeStartFor({
      owner: to,
      amount: amount,
      newStakedDays: newStakedDays,
      index: _stakeCount(address(this))
    });
    _logPreservedSettingsUpdate({
      stakeId: stakeId,
      settings: settings
    });
  }
  /**
   * start a numbeer of stakes for an address from the withdrawable
   * @param to the account to start a stake for
   * @param amount the number of tokens to start a stake for
   * @param newStakedDays the number of days to stake for
   */
  function stakeStartFromWithdrawableFor(
    address to,
    uint256 amount,
    uint256 newStakedDays,
    uint256 settings
  ) external payable returns(uint256 stakeId) {
    stakeId = _stakeStartFor({
      owner: to,
      amount: _deductWithdrawable({
        token: TARGET,
        account: msg.sender,
        amount: amount
      }),
      newStakedDays: newStakedDays,
      index: _stakeCount({
        staker: address(this)
      })
    });
    _logPreservedSettingsUpdate({
      stakeId: stakeId,
      settings: settings
    });
  }
  /**
   * stake a number of tokens for a given number of days, pulling from
   * the unattributed tokens in this contract
   * @param to the owner of the stake
   * @param amount the amount of tokens to stake
   * @param newStakedDays the number of days to stake
   */
  function stakeStartFromUnattributedFor(
    address to,
    uint256 amount,
    uint256 newStakedDays,
    uint256 settings
  ) external payable returns(uint256 stakeId) {
    stakeId = _stakeStartFor({
      owner: to,
      amount: _clamp(amount, _getUnattributed(TARGET)),
      newStakedDays: newStakedDays,
      index: _stakeCount(address(this))
    });
    _logPreservedSettingsUpdate({
      stakeId: stakeId,
      settings: settings
    });
  }
}

// File: contracts/test/GasReimberser.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Utils } from "../Utils.sol";
import { IGasReimberser } from "../interfaces/IGasReimberser.sol";
import { IPoolContract } from "../interfaces/IPoolContract.sol";

// this contract was modeled after the following tweet:
// https://twitter.com/TantoNomini/status/1630677746795057152
contract GasReimberser is IGasReimberser, Utils {
  using Address for address payable;
  address public immutable POOL_ADDRESS;
  constructor(address poolAddress) {
    POOL_ADDRESS = poolAddress;
  }
  receive() external payable {}
  function flush() external {
    IPoolContract pc = IPoolContract(POOL_ADDRESS);
    address payable ender = payable(pc.getEndStaker());
    require(msg.sender == ender, "Only End Staker can run this function.");
    uint256 amount = address(this).balance;
    if (amount > 0) {
      ender.sendValue(amount);
    }
  }
  function flush_erc20(address token_contract_address) external {
    IPoolContract pc = IPoolContract(POOL_ADDRESS);
    address ender = pc.getEndStaker();
    require(msg.sender == ender, "Only End Staker can run this function.");
    uint256 balance = IERC20(token_contract_address).balanceOf(address(this));
    if (balance > 0) {
      IERC20(token_contract_address).transfer(ender, balance);
    }
  }
}

// File: contracts/test/IMulticall.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

interface IMulticall3 {
  struct Call {
    address target;
    bytes callData;
  }

  struct Call3 {
    address target;
    bool allowFailure;
    bytes callData;
  }

  struct Call3Value {
    address target;
    bool allowFailure;
    uint256 value;
    bytes callData;
  }

  struct Result {
    bool success;
    bytes returnData;
  }

  function aggregate(Call[] calldata calls)
    external
    payable
    returns (uint256 blockNumber, bytes[] memory returnData);

  function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData);

  function aggregate3Value(Call3Value[] calldata calls)
    external
    payable
    returns (Result[] memory returnData);

  function blockAndAggregate(Call[] calldata calls)
    external
    payable
    returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData);

  function getBasefee() external view returns (uint256 basefee);

  function getBlockHash(uint256 blockNumber) external view returns (bytes32 blockHash);

  function getBlockNumber() external view returns (uint256 blockNumber);

  function getChainId() external view returns (uint256 chainid);

  function getCurrentBlockCoinbase() external view returns (address coinbase);

  function getCurrentBlockDifficulty() external view returns (uint256 difficulty);

  function getCurrentBlockGasLimit() external view returns (uint256 gaslimit);

  function getCurrentBlockTimestamp() external view returns (uint256 timestamp);

  function getEthBalance(address addr) external view returns (uint256 balance);

  function getLastBlockHash() external view returns (bytes32 blockHash);

  function tryAggregate(bool requireSuccess, Call[] calldata calls)
    external
    payable
    returns (Result[] memory returnData);

  function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls)
    external
    payable
    returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData);
}

// File: contracts/test/MockExternalPerpetualFilter.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

import { IExternalPerpetualFilter } from "../interfaces/IExternalPerpetualFilter.sol";

contract MockExternalPerpetualFilter is IExternalPerpetualFilter {
  bool internal _isPerpetual = false;
  function setVerifyPerpetualResult(bool result) external {
    _isPerpetual = result;
  }
  function verifyPerpetual(address) external view returns(bool) {
    return _isPerpetual;
  }
}

// File: contracts/test/MockPerpetual.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

import { IPublicEndStakeable } from "../interfaces/IPublicEndStakeable.sol";
import { IHEX } from "../interfaces/IHEX.sol";
import { IHedron } from "../interfaces/IHedron.sol";
import { UnderlyingStakeable } from "../UnderlyingStakeable.sol";

contract MockPerpetual is IPublicEndStakeable {
  address internal constant TARGET = 0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39;
  address internal constant HEDRON = 0x3819f64f282bf135d62168C1e513280dAF905e06;
  function startStakeHEX() external {
    uint256 lockedDays = 1;
    STAKE_END_DAY = UnderlyingStakeable(TARGET).currentDay() + lockedDays;
    IHEX(TARGET).stakeStart(IHEX(TARGET).balanceOf(address(this)), lockedDays);
  }
  uint256 public STAKE_END_DAY = 0;
  bool public STAKE_IS_ACTIVE = true;
  uint256 public currentPeriod = 1;
  function mintHedron(uint256 stakeIndex, uint40 stakeIdParam) external {
    IHedron(HEDRON).mintNative(stakeIndex, stakeIdParam);
  }
  function endStakeHEX(uint256 stakeIndex, uint40 stakeIdParam) external {
    // these resets are handled by stake restarts
    // internally managed by perpetual contract
    STAKE_IS_ACTIVE = false;
    this.mintHedron(stakeIndex, stakeIdParam);
    IHEX(TARGET).stakeEnd(stakeIndex, stakeIdParam);
    currentPeriod++;
  }
  function getCurrentPeriod() external view returns (uint256) {
    return currentPeriod;
  }
}

// File: contracts/test/TransferReceiver.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { IStakeReceiver } from "../interfaces/IStakeReceiver.sol";

contract TransferReceiver is IStakeReceiver {
  event StakeReceived(address sender, uint256 stakeId);
  error FailedToReceive(uint256 stakeId);
  uint256 internal _shouldErr;
  function setReceiveAction(uint256 shouldErr) external {
    _shouldErr = shouldErr;
  }
  function onStakeReceived(address from, uint256 stakeId) external {
    if (_shouldErr == 0) {
      emit StakeReceived(from, stakeId);
      return;
    }
    if (_shouldErr == 1) revert();
    if (_shouldErr == 2) revert("Failed to receive");
    if (_shouldErr == 3) revert FailedToReceive({
      stakeId: stakeId
    });
    if (_shouldErr == 4) {
      uint256[] memory list = new uint256[](3);
      emit StakeReceived(from, list[list.length]);
    }
  }
}

// File: contracts/Tipper.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { UnderlyingStakeable } from "./UnderlyingStakeable.sol";
import { Bank } from "./Bank.sol";
import { CurrencyList } from "./CurrencyList.sol";
import { EncodableSettings } from "./EncodableSettings.sol";

abstract contract Tipper is Bank, UnderlyingStakeable, CurrencyList, EncodableSettings {
  // 2^56 is a lot harder to grief than 2^32
  uint256 internal constant INDEX_EXTERNAL_TIP_CURRENCY = 200;
  uint256 internal constant INDEX_EXTERNAL_TIP_CURRENCY_ONLY = INDEX_EXTERNAL_TIP_CURRENCY + ONE;
  uint256 internal constant INDEX_EXTERNAL_TIP_LIMIT = SEVENTY_TWO; // 128 bits long
  constructor()
    Bank()
    UnderlyingStakeable()
    CurrencyList()
    EncodableSettings()
  {
    _addCurrencyToList(address(0));
    // this line allows hex to be tipped by factor of basefee
    _addCurrencyToList(TARGET);
    _addCurrencyToList(HEDRON);
  }
  /**
   * @dev this mapping is needed for the case where a tip is added to a stake
   * but the staker ends the stake on a lower level which never checks for tips
   * this mapping slightly increases the cost of initializing tips as well as transferring them
   * but that is ok, because we generally do not want people to be trading stakes at this level
   * of anyone wants to be swapping ownership over stakes then they can create
   * an erc721 and trade at a higher level
   * also end stakers get a larger refund due to more information being zero'd out
   * it is set to internal because, generally, the stake id should be going
   * to the lower level `stakeIdInfo` mapping and individuals who do not wish to tip
   * should not be charged 2k gas for checking if this mapping exists
   */
  mapping(uint256 stakeId => address staker) internal tipStakeIdToStaker;
  mapping(uint256 stakeId => uint256[] tips) public stakeIdTips;
  event AddTip(
    uint256 indexed stakeId,
    address indexed token,
    uint256 indexed index,
    uint256 settings
  );
  event RemoveTip(
    uint256 indexed stakeId,
    address indexed token,
    uint256 indexed index,
    uint256 settings
  );
  /**
   * tip an address a defined amount and token
   * @param stakeId the stake id being targeted
   * @param token the token being accounted
   * @param to the address to attribute rewards to
   * @param amount the amount of the token
   */
  event Tip(
    uint256 indexed stakeId,
    address indexed token,
    address indexed to,
    uint256 amount
  );
  /**
   * check the count of a list of tips provided by the staker
   * @param stakeId the stake id to check the list of tips
   */
  function stakeIdTipSize(uint256 stakeId) external view returns(uint256) {
    return _stakeIdTipSize({
      stakeId: stakeId
    });
  }
  /**
   * check the count of a list of tips provided by the staker
   * @param stakeId the stake id to check the list of tips
   */
  function _stakeIdTipSize(uint256 stakeId) internal view returns(uint256) {
    return stakeIdTips[stakeId].length;
  }
  function computeTip(uint256 tip) external view returns(uint256 limit, uint256 consumed) {
    return _computeTip(tip);
  }
  function _computeTip(uint256 tip) internal view returns (uint256 limit, uint256 consumed) {
    unchecked {
      limit = uint128(tip >> INDEX_EXTERNAL_TIP_LIMIT);
      if (uint72(tip) == ZERO) {
        // checking for existance of a tip number
        // allows us to use ZERO as simplest pathway
        consumed = limit;
      } else {
        consumed = _computeMagnitude({
          limit: limit,
          linear: uint72(tip),
          v2: limit,
          v1: block.basefee
        });
      }
      return (limit, consumed);
    }
  }
  /**
   * execute a list of tips and leave them in the unattributed space
   * @param stakeId the stake id whose tips should be executed
   * @param staker the staker that owns the stake id
   * @param nextStakeId the next stake id if tips are to be copied / rolled over
   */
  function _executeTipList(
    uint256 stakeId, address staker, uint256 nextStakeId, address tipTo
  ) internal returns(uint256 nextStakeTipsLength) {
    uint256 i;
    uint256 len = stakeIdTips[stakeId].length;
    uint256 tip;
    uint256 cachedTip;
    uint256 limit;
    // disallows future removal of tips
    tipStakeIdToStaker[stakeId] = address(0);
    unchecked {
      do {
        // tips get executed in reverse order so that the contract
        // can clean itself up (gas refund) as it goes along
        tip = stakeIdTips[stakeId][len - ONE - i];
        cachedTip = tip;
        stakeIdTips[stakeId].pop();
        uint256 idx = tip << ONE >> INDEX_EXTERNAL_TIP_CURRENCY_ONLY;
        address token = indexToToken[idx];
        uint256 withdrawableBalance = withdrawableBalanceOf[token][staker];
        uint256 cachedWithdrawableBalance = withdrawableBalance;
        (limit, tip) = _computeTip(tip);
        // this is a refund
        withdrawableBalance += (limit - tip);
        if (tip > ZERO) {
          emit Tip({
            stakeId: stakeId,
            token: token,
            to: tipTo,
            amount: tip
          });
          if (tipTo == address(0)) {
            attributed[token] -= tip;
          } else {
            if (tipTo == staker) {
              withdrawableBalance += tip;
            } else {
              withdrawableBalanceOf[token][tipTo] = withdrawableBalanceOf[token][tipTo] + tip;
              // because attributed already has the tip, we should not double count it
            }
          }
        }
        if (_isOneAtIndex({
          settings: cachedTip,
          index: MAX_UINT_8
        })) {
          if (nextStakeId > ZERO) {
            limit = _clamp({
              amount: limit,
              max: withdrawableBalance
            });
            if (limit > ZERO) {
              cachedTip = _encodeTipSettings(true, idx, limit, cachedTip);
              withdrawableBalance -= limit;
              // we no longer need currency idx
              // so this line takes it over / reuses it
              stakeIdTips[nextStakeId].push(cachedTip);
              if (tipStakeIdToStaker[nextStakeId] == address(0)) {
                tipStakeIdToStaker[nextStakeId] = staker;
              }
              emit AddTip({
                stakeId: nextStakeId,
                token: token,
                index: nextStakeTipsLength,
                settings: cachedTip
              });
              ++nextStakeTipsLength;
            }
          }
        }
        if (withdrawableBalance != cachedWithdrawableBalance) {
          withdrawableBalanceOf[token][staker] = withdrawableBalance;
        }
        ++i;
      } while (i < len);
    }
  }
  /**
   * encodes a series of data in 32+96+64+64 to fit into 256 bits to define
   * how a tip should be executed
   * @param currencyIndex the index of the currency on the list
   * @param amount the number of tokens to delineate as tips
   * @param encodedLinear the method+xyb function to use
   */
  function encodeTipSettings(
    bool reusable,
    uint256 currencyIndex,
    uint256 amount,
    uint256 encodedLinear
  ) external pure returns(uint256) {
    return _encodeTipSettings({
      reusable: reusable,
      currencyIndex: currencyIndex,
      amount: amount,
      encodedLinear: encodedLinear
    });
  }
  /**
   * encodes tip settings into a uint256
   * @param reusable the tip can be reused if there is amount left over
   * @param currencyIndex the index of the currency on the list
   * @param amount the number of tokens deposited into the contract
   * @param encodedLinear an (x/y)+b equation inside of uint72
   */
  function _encodeTipSettings(
    bool reusable,
    uint256 currencyIndex,
    uint256 amount,
    uint256 encodedLinear
  ) internal pure returns(uint256) {
    if (uint8(encodedLinear) == ZERO) {
      if (encodedLinear != ZERO) {
        revert NotAllowed();
      }
    }
    unchecked {
      return uint256(reusable ? ONE : ZERO) << MAX_UINT_8
        | (uint256(currencyIndex) << INDEX_EXTERNAL_TIP_CURRENCY)
        | (uint256(uint128(amount)) << INDEX_EXTERNAL_TIP_LIMIT)
        | uint256(uint72(encodedLinear));
    }
  }
  /**
   * create a tip and back it with a token, to be executed by the stake ender
   * @param reusable the tip can be reused if value is still present after it has been executed
   * @param token the token to fund the tip
   * @param stakeId the stake id that the tip belongs to
   * @param amount the number of tokens to back the tip with use zero to move all withdrawableBalanceOf value
   * @param encodedLinear the (x/y)+b equation to define how much of the tip to spend
   * @return index of the tip in the list
   * @return tipAmount the final backing value of the tip
   */
  function depositAndAddTipToStake(
    bool reusable,
    address token,
    uint256 stakeId,
    uint256 amount,
    uint256 encodedLinear
  ) external virtual payable returns(uint256, uint256) {
    uint256 depositedAmount = _depositTokenFrom({
      token: token,
      depositor: msg.sender,
      amount: amount
    });
    address recipient = _verifyTipAmountAllowed({
      stakeId: stakeId,
      amount: amount
    });
    _addToTokenWithdrawable({
      token: token,
      to: recipient,
      amount: depositedAmount
    });
    _checkStakeCustodian({
      stakeId: stakeId
    });
    return _addTipToStake({
      reusable: reusable,
      token: token,
      account: recipient,
      stakeId: stakeId,
      amount: amount,
      encodedLinear: encodedLinear
    });
  }
  /**
   * remove all tips from a stake id and moves them to the
   * withdrawableBalanceOf the owner of the stake
   * @param stakeId the stake id to remove all tips from
   * @dev if the sender does not own the stake id, the call will fail
   */
  function removeAllTips(uint256 stakeId) external {
    _verifyStakeOwnership({
      owner: msg.sender,
      stakeId: stakeId
    });
    _removeAllTips({
      stakeId: stakeId,
      settings: stakeIdToSettings[stakeId]
    });
  }
  /**
   * remove all tips from a stake id and moves them to the
   * withdrawableBalanceOf the owner of the stake
   * @param stakeId the stake id to remove all tips from
   * @param settings the settings of the stake used for
   * determining whether or not to send funds back to staker
   */
  function _removeAllTips(uint256 stakeId, uint256 settings) internal {
    uint256 tipCount = _stakeIdTipSize({
      stakeId: stakeId
    });
    unchecked {
      if (tipCount > ZERO) {
        uint256 i;
        uint256[] memory indexes = new uint256[](tipCount);
        --tipCount;
        do {
          indexes[i] = tipCount - i;
          ++i;
        } while (i <= tipCount);
        _removeTipsFromStake({
          stakeId: stakeId,
          settings: settings,
          indexes: indexes
        });
      }
    }
  }
  /**
   * remove a list of tip indexes from a given stake
   * @param stakeId the stake id to remove tips from
   * @param indexes the list of indexes of tips to be removed from the list
   * @dev notice that the list of stakes will be mutated as each tip is removed
   * so you will have to calculate off chain where tips will move to or provide a list
   * such as [0, 0, 0] or decrementing [5,4,3,2,1,0] that will not be affected by the list mutating
   */
  function removeTipsFromStake(
    uint256 stakeId,
    uint256[] calldata indexes
  ) external payable {
    _removeTipsFromStake({
      stakeId: stakeId,
      settings: stakeIdToSettings[stakeId],
      indexes: indexes
    });
  }
  function _removeTipsFromStake(
    uint256 stakeId,
    uint256 settings,
    uint256[] memory indexes
  ) internal {
    // if the stake has already ended, we don't care
    // who sends funds back to staking address
    // only one who is incensed to unwind tips is the staker
    // but realistically, anyone can if they wish
    address staker;
    if (stakeIdInfo[stakeId] != ZERO) {
      _verifyStakeOwnership({
        owner: msg.sender,
        stakeId: stakeId
      });
      staker = msg.sender;
    } else {
      staker = tipStakeIdToStaker[stakeId];
    }
    uint256[] storage tips = stakeIdTips[stakeId];
    // this will fail if no tips exist
    uint256 tipsLast = tips.length - 1;
    uint256 len = indexes.length;
    uint256 i;
    do {
      uint256 index = indexes[i];
      uint256 tip = tips[index];
      if (tipsLast > index) {
        // if we are not currently targeting the last in the tips array
        // then we have to move tips around to prevent gaps in list
        tips[index] = tips[tipsLast];
      }
      tips.pop();
      unchecked {
        // now do something with the tip
        address token = address(indexToToken[tip << ONE >> INDEX_EXTERNAL_TIP_CURRENCY_ONLY]);
        _attributeFunds({
          token: token,
          settings: settings,
          staker: staker,
          amount: uint128(tip >> INDEX_EXTERNAL_TIP_LIMIT)
        });
        emit RemoveTip({
          stakeId: stakeId,
          token: token,
          index: index,
          settings: tip
        });
        // this overflows when tips are empty
        --tipsLast;
        ++i;
      }
    } while (i < len);
    if (tipsLast == MAX_UINT_256) {
      // remove from settings
      uint256 currentSettings = stakeIdToSettings[stakeId];
      unchecked {
        _logSettingsUpdate({
          stakeId: stakeId,
          settings: (
            (currentSettings >> INDEX_RIGHT_COPY_ITERATIONS << INDEX_RIGHT_COPY_ITERATIONS)
            // only remove information about the existance of a list
            // not whether or not to copy said list in subsequent stakes
            // should it exist again
            // in other words, copying remains in the hands of the staker
            | (uint8(currentSettings << TWO) >> TWO)
          )
        });
      }
    }
  }
  /**
   * create and back a tip with a given number of tokens
   * @param reusable the tip is reusable
   * @param token the token to use in the tip
   * @param stakeId the stake id to attribute the tip to
   * @param amount the number of tokens to tip
   * @param encodedLinear the (x/y)+b equation to use for determining the magnitude of the tip
   * @return the index of the tip in the list
   * @return the final tip amount
   */
  function addTipToStake(
    bool reusable,
    address token,
    uint256 stakeId,
    uint256 amount,
    uint256 encodedLinear
  ) external virtual payable returns(uint256, uint256) {
    _verifyTipAmountAllowed({
      stakeId: stakeId,
      amount: amount
    });
    _checkStakeCustodian({
      stakeId: stakeId
    });
    // deduct from sender account
    return _addTipToStake({
      reusable: reusable,
      token: token,
      account: msg.sender,
      stakeId: stakeId,
      amount: amount,
      encodedLinear: encodedLinear
    });
  }
  /**
   * verify that the inputs of the tip are allowed and will
   * not conflict with downstream requirements
   * @param stakeId the stake id to verify
   * @param amount the amount to verify. notice that zero cannot
   * be used unless the sender owns the stake. this is to prevent addresses
   * from taking other accounts funding
   * @return recipient who will be the effective owner of the tip
   */
  function _verifyTipAmountAllowed(uint256 stakeId, uint256 amount) internal view returns(address recipient) {
    (, recipient) = _stakeIdToInfo(stakeId);
    if (amount == ZERO) {
      if (msg.sender != recipient) {
        // cannot allow other people to take staker deposits
        revert NotAllowed();
      }
    }
  }
  /**
   * check that this contract is custodian of the given stake id
   * @param stakeId the stake id to check that this address is the custodian
   */
  function _checkStakeCustodian(uint256 stakeId) internal virtual view {
    if (_stakeCount({
      staker: address(this)
    }) == ZERO) {
      revert NotAllowed();
    }
    // cannot add a tip to a stake that has already ended
    // if (_stakeById(stakeId).stakeId != stakeId) {
    if (_getStake({
      custodian: address(this),
      index: _stakeIdToIndex({
        stakeId: stakeId
      })
    }).stakeId != stakeId) {
      revert NotAllowed();
    }
  }
  /**
   * create a tip and back it with given tokens
   * @param reusable the tip should be reused if it is not consumed during execution
   * @param token the token that is backing the tips value
   * @param account the account that is providing the tokens
   * @param stakeId the stake id to point the tip to
   * @param amount the number of tokens to back the tip
   * @param encodedLinear the (x/y)+b equation
   * @return index the index of the tip in the tips list
   * @return tipAmount the amount of tokens added to the tip
   */
  function _addTipToStake(
    bool reusable,
    address token,
    address account,
    uint256 stakeId,
    uint256 amount,
    uint256 encodedLinear
  ) internal returns(uint256 index, uint256 tipAmount) {
    tipAmount = _clamp({
      amount: amount,
      max: withdrawableBalanceOf[token][account]
    });
    if (tipAmount == ZERO) {
      return (ZERO, ZERO);
    }
    if (tipStakeIdToStaker[stakeId] == address(0)) {
      address owner = _stakeIdToOwner({
        stakeId: stakeId
      });
      tipStakeIdToStaker[stakeId] = owner;
    }
    // set the tip flag to 1
    // 0b00000001 | 0b10000000 => 0b10000001
    // 0b10000001 | 0b10000000 => 0b10000001
    uint256 currentSettings = stakeIdToSettings[stakeId];
    unchecked {
      uint256 updatedSettings = currentSettings | (ONE << INDEX_RIGHT_HAS_EXTERNAL_TIPS);
      if (updatedSettings != currentSettings) {
        _logSettingsUpdate({
          stakeId: stakeId,
          settings: updatedSettings
        });
      }
      // we do not change attributed here because the funds are still attributed
      // but the number will instead be stored in a tip
      withdrawableBalanceOf[token][account] -= tipAmount;
    }
    // settings must be provided with each addition
    // this result provides 15*basefee/2, up to 10m hedron as a contrived example
    // 0x0000000200000000002386f26fc10000000000000000000f0000000000000002
    uint256 currencyIndex = currencyToIndex[token];
    if (currencyIndex == ZERO) {
      if (token != address(0)) {
        revert NotAllowed();
      }
    }
    uint256 settings = _encodeTipSettings({
      reusable: reusable,
      currencyIndex: currencyIndex,
      amount: tipAmount,
      encodedLinear: encodedLinear
    });
    index = stakeIdTips[stakeId].length;
    stakeIdTips[stakeId].push(settings);
    emit AddTip({
      stakeId: stakeId,
      token: token,
      index: index,
      settings: settings
    });
    return (index, tipAmount);
  }
  // thank you for your contribution to the protocol
  // the mev bots smile upon thee
  receive() external payable {}
}

// File: contracts/TransferableStakeManager.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { StakeStarter } from "./StakeStarter.sol";
import { IStakeReceiver } from "./interfaces/IStakeReceiver.sol";

contract TransferableStakeManager is StakeStarter {
  event TransferStake(address from, address to, uint256 stakeId);
  /**
   * removes transfer abilities from a stake
   * @param stakeId the stake that the sender owns and wishes to remove transfer abilities from
   */
  function removeTransferrability(uint256 stakeId) external payable returns(uint256 settings) {
    return _removeTransferrability({
      stakeId: stakeId
    });
  }
  /**
   * removes transfer abilities from a stake
   * @param stakeId the stake that the sender owns and wishes to remove transfer abilities from
   */
  function _removeTransferrability(uint256 stakeId) internal returns(uint256 settings) {
    _verifyStakeOwnership({
      owner: msg.sender,
      stakeId: stakeId
    });
    settings = stakeIdToSettings[stakeId];
    settings = _removeTransferrabilityFromEncodedSettings(settings);
    _logSettingsUpdate({
      stakeId: stakeId,
      settings: settings
    });
  }
  /**
   * rewrite encoded settings to remove the transferable flag and leave all other settings in tact
   * @param settings encoded settings to rewrite without a transferable flag
   */
  function removeTransferrabilityFromEncodedSettings(uint256 settings) external pure returns(uint256) {
    return _removeTransferrabilityFromEncodedSettings(settings);
  }
  /**
   * rewrite encoded settings to remove the transferable flag and leave all other settings in tact
   * @param settings encoded settings to rewrite without a transferable flag
   */
  function _removeTransferrabilityFromEncodedSettings(uint256 settings) internal pure returns(uint256) {
    unchecked {
      return (
        (settings >> INDEX_RIGHT_COPY_EXTERNAL_TIPS << INDEX_RIGHT_COPY_EXTERNAL_TIPS)
        | (settings << INDEX_LEFT_STAKE_IS_TRANSFERABLE >> INDEX_LEFT_STAKE_IS_TRANSFERABLE) // wipe transferable
      );
    }
  }
  /**
   * check if a given stake under a stake id can be transferred
   * @param stakeId the stake id to check transferrability settings
   */
  function canTransfer(uint256 stakeId) external view returns(bool) {
    return _canTransfer({
      stakeId: stakeId
    });
  }
  /**
   * check if a given stake under a stake id can be transferred
   * @param stakeId the stake id to check transferrability settings
   */
  function _canTransfer(uint256 stakeId) internal view returns(bool) {
    return _isOneAtIndex({
      settings: stakeIdToSettings[stakeId],
      index: INDEX_RIGHT_STAKE_IS_TRANSFERABLE
    });
  }
  /**
   * transfer a stake from one owner to another
   * @param stakeId the stake id to transfer
   * @param to the account to receive the stake
   * @dev this method is only payable to reduce gas costs.
   * Any value sent to this method will be unattributed
   */
  function stakeTransfer(uint256 stakeId, address to) external payable {
    _verifyStakeOwnership({
      owner: msg.sender,
      stakeId: stakeId
    });
    if (!_canTransfer({ stakeId: stakeId })) {
      revert NotAllowed();
    }
    (uint256 index, ) = _stakeIdToInfo({
      stakeId: stakeId
    });
    stakeIdInfo[stakeId] = _encodeInfo({
      index: index,
      owner: to
    });
    if (tipStakeIdToStaker[stakeId] != address(0)) {
      tipStakeIdToStaker[stakeId] = to;
    }
    (bool success, bytes memory data) = to.call(
      abi.encodeCall(IStakeReceiver.onStakeReceived, (msg.sender, stakeId))
    );
    if (!success) {
      _bubbleRevert(data);
    }
    emit TransferStake({
      from: msg.sender,
      to: to,
      stakeId: stakeId
    });
  }
}

// File: contracts/UnderlyingStakeable.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IUnderlyingStakeable } from "./interfaces/IUnderlyingStakeable.sol";
import { IHedron } from "./interfaces/IHedron.sol";
import { IHEX } from  "./interfaces/IHEX.sol";
import { MulticallExtension } from "./MulticallExtension.sol";
import { Utils } from "./Utils.sol";

abstract contract UnderlyingStakeable is MulticallExtension, IUnderlyingStakeable {
  /**
   * gets the stake store at the provided index
   * @param custodian the custodian (usually this) whose list to check
   * @param index the index of the stake to get
   * @return the stake on the list at the provided index
   */
  function _getStake(
    address custodian, uint256 index
  ) virtual internal view returns(StakeStore memory) {
    return IUnderlyingStakeable(TARGET).stakeLists(custodian, index);
  }
  /**
   * the count of stakes for a given custodian / staker
   * @param staker the custodian in question
   * @return count of the stakes under a given custodian / staker
   */
  function stakeCount(address staker) external view returns(uint256 count) {
    return _stakeCount({
      staker: staker
    });
  }
  /**
   * the count of stakes for a given custodian / staker
   * @param staker the custodian in question
   * @return count of the stakes under a given custodian / staker
   */
  function _stakeCount(address staker) internal view returns(uint256 count) {
    return IUnderlyingStakeable(TARGET).stakeCount(staker);
  }
  function _getStakeCount(address staker) internal view virtual returns(uint256 count) {
    return _stakeCount({
      staker: staker
    });
  }
  /**
   * retrieve the balance of a given owner
   * @param owner the owner of the tokens
   * @return amount a balance amount
   */
  function balanceOf(address owner) external view returns(uint256 amount) {
    return _balanceOf({
      owner: owner
    });
  }
  /**
   * retrieve the balance of a given owner
   * @param owner the owner of the tokens
   * @return amount a balance amount
   */
  function _balanceOf(address owner) internal view returns(uint256 amount) {
    return IERC20(TARGET).balanceOf(owner);
  }
  /**
   * retrieve a stake at a staker's index given a staker address and an index
   * @param staker the staker in question
   * @param index the index to focus on
   * @return stake the stake custodied by a given staker at a given index
   */
  function stakeLists(
    address staker, uint256 index
  ) view external returns(StakeStore memory stake) {
    return _getStake({
      custodian: staker,
      index: index
    });
  }
  /**
   * retrieve the current day from the target contract
   * @return day the current day according to the hex contract
   */
  function currentDay() external view returns (uint256 day) {
    return _currentDay();
  }
  /**
   * retrieve the current day from the target contract
   * @return day the current day according to the hex contract
   */
  function _currentDay() internal view returns(uint256) {
    return IUnderlyingStakeable(TARGET).currentDay();
  }
  /**
   * retrieve the global info from the target contract (hex)
   * updated at the first start or end stake on any given day
   */
  function globalInfo() external view returns(uint256[13] memory) {
    return IUnderlyingStakeable(TARGET).globalInfo();
  }
  /**
   * check whether or not the stake is being ended early
   * @param lockedDay the day after the stake was locked
   * @param stakedDays the number of days that the stake is locked
   * @param targetDay the day to check whether it will be categorized as ending early
   * @return isEarly the locked and staked days are greater than the target day (usually today)
   */
  function isEarlyEnding(
    uint256 lockedDay, uint256 stakedDays, uint256 targetDay
  ) external pure returns(bool isEarly) {
    return _isEarlyEnding({
      lockedDay: lockedDay,
      stakedDays: stakedDays,
      targetDay: targetDay
    });
  }
  /**
   * check whether or not the stake is being ended early
   * @param lockedDay the day after the stake was locked
   * @param stakedDays the number of days that the stake is locked
   * @param targetDay the day to check whether it will be categorized as ending early
   * @return isEarly the locked and staked days are greater than the target day (usually today)
   */
  function _isEarlyEnding(
    uint256 lockedDay,
    uint256 stakedDays,
    uint256 targetDay
  ) internal pure returns(bool isEarly) {
    unchecked {
      return (lockedDay + stakedDays) > targetDay;
    }
  }
  /**
   * starts a stake from the provided amount
   * @param newStakedHearts amount of tokens to stake
   * @param newStakedDays the number of days for this new stake
   * @dev this method interface matches the original underlying token contract
   */
  function stakeStart(uint256 newStakedHearts, uint256 newStakedDays) external virtual;
  /**
   * end your own stake which is custodied by the stake manager. skips tip computing
   * @param stakeIndex the index on the underlying contract to end stake
   * @param stakeId the stake id from the underlying contract to end stake
   * @notice this is not payable to match the underlying contract
   * @notice this moves funds back to the sender to make behavior match underlying token
   * @notice this method only checks that the sender owns the stake it does not care
   * if it is managed in a created contract and externally endable by this contract (1)
   * or requires that the staker send start and end methods (0)
   */
  function stakeEnd(uint256 stakeIndex, uint40 stakeId) external virtual;
  function stakeGoodAccounting(
    address stakerAddr, uint256 stakeIndex, uint40 stakeIdParam
  ) external virtual;
}

// File: contracts/UnderlyingStakeManager.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { GoodAccounting } from "./GoodAccounting.sol";
import { UnderlyingStakeable } from "./UnderlyingStakeable.sol";

contract UnderlyingStakeManager is GoodAccounting {
  /**
   * start a stake for the staker given the amount and number of days
   * @param owner the underlying owner of the stake
   * @param amount the amount to add to the stake
   * @param newStakedDays the number of days that the stake should run
   * @param index where in the list the stake will be placed.
   * this is a param because it can be cached for internal loops
   */
  function _stakeStartFor(
    address owner,
    uint256 amount,
    uint256 newStakedDays,
    uint256 index
  ) internal virtual returns(uint256 stakeId) {
    UnderlyingStakeable(TARGET).stakeStart(amount, newStakedDays);
    // get the stake id
    stakeId = UnderlyingStakeable(TARGET).stakeLists(address(this), index).stakeId;
    stakeIdInfo[stakeId] = _encodeInfo({
      index: index,
      owner: owner
    });
  }
  /**
   * ends a stake for someone else
   * @param stakeIndex the stake index on the underlying contract to end
   * @param stakeId the stake id on the underlying contract to end
   * @param stakeCountAfter the stake count after the stake is ended (current length - 1)
   * @return delta the number of tokens that have been received from ending the stake
   */
  function _stakeEnd(
    uint256 stakeIndex, uint256 stakeId, uint256 stakeCountAfter
  ) internal virtual returns(uint256 delta) {
    // calculate the balance before
    // cannot use tokens attributed here because of tipping
    uint256 balanceBefore = _balanceOf({
      owner: address(this)
    });
    // end the stake - attributed to contract or through the managed stake
    UnderlyingStakeable(TARGET).stakeEnd(stakeIndex, uint40(stakeId));
    // for the last stake in this list, this branch will not be hit
    if (stakeCountAfter > stakeIndex) {
      uint256 shiftingStakeId = _getStake({
        custodian: address(this),
        index: stakeIndex
      }).stakeId;
      uint256 stakeInfo = stakeIdInfo[shiftingStakeId];
      stakeIdInfo[shiftingStakeId] = _encodeInfo({
        index: stakeIndex,
        owner: address(uint160(stakeInfo))
      });
    }
    // because the delta is only available in the logs
    // we need to calculate the delta to use it
    unchecked {
      delta = _balanceOf({
        owner: address(this)
      }) - balanceBefore;
    }
    stakeIdInfo[stakeId] = ZERO;
  }
  /**
   * starts a stake from the provided amount
   * @param amount amount of tokens to stake
   * @param newStakedDays the number of days for this new stake
   * @dev this method interface matches the original underlying token contract
   */
  function stakeStart(uint256 amount, uint256 newStakedDays) external override virtual {
    // ensures amount under/from sender is sufficient
    _depositTokenFrom({
      token: TARGET,
      depositor: msg.sender,
      amount: amount
    });
    _stakeStartFor({
      owner: msg.sender,
      amount: amount,
      newStakedDays: newStakedDays,
      index: _getStakeCount({
        staker: address(this)
      })
    });
  }
  /**
   * end your own stake which is custodied by the stake manager. skips tip computing
   * @param stakeIndex the index on the underlying contract to end stake
   * @param stakeId the stake id from the underlying contract to end stake
   * @notice this is not payable to match the underlying contract
   * @notice this moves funds back to the sender to make behavior match underlying token
   * @notice this method only checks that the sender owns the stake it does not care
   * if it is managed in a created contract and externally endable by this contract (1)
   * or requires that the staker send start and end methods (0)
   */
  function stakeEnd(uint256 stakeIndex, uint40 stakeId) external override virtual {
    _stakeEndByIndexAndId(stakeIndex, stakeId);
  }
  /**
   * end a stake given its index and id
   * @param stakeIndex the index of the stake to end
   * @param stakeId the stake id to end
   */
  function _stakeEndByIndexAndId(
    uint256 stakeIndex, uint256 stakeId
  ) internal virtual returns(uint256 amount) {
    _verifyStakeOwnership({
      owner: msg.sender,
      stakeId: stakeId
    });
    amount = _stakeEnd({
      stakeIndex: stakeIndex,
      stakeId: stakeId,
      stakeCountAfter: _getStakeCount({
        staker: address(this)
      }) - ONE
    });
    _withdrawTokenTo({
      token: TARGET,
      to: payable(msg.sender),
      amount: amount
    });
  }
  /**
   * end your own stake which is custodied by the stake manager. skips tip computing
   * @param stakeId the stake id from the underlying contract to end stake
   * @notice this is not payable to match the underlying contract
   * @notice this moves funds back to the sender to make behavior match underlying token
   * @notice this method only checks that the sender owns the stake it does not care
   * if it is managed in a created contract and externally endable by this contract (1)
   * or requires that the staker send start and end methods (0)
   */
  function stakeEndById(uint256 stakeId) external virtual returns(uint256 amount) {
    (uint256 stakeIndex, ) = _stakeIdToInfo({
      stakeId: stakeId
    });
    return _stakeEndByIndexAndId(stakeIndex, stakeId);
  }
  function _getStakeInfo(uint256 stakeId) internal view virtual returns(
    bool valid,
    address staker,
    uint256 stakeIndex,
    UnderlyingStakeable.StakeStore memory stake
  ) {
    (stakeIndex, staker) = _stakeIdToInfo(stakeId);
    if (staker == address(0)) {
      return (valid, staker, stakeIndex, stake);
    }
    stake = _getStake({
      custodian: address(this),
      index: stakeIndex
    });
    valid = stake.stakeId == stakeId;
  }
  /**
   * given ownership over a stake, end the stake
   * and restart all of the proceeds
   * @param stakeId the stake id to restart
   * @return amount the amount ended and re-staked
   * @return newStakeId the newly recreated stake id
   */
  function _stakeRestartById(uint256 stakeId) internal returns(
    uint256 amount, uint256 newStakeId
  ) {
    (
      bool valid, address staker, uint256 stakeIndex,
      UnderlyingStakeable.StakeStore memory stake
    ) = _getStakeInfo(stakeId);
    if (!valid) {
      return (ZERO, ZERO);
    }
    // move to after valid check since this should happen less often
    if (staker != msg.sender) {
      revert StakeNotOwned(msg.sender, staker);
    }
    uint256 stakeCountAfter = _getStakeCount({
      staker: address(this)
    }) - ONE;
    // at this point, we can guarantee that we own the stake
    amount = _stakeEnd({
      stakeIndex: stakeIndex,
      stakeId: stakeId,
      stakeCountAfter: stakeCountAfter
    });
    newStakeId = _stakeStartFor({
      owner: staker,
      amount: amount,
      newStakedDays: stake.stakedDays,
      index: stakeCountAfter
    });
  }
  /**
   * given ownership over a stake, stop and restart a stake with all proceeds
   * @param stakeId the stake id to restart
   * @return amount the number of tokens that were ended and added to new stake
   * @return newStakeId the newly created stake id
   */
  function stakeRestartById(uint256 stakeId) external returns(
    uint256 amount, uint256 newStakeId
  ) {
    return _stakeRestartById({
      stakeId: stakeId
    });
  }
  /**
   * given ownership over a list of ids of stakes, restart a list of stakes
   * @param stakeIds the list of stake ids to iterate over and restart
   */
  function stakeRestartManyById(uint256[] calldata stakeIds) external {
    unchecked {
      uint256 i;
      uint256 len = stakeIds.length;
      do {
        _stakeRestartById({
          stakeId: stakeIds[i]
        });
        ++i;
      } while (i < len);
    }
  }
}

// File: contracts/Utils.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

contract Utils {
  /**
   * @notice the not allowed method is a general error that signifies
   * non descript permissions issues. All transactions should always be simulated
   * either by using gas estimations or through a static call
   */
  error NotAllowed();
  /**
   * @notice the hex contract to target - because this is the same on ethereum
   * and pulsechain, we can leave it as a constant
   */
  address public constant TARGET = 0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39;
  /**
   * @notice a constant for the max number of days that can be used
   * when determining the number algorhythmically
   */
  uint256 public constant MAX_DAYS = uint256(5555);
  /** @notice the max uint256 that can be used */
  uint256 internal constant MAX_UINT_256 = type(uint256).max;
  /** @notice the number of binary slots in a 256 sized uint */
  uint256 internal constant SLOTS = uint256(256);
  /** @notice a number to use as the denominator when determining basis points */
  uint256 internal constant TEN_K = uint256(10_000);
  /** @notice the number of bits in an address */
  uint256 internal constant ADDRESS_BIT_LENGTH = uint256(160);
  /** @notice the minimum value that can exist in a int16 (-2^15) */
  int256 constant internal MIN_INT_16 = int256(type(int16).min);
  /** @notice the max value that can fit in a uint8 slot (255) */
  uint256 internal constant MAX_UINT_8 = uint256(type(uint8).max);
  /** @notice a uint256 as 0 in a constant */
  uint256 internal constant ZERO = uint256(0);
  /** @notice a uint256 as 1 in a constant */
  uint256 internal constant ONE = uint256(1);
  /** @notice a uint256 as 2 in a constant */
  uint256 internal constant TWO = uint256(2);
  /** @notice a uint256 as 3 in a constant */
  uint256 internal constant THREE = uint256(3);
  /** @notice a uint256 as 4 in a constant */
  uint256 internal constant FOUR = uint256(4);
  /** @notice a uint256 as 8 in a constant */
  uint256 internal constant EIGHT = uint256(8);
  /** @notice a uint256 as 16 in a constant */
  uint256 internal constant SIXTEEN = uint256(16);
  /** @notice a uint256 as 24 in a constant */
  uint256 internal constant TWENTY_FOUR = uint256(24);
  /** @notice a uint256 as 32 in a constant */
  uint256 internal constant THIRTY_TWO = uint256(32);
  /** @notice a uint256 as 48 in a constant */
  uint256 internal constant FOURTY_EIGHT = uint256(48);
  /** @notice a uint256 as 56 in a constant */
  uint256 internal constant FIFTY_SIX = uint256(56);
  /** @notice a uint256 as 64 in a constant */
  uint256 internal constant SIXTY_FOUR = uint256(64);
  /** @notice a uint256 as 72 in a constant */
  uint256 internal constant SEVENTY_TWO = uint256(72);
  /** @notice the hedron contract to interact with and mint hedron tokens from */
  address public constant HEDRON = 0x3819f64f282bf135d62168C1e513280dAF905e06;
  /**
   * @notice the hedron stake instance manager contract
   * to interact with and transfer hsi tokens from and end stakes through
   */
  address public constant HSIM = 0x8BD3d1472A656e312E94fB1BbdD599B8C51D18e3;
  /**
   * check if the number, in binary form, has a 1 at the provided index
   * @param settings the settings number that holds up to 256 flags as 1/0
   * @param index the index to check for a 1
   */
  function isOneAtIndex(uint256 settings, uint256 index) external pure returns(bool isOne) {
    return _isOneAtIndex({
      settings: settings,
      index: index
    });
  }
  function _isOneAtIndex(uint256 settings, uint256 index) internal pure returns(bool isOne) {
    // in binary checks:
    // take the settings and shift it some number of bits left (leaving space for 1)
    // then go the opposite direction, once again leaving only space for 1
    unchecked {
      return ONE == (settings << (MAX_UINT_8 - index) >> MAX_UINT_8);
    }
  }
  /**
   * after an error is caught, it can be reverted again
   * @param data the data to repackage and revert with
   */
  function _bubbleRevert(bytes memory data) internal pure {
    if (data.length == ZERO) revert();
    assembly {
      revert(add(32, data), mload(data))
    }
  }
}