How to transfer a NFT from one account to another using ERC721?
Asked Answered
B

3

8

I'm writing an NFT smart contract using the OpenZeppelin ERC721Full contract. I'm able to mint NFTs, but I want to have a button that enables them to be bought. I'm trying writing this function:

function buyNFT(uint _id) public payable{
    //Get NFT owner address
    address payable _seller = ownerOf(_id);

    // aprove nft sell
    approve(_seller, _id);
    setApprovalForAll(msg.sender, true);

    //transfer NFT
    transferFrom(_seller, msg.sender, _id);

    // transfer price in ETH
    address(_seller).transfer(msg.value);

    emit NftBought(_seller, msg.sender, msg.value);

  }

This does not work because function approve must be called by the owner or an already approved address. I have no clue on how a buy function should be built. I know that I must use some requirements but first I want the function to work on tests and then I'll write the requirements.

How should a buy function be coded? Because the only solution I have found is to overwrite the approve function and omit the require of who can call this function. But it looks like it isn't the way it should be done.

Thank you!

Beauteous answered 29/4, 2021 at 12:29 Comment(0)
R
14

You can use just the _transfer() function, see my buy() function for an example of implementation.

The approvals for sale can be done using a custom mapping - in my example tokenIdToPrice. If the value is non-zero, the token ID (mapping key) is for sale.

This is a basic code that allows selling an NTF. Feel free to expand on my code to allow "give away for free", "whitelist buyers" or any other feature.

pragma solidity ^0.8.4;

import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol';

contract MyToken is ERC721 {
    event NftBought(address _seller, address _buyer, uint256 _price);

    mapping (uint256 => uint256) public tokenIdToPrice;

    constructor() ERC721('MyToken', 'MyT') {
        _mint(msg.sender, 1);
    }

    function allowBuy(uint256 _tokenId, uint256 _price) external {
        require(msg.sender == ownerOf(_tokenId), 'Not owner of this token');
        require(_price > 0, 'Price zero');
        tokenIdToPrice[_tokenId] = _price;
    }

    function disallowBuy(uint256 _tokenId) external {
        require(msg.sender == ownerOf(_tokenId), 'Not owner of this token');
        tokenIdToPrice[_tokenId] = 0;
    }
    
    function buy(uint256 _tokenId) external payable {
        uint256 price = tokenIdToPrice[_tokenId];
        require(price > 0, 'This token is not for sale');
        require(msg.value == price, 'Incorrect value');
        
        address seller = ownerOf(_tokenId);
        _transfer(seller, msg.sender, _tokenId);
        tokenIdToPrice[_tokenId] = 0; // not for sale anymore
        payable(seller).transfer(msg.value); // send the ETH to the seller

        emit NftBought(seller, msg.sender, msg.value);
    }
}

How to simulate the sale:

  1. The contract deployer (msg.sender) gets token ID 1.
  2. Execute allowBuy(1, 2) that will allow anyone to buy token ID 1 for 2 wei.
  3. From a second address, execute buy(1) sending along 2 wei, to buy the token ID 1.
  4. Call (the parent ERC721) function ownerOf(1) to validate that the owner is now the second address.
Runt answered 4/5, 2021 at 11:17 Comment(7)
I've used _transferFrom(seller, msg.sender, _tokenId); insted of _transfer(seller, msg.sender, _tokenId); because I'm using ERC721Full but it worked nicely. Thank you!Beauteous
What is the line that starts with "mapping" doing?Pippy
@Pippy A mapping is a dictionary-like datatype. You can easily retrieve a value by its key, but you cannot retrieve a key by a value. Note that the keys have to be unique, the values don't... In the example above, the key is the token ID, and the value is the token price. So in this case its easy to query a token price by its ID.Runt
Got it, thank you! what is the value of this mapping when the contract is initially used to mint an NFT? Is it 0? That is, does allowBuy have to be explicitly called with a positive value before the NFT is purchasable? Thanks againPippy
@Pippy Exactly. Default value for each key is 0. And because the buy() function requires price > 0 (i.e. non-default value), you effectively need to invoke allowBuy() before the NFT is purchasable.Runt
In order to call buy and purchase an NFT, is it necessary that the person who sends the buy transaction has to sign the transaction with their private key? I tried calling the buy function using a web3 transaction, and I set the from field to be my friend's public key, but I signed the transaction with my own private key. The token was transferred to myself and not to my friend, as I intended. Thanks for your help!Pippy
@Pippy This snippet allows buying only for yourself, but by expanding the code, you could make a functionality that allows buying for another address... During the process of signing the transaction with your own private key, you most likely rewrote the from field to your address through some web3js internal code... On the raw transaction level, it's technically possible to sign a transaction with a different key that doesn't match the from field (which was probably your intention), but that would be rejected by the network as invalid transaction.Runt
D
6

If you let anyone call the approve function, it would allow anyone to approve themselves to take NFTs! The purpose of approve is to give the owner of an asset the ability to give someone else permission to transfer that asset as if it was theirs.

The basic premise of any sale is that you want to make sure that you get paid, and that the buyer receives the goods in return for the sale. Petr Hedja's solution takes care of this by having the buy function not only transfer the NFT, but also include the logic for sending the price of the token. I'd like to recommend a similar structure with a few changes. One is so that the function will also work with ERC20 tokens, the other is to prevent an edge case where if gas runs out during execution, the buyer could end up with their NFT for free. This is building on his answer, though, and freely uses some of the code in that answer for architecture.

Ether can still be set as the accepted currency by inputting the zero address (address(0)) as the contract address of the token.

If the sale is in an ERC20 token, the buyer will need to approve the NFT contract to spend the amount of the sale since the contract will be pulling the funds from the buyer's account directly.

pragma solidity ^0.8.4;

import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol';
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol';

contract MyToken is ERC721 {
    event NftBought(address _seller, address _buyer, uint256 _price);

    mapping (uint256 => uint256) public tokenIdToPrice;
    mapping (uint256 => address) public tokenIdToTokenAddress;

    constructor() ERC721('MyToken', 'MyT') {
        _mint(msg.sender, 1);
    }

    function setPrice(uint256 _tokenId, uint256 _price, address _tokenAddress) external {
        require(msg.sender == ownerOf(_tokenId), 'Not owner of this token');
        tokenIdToPrice[_tokenId] = _price;
        tokenIdToTokenAddress[_tokenId] = _tokenAddress;
    }

    function allowBuy(uint256 _tokenId, uint256 _price) external {
        require(msg.sender == ownerOf(_tokenId), 'Not owner of this token');
        require(_price > 0, 'Price zero');
        tokenIdToPrice[_tokenId] = _price;
    }

    function disallowBuy(uint256 _tokenId) external {
        require(msg.sender == ownerOf(_tokenId), 'Not owner of this token');
        tokenIdToPrice[_tokenId] = 0;
    }
    
    function buy(uint256 _tokenId) external payable {
        uint256 price = tokenIdToPrice[_tokenId];
        require(price > 0, 'This token is not for sale');
        require(msg.value == price, 'Incorrect value');
        address seller = ownerOf(_tokenId);
        address tokenAddress = tokenIdToTokenAddress[_tokenId];
        if(address != address(0){
            IERC20 tokenContract = IERC20(tokenAddress);
            require(tokenContract.transferFrom(msg.sender, address(this), price),
                "buy: payment failed");
        } else {
            payable(seller).transfer(msg.value);
        }
        _transfer(seller, msg.sender, _tokenId);
        tokenIdToPrice[_tokenId] = 0;
        

        emit NftBought(seller, msg.sender, msg.value);
    }
}
Disjoin answered 4/5, 2021 at 11:51 Comment(3)
There's something wrong with if(address != address(0){, it's missing parenthesis and the comparison doesn't seem right.Reinhardt
The Renaissance, can you explain to me the tokenIdToTokenAddress and _tokenAddress and why you created the setPrice function? I suppose it's to keep track of what ERC20 currency the NFT is for sale.Reinhardt
Hey I have made a question elaborating on your answer here, I wondered if @The Renaissance might be able to have a look and answer it? Thanks #69194220Pile
D
1
// mapping is for fast lookup. the longer operation, the more gas
mapping(uint => NftItem) private _idToNftItem;

function buyNft(uint tokenId) public payable{
    uint price=_idToNftItem[tokenId].price;
    // this is set in erc721 contract
    // Since contracts are inheriting, I want to make sure I use this method in ERC721
    address owner=ERC721.ownerOf(tokenId);
    require(msg.sender!=owner,"You already own this nft");
    require(msg.value==price,"Please submit the asking price");
    // since this is purchased, it is not for sale anymore 
    _idToNftItem[tokenId].isListed=false;
    _listedItems.decrement();
    // this is defined in ERC721
    // this already sets owner _owners[tokenId] = msg.sender;
    _transfer(owner,msg.sender,tokenId);
    payable(owner).transfer(msg.value);
  }

this is Nft struct

struct NftItem{
    uint tokenId;
    uint price;
    // creator and owner are not same. creator someone who minted. creator does not change
    address creator;
    bool isListed;
  }
Denoting answered 8/5, 2022 at 18:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.