NFTx闪电贷

NFTx 基本概念

功能:将NFT存入Vault铸造为vToken(1个NFT可换少于1个vToken),vToken可在sushiswap上交易。

铸造:NFT-> vToken 赎回:vToken -> NFT

闪电贷服务:任意铸造vToken。

  1. 闪电贷目前免费

  2. 从NFT铸造vToken费用(mintFee)为0.1

  3. 从vToken赎回NFT的的费用分为两种: -随机赎回NFT,费用为randomRedeemFee 0.04; -指定NFT tokenID赎回,费用为targetRedeemFee 0.06

mintFee:NFT->vToken redeemFee:vToken->NFT

手续费计算: 凭空借随机1个NFT的费用为mintFee + randomRedeemFee= 0.14个vToken。vToken在sushi中可获取价格。

核心合约

NFTXVault:本合约存放了NFT,同时该合约为ERC20,即vToken。该合约提供闪电贷服务,提供铸造vToken和赎回NFT的功能。Vault需要通过代理访问,代理地址可通过NFTXVaultFactory.vaultsForAsset(nftAddress)得到。 NFTXVaultFactory:负责创造各种NFTVault。通过该合约可以查询到NFTXVault的地址。

NFTXVaultFactory:0xBE86f647b167567525cCAAfcd6f881F1Ee558216

例子:$Ape套利

tx: https://etherscan.io/tx/0xeb8c3bebed11e2e4fcd30cbfc2fb3c55c4ca166003c7f7d319e78eaab9747098

  1. 闪电借5.2 个vToken

  2. 通过借到的5.2个vToke赎回5个BAYC,这里费用为randomRedeemFee为0.04,所以 5* 0.04 = 0.2个vToken交给FeeDistributor进行分配。到此时,0x77有6个BAYC。

  3. 用6个BAYC领取$APE空投。

  4. 将6个BAYC铸造为vToken,这里因为有mintFee 0.1,所以0.6个vToken再次被分配。最终得到5.4个vToken。

  5. 将5.2个vToken归还闪电贷,0.2个vToken在sushi上卖出得到14个eth。

套利: 投入:1 BAYC 100eth 产出:14eth 60000 个$APE(213 eth)利润: 227eth - 100eth = 127 eth

无BAYC操作套利: 套利3个:61 eth -> 30000 套利1个:14eth -> 10000 (35 eth)

手续费计算脚本

const sushiRouterV2Addr = '0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F';
const weth_addr = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
const vaultFactoryAddr = '0xBE86f647b167567525cCAAfcd6f881F1Ee558216';
const getFeeByNFTAddress = async (nftaddr) => {
    // console.log(`get `)
    let provider = ethers.getDefaultProvider();
    let nft = new ethers.Contract(nftaddr,erc721_abi,provider);
    let factory = new ethers.Contract(vaultFactoryAddr,vault_factory_abi,provider);
    let vaultAddrList = await factory.vaultsForAsset(nftaddr);
    
    if (vaultAddrList.length > 1) {
        console.log(`more vaults found`);
    } else if (vaultAddrList.length == 0) {
        console.log('no vaults');
        return;
    }
    let vaultAddr = vaultAddrList[0];
    let vault = new ethers.Contract(vaultAddr,vault_abi,provider);
    const redeemFee = await vault.randomRedeemFee();
    const mintFee = await vault.mintFee()
    const totalFee = redeemFee.add(mintFee);
    const router = new ethers.Contract(sushiRouterV2Addr,router_abi,provider);
    //   const amountOutMin;
    let [amountIn, _] = await router.getAmountsIn(totalFee,[weth_addr,vaultAddr]);
    console.log(`NFTx flashloan: borrow 1 * ${await nft.name()} NFT need fee ${ethers.utils.formatEther(amountIn)} e`);
    return amountIn;
}

async function main() {
    await getFeeByNFTAddress('0x7Bd29408f11D2bFC23c34f18275bBf23bB716Bc7');
}

NFTx闪电贷使用方法

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "./interface/INFTXVaultFactory.sol";
import "./interface/INFTXVault.sol";
import "./interface/IERC3156Upgradeable.sol";
import "./IClaim.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/interfaces/IERC721Receiver.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "hardhat/console.sol";

// IERC3156FlashBorrower 保证可使用闪电贷
// IERC721Receiver 是因为要将借到的vToken赎回为NFT,所以需要可以接收NFT
contract NFTxFlashLoanExample is IERC3156FlashBorrower ,IERC721Receiver{
    bytes32 constant private RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
    
    INFTXVaultFactory public factory;
    
    struct FlashLoanParams {
        address nft; // NFT合约地址
        uint256 amount; // num of nft
        address claim; // 领取空投的合约地址
        uint256[] ids; // 可指定从金库Vault赎回的NFT
    }

    struct FlashLoanCallbackParams {
        address nft;
        address claim; 
        address vault;
        uint256 amount; // 赎回几个nft
        uint256[] ids; 
    }
    
    constructor(address _factory) {
        factory = INFTXVaultFactory(_factory);
    }

    function initFlashLoan(FlashLoanParams memory params) external {
        // factory.vaultsForAsset(nft);
        require(!factory.isLocked(4), "flashloan locked"); // 保证NFTx未关闭闪电贷
        address[] memory addrs = factory.vaultsForAsset(params.nft); // 查找对应NFT的zVault
        address token = addrs[0];
        IERC3156FlashLenderUpgradeable lender = IERC3156FlashLenderUpgradeable(token);        
        lender.flashLoan(IERC3156FlashBorrowerUpgradeable(address(this)),
                             token,  // token即为NFTVault vToken
                             params.amount * 10 ** 18,  // 一个nft需要1个vtoken
                             abi.encode(FlashLoanCallbackParams({
                                nft:params.nft,
                                claim:params.claim,
                                amount:params.amount,
                                vault:token,
                                ids:params.ids // 不指定id的话手续费会低
                            })));

    }

    function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes memory data) external override 
        returns (bytes32)
    {
        console.log("onFlashLoan called");        
        FlashLoanCallbackParams memory params = abi.decode(data,(FlashLoanCallbackParams));
        INFTXVault vault = INFTXVault(params.vault);
        uint256[] memory ids = vault.redeem(params.amount, params.ids); // 从vToken 换为NFT,返回得到的tokenId
        uint256[] memory amounts;
        for (uint i = 0; i < ids.length; i++) {
            IClaim(params.claim).claimTokens(); // 领取$APE空投
            IERC721(params.nft).approve(address(vault), ids[i]); // 授权Vault可将NFT从this转移到Vault
        }
        vault.mint(ids, amounts); // 铸造vToken,转移NFT到Valut
        IERC20(params.vault).approve(msg.sender, amount + fee); //授权保证闪电贷结束后可以_burn销毁,即归还vToken
        return RETURN_VALUE; // 返回该值才可成功进行闪电贷
    }
    
    function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external override returns (bytes4){
        console.log(string(abi.encodePacked('received ',tokenId, ' from ', from)));
        return IERC721Receiver.onERC721Received.selector;
    }
}