cennznet icon indicating copy to clipboard operation
cennznet copied to clipboard

Unable to call ERC721's WRITE method on NFT from a smart contract

Open ken-futureverse opened this issue 2 years ago • 4 comments

Description

When trying to write up the Wiki on how to interact with NFT precompile using EVM, me and @KarishmaBothara found out that trying to call a WRITE method (e.g "approve") on an NFT will result in an error

Screen Shot 2022-08-29 at 17 53 51

READ methods (e.g "symbolOf") work as expected.

Steps to Produce

  • Deploy the following contract on Nikau using the account with the following private key (d0893777...) (check with me or @KarishmaBothara for the actual key)
NFT.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/interfaces/IERC721.sol";
import "@openzeppelin/contracts/interfaces/IERC165.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";


contract Test {
     // nft testnet https://nikau.uncoverexplorer.com/block/4969361 collection id - 266 and series id - 0
     // 0x010a
    // address nft = 0xAaAAAaaa0000010a000000000000000000000000;

    address nft = 0xAaAAAaaa0000010a000000000000000000000000;

    function balanceOfProxy(address who) public view returns (uint256) {
        return IERC721(nft).balanceOf(who);
    }

    function ownerOfProxy(uint256 tokenId) public view returns (address) {
        return IERC721(nft).ownerOf(tokenId);
    }

    function nameOfNFT() public view returns (string memory) {
        return IERC721Metadata(nft).name();
    }

    function symbolOfNFT() public view returns (string memory) {
        return IERC721Metadata(nft).symbol();
    }

    function tokenURIOfNFT(uint256 serial_number) public view returns (string memory) {
        return IERC721Metadata(nft).tokenURI(serial_number);
    }
    
    
    function transferFromProxy(
        address to,
        uint256 serial_number
    ) external {
        IERC721(nft).transferFrom(to, msg.sender, serial_number);
    }

     function approveFromProxy(
        address to,
        uint256 serial_number
    ) external {
        IERC721(nft).approve(to, serial_number);
    }
}
  • Try to run approveFromProxy method

Expected Result

  • approveFromProxy works as expected

Actual Result

  • Error as per the screenshot above
  • Click "Send Transaction" on the popup alert resulting to this

Screen Shot 2022-08-29 at 17 54 19

ken-futureverse avatar Aug 29 '22 05:08 ken-futureverse

solidity inserts an EXTCODESIZE check when calling a contract with this casting syntax e.g IContract(address).method(…) when it calls a precompile address EXTCODESIZE is 0 so it reverts. Using address.call{} syntax doesn’t insert this check so it works.

The confusing part is if the method you call returns some data with the casting syntax IContract(address).methodWithReturnData(…) it doesn’t insert the EXTCODESIZE check so. IERC20.transferFrom returns (bool) is ok but IERC721.tranfserFrom does not return anything and fails

workaround use precompile.call {} syntax

jordy25519 avatar Sep 07 '22 22:09 jordy25519

thanks, @KarishmaBothara are you able to try using address.call{} to confirm this?

https://github.com/futureversecom/seed/blob/6ddc35fd9a56608c834e89364d901a9a156198ce/test-ts/contracts/ERC721PrecompileCaller.sol

function transferFromProxy(
        address from,
        address to,
        uint256 token_id
    ) external {
        (bool success, bytes memory returnData) = precompile.call(
            abi.encodeWithSignature(
                "transferFrom(address,address,uint256)",
                from,
                to,
                token_id
            )
        );
        require(success, "call failed");
    }

ken-futureverse avatar Sep 08 '22 00:09 ken-futureverse

It looks like this issue is due to the accountId type when calling from the EVM. The accountId that is converted using the AddressMapping trait

  1. An NFT is minted using the CENNZnet runtime, then the owner of that NFT is the 32 Byte address.
  2. The Approve function on the EVM is then called with a 20 Byte Ethereum address as the approved address
  3. When the approval is set, ownership from the caller is checked over the token_id being approved
  4. The owner account is a 20 Byte Ethereum address that's converted to a 32 Byte CENNZnet address, which is not the same account that owns the NFT in CENNZnet so the approve fails

Calling the NFT precompiles initializeSeries and mint also fail due to the same accountId conversions. The collection owner is a CENNZnet address and when minting the series, the ownership check fails due to the difference in conversion

JasonTulp avatar Sep 12 '22 22:09 JasonTulp

Branch with e2e tests: https://github.com/cennznet/cennznet/tree/feat/erc721-e2e-tests

JasonTulp avatar Sep 12 '22 22:09 JasonTulp