ETH Price: $1,618.11 (+1.78%)

Transaction Decoder

Block:
20624083 at Aug-28-2024 02:23:59 AM +UTC
Transaction Fee:
0.000138005971564608 ETH $0.22
Gas Used:
106,848 Gas / 1.291610246 Gwei

Emitted Events:

145 NodeStaking.RewardPaid( user=[Sender] 0xf5b1973d916fc4c30cf88f2398a2d4d80a216eeb, reward=32577517493288820 )

Account State Difference:

  Address   Before After State Difference Code
(Titan Builder)
5.318977045418995269 Eth5.319030469418995269 Eth0.000053424
0xF4dA8857...e2eeA6C85 35.079402205953690591 Eth35.046824688460401771 Eth0.03257751749328882
0xf5b1973d...80A216eeB
0.007612694131308086 Eth
Nonce: 37
0.040052205653032298 Eth
Nonce: 38
0.032439511521724212

Execution Trace

NodeStaking.CALL( )
  • ETH 0.03257751749328882 0xf5b1973d916fc4c30cf88f2398a2d4d80a216eeb.CALL( )
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
    pragma solidity ^0.8.20;
    import {Context} from "../utils/Context.sol";
    /**
     * @dev Contract module which provides a basic access control mechanism, where
     * there is an account (an owner) that can be granted exclusive access to
     * specific functions.
     *
     * The initial owner is set to the address provided by the deployer. This can
     * later be changed with {transferOwnership}.
     *
     * This module is used through inheritance. It will make available the modifier
     * `onlyOwner`, which can be applied to your functions to restrict their use to
     * the owner.
     */
    abstract contract Ownable is Context {
        address private _owner;
        /**
         * @dev The caller account is not authorized to perform an operation.
         */
        error OwnableUnauthorizedAccount(address account);
        /**
         * @dev The owner is not a valid owner account. (eg. `address(0)`)
         */
        error OwnableInvalidOwner(address owner);
        event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
        /**
         * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
         */
        constructor(address initialOwner) {
            if (initialOwner == address(0)) {
                revert OwnableInvalidOwner(address(0));
            }
            _transferOwnership(initialOwner);
        }
        /**
         * @dev Throws if called by any account other than the owner.
         */
        modifier onlyOwner() {
            _checkOwner();
            _;
        }
        /**
         * @dev Returns the address of the current owner.
         */
        function owner() public view virtual returns (address) {
            return _owner;
        }
        /**
         * @dev Throws if the sender is not the owner.
         */
        function _checkOwner() internal view virtual {
            if (owner() != _msgSender()) {
                revert OwnableUnauthorizedAccount(_msgSender());
            }
        }
        /**
         * @dev Leaves the contract without owner. It will not be possible to call
         * `onlyOwner` functions. Can only be called by the current owner.
         *
         * NOTE: Renouncing ownership will leave the contract without an owner,
         * thereby disabling any functionality that is only available to the owner.
         */
        function renounceOwnership() public virtual onlyOwner {
            _transferOwnership(address(0));
        }
        /**
         * @dev Transfers ownership of the contract to a new account (`newOwner`).
         * Can only be called by the current owner.
         */
        function transferOwnership(address newOwner) public virtual onlyOwner {
            if (newOwner == address(0)) {
                revert OwnableInvalidOwner(address(0));
            }
            _transferOwnership(newOwner);
        }
        /**
         * @dev Transfers ownership of the contract to a new account (`newOwner`).
         * Internal function without access restriction.
         */
        function _transferOwnership(address newOwner) internal virtual {
            address oldOwner = _owner;
            _owner = newOwner;
            emit OwnershipTransferred(oldOwner, newOwner);
        }
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
    pragma solidity ^0.8.20;
    /**
     * @dev Provides information about the current execution context, including the
     * sender of the transaction and its data. While these are generally available
     * via msg.sender and msg.data, they should not be accessed in such a direct
     * manner, since when dealing with meta-transactions the account sending and
     * paying for execution may not be the actual sender (as far as an application
     * is concerned).
     *
     * This contract is only required for intermediate, library-like contracts.
     */
    abstract contract Context {
        function _msgSender() internal view virtual returns (address) {
            return msg.sender;
        }
        function _msgData() internal view virtual returns (bytes calldata) {
            return msg.data;
        }
        function _contextSuffixLength() internal view virtual returns (uint256) {
            return 0;
        }
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (utils/Pausable.sol)
    pragma solidity ^0.8.20;
    import {Context} from "../utils/Context.sol";
    /**
     * @dev Contract module which allows children to implement an emergency stop
     * mechanism that can be triggered by an authorized account.
     *
     * This module is used through inheritance. It will make available the
     * modifiers `whenNotPaused` and `whenPaused`, which can be applied to
     * the functions of your contract. Note that they will not be pausable by
     * simply including this module, only once the modifiers are put in place.
     */
    abstract contract Pausable is Context {
        bool private _paused;
        /**
         * @dev Emitted when the pause is triggered by `account`.
         */
        event Paused(address account);
        /**
         * @dev Emitted when the pause is lifted by `account`.
         */
        event Unpaused(address account);
        /**
         * @dev The operation failed because the contract is paused.
         */
        error EnforcedPause();
        /**
         * @dev The operation failed because the contract is not paused.
         */
        error ExpectedPause();
        /**
         * @dev Initializes the contract in unpaused state.
         */
        constructor() {
            _paused = false;
        }
        /**
         * @dev Modifier to make a function callable only when the contract is not paused.
         *
         * Requirements:
         *
         * - The contract must not be paused.
         */
        modifier whenNotPaused() {
            _requireNotPaused();
            _;
        }
        /**
         * @dev Modifier to make a function callable only when the contract is paused.
         *
         * Requirements:
         *
         * - The contract must be paused.
         */
        modifier whenPaused() {
            _requirePaused();
            _;
        }
        /**
         * @dev Returns true if the contract is paused, and false otherwise.
         */
        function paused() public view virtual returns (bool) {
            return _paused;
        }
        /**
         * @dev Throws if the contract is paused.
         */
        function _requireNotPaused() internal view virtual {
            if (paused()) {
                revert EnforcedPause();
            }
        }
        /**
         * @dev Throws if the contract is not paused.
         */
        function _requirePaused() internal view virtual {
            if (!paused()) {
                revert ExpectedPause();
            }
        }
        /**
         * @dev Triggers stopped state.
         *
         * Requirements:
         *
         * - The contract must not be paused.
         */
        function _pause() internal virtual whenNotPaused {
            _paused = true;
            emit Paused(_msgSender());
        }
        /**
         * @dev Returns to normal state.
         *
         * Requirements:
         *
         * - The contract must be paused.
         */
        function _unpause() internal virtual whenPaused {
            _paused = false;
            emit Unpaused(_msgSender());
        }
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts (last updated v5.0.0) (utils/ReentrancyGuard.sol)
    pragma solidity ^0.8.20;
    /**
     * @dev Contract module that helps prevent reentrant calls to a function.
     *
     * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
     * available, which can be applied to functions to make sure there are no nested
     * (reentrant) calls to them.
     *
     * Note that because there is a single `nonReentrant` guard, functions marked as
     * `nonReentrant` may not call one another. This can be worked around by making
     * those functions `private`, and then adding `external` `nonReentrant` entry
     * points to them.
     *
     * TIP: If you would like to learn more about reentrancy and alternative ways
     * to protect against it, check out our blog post
     * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
     */
    abstract contract ReentrancyGuard {
        // Booleans are more expensive than uint256 or any type that takes up a full
        // word because each write operation emits an extra SLOAD to first read the
        // slot's contents, replace the bits taken up by the boolean, and then write
        // back. This is the compiler's defense against contract upgrades and
        // pointer aliasing, and it cannot be disabled.
        // The values being non-zero value makes deployment a bit more expensive,
        // but in exchange the refund on every call to nonReentrant will be lower in
        // amount. Since refunds are capped to a percentage of the total
        // transaction's gas, it is best to keep them low in cases like this one, to
        // increase the likelihood of the full refund coming into effect.
        uint256 private constant NOT_ENTERED = 1;
        uint256 private constant ENTERED = 2;
        uint256 private _status;
        /**
         * @dev Unauthorized reentrant call.
         */
        error ReentrancyGuardReentrantCall();
        constructor() {
            _status = NOT_ENTERED;
        }
        /**
         * @dev Prevents a contract from calling itself, directly or indirectly.
         * Calling a `nonReentrant` function from another `nonReentrant`
         * function is not supported. It is possible to prevent this from happening
         * by making the `nonReentrant` function external, and making it call a
         * `private` function that does the actual work.
         */
        modifier nonReentrant() {
            _nonReentrantBefore();
            _;
            _nonReentrantAfter();
        }
        function _nonReentrantBefore() private {
            // On the first call to nonReentrant, _status will be NOT_ENTERED
            if (_status == ENTERED) {
                revert ReentrancyGuardReentrantCall();
            }
            // Any calls to nonReentrant after this point will fail
            _status = ENTERED;
        }
        function _nonReentrantAfter() private {
            // By storing the original value once again, a refund is triggered (see
            // https://eips.ethereum.org/EIPS/eip-2200)
            _status = NOT_ENTERED;
        }
        /**
         * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
         * `nonReentrant` function in the call stack.
         */
        function _reentrancyGuardEntered() internal view returns (bool) {
            return _status == ENTERED;
        }
    }
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.24;
    import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
    import '@openzeppelin/contracts/access/Ownable.sol';
    import '@openzeppelin/contracts/utils/Pausable.sol';
    interface IERC20 {
      function transfer(address recipient, uint256 amount) external returns (bool);
      function transferFrom(
        address sender,
        address recipient,
        uint256 amount
      ) external returns (bool);
      function balanceOf(address account) external view returns (uint256);
      function approve(address spender, uint256 value) external returns (bool);
    }
    contract NodeStaking is ReentrancyGuard, Ownable, Pausable {
      IERC20 public stakingToken;
      struct Stake {
        uint256 amount;
        uint256 startTime;
        uint256 rewardPaid;
      }
      mapping(address => Stake[]) public stakes;
      mapping(address => uint256) public totalUserRewards;
      address[] public stakers;
      mapping(address => bool) public isStaker;
      uint256 public totalRewards = 0;
      uint256 public totalStaked;
      uint256 public earlyUnstakePenality = 30;
      uint256 public maxTotalStaked = 10_000_000 * (10 ** 18); // Example: 10 million tokens
      uint256 public maxPerWallet = 1_000 * (10 ** 18); // Example: 1,000 tokens per wallet
      uint256 public constant stakingPeriod = 30 days;
      address public constant deadWallet =
        0x000000000000000000000000000000000000dEaD;
      event Staked(address indexed user, uint256 amount, uint256 index);
      event Unstaked(address indexed user, uint256 amount, uint256 index);
      event RewardPaid(address indexed user, uint256 reward);
      event Migrated(
        address indexed newStakingContract,
        uint256 tokenAmount,
        uint256 ethAmount
      );
      /* Initializes the constructure with the staking token set and owner */
      constructor(address _stakingToken) Ownable(_msgSender()) {
        stakingToken = IERC20(_stakingToken);
      }
      /* Sets the staking token */
      function setStakingToken(address _stakingToken) external onlyOwner {
        require(_stakingToken != address(0), 'Invalid address');
        stakingToken = IERC20(_stakingToken);
      }
      /* Sets the maximum total staked amount */
      function setMaxTotalStaked(uint256 _maxTotalStaked) external onlyOwner {
        maxTotalStaked = _maxTotalStaked;
      }
      /* Sets the maximum staked amount per wallet */
      function setMaxPerWallet(uint256 _maxPerWallet) external onlyOwner {
        maxPerWallet = _maxPerWallet;
      }
      /* Sets the pause state of the contract */
      function setPaused(bool _paused) external onlyOwner {
        require(_paused != paused(), 'Already in the requested state');
        if (_paused) {
          _pause();
        } else {
          _unpause();
        }
      }
      /* Migrates the staking contract to a new contract */
      function migrate(address _newStakingContract) external onlyOwner {
        require(_newStakingContract != address(0), 'Invalid address');
        uint256 contractTokenBalance = stakingToken.balanceOf(address(this));
        require(
          stakingToken.transfer(_newStakingContract, contractTokenBalance),
          'Failed to transfer tokens to owner'
        );
        uint256 contractETHBalance = address(this).balance;
        (bool sent, ) = _newStakingContract.call{value: contractETHBalance}('');
        require(sent, 'Failed to transfer ETH');
        emit Migrated(
          _newStakingContract,
          contractTokenBalance,
          contractETHBalance
        );
      }
      function stake(uint256 _amount) external nonReentrant whenNotPaused {
        require(_amount > 0, 'Amount must be greater than 0');
        require(totalStaked + _amount <= maxTotalStaked, 'Staking limit exceeded');
        uint256 walletStaked = getWalletStaked(msg.sender);
        require(walletStaked + _amount <= maxPerWallet, 'Staking limit exceeded');
        if(!isStaker[msg.sender]) {
          stakers.push(msg.sender);
          isStaker[msg.sender] = true;
        }
        bool success = stakingToken.transferFrom(msg.sender, address(this), _amount);
        require(success, 'Failed to transfer tokens');
        stakes[msg.sender].push(Stake(_amount, block.timestamp, 0));
        totalStaked += _amount;
        emit Staked(msg.sender, _amount, stakes[msg.sender].length - 1);
      }
      function unstake(uint256 _index, uint256 _amount) external nonReentrant {
        require(_index < stakes[msg.sender].length, 'Invalid stake index');
        Stake storage userStake = stakes[msg.sender][_index];
        require(
          block.timestamp >= userStake.startTime + stakingPeriod,
          'Stake is still locked'
        );
        require(userStake.amount >= _amount, 'Insufficient staked amount');
        uint256 reward = _claimRewards(msg.sender, _index);
        if(reward > 0) {
          (bool sent, ) = payable(msg.sender).call{value: reward}('');
          require(sent, 'Failed to send Ether');
          emit RewardPaid(msg.sender, reward);
        }
        userStake.amount -= _amount;
        totalStaked -= _amount;
        if (userStake.amount == 0) {
          removeStake(msg.sender, _index);
        }
        bool success = stakingToken.transfer(msg.sender, _amount);
        require(success, 'Failed to transfer tokens');
        emit Unstaked(msg.sender, _amount, _index);
      }
      function earlyUnstake(uint256 _index, uint256 _amount) external nonReentrant {
        require(_index < stakes[msg.sender].length, 'Invalid stake index');
        Stake storage userStake = stakes[msg.sender][_index];
        require(userStake.amount >= _amount, 'Insufficient staked amount');
        uint256 timeElapsed = block.timestamp - userStake.startTime;
        require(
          timeElapsed < stakingPeriod,
          'Stake is not in early unstake period'
        );
        uint256 reward = _claimRewards(msg.sender, _index);
        if(reward > 0) {
          (bool sent, ) = payable(msg.sender).call{value: reward}('');
          require(sent, 'Failed to send Ether');
          emit RewardPaid(msg.sender, reward);
        }
        // Calculate the penalty fee, which linearly decreases from 50% to 0% over the lock-up period
        uint256 penaltyPercentage = earlyUnstakePenality -
          ((timeElapsed * earlyUnstakePenality) / stakingPeriod);
        uint256 penaltyAmount = (_amount * penaltyPercentage) / 100;
        // Apply the penalty
        uint256 returnAmount = _amount - penaltyAmount;
        // Update the stake and total staked amount
        userStake.amount -= _amount;
        totalStaked -= _amount;
        // Burn the penalty amount
        bool successBurn = stakingToken.transfer(deadWallet, penaltyAmount);
        require(successBurn, 'Failed to burn tokens');
        // Return the remaining tokens to the user
        bool sucessUnstake = stakingToken.transfer(msg.sender, returnAmount);
        require(sucessUnstake, 'Failed to transfer tokens');
        // Remove the stake if it's fully unstaked
        if (userStake.amount == 0) {
          removeStake(msg.sender, _index);
        }
        emit Unstaked(msg.sender, returnAmount, _index);
      }
      function _claimRewards(
        address user,
        uint256 index
      ) private returns (uint256) {
        require(index < stakes[user].length, 'Invalid stake index');
        Stake storage userStake = stakes[user][index];
        uint256 reward = calculateReward(user, index);
        if (reward > 0) {
          userStake.rewardPaid += reward;
          totalUserRewards[user] += reward;
          totalRewards += reward;
        }
        return reward;
      }
      function claimRewards() external nonReentrant {
        uint256 stakeCount = stakes[msg.sender].length;
        require(stakeCount > 0, 'No stakes available');
        uint256 totalReward = 0;
        for (uint256 i = 0; i < stakeCount; i++) {
          totalReward += _claimRewards(msg.sender, i); // Aggregate rewards
        }
        if(totalReward <= 0) {
          return;
        }
        (bool sent, ) = payable(msg.sender).call{value: totalReward}('');
        require(sent, 'Failed to send Ether');
        emit RewardPaid(msg.sender, totalReward);
      }
      function getWalletStaked(address _user) public view returns (uint256) {
        uint256 walletStaked = 0;
        for (uint256 i = 0; i < stakes[_user].length; i++) {
          walletStaked += stakes[_user][i].amount;
        }
        return walletStaked;
      }
      function getWalletReward(address _user) public view returns (uint256) {
        uint256 walletReward = 0;
        for (uint256 i = 0; i < stakes[_user].length; i++) {
          walletReward += stakes[_user][i].rewardPaid;
        }
        return walletReward;
      }
      function getWalletStakes(address _user) public view returns (Stake[] memory) {
        return stakes[_user];
      }
      function getWalletClaimableRewards(
        address _user
      ) public view returns (uint256 totalClaimable) {
        totalClaimable = 0;
        for (uint256 i = 0; i < stakes[_user].length; i++) {
          uint256 reward = calculateReward(_user, i);
          totalClaimable += reward;
        }
      }
      function calculateAllPendingRewards() public view returns (uint256 totalClaimable) {
        totalClaimable = 0;
        for (uint256 i = 0; i < stakers.length; i++) {
          address staker = stakers[i];
          totalClaimable += getWalletClaimableRewards(staker);
        }
      }
      function calculateReward(
        address _user,
        uint256 _index
      ) public view returns (uint256) {
        Stake storage userStake = stakes[_user][_index];
        uint256 stakeDuration = block.timestamp - userStake.startTime;
        if (stakeDuration > stakingPeriod) {
          stakeDuration = stakingPeriod; // Cap the stake duration to the lock-up period for reward calculation
        }
        // Scale the numerator before dividing
        uint256 scaledReward = (address(this).balance *
          userStake.amount *
          stakeDuration) / stakingPeriod;
        uint256 reward = scaledReward / totalStaked;
        if(userStake.rewardPaid > reward) {
          return 0;
        }
        return reward - userStake.rewardPaid; // Assuming rewardPaid is correctly managed elsewhere
      }
      function getUserStakingDetails(
        address _user
      )
        public
        view
        returns (
          uint256 _totalStaked,
          uint256 totalRewardsInEth,
          uint256[] memory timeElapsedPerStake
        )
      {
        _totalStaked = 0;
        totalRewardsInEth = 0;
        timeElapsedPerStake = new uint256[](stakes[_user].length);
        for (uint256 i = 0; i < stakes[_user].length; i++) {
          _totalStaked += stakes[_user][i].amount;
          totalRewardsInEth += calculateReward(_user, i); // This should return ETH rewards
          timeElapsedPerStake[i] = block.timestamp - stakes[_user][i].startTime;
        }
      }
      function removeStake(address _user, uint256 _index) private {
        require(_index < stakes[_user].length, 'Invalid stake index');
        stakes[_user][_index] = stakes[_user][stakes[_user].length - 1];
        stakes[_user].pop();
      }
      receive() external payable {}
    }