Issue with minting in ERC721 -- minted token disappears after first transaction?

Have a contract thats extending ERC721, and has added minting functionality. I'm having issues testing this, because it seems like _owners gets refreshed between transactions somehow.

This is the contract itself (relevant parts). LoanManager doesn't override any interesting functions.

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "./LoanManager.sol";

contract LoanToken is LoanManager, ERC721URIStorage {
    modifier onlyApprovedOrOwner(uint256 id) {
        require(
            _isApprovedOrOwner(msg.sender, id),
            "LoanToken: Caller must have access to this token"
        );
        _;
    }

    constructor(address manager)
        LoanManager(manager)
        ERC721("PeriodicLoanToken", "PLT")
    {}

    /**
     * @dev Mint a new loan token with given information
     * @param maturity The maturity of the loan
     * @param period The period of the loan
     * @param totalBalance The total of service payments to the loan
     */
    function mintLoan(
        uint256 maturity,
        uint256 period,
        uint256 totalBalance
    ) external returns (uint256) {
        require(
            period >= 900,
            "LoanToken: Period must be at least 900 seconds"
        );
        require(
            maturity - block.timestamp >= period,
            "LoanToken: Maturity must be at least one period after current block timestamp"
        );
        require(
            totalBalance > 0,
            "LoanToken: Total balance must be greater than 0"
        );

        uint256 id = _createLoan(maturity, period, totalBalance);
        _mint(msg.sender, id);
        require(_exists(id), "LoanToken: failed to mint loan");
        return id;
    }

    function ownerOf(uint256 id) public view override returns (address) {
        require(id == 0, "Only checking 0 for now, sorry");
        require(_exists(id), "Doesn't exist");
        return address(this);
    }

}

Aaaand this is the test I'm writing for it:

const LoanToken = artifacts.require('LoanToken');

const truffleAssert = require('truffle-assertions');

const TIME_UNIT = {
    DAY: 86400,
    WEEK: 604800,
    MONTH: 2592000,
};

contract('LoanToken', accounts => {
    let instance;
    const ownerAccount = accounts[0];

    before(async () => {
        instance = await LoanToken.deployed();
    });

    it('should allow minting loans', async () => {
        let id = await instance.mintLoan.call(Date.now() - TIME_UNIT.WEEK, TIME_UNIT.DAY, 100);
        assert.equal(id, 0);
        // let l = await instance.loans(id);
        // console.log(l);

        const owner = await debug(instance.ownerOf.call(0));
        assert.equal(owner, ownerAccount, 'Loan owner was not set to creator by default');
    });

This test fails, with Doesn't Exist as the revert error. After doing some debugging with truffle breakpoints, I realized that _owners does get set during the minting process, but is back to being just Map(0) {} (an empty map) as soon as second call begins.

Black magic. Why isn't this being persisted? Am I testing this wrong?

:computer: Environment

Using truffle

Can you print what owner is? Also can you try await debug(instance.ownerOf(0));?

1 Like

Hi, welcome! :wave:

You do not share the full source code, so I can not test it.

And I am not sure what kind of result you expect to get.
Call mintLoan() twice to get two different ids and then call ownerOf() separately to get correct token owner?

This is what I'm getting when I step through the second call in debug mode (also, changed _owners to internal to make things more visible):

debug(development:0x41c2482c...)>

LoanToken.sol:

50:
51:     function ownerOf(uint256 id) public view override returns (address) {
52:         require(id == 0, "Only checking 0 for now, sorry");
                          ^

debug(development:0x41c2482c...)> v

Solidity built-ins:
    msg: {
           data: hex'6352211e0000000000000000000000000000000000000000000000000000000000000000',
           sig: 0x6352211e,
           sender: 0x5Ab835aAaB783e8AEB344e24a29f9Ad955119DF5,
           value: 0
         }
     tx: {
           origin: 0x5Ab835aAaB783e8AEB344e24a29f9Ad955119DF5,
           gasprice: 1
         }
  block: {
           coinbase: 0x0000000000000000000000000000000000000000,
           difficulty: 0,
           gaslimit: 6721975,
           number: 610,
           timestamp: 1630275030
         }
   this: 0x7b74FEB9Cb2963e669804Ca77677669ad4e97484 (LoanToken)
    now: 1630275030

Contract variables:
              loans: []
  collateralManager: 0x0000000000000000000000000000000000000000 of unknown class
            _owners: Map(0) {}

Local variables:
  id: 0

As you can see, _owners is empty, when it should have an entry like 0 => 0x5Ab83.... Notably, so is loans, where it should have an entry too. I would have expected that data to get persisted.

Also notably, is that when I run this test with a --show-events flag, and the tests fail, it tells me that no events where emitted. However, if I let this succeed by overriding ownerOf and not asserting the result matches anything, those events are printed out (with the proper data btw). It seems like an async issue... like I'm using it somehow wrong here.

Is it possible that call 2 is being executed before call 1 has finished / been fully applied? Might they be going in the same transaction somehow? If so, how do I even separate them? I'm new to this btw.

Thanks for your insight so far

Okay, in summary this is caused by a similar issue -- my testing code is wrong here.

(https://www.trufflesuite.com/docs/truffle/getting-started/interacting-with-your-contracts#transactions)

According to the truffle suite docs, one can either send an actual transaction with instance.mintLoan(), which will actually update the instance, or send a call with instance.mintLoan.call(), which will not mutate anything. Now I have the issue of: how do I even get a return value out of the first one, if instead its returning a whole transaction? Should I be changing my contract?

2 Likes

From the link you shared

Calls, on the other hand, are very different. Calls can be used to execute code on the network, though no data will be permanently changed. Calls are free to run, and their defining characteristic is that they read data. When you execute a contract function via a call you will receive the return value immediately. In summary, calls:

  • Are free (do not cost gas)
  • Do not change the state of the network
  • Are processed immediately
  • Will expose a return value (hooray!)

So I guess you should do instance.ownerOf.call(0)?