cennznet
cennznet copied to clipboard
Unable to call ERC721's WRITE method on NFT from a smart contract
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
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
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
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");
}
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
- An NFT is minted using the CENNZnet runtime, then the owner of that NFT is the 32 Byte address.
- The Approve function on the EVM is then called with a 20 Byte Ethereum address as the approved address
- When the approval is set, ownership from the caller is checked over the token_id being approved
- 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
Branch with e2e tests: https://github.com/cennznet/cennznet/tree/feat/erc721-e2e-tests