Skip to main content
PulseScanner.io

Address

0x1dafea6ebdafd2bd9c3fbf86dfccccabbb0e3bda
Current 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;
            }
        }
    }
}