ElementStaking

0x904353dfedf884b474ab1f474cf88ee43bc7aa40

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
4,376 bytes
ABI entries
44
15 read · 8 write
License
none

Contract information

Address
0x904353dfedf884b474ab1f474cf88ee43bc7aa40
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x41afed14ed…f4f2a26ed2

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (15)

element()address
lastUpdate()uint64
owner()address
pendingOwner()address
pendingReward(address)uint256
positions(address)uint256, uint8, uint64, uint256, uint256, uint256
rewardIndex()uint256
stakedBalanceOf(address)uint256
tierDurations(uint256)uint64
tierMultipliers(uint256)uint256
totalClaimed()uint256
totalDistributed()uint256
totalStaked()uint256
totalWeightedStake()uint256
usdg()address

Events (7)

OwnershipTransferStartedOwnershipTransferredRewardClaimedRewardIndexUpdatedStakedTiersSetUnstaked

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "element_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "usdg_",
        "type": "address"
      },
      {
        "internalType": "uint64[3]",
        "name": "durations_",
        "type": "uint64[3]"
      },
      {
        "internalType": "uint256[3]",
        "name": "multipliers_",
        "type": "uint256[3]"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AmountMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadTierConfig",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidTier",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LockupNotExpired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoRewards",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoStake",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TierDowngrade",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TierMultiplierTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountUsdg",
        "type": "uint256"
      }
    ],
    "name": "RewardClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "rewardIndex",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newRewards",
        "type": "uint256"
      }
    ],
    "name": "RewardIndexUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "tier",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint64",
        "name": "lockupExpiry",
        "type": "uint64"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "weightedAmount",
        "type": "uint256"
      }
    ],
    "name": "Staked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint64[3]",
        "name": "durations",
        "type": "uint64[3]"
      },
      {
        "indexed": false,
        "internalType": "uint256[3]",
        "name": "multipliers",
        "type": "uint256[3]"
      }
    ],
    "name": "TiersSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Unstaked",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimReward",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "element",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lastUpdate",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "pendingReward",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "positions",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint8",
        "name": "tier",
        "type": "uint8"
      },
      {
        "internalType": "uint64",
        "name": "lockupExpiry",
        "type": "uint64"
      },
      {
        "internalType": "uint256",
        "name": "weightedAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "rewardIndexSnap",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "accruedReward",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rewardIndex",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint64[3]",
        "name": "durations_",
        "type": "uint64[3]"
      },
      {
        "internalType": "uint256[3]",
        "name": "multipliers_",
        "type": "uint256[3]"
      }
    ],
    "name": "setTiers",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint8",
        "name": "tier",
        "type": "uint8"
      }
    ],
    "name": "stake",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "stakedBalanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "tierDurations",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "tierMultipliers",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalClaimed",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalDistributed",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalStaked",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalWeightedStake",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "unstake",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "updateRewardIndex",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "usdg",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {IStakingPool} from "./interfaces/IStakingPool.sol";

contract ElementStaking is IStakingPool, Ownable2Step {
    using SafeERC20 for IERC20;

    uint256 internal constant PRECISION = 1e18;
    uint256 internal constant NUM_TIERS = 3;

    uint256 internal constant MAX_TIER_MULTIPLIER = 1000;

    IERC20 public immutable element;
    IERC20 public immutable usdg;

    uint64[NUM_TIERS] public tierDurations;
    uint256[NUM_TIERS] public tierMultipliers;

    uint256 public rewardIndex;
    uint256 public totalWeightedStake;
    uint256 public totalStaked;

    uint256 public totalDistributed;
    uint256 public totalClaimed;
    uint64 public lastUpdate;

    struct Position {
        uint256 amount;
        uint8 tier;
        uint64 lockupExpiry;
        uint256 weightedAmount;
        uint256 rewardIndexSnap;
        uint256 accruedReward;
    }

    mapping(address => Position) public positions;

    error ZeroAddress();
    error ZeroAmount();
    error InvalidTier();
    error TierDowngrade();
    error LockupNotExpired();
    error NoStake();
    error NoRewards();
    error AmountMismatch();
    error BadTierConfig();
    error TierMultiplierTooHigh();

    event Staked(address indexed owner, uint256 amount, uint8 tier, uint64 lockupExpiry, uint256 weightedAmount);
    event Unstaked(address indexed owner, uint256 amount);
    event RewardClaimed(address indexed owner, uint256 amountUsdg);
    event RewardIndexUpdated(uint256 rewardIndex, uint256 newRewards);
    event TiersSet(uint64[3] durations, uint256[3] multipliers);

    constructor(address element_, address usdg_, uint64[3] memory durations_, uint256[3] memory multipliers_)
        Ownable(msg.sender)
    {
        if (element_ == address(0) || usdg_ == address(0)) revert ZeroAddress();
        element = IERC20(element_);
        usdg = IERC20(usdg_);
        _setTiers(durations_, multipliers_);
        lastUpdate = uint64(block.timestamp);
    }

    function _cumulativeReceived() internal view returns (uint256) {
        return usdg.balanceOf(address(this)) + totalClaimed;
    }

    function updateRewardIndex() public {
        uint256 cumulative = _cumulativeReceived();
        uint256 newRewards = cumulative > totalDistributed ? cumulative - totalDistributed : 0;
        if (totalWeightedStake != 0 && newRewards != 0) {
            rewardIndex += (newRewards * PRECISION) / totalWeightedStake;
            totalDistributed += newRewards;
            emit RewardIndexUpdated(rewardIndex, newRewards);
        }
        lastUpdate = uint64(block.timestamp);
    }

    function _pending(Position storage p) internal view returns (uint256) {
        return (p.weightedAmount * (rewardIndex - p.rewardIndexSnap)) / PRECISION + p.accruedReward;
    }

    function pendingReward(address owner) external view returns (uint256) {
        Position storage p = positions[owner];
        uint256 idx = rewardIndex;
        if (totalWeightedStake != 0) {
            uint256 cumulative = _cumulativeReceived();
            uint256 newRewards = cumulative > totalDistributed ? cumulative - totalDistributed : 0;
            if (newRewards != 0) idx += (newRewards * PRECISION) / totalWeightedStake;
        }
        return (p.weightedAmount * (idx - p.rewardIndexSnap)) / PRECISION + p.accruedReward;
    }

    function stakedBalanceOf(address owner) external view returns (uint256) {
        return positions[owner].amount;
    }

    function stake(uint256 amount, uint8 tier) external {
        if (tier >= NUM_TIERS) revert InvalidTier();
        Position storage p = positions[msg.sender];
        bool existing = p.amount > 0;
        if (amount == 0 && !existing) revert ZeroAmount();

        updateRewardIndex();

        if (existing) {
            if (tier < p.tier) revert TierDowngrade();
            p.accruedReward = _pending(p);
        }
        p.rewardIndexSnap = rewardIndex;

        if (amount > 0) {
            uint256 balBefore = element.balanceOf(address(this));
            element.safeTransferFrom(msg.sender, address(this), amount);
            uint256 received = element.balanceOf(address(this)) - balBefore;
            if (received != amount) revert AmountMismatch();
        }

        uint256 newAmount = p.amount + amount;
        uint256 newWeighted = newAmount * tierMultipliers[tier];
        totalWeightedStake = totalWeightedStake - p.weightedAmount + newWeighted;
        totalStaked = totalStaked - p.amount + newAmount;

        p.amount = newAmount;
        p.tier = tier;
        uint64 newExpiry = uint64(block.timestamp) + tierDurations[tier];
        if (newExpiry > p.lockupExpiry) p.lockupExpiry = newExpiry;
        p.weightedAmount = newWeighted;

        emit Staked(msg.sender, amount, tier, p.lockupExpiry, newWeighted);
    }

    function claimReward() external {
        updateRewardIndex();
        Position storage p = positions[msg.sender];
        uint256 pending = _pending(p);
        if (pending == 0) revert NoRewards();
        p.accruedReward = 0;
        p.rewardIndexSnap = rewardIndex;
        totalClaimed += pending;
        usdg.safeTransfer(msg.sender, pending);
        emit RewardClaimed(msg.sender, pending);
    }

    function unstake() external {
        Position storage p = positions[msg.sender];
        if (p.amount == 0) revert NoStake();
        if (block.timestamp < p.lockupExpiry) revert LockupNotExpired();

        updateRewardIndex();
        uint256 pending = _pending(p);
        uint256 amount = p.amount;

        totalWeightedStake -= p.weightedAmount;
        totalStaked -= amount;
        delete positions[msg.sender];

        if (pending > 0) {
            totalClaimed += pending;
            usdg.safeTransfer(msg.sender, pending);
            emit RewardClaimed(msg.sender, pending);
        }
        element.safeTransfer(msg.sender, amount);
        emit Unstaked(msg.sender, amount);
    }

    function setTiers(uint64[3] calldata durations_, uint256[3] calldata multipliers_) external onlyOwner {
        _setTiers(durations_, multipliers_);
    }

    function _setTiers(uint64[3] memory durations_, uint256[3] memory multipliers_) internal {
        for (uint256 i = 0; i < NUM_TIERS; i++) {
            if (durations_[i] == 0 || multipliers_[i] == 0) revert BadTierConfig();
            if (multipliers_[i] > MAX_TIER_MULTIPLIER) revert TierMultiplierTooHigh();
            tierDurations[i] = durations_[i];
            tierMultipliers[i] = multipliers_[i];
        }
        emit TiersSet(durations_, multipliers_);
    }
}
Chain explorer1315msChain node83ms