Search code examples
functioninheritancesolidity

Vault that receives NFTs and send tokens in exchange, vice-versa


I'm trying to build a contract that creates a parity 1:1 in between a token (e.g. PunkToken) and an NFT collection (e.g. CryptoPunk).

The idea is to deploy both the token and the NFT collection contracts. Then deploy a vault that stores all the created NFTs. Users can then exchange in both directions PunkToken for CryptoPunk vice-versa at a fixed 1:1 parity.

'Require' asides, my idea was to do something like this:

    // create two functions for doing the swap both ways

    function tokenToNft() public payable {
        _transfer(address(this), msg.sender, 1);   // _transfer function from ERC721
    }

    function nftToToken() public payable {
        _transfer(address(this), msg.sender, 1);   // _transfer function from ERC20
    }

To do so, I tried importing both ERC20 and ERC721 but quickly ran into clashes due to the identical functions names. I then tried to change the names of the ERC20 functions. Not working since the token became unrecognized by the blockchain (I believe).


Solution

  • You can make use of the ERC721 function onERC721Received() that the collection is supposed to invoke on your contract as the recipient when a safe transfer to your contract occurs.

    ERC20 does not implement any hook, so you'll need to use the approve() + transferFrom() approach to pull the ERC20 tokens from their wallet.

    Your contract will act as an intermediary, so it needs to hold these specific tokens.

    pragma solidity ^0.8.16;
     
    import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
    import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
    import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
     
    contract MyVault is IERC721Receiver {
        IERC20 immutable public erc20Token;
        IERC721 immutable public erc721Collection;
    
        uint256 constant public AMOUNT_OF_ERC20_PER_ERC721 = 1 * 1e18; // 1 token, 18 decimals
    
        constructor(IERC20 _erc20Token, IERC721 _erc721Collection) {
            erc20Token = _erc20Token;
            erc721Collection = _erc721Collection;
        }
    
        // ERC721 to ERC20
        function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) external returns(bytes4) {
            // TODO validate that you're receiving from a whitelisted collection
            // TODO store which token ID you have received from whom, so that you can return it later
    
            // Transfer 1 ERC20 token in exchange for the NFT
            bool success = erc20Token.transfer(_operator, AMOUNT_OF_ERC20_PER_ERC721);
            require(success);
    
            // required by the ERC721 standard
            return this.onERC721Received.selector;
        }
    
        function erc20toErc721() external {
            // Pull 1 ERC20 token from the user's address to your contract.
            // The user needs to `approve()` your contract to spend their tokens first
            // directly on the ERC20 address, without your contract as the intermediary,
            // otherwise this `transferFrom()` fails
            bool success = erc20Token.transferFrom(msg.sender, address(this), AMOUNT_OF_ERC20_PER_ERC721);
            require(success);
    
            // Hardcoded for simplification. TODO retrieve which token ID they are eligigle to claim
            uint256 tokenId = 1;
    
            // send the NFT from your Vault contract to the user
            erc721Collection.safeTransferFrom(address(this), msg.sender, tokenId);
        }
    }