Skip to content

Token Time Based Locking

The problem with this Smart Contract is that the transfer is always possible. That means, if we ship the actual physical product to someone, he can just send the NFT somewhere else in the meantime. There is no proof that the product ever received correctly. It is possible to send the NFT to someone else, that person now knowing if the product was already shipped - a property we want to have on-chain.

Our Token just doesn't have the Supply Chain component yet built in.

Let's add a locking mechanism to the Smart Contract.

We need several things for this to work:

  1. A timestamp for each token from when this token isn't transferrable anymore. This can be potentially be set on minting.
  2. Some unlock code, which is hashed twice (e.g. hash(hash(code))), so on the blockchain the original passphrase is never revealed, we just send hash(code). More on that later.
  3. A variable if the token was unlocked already or not.
  4. A hook into the transfer function to only allow token transfer of tokens which are not yet locked or which have been successfully unlocked

Let's have a look at how we can do this in code with the OpenZeppelin Preset.

Modifying the Smart Contract

And here is the final contract for a first try:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.3;

import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract AisthisiToken is ERC721PresetMinterPauserAutoId {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    mapping (uint => uint) public tokenLockedFromTimestamp;
    mapping (uint => bytes32) public tokenUnlockCodeHashes;
    mapping (uint => bool) public tokenUnlocked;

    event TokenUnlocked(uint tokenId, address unlockerAddress);

    constructor() ERC721PresetMinterPauserAutoId("AisthisiToken", "AIS", "https://aisthisi.art/metadata/") {}

    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override {
        require(tokenLockedFromTimestamp[tokenId] > block.timestamp || tokenUnlocked[tokenId], "AishtisiToken: Token locked");
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function unlockToken(bytes32 unlockHash, uint256 tokenId) public {
        require(msg.sender == ownerOf(tokenId), "AishtisiToken: Only the Owner can unlock the Token"); //not 100% sure about that one yet
        require(keccak256(abi.encode(unlockHash)) == tokenUnlockCodeHashes[tokenId], "AishtisiToken: Unlock Code Incorrect");
        tokenUnlocked[tokenId] = true;
        emit TokenUnlocked(tokenId, msg.sender);
    }

    /**
    * This one is the mint function that sets the unlock code, then calls the parent mint
    */
    function mint(address to, uint lockedFromTimestamp, bytes32 unlockHash) public {
        tokenLockedFromTimestamp[_tokenIds.current()] = lockedFromTimestamp;
        tokenUnlockCodeHashes[_tokenIds.current()] = unlockHash;
        _tokenIds.increment();
        super.mint(to);
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
    return string(abi.encodePacked(super.tokenURI(tokenId),".json"));
    }
}

Testing it in the Truffle Development Console

Let's give it a try again: 1. Open truffle develop 2. Deploy the contracts to this test-network with migrate 3. Then mint a new token which locks 3 seconds in the future:

let token = await AisthisiToken.deployed()
let txMint = await token.mint(accounts[1], Math.round(Date.now()/1000)+3, web3.utils.sha3(web3.utils.sha3('mySecretHash')))

txMint.receipt.status
That should return true

Let's wait 3 seconds and then try to send the token from account[1] to account[2]:

let txTransfer = await token.transferFrom(accounts[1], accounts[2], 0, {from: accounts[1]});

This will output an error:

Error: Returned error: VM Exception while processing transaction: revert AishtisiToken: Token locked -- Reason given: AishtisiToken: Token locked.

Let's unlock the token then. We have to do it from the current token holder address, because only the current NFT token holder can unlock the token:

let txUnlock = await token.unlockToken(web3.utils.sha3('mySecretHash'), 0, {from: accounts[1]})

txUnlock.receipt.status

That should return true:

Now it should be possible to send tokens again:

let txTransfer2 = await token.transferFrom(accounts[1], accounts[2], 0, {from: accounts[1]});

txTransfer2.receipt.status

should return true.

The current owner of token #0 is account #2:

await token.ownerOf(0)

accounts[2]

Should be equal:

Now, let's exit the truffle developer console with .exit so you are on a regular terminal inside the project folder.