Getting saved block.timestamp in solidity Struct, but function is returning timestamp of current time

Viewed 671

This is my 1st time asking a question on stackoverflow so i hope i provided all the information needed here

So i wrote a solidity smart contract to stake nfts, the staking part is working well but the unstaking part is not, im trying to write the unstaking function so that the owner will only be able to unstake their nfts if a certain time has passed, this time is a uint48 value stored in a solidity Struct of the stakeNft:

    struct Stake {
        uint24 tokenId;
        uint48 timestamp; <------
        address owner;
    }

This is the staking function:

    function stake(uint256[] calldata tokenIds) external {
        IERC721N nft = IERC721N(NftAddress);
        uint256 tokenId;
        totalStaked += tokenIds.length;
        for (uint256 i = 0; i < tokenIds.length; i++) {
            tokenId = tokenIds[i];
            require(nft.ownerOf(tokenId) == msg.sender, "not your token");
            require(vault[tokenId].tokenId == 0, "already staked");

            nft.transferFrom(msg.sender, address(this), tokenId);
            emit BlockStaked(msg.sender, tokenId, block.timestamp);

            vault[tokenId] = Stake({
                owner: msg.sender,
                tokenId: uint24(tokenId),
                timestamp: uint48(block.timestamp)
            });
        }
    }

And this is the Unstaking function:

    function _unstakeMany(address account, uint256[] calldata tokenIds)
        internal
    {
        IERC721N nft = IERC721N(NftAddress);
        // uint256 tokenId;
        Stake memory staked;
        totalStaked -= tokenIds.length;
        for (uint256 i = 0; i < tokenIds.length; i++) {
            // tokenId = tokenIds[i];
            staked = vault[tokenIds[i]];
            uint256 timeStamp = stakeStamp(tokenIds[i]);
            require(staked.owner == msg.sender, "not an owner");
            if(block.timestamp < timeStamp + 60){
                revert timeError(timeStamp, tokenIds[i]);
            }
            delete vault[tokenIds[i]];
            emit BlockUnstaked(account, tokenIds[i], block.timestamp);
            nft.transferFrom(address(this), account, tokenIds[i]);
            
        }
    }

This is the full code:

// SPDX-License-Identifier: MIT LICENSE

pragma solidity ^0.8.9;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";


interface IERC20N is IERC20 {
    function mint(address to, uint256 amount) external;
}

interface IERC721N is IERC721 {
    function totalSupply() external view returns (uint256);
}

contract Vault is Ownable, IERC721Receiver {
    using SafeMath for uint256;
    uint256 public totalStaked;
    
    // struct to store a stake's token, owner, and earning values
    struct Stake {
        uint24 tokenId;
        uint48 timestamp;
        address owner;
    }

    event BlockStaked(address owner, uint256 tokenId, uint256 value);
    event BlockUnstaked(address owner, uint256 tokenId, uint256 value);
    event Claimed(address owner, uint256 amount);

    // maps tokenId to stake
    mapping(uint256 => Stake) public vault;

    // initialising Nft cotract and coin contract
    address public NftAddress;
    address public TokenAddress;

    // IERC721N nft1 = IERC721N(NftAddress);
    // IERC20N token = IERC20N(TokenAddress);

    error timeError(uint256 timeleft, uint256 tokenId);
    // error timeError(uint256 timeleft, uint256 blockStamp, uint256 tokenId);

    constructor() {}

    function setNftAddress(address _address) public onlyOwner {
        NftAddress = _address;
    }

    function setTokenAddress(address _address) public onlyOwner {
        TokenAddress = _address;
    }

    function stake(uint256[] calldata tokenIds) external {
        IERC721N nft = IERC721N(NftAddress);
        uint256 tokenId;
        totalStaked += tokenIds.length;
        for (uint256 i = 0; i < tokenIds.length; i++) {
            tokenId = tokenIds[i];
            require(nft.ownerOf(tokenId) == msg.sender, "not your token");
            require(vault[tokenId].tokenId == 0, "already staked");

            nft.transferFrom(msg.sender, address(this), tokenId);
            emit BlockStaked(msg.sender, tokenId, block.timestamp);

            vault[tokenId] = Stake({
                owner: msg.sender,
                tokenId: uint24(tokenId),
                timestamp: uint48(block.timestamp)
            });
        }
    }


    uint256 public TIMe;



    function _unstakeMany(address account, uint256[] calldata tokenIds)
        internal
    {
        // IERC721N nft = IERC721N(NftAddress);
        // uint256 tokenId;
        Stake memory staked;
        totalStaked -= tokenIds.length;
        for (uint256 i = 0; i < tokenIds.length; i++) {
            // tokenId = tokenIds[i];
            staked = vault[tokenIds[i]];
            uint256 timeStamp = stakeStamp(tokenIds[i]);
            require(staked.owner == msg.sender, "not an owner");
            if(block.timestamp < timeStamp + 60){
                revert timeError(timeStamp, tokenIds[i]);
            }
            delete vault[tokenIds[i]];
            emit BlockUnstaked(account, tokenIds[i], block.timestamp);
            // nft.transferFrom(address(this), account, tokenIds[i]);
            
        }
    }

    function blockStamp() public view returns(uint256){
        return block.timestamp;
    }

    function stakeStamp(uint256 id) public view returns(uint256){
        return vault[id].timestamp;
    }


    function unstake(uint256[] calldata tokenIds) external {
        _claim(msg.sender, tokenIds, true);
    }

    function _claim(
        address account,
        uint256[] calldata tokenIds,
        bool _unstake
    ) internal {
        uint256 tokenId;
        uint256 earned = 0;
        IERC20N token = IERC20N(TokenAddress);

        for (uint256 i = 0; i < tokenIds.length; i++) {
            tokenId = tokenIds[i];

            Stake memory staked = vault[tokenId];
            require(staked.owner == account, "not an owner");

            uint256 stakedAt = staked.timestamp;

            vault[tokenId] = Stake({
                owner: account,
                tokenId: uint24(tokenId),
                timestamp: uint48(block.timestamp)
            });

            if (block.timestamp - stakedAt > 300) {
                earned += 1000 ether;
            }
        }
        if (earned > 0) {
            token.mint(msg.sender, earned);
        }
        if (_unstake) {
            _unstakeMany(account, tokenIds);
        }
        emit Claimed(account, earned);
    }

    function timeFromStaked(uint256[] calldata tokenIds)
        public
        view
        returns (uint256[] memory)
    {
        uint256[] memory list = new uint256[](tokenIds.length);

        for (uint256 i = 0; i < tokenIds.length; i++) {
            uint256 tokenId = tokenIds[i];
            Stake memory staked = vault[tokenId];
            uint256 stakedAt = staked.timestamp;
            list[i] = uint48(block.timestamp) - stakedAt;
        }
        return list;
    }

    // should never be used inside of transaction because of gas fee
    function balanceOf(address account) public view returns (uint256) {
        IERC721N nft = IERC721N(NftAddress);
        uint256 balance = 0;
        uint256 supply = nft.totalSupply();
        for (uint256 i = 1; i <= supply; i++) {
            if (vault[i].owner == account) {
                balance += 1;
            }
        }
        return balance;
    }

    // should never be used inside of transaction because of gas fee
    function tokensOfOwner(address account)
        public
        view
        returns (uint256[] memory ownerTokens)
    {
        IERC721N nft = IERC721N(NftAddress);
        uint256 supply = nft.totalSupply();
        uint256[] memory tmp = new uint256[](supply);

        uint256 index = 0;
        for (uint256 tokenId = 1; tokenId <= supply; tokenId++) {
            if (vault[tokenId].owner == account) {
                tmp[index] = vault[tokenId].tokenId;
                index += 1;
            }
        }

        uint256[] memory tokens = new uint256[](index);
        for (uint256 i = 0; i < index; i++) {
            tokens[i] = tmp[i];
        }

        return tokens;
    }

    function onERC721Received(
        address,
        address from,
        uint256,
        bytes calldata
    ) external pure override returns (bytes4) {
        require(from == address(0x0), "Cannot send nfts to Vault directly");
        return IERC721Receiver.onERC721Received.selector;
    }
}

after i run it on ganache-cli and do the steps required to initialize the contracts i stake one nft

then after sometime i do another transaction on my local blockchain to update the block.timestamp value and try to unstake

when i try to unstake before the time passes the reverted timeError returns the value of the timestamps of the corresponding stake, but its not the right value because its always changing everytime i run the unstake function, and its always equal to the block.timestamp value

this time stamp value is fetched using a function called stakeStamp, the stakeStamp function always returns the right value from the struct, but whenever i use it in the unstake function it returns the block.timestamp value instead of the saved timeestamp in the struct

this is the stakeStamp function:

    function stakeStamp(uint256 id) public view returns(uint256){
        return vault[id].timestamp;
    }

u can check how i used it in the unstake function up above in the 3rd code block

i hope i provided good information on the problem.

2 Answers

First of all, I think you should make some automated tests in order to identify what is causing this timestamp bug. I've made some for you already that I will be appending to this answer.

There are some things that could be better in your contract.

For example, in line 99 (at least in my editor after some linting)

            if(block.timestamp < timeStamp + 60){
                revert timeError(timeStamp, tokenIds[i]);
            }

block.timestamp is a value that keeps growing. It's the timestamp of the EVM, so it's increasing by the seconds. checking block.timestamp < timeStamp + 60 will throw error after 60 seconds of calling stake(). Maybe you should rewrite it to

            require(block.timestamp > timeStamp + 60,"not past due time");

As the blocking timers seem to be small, I assume you are not using a testing environment and testing the contract on Remix or something else.

I've made some testing on your contract using Hardhat and made a gist that you can access. Take a look:

https://remix.ethereum.org/#version=soljson-v0.8.7+commit.e28d00a7.js&optimize=false&runs=200&gist=3ad1a14ab5ad4ec5aca16ae6414ffb67

It basically tests if the Vault can stake and unstake.

Regarding to your question in this point:

when i try to unstake before the time passes the reverted timeError returns the value of the timestamps of the corresponding stake, but its not the right value because its always changing everytime i run the unstake function, and its always equal to the block.timestamp value

You stake to a specific block.timestamp in the moment the function was called. It is static, doesn't change. I quite doesn't understand what you mean there. Also, timeStamp returned from the stakeStamp() is always different from block.timestmap. More specifically, it's always lower than block.timestmap since the intention is to set it stake() and check it on unstake()

Also, is there any specific reason to use uint48 as the timestamp? Given that it is stated in the Solidity documentation that it is an uint256 I don't know if this cast has any unwanted behavior. Probably not, given that the timestamp is in seconds and it would perfectly fit in a uint48.

Import safeTransferFrom function from IERC721. use safeTransferFrom function in stake and _unstakeMany functions. if you dont get solution , just ping me in twitter(@bhargav_dasappa).

Related