Skip to main content
PulseScanner.io

Address

0xebbb787676efc7fb0ee33ed8d4373cc2dda0eb43
Current Holdings
$0.00
TXs sent
0
First Active
2026-07-30
block 27,163,209
Last Active
9 days ago
block 27,491,212
Funded By
not identified

Net worth historyi

4 snapshots · to block 27,491,212coverage change 26 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Aug
exact matchBloomStaking1solc 0.8.34+commit.80d5c536runtime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";

/// @notice One staking pool. Deploy once per staking/reward pair.
///
/// PulseChain Remix note:
/// Compile with EVM VERSION `paris`.
contract BloomStaking1 is Ownable2Step, ReentrancyGuard, Pausable {
    using SafeERC20 for IERC20;

    uint256 private constant RATE_SCALE = 1e18;

    IERC20 public immutable stakingToken;
    IERC20 public immutable rewardToken;
    uint8 public immutable stakingDecimals;
    uint8 public immutable rewardDecimals;

    uint256 public totalStaked;
    mapping(address => uint256) public stakedBalance;

    // Reward-token base units per second, scaled by 1e18 for low-decimal tokens.
    uint256 public rewardRateX18;
    uint256 public periodFinish;
    uint256 public lastUpdateTime;
    uint256 public rewardPerTokenStored;

    // Rewards funded while nobody is staked wait here and start on the first stake.
    uint256 public queuedRewards;
    uint256 public queuedDuration;

    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public rewards;

    event Staked(address indexed user, uint256 amount);
    event Unstaked(address indexed user, uint256 amount);
    event RewardPaid(address indexed user, uint256 amount);
    event RewardsQueued(uint256 amount, uint256 duration);
    event RewardsFunded(uint256 amount, uint256 duration, uint256 rewardRate, uint256 periodFinish);
    event RewardsStarted(uint256 amount, uint256 duration, uint256 rewardRateX18, uint256 periodFinish);
    event ForeignTokenRescued(address indexed token, uint256 amount);

    modifier updateReward(address account) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = lastTimeRewardApplicable();

        if (account != address(0)) {
            rewards[account] = earned(account);
            userRewardPerTokenPaid[account] = rewardPerTokenStored;
        }

        _;
    }

    constructor(address stakingToken_, address rewardToken_, address owner_) Ownable(owner_) {
        require(stakingToken_ != address(0), "staking token required");
        require(rewardToken_ != address(0), "reward token required");
        require(owner_ != address(0), "owner required");

        stakingToken = IERC20(stakingToken_);
        rewardToken = IERC20(rewardToken_);
        stakingDecimals = IERC20Metadata(stakingToken_).decimals();
        rewardDecimals = IERC20Metadata(rewardToken_).decimals();
        lastUpdateTime = block.timestamp;
    }

    function stake(uint256 amount) external nonReentrant whenNotPaused updateReward(msg.sender) {
        require(amount > 0, "zero amount");

        uint256 beforeTotalStaked = totalStaked;
        uint256 beforeBalance = stakingToken.balanceOf(address(this));
        stakingToken.safeTransferFrom(msg.sender, address(this), amount);
        uint256 received = stakingToken.balanceOf(address(this)) - beforeBalance;
        require(received > 0, "nothing received");

        totalStaked += received;
        stakedBalance[msg.sender] += received;
        emit Staked(msg.sender, received);

        if (beforeTotalStaked == 0) {
            _startQueuedRewards();
        }
    }

    function unstake(uint256 amount) public nonReentrant updateReward(msg.sender) {
        require(amount > 0, "zero amount");
        require(stakedBalance[msg.sender] >= amount, "insufficient stake");

        totalStaked -= amount;
        stakedBalance[msg.sender] -= amount;
        stakingToken.safeTransfer(msg.sender, amount);
        emit Unstaked(msg.sender, amount);

        if (totalStaked == 0) {
            _pauseActiveRewards();
        }
    }

    function claimReward() public nonReentrant updateReward(msg.sender) {
        uint256 reward = rewards[msg.sender];
        require(reward > 0, "no reward");

        rewards[msg.sender] = 0;
        rewardToken.safeTransfer(msg.sender, reward);
        emit RewardPaid(msg.sender, reward);
    }

    function exit() external {
        uint256 balance = stakedBalance[msg.sender];
        if (balance > 0) {
            unstake(balance);
        }

        if (earned(msg.sender) > 0) {
            claimReward();
        }
    }

    /// @notice Approximate raw reward-token base units per second.
    /// @dev Kept for frontends that already read rewardRate().
    function rewardRate() external view returns (uint256) {
        return rewardRateX18 / RATE_SCALE;
    }

    /// @notice Fund rewards using raw token units.
    /// @dev Owner must approve this contract to spend rewardAmount before calling.
    function fundRewards(uint256 rewardAmount, uint256 durationSeconds)
        external
        onlyOwner
        nonReentrant
        updateReward(address(0))
    {
        _fundRewards(rewardAmount, durationSeconds);
    }

    /// @notice Easier Remix helper. Amount is whole reward tokens, duration is seconds.
    function fundRewardsWholeTokens(uint256 rewardWholeTokens, uint256 durationSeconds)
        external
        onlyOwner
        nonReentrant
        updateReward(address(0))
    {
        uint256 rewardScale = 10 ** uint256(rewardDecimals);
        require(rewardWholeTokens <= type(uint256).max / rewardScale, "reward overflow");
        _fundRewards(rewardWholeTokens * rewardScale, durationSeconds);
    }

    function earned(address account) public view returns (uint256) {
        return ((stakedBalance[account] * (rewardPerToken() - userRewardPerTokenPaid[account])) / RATE_SCALE)
            + rewards[account];
    }

    function rewardPerToken() public view returns (uint256) {
        if (totalStaked == 0) {
            return rewardPerTokenStored;
        }

        uint256 applicableTime = lastTimeRewardApplicable();
        if (applicableTime <= lastUpdateTime) {
            return rewardPerTokenStored;
        }

        return rewardPerTokenStored + Math.mulDiv(applicableTime - lastUpdateTime, rewardRateX18, totalStaked);
    }

    function lastTimeRewardApplicable() public view returns (uint256) {
        if (rewardRateX18 == 0) {
            return lastUpdateTime;
        }
        return block.timestamp < periodFinish ? block.timestamp : periodFinish;
    }

    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    /// @notice Rescue unrelated tokens accidentally sent here.
    /// @dev Does not allow rescuing staking or reward tokens.
    function rescueForeignToken(address token, uint256 amount) external onlyOwner {
        require(token != address(stakingToken), "cannot rescue staking token");
        require(token != address(rewardToken), "cannot rescue reward token");
        IERC20(token).safeTransfer(msg.sender, amount);
        emit ForeignTokenRescued(token, amount);
    }

    function _fundRewards(uint256 rewardAmount, uint256 durationSeconds) internal {
        require(rewardAmount > 0, "zero reward");
        require(durationSeconds > 0, "zero duration");

        uint256 beforeBalance = rewardToken.balanceOf(address(this));
        rewardToken.safeTransferFrom(msg.sender, address(this), rewardAmount);
        uint256 received = rewardToken.balanceOf(address(this)) - beforeBalance;
        require(received > 0, "nothing received");

        if (totalStaked == 0) {
            _queueRewards(received, durationSeconds);
            return;
        }

        uint256 rewardAmountToSchedule = received;
        if (block.timestamp < periodFinish && rewardRateX18 > 0) {
            uint256 remaining = periodFinish - block.timestamp;
            rewardAmountToSchedule += Math.mulDiv(remaining, rewardRateX18, RATE_SCALE);
        }

        _startRewardPeriod(rewardAmountToSchedule, durationSeconds);
    }

    function _queueRewards(uint256 amount, uint256 durationSeconds) internal {
        if (amount == 0) {
            return;
        }
        require(durationSeconds > 0, "zero duration");

        if (queuedRewards == 0) {
            queuedDuration = durationSeconds;
        } else {
            uint256 totalQueued = queuedRewards + amount;
            uint256 existingWeight = Math.mulDiv(queuedRewards, queuedDuration, totalQueued);
            uint256 newWeight = Math.mulDiv(amount, durationSeconds, totalQueued);
            queuedDuration = existingWeight + newWeight;
            if (queuedDuration == 0) {
                queuedDuration = durationSeconds;
            }
        }

        queuedRewards += amount;
        emit RewardsQueued(amount, queuedDuration);
    }

    function _startQueuedRewards() internal {
        if (totalStaked == 0 || queuedRewards == 0) {
            return;
        }

        uint256 amount = queuedRewards;
        uint256 durationSeconds = queuedDuration;
        queuedRewards = 0;
        queuedDuration = 0;
        _startRewardPeriod(amount, durationSeconds);
    }

    function _startRewardPeriod(uint256 rewardAmount, uint256 durationSeconds) internal {
        require(rewardAmount > 0, "zero reward");
        require(durationSeconds > 0, "zero duration");

        uint256 newRewardRateX18 = Math.mulDiv(rewardAmount, RATE_SCALE, durationSeconds);
        require(newRewardRateX18 > 0, "reward too small");

        rewardRateX18 = newRewardRateX18;
        lastUpdateTime = block.timestamp;
        periodFinish = block.timestamp + durationSeconds;

        emit RewardsFunded(rewardAmount, durationSeconds, rewardRateX18 / RATE_SCALE, periodFinish);
        emit RewardsStarted(rewardAmount, durationSeconds, rewardRateX18, periodFinish);
    }

    function _pauseActiveRewards() internal {
        if (rewardRateX18 == 0) {
            return;
        }

        if (block.timestamp >= periodFinish) {
            rewardRateX18 = 0;
            periodFinish = block.timestamp;
            lastUpdateTime = block.timestamp;
            return;
        }

        uint256 remaining = periodFinish - block.timestamp;
        uint256 undistributed = Math.mulDiv(remaining, rewardRateX18, RATE_SCALE);
        rewardRateX18 = 0;
        periodFinish = block.timestamp;
        lastUpdateTime = block.timestamp;

        _queueRewards(undistributed, remaining);
    }
}