Address
0x1dafea6ebdafd2bd9c3fbf86dfccccabbb0e3bdaCurrent Holdings
$0.2555
TXs sent
not counted
First Active
2026-07-02
block 26,932,070
Last Active
75 days ago
block 26,942,104
Funded By
not identified
Net worth historyi
7 snapshots · to block 27,472,065coverage change 26 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Aug
exact matchStakingsolc 0.8.20+commit.a1b79de6runtime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function balanceOf(address) external view returns (uint256);
function transfer(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
}
contract Staking {
uint256 public constant PRECISION = 1e18;
uint256 public constant DECAY_GRACE = 4 weeks;
uint256 public constant MAX_DECAY_WEEKS = 50;
uint256 public constant MAX_REWARD_TOKENS = 10;
uint256 public constant MAX_DECAY_BATCH = 5;
uint256 public constant BOT_WINDOW = 24 hours;
uint256 public constant MIN_KEEP_PCT = 40;
uint256 public constant TIER_COUNT = 5;
uint256 public constant EMERGENCY_UNSTAKE_FORFEIT_MULTIPLIER = 200; // 2.0x
uint256[5] public DURATIONS = [30 days, 90 days, 180 days, 365 days, 730 days];
uint256[5] public MULTIPLIERS = [100, 150, 200, 300, 500];
uint256[5] public REBATES = [500, 1000, 2000, 3000, 4000];
string[5] public TIER_NAMES = ["Hatchling", "Drake", "Dragon", "Elder Dragon", "Smaug"];
IERC20 public immutable smaug;
address public owner;
uint256 public minStakeAmount;
uint256 public processDecayBounty;
uint256 public bountyPool;
uint256 public decayProcessHead;
uint256 public totalStaked;
uint256 public totalWeightedStake;
uint256 public stakeCount;
uint256 public totalStakers;
struct StakeInfo {
address owner;
uint256 amountStaked;
uint256 stakeStartTime;
uint256 intendedDuration;
uint256 endTime;
uint256 tierIndex;
uint256 multiplierBps;
uint256 weightedAmount;
}
mapping(uint256 => StakeInfo) public stakes;
mapping(address => uint256[]) public userStakeIds;
address[] public rewardTokenList;
mapping(address => bool) public isRewardToken;
struct RewardPool {
uint256 accPerWeightedShare;
uint256 totalPending;
uint256 totalDistributed;
}
mapping(address => RewardPool) public rewardPools;
mapping(uint256 => mapping(address => uint256)) public stakeRewardDebt;
mapping(uint256 => mapping(address => uint256)) public stakeRewardPending;
struct DecayCandidate { uint256 stakeId; uint256 eligibleAt; }
DecayCandidate[] public decayCandidates;
uint256 public decayCandidateHead;
mapping(uint256 => bool) public isDecaying;
uint256[] public decayingStakes;
mapping(uint256 => uint256) public decayingIndex;
mapping(uint256 => mapping(address => uint256)) public weeklyDecayAmount;
mapping(uint256 => uint256) public decayStartTime;
mapping(uint256 => uint256) public decayLastProcessed;
mapping(uint256 => mapping(address => uint256)) public decayRewards;
mapping(uint256 => bool) public principalUnweighted;
mapping(address => mapping(address => uint256)) public totalClaimedByWallet;
mapping(address => uint256) public totalDistributedByToken;
mapping(address => uint256) public totalForfeitedByToken;
bool private _locked;
event Staked(address indexed user, uint256 indexed stakeId, uint256 amount, uint256 duration, string tier);
event Unstaked(address indexed user, uint256 indexed stakeId, uint256 principal, uint256 smaugRewards);
event RewardClaimed(address indexed user, uint256 indexed stakeId, address indexed token, uint256 kept, uint256 forfeited);
event StakeDecayStarted(uint256 indexed stakeId, address indexed owner);
event DecayProcessed(uint256 indexed stakeId, address indexed token, uint256 forfeited, uint256 remaining);
event PrincipalUnweighted(uint256 indexed stakeId);
event RewardDistributed(address indexed token, uint256 amount);
event RewardTokenAdded(address indexed token);
event PLSReceived(address indexed sender, uint256 amount);
event BountyPaid(address indexed caller, uint256 indexed stakeId, uint256 amount);
event BountyPoolFunded(uint256 amount);
event ProcessDecayBountyUpdated(uint256 newBounty);
event RewardTokenRemoved(address indexed token);
event OwnershipTransferred(address indexed from, address indexed to);
modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; }
modifier nonReentrant() { require(!_locked, "Reentrant"); _locked = true; _; _locked = false; }
constructor() {
address _smaugAddress = 0xf4754Aa585caBf38537A68660469A17E203D8632;
uint256 _minStake = 10_000 * 1e18;
smaug = IERC20(_smaugAddress);
owner = msg.sender;
minStakeAmount = _minStake;
_addRewardToken(address(0));
_addRewardToken(_smaugAddress);
}
function stake(uint256 amount, uint256 duration) external nonReentrant {
require(amount >= minStakeAmount, "Below min");
uint256 tier = _matchTier(duration);
uint256 weighted = (amount * MULTIPLIERS[tier]) / 100;
require(smaug.transferFrom(msg.sender, address(this), amount), "Fail");
_sweepReflections();
_processCandidates();
_processOneDecayingStake();
uint256 stakeId = stakeCount++;
stakes[stakeId] = StakeInfo({
owner: msg.sender,
amountStaked: amount,
stakeStartTime: block.timestamp,
intendedDuration: DURATIONS[tier],
endTime: block.timestamp + DURATIONS[tier],
tierIndex: tier,
multiplierBps: MULTIPLIERS[tier],
weightedAmount: weighted
});
// Increment totalStakers only when user has no existing stakes
if (userStakeIds[msg.sender].length == 0) totalStakers++;
userStakeIds[msg.sender].push(stakeId);
totalStaked += amount;
totalWeightedStake += weighted;
decayCandidates.push(DecayCandidate({stakeId: stakeId, eligibleAt: block.timestamp + DURATIONS[tier] + DECAY_GRACE}));
_initRewardDebts(stakeId, weighted);
emit Staked(msg.sender, stakeId, amount, DURATIONS[tier], TIER_NAMES[tier]);
}
function claimRewards(uint256 stakeId) external nonReentrant {
StakeInfo storage s = stakes[stakeId];
require(s.owner == msg.sender && s.amountStaked > 0 && !isDecaying[stakeId], "Invalid");
_sweepReflections();
_processCandidates();
_processOneDecayingStake();
_settlePendingAll(stakeId, s.weightedAmount);
for (uint256 i = 0; i < rewardTokenList.length; i++) {
address token = rewardTokenList[i];
uint256 pending = stakeRewardPending[stakeId][token];
if (pending == 0) continue;
(uint256 keep, uint256 forfeit) = _penaltyAmount(stakeId, pending);
stakeRewardPending[stakeId][token] = 0;
rewardPools[token].totalPending -= pending;
if (forfeit > 0) {
totalForfeitedByToken[token] += forfeit;
_distributeReward(token, forfeit);
}
totalClaimedByWallet[msg.sender][token] += keep;
_transferReward(token, msg.sender, keep);
emit RewardClaimed(msg.sender, stakeId, token, keep, forfeit);
}
}
function unstake(uint256 stakeId) external nonReentrant {
StakeInfo storage s = stakes[stakeId];
require(s.owner == msg.sender && s.amountStaked > 0, "Invalid");
_sweepReflections();
_processCandidates();
_processOneDecayingStake();
uint256 principal = s.amountStaked;
if (isDecaying[stakeId]) {
_processDecayInternal(stakeId);
if (!principalUnweighted[stakeId]) totalStaked -= principal;
for (uint256 i = 0; i < rewardTokenList.length; i++) {
address token = rewardTokenList[i];
uint256 remaining = decayRewards[stakeId][token];
if (remaining == 0) continue;
decayRewards[stakeId][token] = 0;
rewardPools[token].totalPending -= remaining;
totalClaimedByWallet[msg.sender][token] += remaining;
_transferReward(token, msg.sender, remaining);
emit RewardClaimed(msg.sender, stakeId, token, remaining, 0);
}
_removeFromDecayList(stakeId);
_removeUserStake(msg.sender, stakeId);
// Decrement totalStakers when user has no remaining stakes
if (userStakeIds[msg.sender].length == 0) totalStakers--;
delete stakes[stakeId];
require(smaug.transfer(msg.sender, principal), "Fail");
emit Unstaked(msg.sender, stakeId, principal, 0);
return;
}
_settlePendingAll(stakeId, s.weightedAmount);
totalStaked -= principal;
totalWeightedStake -= s.weightedAmount;
uint256 smaugRewardKeep = 0;
for (uint256 i = 0; i < rewardTokenList.length; i++) {
address token = rewardTokenList[i];
uint256 pending = stakeRewardPending[stakeId][token];
if (pending == 0) continue;
(uint256 keep, uint256 forfeit) = _penaltyAmount(stakeId, pending);
// Emergency unstake: double the forfeit
if (forfeit > 0) {
forfeit = (forfeit * EMERGENCY_UNSTAKE_FORFEIT_MULTIPLIER) / 100;
if (forfeit > pending) forfeit = pending;
keep = pending - forfeit;
}
stakeRewardPending[stakeId][token] = 0;
rewardPools[token].totalPending -= pending;
if (forfeit > 0) {
totalForfeitedByToken[token] += forfeit;
_distributeReward(token, forfeit);
}
totalClaimedByWallet[msg.sender][token] += keep;
if (token == address(smaug)) smaugRewardKeep = keep;
else _transferReward(token, msg.sender, keep);
emit RewardClaimed(msg.sender, stakeId, token, keep, forfeit);
}
_removeUserStake(msg.sender, stakeId);
// Decrement totalStakers when user has no remaining stakes
if (userStakeIds[msg.sender].length == 0) totalStakers--;
delete stakes[stakeId];
require(smaug.transfer(msg.sender, principal + smaugRewardKeep), "Fail");
emit Unstaked(msg.sender, stakeId, principal, smaugRewardKeep);
}
function processStakeDecay(uint256 stakeId) external nonReentrant {
require(isDecaying[stakeId], "No");
_sweepReflections();
uint256 last = decayLastProcessed[stakeId];
bool didWork = (block.timestamp - last) >= 1 weeks;
_processDecayInternal(stakeId);
_checkPrincipalExpiry(stakeId);
if (didWork && block.timestamp >= last + 1 weeks + BOT_WINDOW) {
uint256 bounty = processDecayBounty;
if (bounty > 0 && bountyPool >= bounty) {
bountyPool -= bounty;
(bool ok,) = payable(msg.sender).call{value: bounty}("");
if (ok) emit BountyPaid(msg.sender, stakeId, bounty);
else bountyPool += bounty;
}
}
}
function notifyPLSReward() external payable nonReentrant {
require(msg.value > 0, "Zero");
_distributeReward(address(0), msg.value);
}
function notifyERC20Reward(address token, uint256 amount) external nonReentrant {
require(isRewardToken[token] && amount > 0, "Invalid");
require(IERC20(token).transferFrom(msg.sender, address(this), amount), "Fail");
_distributeReward(token, amount);
}
function sweepUnaccountedRewards(address token) external nonReentrant {
require(isRewardToken[token] && token != address(0) && token != address(smaug), "Invalid");
uint256 bal = IERC20(token).balanceOf(address(this));
uint256 acc = rewardPools[token].totalPending;
if (bal > acc) _distributeReward(token, bal - acc);
}
receive() external payable {
if (msg.value > 0 && totalWeightedStake > 0) {
_distributeReward(address(0), msg.value);
emit PLSReceived(msg.sender, msg.value);
}
}
function addRewardToken(address token) external onlyOwner { _addRewardToken(token); }
function removeRewardToken(address token) external onlyOwner {
require(isRewardToken[token] && token != address(smaug) && token != address(0) && rewardPools[token].totalPending == 0, "Invalid");
isRewardToken[token] = false;
for (uint256 i = 0; i < rewardTokenList.length; i++) {
if (rewardTokenList[i] == token) {
rewardTokenList[i] = rewardTokenList[rewardTokenList.length - 1];
rewardTokenList.pop();
break;
}
}
}
function setMinStakeAmount(uint256 amount) external onlyOwner { minStakeAmount = amount; }
function fundBountyPool() external payable onlyOwner { bountyPool += msg.value; }
function setProcessDecayBounty(uint256 amount) external onlyOwner { processDecayBounty = amount; }
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "Zero address");
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
function _sweepReflections() internal {
uint256 bal = smaug.balanceOf(address(this));
uint256 pending = rewardPools[address(smaug)].totalPending;
if (bal > totalStaked + pending && totalWeightedStake > 0) {
_distributeReward(address(smaug), bal - totalStaked - pending);
}
}
function _processCandidates() internal {
uint256 head = decayCandidateHead;
uint256 moved = 0;
while (head < decayCandidates.length && moved < MAX_DECAY_BATCH) {
if (decayCandidates[head].eligibleAt > block.timestamp) break;
uint256 sid = decayCandidates[head].stakeId;
head++;
if (isDecaying[sid] || stakes[sid].amountStaked == 0) continue;
_startDecay(sid);
moved++;
}
decayCandidateHead = head;
}
function _startDecay(uint256 sid) internal {
StakeInfo storage s = stakes[sid];
_settlePendingAll(sid, s.weightedAmount);
for (uint256 i = 0; i < rewardTokenList.length; i++) {
address t = rewardTokenList[i];
uint256 p = stakeRewardPending[sid][t];
if (p > 0) {
decayRewards[sid][t] = p;
stakeRewardPending[sid][t] = 0;
weeklyDecayAmount[sid][t] = p / MAX_DECAY_WEEKS;
}
}
totalWeightedStake -= s.weightedAmount;
s.weightedAmount = 0;
isDecaying[sid] = true;
decayingIndex[sid] = decayingStakes.length;
decayingStakes.push(sid);
decayStartTime[sid] = block.timestamp;
decayLastProcessed[sid] = block.timestamp;
emit StakeDecayStarted(sid, s.owner);
}
function _processOneDecayingStake() internal {
if (decayingStakes.length == 0) return;
if (decayProcessHead >= decayingStakes.length) decayProcessHead = 0;
uint256 sid = decayingStakes[decayProcessHead];
decayProcessHead = (decayProcessHead + 1) % decayingStakes.length;
if ((block.timestamp - decayLastProcessed[sid]) >= 1 weeks) {
_processDecayInternal(sid);
_checkPrincipalExpiry(sid);
}
}
function _processDecayInternal(uint256 sid) internal {
uint256 last = decayLastProcessed[sid];
uint256 weeksPassed = (block.timestamp - last) / 1 weeks;
if (weeksPassed == 0) return;
decayLastProcessed[sid] = last + weeksPassed * 1 weeks;
if (weeksPassed > MAX_DECAY_WEEKS) weeksPassed = MAX_DECAY_WEEKS;
for (uint256 i = 0; i < rewardTokenList.length; i++) {
address t = rewardTokenList[i];
uint256 rem = decayRewards[sid][t];
if (rem == 0) continue;
uint256 f;
if (block.timestamp >= decayStartTime[sid] + MAX_DECAY_WEEKS * 1 weeks) f = rem;
else {
f = weeklyDecayAmount[sid][t] * weeksPassed;
if (f > rem) f = rem;
}
decayRewards[sid][t] = rem - f;
rewardPools[t].totalPending -= f;
_distributeReward(t, f);
emit DecayProcessed(sid, t, f, rem - f);
}
}
function _checkPrincipalExpiry(uint256 sid) internal {
if (!principalUnweighted[sid] && block.timestamp >= decayStartTime[sid] + MAX_DECAY_WEEKS * 1 weeks) {
totalStaked -= stakes[sid].amountStaked;
principalUnweighted[sid] = true;
}
}
function _distributeReward(address t, uint256 a) internal {
if (totalWeightedStake == 0 || a == 0) return;
rewardPools[t].accPerWeightedShare += (a * PRECISION) / totalWeightedStake;
rewardPools[t].totalPending += a;
rewardPools[t].totalDistributed += a;
totalDistributedByToken[t] += a;
emit RewardDistributed(t, a);
}
function _settlePendingAll(uint256 sid, uint256 w) internal {
if (w == 0) return;
for (uint256 i = 0; i < rewardTokenList.length; i++) {
address t = rewardTokenList[i];
uint256 acc = (w * rewardPools[t].accPerWeightedShare) / PRECISION;
uint256 d = stakeRewardDebt[sid][t];
if (acc > d) stakeRewardPending[sid][t] += acc - d;
stakeRewardDebt[sid][t] = acc;
}
}
function _initRewardDebts(uint256 sid, uint256 w) internal {
for (uint256 i = 0; i < rewardTokenList.length; i++) {
address t = rewardTokenList[i];
stakeRewardDebt[sid][t] = (w * rewardPools[t].accPerWeightedShare) / PRECISION;
}
}
function _addRewardToken(address t) internal {
if (!isRewardToken[t] && rewardTokenList.length < MAX_REWARD_TOKENS) {
isRewardToken[t] = true;
rewardTokenList.push(t);
emit RewardTokenAdded(t);
}
}
function _penaltyAmount(uint256 sid, uint256 a) internal view returns (uint256 k, uint256 f) {
StakeInfo storage s = stakes[sid];
uint256 elapsed = block.timestamp - s.stakeStartTime;
if (elapsed >= s.intendedDuration) return (a, 0);
// Quadratic curve: keepPct = MIN_KEEP_PCT + 60 * t^2
// t scaled to PRECISION (1e18) for consistent fixed-point math
uint256 t = (elapsed * PRECISION) / s.intendedDuration;
uint256 tSquared = (t * t) / PRECISION;
uint256 maxBonus = 100 - MIN_KEEP_PCT;
uint256 keepPct = MIN_KEEP_PCT + (maxBonus * tSquared) / PRECISION;
if (keepPct > 100) keepPct = 100;
k = (a * keepPct) / 100;
f = a - k;
}
function _matchTier(uint256 d) internal view returns (uint256) {
for (uint256 i = TIER_COUNT - 1; ; i--) {
if (d >= DURATIONS[i]) return i;
if (i == 0) break;
}
revert("Short");
}
function _transferReward(address t, address to, uint256 a) internal {
if (a == 0) return;
if (t == address(0)) { (bool ok,) = payable(to).call{value: a}(""); require(ok, "Fail"); }
else require(IERC20(t).transfer(to, a), "Fail");
}
function _removeFromDecayList(uint256 sid) internal {
uint256 idx = decayingIndex[sid];
uint256 last = decayingStakes.length - 1;
if (idx != last) {
uint256 lastId = decayingStakes[last];
decayingStakes[idx] = lastId;
decayingIndex[lastId] = idx;
}
decayingStakes.pop();
delete isDecaying[sid];
delete decayingIndex[sid];
}
function _removeUserStake(address u, uint256 sid) internal {
uint256[] storage ids = userStakeIds[u];
for (uint256 i = 0; i < ids.length; i++) {
if (ids[i] == sid) { ids[i] = ids[ids.length - 1]; ids.pop(); return; }
}
}
function pendingReward(uint256 sid, address t) public view returns (uint256) {
if (isDecaying[sid]) {
uint256 rem = decayRewards[sid][t];
if (rem == 0 || block.timestamp >= decayStartTime[sid] + MAX_DECAY_WEEKS * 1 weeks) return 0;
uint256 weeksSince = (block.timestamp - decayLastProcessed[sid]) / 1 weeks;
if (weeksSince == 0) return rem;
uint256 f = weeklyDecayAmount[sid][t] * weeksSince;
return f >= rem ? 0 : rem - f;
}
StakeInfo storage s = stakes[sid];
uint256 acc = (s.weightedAmount * rewardPools[t].accPerWeightedShare) / PRECISION;
uint256 d = stakeRewardDebt[sid][t];
return stakeRewardPending[sid][t] + (acc > d ? acc - d : 0);
}
function estimateClaimPenalty(uint256 sid) external view returns (uint256 k, uint256 r) {
StakeInfo storage s = stakes[sid];
uint256 e = block.timestamp - s.stakeStartTime;
if (e >= s.intendedDuration) return (100, 0);
// Quadratic curve: matches _penaltyAmount exactly
uint256 t = (e * PRECISION) / s.intendedDuration;
uint256 tSquared = (t * t) / PRECISION;
uint256 maxBonus = 100 - MIN_KEEP_PCT;
k = MIN_KEEP_PCT + (maxBonus * tSquared) / PRECISION;
if (k > 100) k = 100;
r = s.endTime - block.timestamp;
}
function stakingRebateBps(address u) external view returns (uint256 max) {
uint256[] storage ids = userStakeIds[u];
for (uint256 i = 0; i < ids.length; i++) {
if (!isDecaying[ids[i]] && stakes[ids[i]].amountStaked > 0) {
uint256 r = REBATES[stakes[ids[i]].tierIndex];
if (r > max) max = r;
}
}
}
}