// File: /home/keso/projects/crypto/uptown-panda-token/contracts/UptownPandaFarm.sol



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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/math/Math.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "./interfaces/IUptownPandaFarm.sol";

contract UptownPandaFarm is IUptownPandaFarm {
    event SupplySnapshotAdded(uint256 idx, uint256 intervalIdx, uint256 timestamp, uint256 totalAmount);
    event SupplySnapshotUpdated(uint256 idx, uint256 intervalIdx, uint256 timestamp, uint256 totalAmount);
    event HarvestChunkAdded(address indexed staker, uint256 idx, uint256 timestamp, uint256 amount);
    event RewardClaimed(address indexed staker, uint256 indexed harvestChunkIdx, uint256 timestamp, uint256 amount);

    using Math for uint256;
    using SafeMath for uint256;
    using Address for address;

    uint256 public constant REWARD_HALVING_INTERVAL = 10 days; // interval for halving the rewards in seconds
    uint256 public constant HARVEST_INTERVAL = 1 days; // how long does it take to increase % of allowed reward withdraw
    uint256 public constant HARVEST_STEP = 10; // how much percent you're allowed to withdraw (after 1 day 10%, after 2 days 20%,...)
    uint256 public immutable HARVEST_CHUNKS_COUNT = uint256(100).div(HARVEST_STEP); // how many parts harvest is split into

    bool public hasFarmingStarted = false; // indicates if farming has begun
    uint256 public initialFarmUpSupply; // inidicates how many $UP tokens were minted for the farm
    address public upTokenAddress; // $UP token address
    address public farmTokenAddress; // address of token to farm with

    IERC20 private upToken; // $UP IERC20 token
    IERC20 private farmToken; // farm IERC20 token

    address private owner;

    struct SupplySnapshot {
        uint256 intervalIdx;
        uint256 timestamp;
        uint256 amount;
    }

    struct HarvestChunk {
        uint256 timestamp;
        uint256 totalAmount;
        uint256 claimedAmount;
    }

    SupplySnapshot[] public supplySnapshots;
    mapping(address => uint256) public balances;
    mapping(address => HarvestChunk[]) public harvestChunks;
    mapping(address => uint256) public harvestSnapshotIdxs;

    constructor() public {
        owner = msg.sender;
    }

    modifier originIsOwner() {
        require(owner == tx.origin, "Tx origin is not the owner of the contract.");
        _;
    }

    modifier farmNotStarted() {
        require(!hasFarmingStarted, "Farm has already been started.");
        _;
    }

    modifier farmStarted() {
        require(hasFarmingStarted, "Farm has not been started yet.");
        _;
    }

    modifier farmUpSupplySetCorrectly(address _upToken, uint256 supplyToCheck) {
        require(
            IERC20(_upToken).balanceOf(address(this)) == supplyToCheck,
            "Token supply for this farm is not set correctly!"
        );
        _;
    }

    modifier stakeAddressIsValid() {
        require(!address(msg.sender).isContract(), "Staking from contracts is not allowed.");
        _;
    }

    modifier stakeAmountIsValid(uint256 _amount) {
        require(_amount > 0, "Staking amount must be bigger than 0.");
        _;
    }

    modifier withdrawAmountIsValid(uint256 _amount) {
        require(_amount > 0, "Withdraw amount must be bigger than 0.");
        require(balances[msg.sender] >= _amount, "Requested amount is bigger than your balance.");
        _;
    }

    function startFarming(
        address _upToken,
        address _farmToken,
        uint256 _initialFarmUpSupply
    ) external override originIsOwner farmNotStarted farmUpSupplySetCorrectly(_upToken, _initialFarmUpSupply) {
        addSupplySnapshot(0, block.timestamp, 0);
        upTokenAddress = _upToken;
        farmTokenAddress = _farmToken;
        upToken = IERC20(_upToken);
        farmToken = IERC20(_farmToken);
        initialFarmUpSupply = _initialFarmUpSupply;
        hasFarmingStarted = true;
    }

    function stake(uint256 _amount) external override farmStarted stakeAddressIsValid stakeAmountIsValid(_amount) {
        harvestReward(totalStakedSupply().add(_amount));
        farmToken.transferFrom(msg.sender, address(this), _amount);
        balances[msg.sender] = balances[msg.sender].add(_amount);
    }

    function withdraw(uint256 _amount) external override farmStarted withdrawAmountIsValid(_amount) {
        harvestReward(totalStakedSupply().sub(_amount));
        farmToken.transfer(msg.sender, _amount);
        balances[msg.sender] = balances[msg.sender].sub(_amount);
    }

    function harvest() external override farmStarted {
        harvestReward(totalStakedSupply()); // since no funds are added(staking)/subtracted(withdrawing) we just pass in the current supply
    }

    function claim() external override farmStarted {
        claimHarvestedReward();
    }


    function harvestableReward() external view override returns (uint256) {
        if (supplySnapshots.length == 0) {
            return 0;
        }

        uint256 latestSupplySnapshotIdx = supplySnapshots.length.sub(1);
        uint256 total = 0;
        for (uint256 i = harvestSnapshotIdxs[msg.sender]; i < latestSupplySnapshotIdx; i++) {
            total = total.add(calculateChunkRewardFromSupplySnapshot(i));
        }

        SupplySnapshot storage latestSupplySnapshot = supplySnapshots[latestSupplySnapshotIdx];
        uint256 currentIntervalIdx = latestSupplySnapshot.intervalIdx;
        uint256 currentTimestamp = latestSupplySnapshot.timestamp;
        while (true) {
            uint256 nextIntervalIdx = currentIntervalIdx.add(1);
            uint256 nextTimestamp = Math.min(block.timestamp, getIntervalTimestamp(nextIntervalIdx));
            uint256 intervalChunkLength = nextTimestamp.sub(currentTimestamp);
            total = total.add(
                calculateChunkReward(intervalChunkLength, currentIntervalIdx, latestSupplySnapshot.amount)
            );
            if (nextTimestamp == block.timestamp) {
                break;
            }
            currentIntervalIdx = nextIntervalIdx;
            currentTimestamp = nextTimestamp;
        }

        return total;
    }

    function claimableHarvestedReward() external view override returns (uint256) {
        HarvestChunk[] storage stakerHarvestChunks = harvestChunks[msg.sender];
        uint256 total = 0;
        for (uint256 i = 0; i < stakerHarvestChunks.length; i++) {
            total = total.add(getHarvestChunkClaimableAmount(stakerHarvestChunks[i]));
        }
        return total;
    }

    function totalHarvestedReward() external view override returns (uint256) {
        HarvestChunk[] storage stakerHarvestChunks = harvestChunks[msg.sender];
        uint256 total = 0;
        for (uint256 i = 0; i < stakerHarvestChunks.length; i++) {
            total = total.add(stakerHarvestChunks[i].totalAmount.sub(stakerHarvestChunks[i].claimedAmount));
        }
        return total;
    }

    function getHarvestChunkClaimableAmount(HarvestChunk storage _stakerHarvestChunk) private view returns (uint256) {
        uint256 claimPercent = getHarvestChunkClaimPercent(_stakerHarvestChunk.timestamp);
        uint256 claimAmount = _stakerHarvestChunk.totalAmount.mul(claimPercent).div(100);
        return _stakerHarvestChunk.claimedAmount > claimAmount ? 0 : claimAmount.sub(_stakerHarvestChunk.claimedAmount);
    }

    function getHarvestChunkClaimPercent(uint256 _harvestTimestamp) private view returns (uint256) {
        if (_harvestTimestamp >= block.timestamp) {
            return 0;
        }
        uint256 currentChunk = block.timestamp.sub(_harvestTimestamp).div(HARVEST_INTERVAL);
        return currentChunk >= HARVEST_CHUNKS_COUNT ? 100 : currentChunk.mul(HARVEST_STEP);
    }

    function claimHarvestedReward() private {
        HarvestChunk[] storage stakerHarvestChunks = harvestChunks[msg.sender];
        for (uint256 i = 0; i < stakerHarvestChunks.length; i++) {
            uint256 claimableAmount = getHarvestChunkClaimableAmount(stakerHarvestChunks[i]);
            if (claimableAmount == 0) {
                continue;
            }
            upToken.transfer(msg.sender, claimableAmount);
            stakerHarvestChunks[i].claimedAmount = stakerHarvestChunks[i].claimedAmount.add(claimableAmount);
            emit RewardClaimed(msg.sender, i, block.timestamp, claimableAmount);
        }
    }

    function harvestReward(uint256 _newTotalSupply) private {
        updateSupplySnapshots(_newTotalSupply);

        uint256 latestSupplySnapshotIdx = supplySnapshots.length.sub(1);

        if (balances[msg.sender] == 0) {
            harvestSnapshotIdxs[msg.sender] = latestSupplySnapshotIdx;
            return;
        }

        claimHarvestedReward();

        uint256 rewardToHarvest = 0;
        for (uint256 i = harvestSnapshotIdxs[msg.sender]; i < latestSupplySnapshotIdx; i++) {
            rewardToHarvest = rewardToHarvest.add(calculateChunkRewardFromSupplySnapshot(i));
        }
        harvestSnapshotIdxs[msg.sender] = latestSupplySnapshotIdx;
        addHarvestChunk(rewardToHarvest);
    }

    function addHarvestChunk(uint256 _rewardToHarvest) private {
        uint256 idx = harvestChunks[msg.sender].length;
        harvestChunks[msg.sender].push(HarvestChunk(block.timestamp, _rewardToHarvest, 0));
        emit HarvestChunkAdded(msg.sender, idx, block.timestamp, _rewardToHarvest);
    }

    function updateSupplySnapshots(uint256 _newTotalSupply) private {
        uint256 latestSnapshotIdx = supplySnapshots.length.sub(1);
        SupplySnapshot storage latestSnapshot = supplySnapshots[latestSnapshotIdx];
        if (latestSnapshot.timestamp >= block.timestamp) {
            updateSupplySnapshot(latestSnapshotIdx, latestSnapshot, _newTotalSupply);
            return;
        }

        uint256 currentIntervalIdx = latestSnapshot.intervalIdx;
        while (true) {
            uint256 nextIntervalIdx = currentIntervalIdx.add(1);
            uint256 nextIntervalTimestamp = getIntervalTimestamp(nextIntervalIdx);
            uint256 snapshotIntervalIdx = block.timestamp < nextIntervalTimestamp
                ? currentIntervalIdx
                : nextIntervalIdx;
            uint256 snapshotTimestamp = Math.min(block.timestamp, nextIntervalTimestamp);
            uint256 snapshotAmount = snapshotTimestamp == block.timestamp ? _newTotalSupply : latestSnapshot.amount;
            addSupplySnapshot(snapshotIntervalIdx, snapshotTimestamp, snapshotAmount);
            if (snapshotTimestamp == block.timestamp) {
                break;
            }
            currentIntervalIdx = nextIntervalIdx;
        }
    }

    function updateSupplySnapshot(
        uint256 _supplySnapshotIdx,
        SupplySnapshot storage _supplySnapshot,
        uint256 _newTotalSupply
    ) private {
        _supplySnapshot.amount = _newTotalSupply;
        emit SupplySnapshotUpdated(
            _supplySnapshotIdx,
            _supplySnapshot.intervalIdx,
            _supplySnapshot.timestamp,
            _supplySnapshot.amount
        );
    }

    function addSupplySnapshot(
        uint256 _intervalIdx,
        uint256 _timestamp,
        uint256 _amount
    ) private {
        uint256 supplySnapshotIdx = supplySnapshots.length;
        supplySnapshots.push(SupplySnapshot(_intervalIdx, _timestamp, _amount));
        emit SupplySnapshotAdded(supplySnapshotIdx, _intervalIdx, _timestamp, _amount);
    }

    function totalStakedSupply() public view returns (uint256) {
        return supplySnapshots.length > 0 ? supplySnapshots[supplySnapshots.length.sub(1)].amount : 0; // latest entry is current total supply
    }

    function calculateChunkRewardFromSupplySnapshot(uint256 supplySnapshotIdx) private view returns (uint256) {
        uint256 intervalChunkLength = supplySnapshots[supplySnapshotIdx.add(1)].timestamp.sub(
            supplySnapshots[supplySnapshotIdx].timestamp
        );
        return
            calculateChunkReward(
                intervalChunkLength,
                supplySnapshots[supplySnapshotIdx].intervalIdx,
                supplySnapshots[supplySnapshotIdx].amount
            );
    }

    function calculateChunkReward(
        uint256 _intervalChunkLength,
        uint256 _intervalIdx,
        uint256 _supplyAmount
    ) private view returns (uint256) {
        if (_supplyAmount == 0) {
            return 0;
        }
        return
            getIntervalTotalReward(_intervalIdx)
                .mul(_intervalChunkLength)
                .div(REWARD_HALVING_INTERVAL)
                .mul(balances[msg.sender])
                .div(_supplyAmount);
    }

    function nextIntervalTimestamp() external view returns (uint256) {
        if (supplySnapshots.length == 0) {
            return 0;
        }
        uint256 currentIntervalIdx = block.timestamp.sub(supplySnapshots[0].timestamp).div(REWARD_HALVING_INTERVAL);
        return getIntervalTimestamp(currentIntervalIdx.add(1));
    }

    function currentIntervalTotalReward() external view returns (uint256) {
        uint256 currentIntervalIdx = supplySnapshots.length == 0
            ? 0
            : block.timestamp.sub(supplySnapshots[0].timestamp).div(REWARD_HALVING_INTERVAL);
        return getIntervalTotalReward(currentIntervalIdx);
    }

    function getIntervalTotalReward(uint256 _intervalIdx) private view returns (uint256) {
        return initialFarmUpSupply.div(2**_intervalIdx.add(1));
    }

    function getIntervalTimestamp(uint256 _intervalIdx) private view returns (uint256) {
        return
            supplySnapshots.length > 0
                ? supplySnapshots[0].timestamp.add(REWARD_HALVING_INTERVAL.mul(_intervalIdx))
                : 0;
    }
}

// File: /home/keso/projects/crypto/uptown-panda-token/contracts/interfaces/IUptownPandaFarm.sol



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

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

interface IUptownPandaFarm {
    function startFarming(
        address _upToken,
        address _farmToken,
        uint256 _initialFarmUpSupply
    ) external;

    function stake(uint256 _amount) external;

    function withdraw(uint256 _amount) external;

    function harvest() external;

    function claim() external;

    function harvestableReward() external view returns (uint256);

    function claimableHarvestedReward() external view returns (uint256);

    function totalHarvestedReward() external view returns (uint256);
}

// File: @openzeppelin/contracts/math/Math.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.6.0;

/**
 * @dev Standard math utilities missing in the Solidity language.
 */
library Math {
    /**
     * @dev Returns the largest of two numbers.
     */
    function max(uint256 a, uint256 b) internal pure returns (uint256) {
        return a >= b ? a : b;
    }

    /**
     * @dev Returns the smallest of two numbers.
     */
    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }

    /**
     * @dev Returns the average of two numbers. The result is rounded towards
     * zero.
     */
    function average(uint256 a, uint256 b) internal pure returns (uint256) {
        // (a + b) / 2 can overflow, so we distribute
        return (a / 2) + (b / 2) + ((a % 2 + b % 2) / 2);
    }
}

// File: @openzeppelin/contracts/math/SafeMath.sol



// SPDX-License-Identifier: MIT

pragma solidity ^0.6.0;

/**
 * @dev Wrappers over Solidity's arithmetic operations with added overflow
 * checks.
 *
 * Arithmetic operations in Solidity wrap on overflow. This can easily result
 * in bugs, because programmers usually assume that an overflow raises an
 * error, which is the standard behavior in high level programming languages.
 * `SafeMath` restores this intuition by reverting the transaction when an
 * operation overflows.
 *
 * Using this library instead of the unchecked operations eliminates an entire
 * class of bugs, so it's recommended to use it always.
 */
library SafeMath {
    /**
     * @dev Returns the addition of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `+` operator.
     *
     * Requirements:
     *
     * - Addition cannot overflow.
     */
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");

        return c;
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting on
     * overflow (when the result is negative).
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     *
     * - Subtraction cannot overflow.
     */
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return sub(a, b, "SafeMath: subtraction overflow");
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting with custom message on
     * overflow (when the result is negative).
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     *
     * - Subtraction cannot overflow.
     */
    function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b <= a, errorMessage);
        uint256 c = a - b;

        return c;
    }

    /**
     * @dev Returns the multiplication of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `*` operator.
     *
     * Requirements:
     *
     * - Multiplication cannot overflow.
     */
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
        // benefit is lost if 'b' is also tested.
        // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");

        return c;
    }

    /**
     * @dev Returns the integer division of two unsigned integers. Reverts on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator. Note: this function uses a
     * `revert` opcode (which leaves remaining gas untouched) while Solidity
     * uses an invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return div(a, b, "SafeMath: division by zero");
    }

    /**
     * @dev Returns the integer division of two unsigned integers. Reverts with custom message on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator. Note: this function uses a
     * `revert` opcode (which leaves remaining gas untouched) while Solidity
     * uses an invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b > 0, errorMessage);
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold

        return c;
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * Reverts when dividing by zero.
     *
     * Counterpart to Solidity's `%` operator. This function uses a `revert`
     * opcode (which leaves remaining gas untouched) while Solidity uses an
     * invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return mod(a, b, "SafeMath: modulo by zero");
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * Reverts with custom message when dividing by zero.
     *
     * Counterpart to Solidity's `%` operator. This function uses a `revert`
     * opcode (which leaves remaining gas untouched) while Solidity uses an
     * invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b != 0, errorMessage);
        return a % b;
    }
}

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



// SPDX-License-Identifier: MIT

pragma solidity ^0.6.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @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 `recipient`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address recipient, 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 `sender` to `recipient` 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 sender, address recipient, uint256 amount) external returns (bool);

    /**
     * @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);
}

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



// SPDX-License-Identifier: MIT

pragma solidity ^0.6.2;

/**
 * @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
     * ====
     */
    function isContract(address account) internal view returns (bool) {
        // This method relies in extcodesize, which returns 0 for contracts in
        // construction, since the code is only stored at the end of the
        // constructor execution.

        uint256 size;
        // solhint-disable-next-line no-inline-assembly
        assembly { size := extcodesize(account) }
        return size > 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://diligence.consensys.net/posts/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.5.11/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");

        // solhint-disable-next-line avoid-low-level-calls, avoid-call-value
        (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 functionCall(target, data, "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");
        return _functionCallWithValue(target, data, value, errorMessage);
    }

    function _functionCallWithValue(address target, bytes memory data, uint256 weiValue, string memory errorMessage) private returns (bytes memory) {
        require(isContract(target), "Address: call to non-contract");

        // solhint-disable-next-line avoid-low-level-calls
        (bool success, bytes memory returndata) = target.call{ value: weiValue }(data);
        if (success) {
            return returndata;
        } else {
            // 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

                // solhint-disable-next-line no-inline-assembly
                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                revert(errorMessage);
            }
        }
    }
}