Solidity.

Post

Share your knowledge.

Christian O'Connor.
May 22, 2023
Expert Q&A

I get a "Verification failed" error when trying to mint an NFT requiring 2 signatures and using API3

I have this solidity contract:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@api3/airnode-protocol/contracts/rrp/requesters/RrpRequesterV0.sol";

contract RandomSurfaceReachT1 is ERC721URIStorage, Ownable, RrpRequesterV0 {
    event RequestedRandom(bytes32 indexed requestId);
    event MintedRandomNFT(bytes32 indexed requestId, uint256 response);
    event MintCostChanged(uint256 newCost);
    event Withdrawn(address indexed to, uint256 amount);

    address public airnode;
    bytes32 public endpointIdUint256;
    address public sponsorWallet;
    address public authorizedAccount;
    uint256 public tokenCounter;
    uint256 public mintCost = 0.01 ether;
    uint256 public constant MAX_MINTS_PER_ADDRESS = 3;

    enum Classifier {FIRST, SECOND, THIRD}
    mapping(uint256 => Classifier) public tokenIdToClassifier;
    mapping(bytes32 => bool) public awaitingFulfillment;
    mapping(bytes32 => address) public requestToMinter;
    mapping(address => uint256) public minterToMintCount;

    struct RandomNft {
        uint256 nonce;
        address from;
    }

    string public firstUri;
    string public secondUri;
    string public thirdUri;

    string private constant ERR_INVALID_SIGNER = "INVALID_SIGNER";
    string private constant ERR_REQUEST_ID_UNKNOWN = "Request ID not known";
    string private constant ERR_MINT_COST_NOT_MET = "Minting cost not met";
    string private constant ERR_MINT_LIMIT_REACHED = "Mint limit reached";
    string private constant ERR_VERIFICATION_FAILED = "Verification failed";

    constructor(address _airnodeRrp) RrpRequesterV0(_airnodeRrp) ERC721("PRIVATE MINT RANDOM NFT", "PMRNFT") {}

    function setParameters(
        address _airnode,
        bytes32 _endpointIdUint256,
        address _sponsorWallet,
        address _authorizedAccount
    ) external onlyOwner() {
        airnode = _airnode;
        endpointIdUint256 = _endpointIdUint256;
        sponsorWallet = _sponsorWallet;
        authorizedAccount = _authorizedAccount;
    }

    function setURIs(
        string calldata _firstUri,
        string calldata _secondUri,
        string calldata _thirdUri
    ) external onlyOwner() {
        firstUri = _firstUri;
        secondUri = _secondUri;
        thirdUri = _thirdUri;
    }

    function setMintCost(uint256 _newCost) public onlyOwner() {
        mintCost = _newCost;
        emit MintCostChanged(_newCost);
    }

    function requestRandomNFT(
        RandomNft memory nft,
        bytes32 sigR,
        bytes32 sigS,
        uint8 sigV
    ) external payable {
        require(msg.value == mintCost, ERR_MINT_COST_NOT_MET);
        require(minterToMintCount[msg.sender] < MAX_MINTS_PER_ADDRESS, ERR_MINT_LIMIT_REACHED);
        require(verify(authorizedAccount, nft, sigR, sigS, sigV), ERR_VERIFICATION_FAILED);
        bytes32 requestId = airnodeRrp.makeFullRequest(
            airnode,
            endpointIdUint256,
            address(this),
            sponsorWallet,
            address(this),
            this.fulfill.selector,
            ""
        );
        awaitingFulfillment[requestId] = true;
        requestToMinter[requestId] = msg.sender;
        emit RequestedRandom(requestId);
    }

    function fulfill(bytes32 requestId, bytes calldata data)
        external
        onlyAirnodeRrp
    {
        require(awaitingFulfillment[requestId], ERR_REQUEST_ID_UNKNOWN);

        uint256 newId = tokenCounter++;
        uint256 randomUint256 = abi.decode(data, (uint256));
        Classifier classifier = Classifier(randomUint256 % 3);
        
        tokenIdToClassifier[newId] = classifier;
        _safeMint(requestToMinter[requestId], newId);
        minterToMintCount[requestToMinter[requestId]]++;
        awaitingFulfillment[requestId] = false;

        if (classifier == Classifier.FIRST) {
            _setTokenURI(newId, firstUri);
        } else if (classifier == Classifier.SECOND) {
            _setTokenURI(newId, secondUri);
        } else if (classifier == Classifier.THIRD) {
            _setTokenURI(newId, thirdUri);
        }

        emit MintedRandomNFT(requestId, randomUint256);
    }

    function verify(
        address signer,
        RandomNft memory nft,
        bytes32 sigR,
        bytes32 sigS,
        uint8 sigV
    ) internal pure returns (bool) {
        require(signer != address(0), ERR_INVALID_SIGNER);
        return
            signer ==
            ecrecover(
                keccak256(abi.encode(nft.nonce, nft.from)),
                sigV,
                sigR,
                sigS
            );
    }

    function withdraw() external onlyOwner() {
        uint balance = address(this).balance;
        payable(owner()).transfer(balance);
        emit Withdrawn(owner(), balance);
    }
}

Notice that it uses API3 to select a random Classifier for the minted NFT from 3 Classification choices. Also an external signature is required. This is so I can have a next.js app with a private key on the backend that enables the website to be the sole source of NFT mints. I deployed the contract then the derived my sponsor wallet with the following command (P.S. my deployed contract instance is 0x8Ca82f3b509F18e79a4880415Df0cCB9807FA39a):

npx @api3/airnode-admin derive-sponsor-wallet-address --airnode-xpub xpub6CuDdF9zdWTRuGybJPuZUGnU4suZowMmgu15bjFZT2o6PUtk4Lo78KGJUGBobz3pPKRaN9sLxzj21CMe6StP3zUsd8tWEJPgZBesYBMY7Wo --airnode-address 0x6238772544f029ecaBfDED4300f13A3c4FE84E1D --sponsor-address 0x8Ca82f3b509F18e79a4880415Df0cCB9807FA39a

This output 0x320ce404b4e9a0ab44e890a91162109bf5b8fe80 as the sponsor wallet.

I ran the following function with hardhat:

const Token = await ethers.getContractFactory("RandomSurfaceReachT1");
const token = await Token.attach("0x8Ca82f3b509F18e79a4880415Df0cCB9807FA39a");

// API3 Nodary address: 0x6238772544f029ecaBfDED4300f13A3c4FE84E1D
// API3 Nodary RPC Connect String: 0xfb6d017bb87991b7495f563db3c8cf59ff87b09781947bb1e417006ad7f55a78
// address for private key that I'm going to use for signing 0xBcc0785B2Fe7e8E2875E8Ee110EBE9A3d948f6D2

await token.setParameters("0x6238772544f029ecaBfDED4300f13A3c4FE84E1D", "0xfb6d017bb87991b7495f563db3c8cf59ff87b09781947bb1e417006ad7f55a78", "0x320ce404b4e9a0ab44e890a91162109bf5b8fe80", "0xBcc0785B2Fe7e8E2875E8Ee110EBE9A3d948f6D2");
await token.setURIs("ipfs://bafybeif7eum33srxx2eeh73vzpzaeidcz4olkak644kcduto3vtnon7bya/firstBlue.json", "ipfs://bafybeif7eum33srxx2eeh73vzpzaeidcz4olkak644kcduto3vtnon7bya/secondGreen.json", "ipfs://bafybeif7eum33srxx2eeh73vzpzaeidcz4olkak644kcduto3vtnon7bya/thirdRed.json");

This is a repo to my next.js app https://github.com/ChristianOConnor/nft-sig-verify-mint-page-pub. My .env.local file looks like this:

NODE_ENV=development
NEXT_PUBLIC_CONTRACTS_ADDRESS='0x8Ca82f3b509F18e79a4880415Df0cCB9807FA39a'
PRIVATE_KEY_FOR_SIGNING='PRIVATE KEY FOR 0xBcc0785B2Fe7e8E2875E8Ee110EBE9A3d948f6D2'
I replaced PRIVATE_KEY_FOR_SIGNING with a placeholder.

When I connect my wallet and click "Mint Now!" I get this in my web browser console:

Browser:
ContractFunctionExecutionError: The contract function "requestRandomNFT" reverted with the following reason:
Verification failed

Contract Call:
  address:   0x8Ca82f3b509F18e79a4880415Df0cCB9807FA39a
  function:  requestRandomNFT((uint256 nonce, address from), bytes32 sigR, bytes32 sigS, uint8 sigV)
  args:                      ({"nonce":7121712914477423,"from":"0x41Ae1A06481FFf8DaE7dBBB90508A0fe50632449"}, 0x04d42a0436d2d5122cbebaca45e89ffef1c221e2c283a7f5216e3129d430271d, 0x3fe532453c5e08a757cc6a3ac4787714f53a9b537a58c118ad0763afe00e73a4, 28)
  sender:    0x41Ae1A06481FFf8DaE7dBBB90508A0fe50632449

Docs: https://viem.sh/docs/contract/simulateContract.html
Version: viem@0.3.31
    at Module.getContractError (getContractError.js:24:12)
    at Module.simulateContract (simulateContract.js:40:15)
    at async Module.prepareWriteContract (chunk-NRSD7F2O.js:2075:31)

And this is the output of (prepareError || error)?.message:

Error: The contract function "requestRandomNFT" reverted with the following reason: Verification failed Contract Call: address: 0x8Ca82f3b509F18e79a4880415Df0cCB9807FA39a function: requestRandomNFT((uint256 nonce, address from), bytes32 sigR, bytes32 sigS, uint8 sigV) args: ({"nonce":7121712914477423,"from":"0x41Ae1A06481FFf8DaE7dBBB90508A0fe50632449"}, 0x04d42a0436d2d5122cbebaca45e89ffef1c221e2c283a7f5216e3129d430271d, 0x3fe532453c5e08a757cc6a3ac4787714f53a9b537a58c118ad0763afe00e73a4, 28) sender: 0x41Ae1A06481FFf8DaE7dBBB90508A0fe50632449 Docs: https://viem.sh/docs/contract/simulateContract.html Version: viem@0.3.31

How do I get the Mint Now! button to successfully mint an NFT?

  • Smart Contract
  • Solidity
1
1
Share
Comments
.

Answers

1
Sergey Ilin.
May 23 2023, 18:25

The most possible issue here is that the message used to verify the signature inside the smart contract differs from the message being signed by the API.

The simplest way to debug would probably be a simple view action where you pass nonce and from arguments that calculates and returns a value of keccak256(abi.encode(nonce, from)). This hash has to match the message that is signed by API here - https://github.com/ChristianOConnor/nft-sig-verify-mint-page-pub/blob/cec00589cb3929118087873a99d80383875d0367/src/app/api/authorizor/route.ts#LL10C1-L11C1

The fact that API calls JSON.stringify(message) makes me believe that they will not match. Contract serializes nonce and form using RLP encoding. ethers js has a function for that ethers.utils.RLP.encode - https://docs.ethers.org/v5/api/utils/encoding/#utils-rlpEncode .

0
Best Answer
Comments
.

Do you know the answer?

Please log in and share it.

We use cookies to ensure you get the best experience on our website.
More info