Address
0xebbb787676efc7fb0ee33ed8d4373cc2dda0eb43Current 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);
}
}