Contract Diff Checker

Contract Name:
DiceGame

Contract Source Code:

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

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

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.4) (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;
    }

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.19;
import "@openzeppelin/contracts/access/Ownable.sol";
import { TransferHelper } from "./libraries/TransferHelper.sol";

contract DiceGame is Ownable {
    using TransferHelper for address;

    struct GameRound {
        bool fulfilled; // whether the request has been successfully fulfilled
        address user;
        uint256 totalBet;
        uint256 totalWinnings;
        uint256[] betAmts;
        uint256[] diceRollResult;
    }
    uint256 public constant SELL_POINTS_LIMIT = 1e24;
    uint256 public constant WIN69_MULTIPLIER = 10;
    uint256 public constant CALLBACK_GAS = 200000;
    uint256 public constant MAX_NUM_WORDS = 3;
    uint256 public constant DELIMITER = 1e18;
    uint8 public constant decimals = 18;
    string public constant name = "Banana Points";
    string public constant symbol = "BPT";

    uint256 public immutable gamePeriod;
    address public immutable coin;
    address public immutable V3Deployer;
    address public immutable gameRngWallet;

    /// @notice Timestamp when the geme ower
    uint256 public endTime;
    /// @notice Initial rate of tokens per coin
    uint256 public initialTokenRate;

    uint256 public gameId;
    uint256 public lastFulfilledGameId;

    bool isSellPointsForbidden;

    // The total supply of points in existence
    uint256 public totalSupply;
    // Maps an address to their current balance
    mapping(address => uint256) private userBalances;
    // Maps a game ID to its round information
    mapping(uint256 => GameRound) private gameRounds; /* gameId --> GameRound */
    // Maps an address to their game IDs
    mapping(address => uint256[]) public userGameIds;

    constructor(
        address _gameRngWalletAddress,
        uint _gamePeriod,
        address _V3Deployer,
        address _coin
    ) {
        gameRngWallet = _gameRngWalletAddress;
        if (_gameRngWalletAddress == address(0) || _V3Deployer == address(0) || _coin == address(0))
            revert ZeroValue();
        if (_gamePeriod < 2 hours || _gamePeriod > 180 days) revert GamePeriod();
        gamePeriod = _gamePeriod;
        coin = _coin;
        V3Deployer = _V3Deployer;
        transferOwnership(_V3Deployer);
    }

    event MintPoints(address recipient, uint256 pointsAmount);
    event BurnPoints(address from, uint256 pointsAmount);
    event Redeem(address user, uint256 amount);
    event PurchasePoints(address user, uint256 paymentAmount);
    event SellPoints(address user, uint256 paymentAmount, uint256 pointsAmount);
    event Bet(uint256 gameId, address user, uint256 totalBetAmt);

    error AmountOfEthSentIsTooSmall(uint256 sent, uint256 minimum);
    error InvalidGameId(uint256 id);
    error InvaliddiceRollResult(uint256 id);
    error GamePeriod();
    error ZeroValue();
    error NotEnoughCoinBalance(uint256 want, uint256 have);
    error SellPointsLimitReached();
    error Forbidden();

    // Modifiers
    modifier shouldGameIsNotOver() {
        require(gameNotOver(), "game over");
        _;
    }

    modifier shouldGameIsOver() {
        require(gameOver(), "game is NOT over");
        _;
    }

    /// @notice Receive ETH and forward to `sponsorWallet`.
    receive() external payable {
        (bool success, ) = gameRngWallet.call{ value: msg.value }("");
        require(success);
    }

    /**
     * @notice Starts a new game with specific parameters Airnode details, initial token rate, etc.
     * non-zero initial token rate, and game not already started (initialTokenRate == 0).
     * @param _initialTokenRate The initial rate used within the game logic, set at the start and never changed afterward.
     * @custom:modifier onlyOwner Restricts the function's execution to the contract's owner.
     */
    function startGame(uint _initialTokenRate) external payable onlyOwner {
        // Ensure the initial token rate is not already set
        require(initialTokenRate == 0, "o-o");
        // Initialize the initial token rate and calculate the end time based on the current timestamp
        initialTokenRate = _initialTokenRate;
        endTime = block.timestamp + gamePeriod;
        if (msg.value > 0) {
            (bool success, ) = gameRngWallet.call{ value: msg.value }("");
            require(success);
        }
    }

    /// @notice Retrieves the balance of a given account
    /// @dev Returns the current balance stored in `userBalances`
    /// @param account The address of the user whose balance we want to retrieve
    /// @return The balance of the user
    function balanceOf(address account) public view returns (uint256) {
        return userBalances[account];
    }

    /// @notice Retrieves info of particular game id
    /// @param _gameId game number/id
    /// @return gameInfo GameRound struct
    function getGameRoundInfo(uint256 _gameId) public view returns (GameRound memory gameInfo) {
        gameInfo = gameRounds[_gameId];
    }

    /// @notice Retrieves the list of game IDs associated with a given user
    /// @dev Fetches the array of game IDs from `userGameIds` using `.values()`
    /// @param user The address of the user whose game IDs we want to retrieve
    /// @return ids An array of game IDs that the user participated in
    function getUserGameIds(address user) public view returns (uint256[] memory ids) {
        ids = userGameIds[user];
    }

    /// @notice Retrieves the number of games a user has participated in
    /// @dev Calculates the length of the user's game IDs set
    /// @param user The address of the user whose number of games we want to know
    /// @return num The number of games the user has participated in
    function getUserGamesNumber(address user) public view returns (uint256 num) {
        num = userGameIds[user].length;
    }

    // @notice Retrieves the last game information for a given user
    /// @dev Fetches the last game ID and corresponding round info from `userGameIds` and `gameRounds`
    /// @param user The address of the user whose last game information we want to retrieve
    /// @return id The ID of the last game the user participated in
    /// @return round The GameRound struct containing the details of the game round
    function getUserLastGameInfo(
        address user
    ) public view returns (uint256 id, GameRound memory round) {
        uint256 length = userGameIds[user].length;
        if (length > 0) {
            id = userGameIds[user][length - 1];
            round = gameRounds[id];
        }
    }

    /// @notice Determines whether the game is still ongoing or not
    /// @dev Compares the current block timestamp against `endTime`; also ensures that the game has started by requiring `_endTime` to be non-zero
    /// @return Whether the current time is before the game's end time (`true`) or after (`false`)
    function gameNotOver() public view returns (bool) {
        uint256 _endTime = endTime;
        _checkZero(_endTime);
        return block.timestamp < _endTime;
    }

    /**
     * @notice Checks if the game has been concluded based on the time limit.
     * @dev Returns true if the current block timestamp exceeds the end time of the game by 10 minutes.
     *      This implies a grace period of 10 minutes after the official end time before declaring the game over.
     *      The function requires that `endTime` is set and the game has started, otherwise it reverts with an error message.
     *
     * @return A boolean value indicating whether the game is over (true) or not (false).
     */
    function gameOver() public view returns (bool) {
        uint256 _endTime = endTime;
        _checkZero(_endTime);
        return (block.timestamp > _endTime && gameId == lastFulfilledGameId);
    }

    struct GameState {
        uint256 gameId;
        uint256 betNumber;
    }

    /// @dev This function returns the state of games that have not yet been fulfilled.
    /// It constructs an array of `GameState` structures representing each unfulfilled game's
    /// ID and the count of bets placed in that game round.
    /// The function only includes games with IDs greater than `lastFulfilledGameId`.
    /// @return state An array of `GameState` structs for each unfulfilled game.
    function getGameState() public view returns (GameState[] memory state) {
        if (gameId > lastFulfilledGameId) {
            uint256 requests = gameId - lastFulfilledGameId;
            state = new GameState[](requests);
            uint256 index;
            while (lastFulfilledGameId + index < gameId) {
                uint256 id = lastFulfilledGameId + index + 1;
                state[index].gameId = id;
                state[index].betNumber = gameRounds[id].betAmts.length;
                index++;
            }
        }
    }

    /// @notice Allows a user to place a bet on a dice roll(s), record the bet details, and request randomness
    /// @dev Transfers the required ETH to sponsor wallet and creates a new game round with provided bets
    /// @param betAmts An array of amounts representing individual bets for each roll of the dice
    /// @return gameId A unique identifier generated for the game round
    function bet(uint256[] memory betAmts) external payable shouldGameIsNotOver returns (uint256) {
        {
            (uint256 id, GameRound memory round) = getUserLastGameInfo(msg.sender);
            require(round.fulfilled || id == 0, "last round not fulfilled");
        }
        // Check if the number of dice rolls is within the permitted range
        uint256 numWords = betAmts.length;
        require(numWords > 0 && numWords <= MAX_NUM_WORDS, "invalid betAmts");
        // Calculate the total bet amount from the array of bets
        uint256 totalBetAmt;
        for (uint i; i < numWords; ) {
            // Each bet amount must be greater than zero
            _checkZero(betAmts[i]);
            unchecked {
                totalBetAmt += betAmts[i];
                ++i;
            }
        }
        // Ensure the user has enough points to cover their total bet
        // It is possible to resend a bid for the same balance,
        // so this check is also added to the callback function
        require(totalBetAmt <= balanceOf(msg.sender), "points are not enough");
        // user needs to send ether with the transaction
        // user must send enough ether for the callback
        // otherwise the transaction will fail
        uint256 minimumSend = tx.gasprice * CALLBACK_GAS;
        if (msg.value < minimumSend) {
            revert AmountOfEthSentIsTooSmall(msg.value, minimumSend);
        }
        _burnPoints(msg.sender, totalBetAmt);

        unchecked {
            ++gameId;
        }
        uint256 _gameId = gameId;

        // Record the game round details in the contract state
        gameRounds[_gameId] = GameRound({
            fulfilled: false,
            user: msg.sender,
            totalBet: totalBetAmt,
            totalWinnings: 0,
            betAmts: betAmts,
            diceRollResult: new uint256[](betAmts.length)
        });

        // Associate the game ID with the user's address
        userGameIds[msg.sender].push(_gameId);

        emit Bet(_gameId, msg.sender, totalBetAmt);
        // Transfer the received Ether to the sponsor's wallet to cover the callback transaction costs
        (bool success, ) = gameRngWallet.call{ value: msg.value }("");
        require(success);
        return _gameId;
    }

    struct RandomData {
        uint256 id;
        uint256[] rn;
    }

    /**
     * @notice Fulfills the generation of random words if gas requirement is met
     * @dev Processes each `RandomData` entries until either all are processed or minimum remaining gas is not met
     * @param minRemainingGas The minimum amount of gas that must be left for the function to continue processing
     * @param randomData An array of `RandomData` structs containing the IDs and random number arrays to process
     * Requirements:
     * - Only callable by the `gameRngWallet`.
     * - Will stop processing if the remaining gas is less than `minRemainingGas`.
     * Emits a `RandomWordsFulfilled` event upon successful processing of an entry.
     * Uses the `_fulfillRandomWords` internal function to process each entry.
     */
    function fulfillRandomWords(uint256 minRemainingGas, RandomData[] memory randomData) external {
        require(msg.sender == gameRngWallet, "invalid caller");
        for (uint256 i; i < randomData.length; ) {
            if (gasleft() < minRemainingGas) {
                break;
            }
            _fulfillRandomWords(randomData[i].id, randomData[i].rn);
            unchecked {
                ++i;
            }
        }
    }

    /// @notice Records the result of dice rolls, updates the game round, and handles payouts
    /// @dev Requires the caller to be the designated AirnodeRrp address and checks if the round can be fulfilled
    /// @param _gameId The unique identifier of the game round that the dice roll results correspond to
    /// @param _randomWords The array of random numbers provided by off-chain QRNG service
    ///  Using the QRNG service is free, meaning there is no subscription fee to pay.
    /// There is a gas cost incurred on-chain when Airnode places the random number on-chain in response to a request,
    /// which the requester needs to pay for.
    function _fulfillRandomWords(uint256 _gameId, uint256[] memory _randomWords) private {
        unchecked {
            ++lastFulfilledGameId;
        }
        // Retrieve the game round using the _gameId
        GameRound storage round = gameRounds[_gameId];
        uint256 totalBet = round.totalBet;
        if (_gameId != lastFulfilledGameId || totalBet == 0) {
            revert InvalidGameId(_gameId);
        }

        uint256 length = _randomWords.length;
        if (length != round.diceRollResult.length) {
            revert InvaliddiceRollResult(_gameId);
        }
        // Mark the round as fulfilled
        round.fulfilled = true;
        uint256 totalWinnings;

        uint256 bitDice;
        bool double3;
        for (uint i; i < length; ) {
            // Get the dice number between 1 and 6
            uint256 num = (_randomWords[i] % 6) + 1;
            // Calculate winnings based on even dice numbers
            if (num % 2 == 0) {
                totalWinnings += round.betAmts[i] * 2;
            }
            // Special logic for determining 33
            if (num == 3 && !double3 && bitDice & (1 << num) == (1 << num)) {
                double3 = true;
            }
            bitDice |= (1 << num);
            round.diceRollResult[i] = num;
            unchecked {
                ++i;
            }
        }
        // Special logic for determining winnings if the special 69 condition is met
        // or if the special 666 condition is met
        // or if the special repdigit condition is met
        if (length == 3) {
            //Repdigit
            if ((bitDice & (bitDice - 1)) == 0) {
                totalWinnings = 0;
                if (bitDice == 64) {
                    // 666
                    uint256 balance = balanceOf(round.user);
                    totalBet += balance;
                    _burnPoints(round.user, balance);
                }
            } else if ((bitDice == 72 && !double3) || bitDice == 112) {
                // 69
                totalWinnings = totalBet * WIN69_MULTIPLIER;
            }
        }
        if (totalWinnings > 0) {
            round.totalWinnings = totalWinnings;
            _mintPoints(round.user, totalWinnings);
        }
    }

    /**
     * @notice Allows users to purchase a specified amount of points.
     * @param desiredAmountOut The exact amount of points the user wants to purchase.
     */
    function purchasePoints(uint256 desiredAmountOut) external shouldGameIsNotOver {
        uint256 paymentAmount = calculatePaymentAmount(desiredAmountOut, true);
        coin.safeTransferFrom(msg.sender, address(this), paymentAmount);
        _mintPoints(msg.sender, desiredAmountOut);
        emit PurchasePoints(msg.sender, paymentAmount);
    }
    /**
     * @notice Allows users to sell a specified amount of points.
     * @param _amount The exact amount of points the user wants to sell. _amount must be for example - 100 points  ( 100e18)
     */
    function sellPoints(uint256 _amount) external payable shouldGameIsNotOver {
        if (isSellPointsForbidden) revert Forbidden();
        uint256 paymentAmount = calculatePaymentAmount(_amount, false);
        uint coinBalance = coin.getBalance();
        if (paymentAmount > coinBalance) revert NotEnoughCoinBalance(paymentAmount, coinBalance);
        if (coinBalance - paymentAmount > SELL_POINTS_LIMIT) revert SellPointsLimitReached();
        if (paymentAmount > 0) {
            _burnPoints(msg.sender, _amount);
            coin.safeTransfer(msg.sender, paymentAmount);
            emit SellPoints(msg.sender, paymentAmount, _amount);
        } else {
            revert ZeroValue();
        }
    }

    /**
     * @notice Calculates the payment amount required for purchasing a specific amount of points.
     * @param desiredPointsAmount The desired amount of points.
     * @return paymentAmount The corresponding amount of payment currency needed to purchase the points.
     */
    function calculatePaymentAmount(
        uint256 desiredPointsAmount,
        bool isBuy
    ) public view returns (uint256 paymentAmount) {
        uint256 _initialTokenRate = initialTokenRate; // 1_000_000e18   1000 points for 0.001 Coin  2
        if (_initialTokenRate > 0) {
            uint256 intermediate = (desiredPointsAmount * DELIMITER); // 1000* 1e18
            paymentAmount = intermediate / _initialTokenRate; // 2
            //round up
            if (isBuy) {
                if (paymentAmount == 0 || intermediate % _initialTokenRate > 0) {
                    paymentAmount += 1;
                }
            }
        } else {
            revert ZeroValue();
        }
    }

    /**
     * @notice Calculates the points amount a user receives for a given payment.
     * @param paymentAmount Amount of the payment currency (e.g., ETH) used to purchase tokens.
     * @return purchaseAmount The resulting amount of tokens that can be purchased with the specified `paymentAmount`.
     */
    function calculatePointsAmount(
        uint256 paymentAmount
    ) public view returns (uint256 purchaseAmount) {
        if (initialTokenRate > 0) {
            purchaseAmount = (paymentAmount * initialTokenRate) / DELIMITER;
        }
    }

    function sendLiquidity()
        external
        shouldGameIsOver
        onlyOwner
        returns (uint amount, uint totalPTS)
    {
        amount = coin.getBalance();
        coin.safeTransfer(V3Deployer, amount);
        totalPTS = totalSupply;
    }

    function setSellPointsMode(bool _sellForbidden) external onlyOwner {
        isSellPointsForbidden = _sellForbidden;
    }

    /// @notice Redeem points for tokens.
    /// @dev Burns points from the redeemer's balance and mints equivalent tokens.
    ///      Emits a Redeem event upon success.
    ///      Requires the game to be over.
    ///      Requires the Token to have been set and the caller to have a non-zero point balance.
    function redeem() external shouldGameIsOver {
        uint256 amount = balanceOf(msg.sender);
        _checkZero(amount);
        _burnPoints(msg.sender, amount);
        (bool success, ) = V3Deployer.call(
            abi.encodeWithSignature("redeem(address,uint256)", msg.sender, amount)
        );
        require(success);
        emit Redeem(msg.sender, amount);
    }

    /// @notice Mints points and assigns them to a specified account
    /// @dev Increments `userBalances` and `totalSupply` by the given `amount`
    /// @param to The address of the recipient to whom points are to be minted
    /// @param amount The quantity of points to be minted
    function _mintPoints(address to, uint256 amount) private {
        _checkZero(amount);
        userBalances[to] += amount;
        totalSupply += amount;
        emit MintPoints(to, amount);
    }

    /// @notice Burns points from a specified account's balance
    /// @dev Decrements `userBalances` and `totalSupply` by the given `amount`
    /// @param from The address from which points are to be burned
    /// @param amount The quantity of points to be burned
    function _burnPoints(address from, uint256 amount) private {
        _checkZero(amount);
        userBalances[from] -= amount;
        totalSupply -= amount;
        emit BurnPoints(from, amount);
    }

    function _checkZero(uint256 amount) private pure {
        require(amount > 0, "is zero");
    }
}

// SPDX-License-Identifier: GPL-2.0-or-later
// https://github.com/Uniswap/v3-periphery/blob/main/contracts/libraries/TransferHelper.sol
pragma solidity 0.8.19;

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

library TransferHelper {
    /// @notice Transfers tokens from the targeted address to the given destination
    /// @notice Errors with 'STF' if transfer fails
    /// @param token The contract address of the token to be transferred
    /// @param from The originating address from which the tokens will be transferred
    /// @param to The destination address of the transfer
    /// @param value The amount to be transferred
    function safeTransferFrom(address token, address from, address to, uint256 value) internal {
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value)
        );
        require(success && (data.length == 0 || abi.decode(data, (bool))), "BP-STF");
    }

    /// @notice Transfers tokens from msg.sender to a recipient
    /// @dev Errors with ST if transfer fails
    /// @param token The contract address of the token which will be transferred
    /// @param to The recipient of the transfer
    /// @param value The value of the transfer
    function safeTransfer(address token, address to, uint256 value) internal {
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(IERC20.transfer.selector, to, value)
        );
        require(success && (data.length == 0 || abi.decode(data, (bool))), "BP-ST");
    }

    function getBalance(address token) internal view returns (uint256 balance) {
        bytes memory callData = abi.encodeWithSelector(IERC20.balanceOf.selector, address(this));
        (bool success, bytes memory data) = token.staticcall(callData);
        require(success && data.length >= 32);
        balance = abi.decode(data, (uint256));
    }

    function getBalanceOf(address token, address target) internal view returns (uint256 balance) {
        bytes memory callData = abi.encodeWithSelector(IERC20.balanceOf.selector, target);
        (bool success, bytes memory data) = token.staticcall(callData);
        require(success && data.length >= 32);
        balance = abi.decode(data, (uint256));
    }

    function safeApprove(address token, address spender, uint256 amount) internal {
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(IERC20.approve.selector, spender, amount)
        );
        require(success && (data.length == 0 || abi.decode(data, (bool))), "BP-SA");
    }
}

Please enter a contract address above to load the contract details and source code.

Context size (optional):