diff --git a/contracts/solidity/testing/MockSushiSwap.sol b/contracts/solidity/testing/MockSushiSwap.sol new file mode 100644 index 00000000..4355d031 --- /dev/null +++ b/contracts/solidity/testing/MockSushiSwap.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + + +contract MockSushiSwap { + + function addLiquidity( + address tokenA, + address tokenB, + uint256 amountADesired, + uint256 amountBDesired, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity) { + amountA = amountADesired; + amountB = amountBDesired; + liquidity = (amountADesired * amountBDesired) / 1e18; + } + + function factory() external returns (address) { + return address(this); + } +} diff --git a/contracts/solidity/testing/WETH.sol b/contracts/solidity/testing/WETH.sol new file mode 100644 index 00000000..c6e50070 --- /dev/null +++ b/contracts/solidity/testing/WETH.sol @@ -0,0 +1,68 @@ +pragma solidity ^0.8.0; + +contract WETH { + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint wad); + event Transfer(address indexed src, address indexed dst, uint wad); + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + mapping (address => uint) public balanceOf; + mapping (address => mapping (address => uint)) public allowance; + + receive() external payable { + deposit(); + } + + function mint(address to, uint256 amount) public { + balanceOf[to] += amount; + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint wad) public payable { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint) { + return address(this).balance; + } + + function approve(address guy, uint wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint wad) + public + returns (bool) + { + require(balanceOf[src] >= wad); + + if (src != msg.sender && allowance[src][msg.sender] >= 0) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} diff --git a/contracts/solidity/util/SushiHelper.sol b/contracts/solidity/util/SushiHelper.sol new file mode 100644 index 00000000..2567ae02 --- /dev/null +++ b/contracts/solidity/util/SushiHelper.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + + +contract SushiHelper { + + /** + * @notice Calculates the CREATE2 address for a sushi pair without making any + * external calls. + * + * @return pair Address of our token pair + */ + + function pairFor(address sushiRouterFactory, address tokenA, address tokenB) external view returns (address pair) { + (address token0, address token1) = sortTokens(tokenA, tokenB); + pair = address(uint160(uint256(keccak256(abi.encodePacked( + hex'ff', + sushiRouterFactory, + keccak256(abi.encodePacked(token0, token1)), + hex'e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303' // init code hash + ))))); + } + + + /** + * @notice Returns sorted token addresses, used to handle return values from pairs sorted in + * this order. + */ + + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES'); + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS'); + } + +} diff --git a/contracts/solidity/zaps/VaultCreationZap.sol b/contracts/solidity/zaps/VaultCreationZap.sol new file mode 100644 index 00000000..7adaf200 --- /dev/null +++ b/contracts/solidity/zaps/VaultCreationZap.sol @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../interface/INFTXInventoryStaking.sol"; +import "../interface/INFTXLPStaking.sol"; +import "../interface/IUniswapV2Router01.sol"; +import "../interface/INFTXVault.sol"; +import "../interface/INFTXVaultFactory.sol"; +import "../token/IERC1155Upgradeable.sol"; +import "../token/ERC1155SafeHolderUpgradeable.sol"; +import "../util/Ownable.sol"; +import "../util/ReentrancyGuard.sol"; +import "../util/SafeERC20Upgradeable.sol"; +import "../util/SushiHelper.sol"; + + +/** + * @notice A partial WETH interface. + */ + +interface IWETH { + function deposit() external payable; + function transfer(address to, uint value) external returns (bool); + function withdraw(uint) external; + function balanceOf(address to) external view returns (uint256); +} + + +/** + * @notice An amalgomation of vault creation steps, merged and optimised in + * a single contract call in an attempt reduce gas costs to the end-user. + * + * @author Twade + */ + +contract NFTXVaultCreationZap is Ownable, ReentrancyGuard, ERC1155SafeHolderUpgradeable { + + using SafeERC20Upgradeable for IERC20Upgradeable; + + /// @notice Allows zap to be paused + bool public paused = false; + + /// @notice An interface for the NFTX Vault Factory contract + INFTXVaultFactory public immutable vaultFactory; + + /// @notice Holds the mapping of our sushi router + IUniswapV2Router01 public immutable sushiRouter; + SushiHelper internal immutable sushiHelper; + + /// @notice An interface for the WETH contract + IWETH public immutable WETH; + + /// @notice An interface for the NFTX Vault Factory contract + INFTXInventoryStaking public immutable inventoryStaking; + INFTXLPStaking public immutable lpStaking; + + /// @notice Basic information pertaining to the vault + struct vaultInfo { + address assetAddress; // 20/32 + bool is1155; // 21/32 + bool allowAllItems; // 22/32 + string name; // ??/32 + string symbol; // ??/32 + } + + /// @notice Fee information in 9-decimal format + struct vaultFeesConfig { + uint32 mintFee; + uint32 randomRedeemFee; + uint32 targetRedeemFee; + uint32 randomSwapFee; + uint32 targetSwapFee; + } + + /// @notice Reference to the vault's eligibility implementation + struct vaultEligibilityStorage { + int moduleIndex; + bytes initData; + } + + /// @notice Valid tokens to be transferred to the vault on creation + struct vaultTokens { + uint[] assetTokenIds; + uint[] assetTokenAmounts; + + // Sushiswap integration for liquidity + uint minTokenIn; + uint minWethIn; + uint wethIn; + } + + + /** + * @notice Initialises our zap by setting contract addresses onto their + * respective interfaces. + */ + + constructor( + address _vaultFactory, + address _inventoryStaking, + address _lpStaking, + address _sushiRouter, + address _sushiHelper, + address _weth + ) Ownable() ReentrancyGuard() { + // Set our staking contracts + inventoryStaking = INFTXInventoryStaking(_inventoryStaking); + lpStaking = INFTXLPStaking(_lpStaking); + + // Set our NFTX factory contract + vaultFactory = INFTXVaultFactory(_vaultFactory); + + // Set our Sushi Router used for liquidity + sushiRouter = IUniswapV2Router01(_sushiRouter); + sushiHelper = SushiHelper(_sushiHelper); + + // Set our chain's WETH contract + WETH = IWETH(_weth); + } + + + /** + * @notice Creates an NFTX vault, handling any desired settings and tokens. + * + * @dev Tokens are deposited into the vault prior to fees being sent. + * + * @param vaultData Basic information about the vault stored in `vaultInfo` struct + * @param vaultFeatures A numeric representation of boolean values for features on the vault + * @param vaultFees Fee definitions stored in a `vaultFeesConfig` struct + * @param eligibilityStorage Eligibility implementation, stored in a `vaultEligibilityStorage` struct + * @param assetTokens Tokens to be transferred to the vault in exchange for vault tokens + * + * @return vaultId_ The numeric ID of the NFTX vault + */ + + function createVault( + vaultInfo calldata vaultData, + uint vaultFeatures, + vaultFeesConfig calldata vaultFees, + vaultEligibilityStorage calldata eligibilityStorage, + vaultTokens calldata assetTokens + ) external nonReentrant payable returns (uint vaultId_) { + // Ensure our zap is not paused + require(!paused, 'Zap is paused'); + + // Get the amount of starting ETH in the contract + uint startingWeth = WETH.balanceOf(address(this)); + + // Create our vault skeleton + vaultId_ = vaultFactory.createVault( + vaultData.name, + vaultData.symbol, + vaultData.assetAddress, + vaultData.is1155, + vaultData.allowAllItems + ); + + // Deploy our vault's xToken + inventoryStaking.deployXTokenForVault(vaultId_); + + // Build our vault interface + INFTXVault vault = INFTXVault(vaultFactory.vault(vaultId_)); + + // If we have a specified eligibility storage, add that on + if (eligibilityStorage.moduleIndex >= 0) { + vault.deployEligibilityStorage( + uint256(eligibilityStorage.moduleIndex), + eligibilityStorage.initData + ); + } + + // Mint and stake liquidity into the vault + uint length = assetTokens.assetTokenIds.length; + + // If we don't have any tokens to send, we can skip our transfers + if (length > 0) { + // Determine the token type to alternate our transfer logic + if (!vaultData.is1155) { + // Iterate over our 721 tokens to transfer them all to our vault + for (uint i; i < length;) { + _transferFromERC721(vaultData.assetAddress, assetTokens.assetTokenIds[i], address(vault)); + unchecked { ++i; } + } + } else { + // Transfer all of our 1155 tokens to our zap, as the `mintTo` call on our + // vault requires the call sender to hold the ERC1155 token. + IERC1155Upgradeable(vaultData.assetAddress).safeBatchTransferFrom( + msg.sender, + address(this), + assetTokens.assetTokenIds, + assetTokens.assetTokenAmounts, + "" + ); + + // Approve our vault to play with our 1155 tokens + IERC1155Upgradeable(vaultData.assetAddress).setApprovalForAll(address(vault), true); + } + + // We can now mint our asset tokens, giving the vault our tokens and storing them + // inside our zap, as we will shortly be staking them. Our zap is excluded from fees, + // so there should be no loss in the amount returned. + vault.mintTo(assetTokens.assetTokenIds, assetTokens.assetTokenAmounts, address(this)); + + // We now have tokens against our provided NFTs that we can now stake through either + // inventory or liquidity. + + // Get our vaults base staking token. This is used to calculate the xToken + address baseToken = address(vault); + + // We first want to set up our liquidity, as the returned values will be variable + if (assetTokens.minTokenIn > 0) { + require(msg.value > assetTokens.wethIn, 'Insufficient vault sent for liquidity'); + + // Wrap ETH into WETH for our contract from the sender + WETH.deposit{value: msg.value}(); + + // Convert WETH to vault token + require(IERC20Upgradeable(baseToken).balanceOf(address(this)) >= assetTokens.minTokenIn, 'Insufficient tokens acquired for liquidity'); + + // Provide liquidity to sushiswap, using the vault tokens and pairing it with the + // liquidity amount specified in the call. + IERC20Upgradeable(baseToken).safeApprove(address(sushiRouter), assetTokens.minTokenIn); + (,, uint256 liquidity) = sushiRouter.addLiquidity( + baseToken, + address(WETH), + assetTokens.minTokenIn, + assetTokens.wethIn, + assetTokens.minTokenIn, + assetTokens.minWethIn, + address(this), + block.timestamp + ); + + // Stake in LP rewards contract + address lpToken = sushiHelper.pairFor(sushiRouter.factory(), baseToken, address(WETH)); + IERC20Upgradeable(lpToken).safeApprove(address(lpStaking), liquidity); + lpStaking.timelockDepositFor(vaultId_, msg.sender, liquidity, 48 hours); + } + + // Return any token dust to the caller + uint256 remainingTokens = IERC20Upgradeable(baseToken).balanceOf(address(this)); + + // Any tokens that we have remaining after our liquidity staking are thrown into + // inventory to ensure what we don't have any token dust remaining. + if (remainingTokens > 0) { + // Make a direct timelock mint using the default timelock duration. This sends directly + // to our user, rather than via the zap, to avoid the timelock locking the tx. + IERC20Upgradeable(baseToken).transfer(inventoryStaking.vaultXToken(vaultId_), remainingTokens); + inventoryStaking.timelockMintFor(vaultId_, remainingTokens, msg.sender, 2); + } + } + + // If we have specified vault features that aren't the default (all enabled) + // then update them + if (vaultFeatures < 31) { + vault.setVaultFeatures( + _getBoolean(vaultFeatures, 4), + _getBoolean(vaultFeatures, 3), + _getBoolean(vaultFeatures, 2), + _getBoolean(vaultFeatures, 1), + _getBoolean(vaultFeatures, 0) + ); + } + + // Set our vault fees, converting our 9-decimal to 18-decimal + vault.setFees( + uint256(vaultFees.mintFee) * 10e9, + uint256(vaultFees.randomRedeemFee) * 10e9, + uint256(vaultFees.targetRedeemFee) * 10e9, + uint256(vaultFees.randomSwapFee) * 10e9, + uint256(vaultFees.targetSwapFee) * 10e9 + ); + + // Finalise our vault, preventing further edits + vault.finalizeVault(); + + // Now that all transactions are finished we can return any ETH dust left over + // from our liquidity staking. + uint remainingWEth = WETH.balanceOf(address(this)) - startingWeth; + if (remainingWEth > 0) { + WETH.withdraw(remainingWEth); + bool sent = payable(msg.sender).send(remainingWEth); + require(sent, "Failed to send Ether"); + } + } + + + /** + * @notice Transfers our ERC721 tokens to a specified recipient. + * + * @param assetAddr Address of the asset being transferred + * @param tokenId The ID of the token being transferred + * @param to The address the token is being transferred to + */ + + function _transferFromERC721(address assetAddr, uint256 tokenId, address to) internal virtual { + bytes memory data; + + if (assetAddr == 0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB) { + // Fix here for frontrun attack. + bytes memory punkIndexToAddress = abi.encodeWithSignature("punkIndexToAddress(uint256)", tokenId); + (bool checkSuccess, bytes memory result) = address(assetAddr).staticcall(punkIndexToAddress); + (address nftOwner) = abi.decode(result, (address)); + require(checkSuccess && nftOwner == msg.sender, "Not the NFT owner"); + data = abi.encodeWithSignature("buyPunk(uint256)", tokenId); + } else { + // We push to the vault to avoid an unneeded transfer. + data = abi.encodeWithSignature("safeTransferFrom(address,address,uint256)", msg.sender, to, tokenId); + } + + (bool success, bytes memory resultData) = address(assetAddr).call(data); + require(success, string(resultData)); + } + + + /** + * @notice Reads a boolean at a set character index of a uint. + * + * @dev 0 and 1 define false and true respectively. + * + * @param _packedBools A numeric representation of a series of boolean values + * @param _boolNumber The character index of the boolean we are looking up + * + * @return bool The representation of the boolean value + */ + + function _getBoolean(uint256 _packedBools, uint256 _boolNumber) internal pure returns(bool) { + uint256 flag = (_packedBools >> _boolNumber) & uint256(1); + return (flag == 1 ? true : false); + } + + + /** + * @notice Allows our zap to be paused to prevent any processing. + * + * @param _paused New pause state + */ + + function pause(bool _paused) external onlyOwner { + paused = _paused; + } + + receive() external payable { + require(msg.sender == address(WETH), "Only WETH"); + } + +} diff --git a/hardhat.config.js b/hardhat.config.js index 91f813bf..b04d0374 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -1,7 +1,8 @@ require("dotenv").config(); require("@nomiclabs/hardhat-waffle"); require("@openzeppelin/hardhat-upgrades"); -// require("hardhat-gas-reporter"); +require("hardhat-gas-reporter"); +require("@nomiclabs/hardhat-etherscan"); /** * @type import('hardhat/config').HardhatUserConfig @@ -12,10 +13,9 @@ module.exports = { url: `https://eth-rinkeby.alchemyapi.io/v2/${process.env.ALCHEMY_RINKEBY_API_KEY}`, accounts: [`0x${process.env.DEV_PRIVATE_KEY}`], }, - ropsten: { - url: `https://eth-ropsten.alchemyapi.io/v2/${process.env.ALCHEMY_ROPSTEN_API_KEY}`, + goerli: { + url: `https://eth-goerli.alchemyapi.io/v2/${process.env.ALCHEMY_RINKEBY_API_KEY}`, accounts: [`0x${process.env.DEV_PRIVATE_KEY}`], - timeout: 600000, }, mainnet: { url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_MAINNET_API_KEY}`, @@ -29,7 +29,7 @@ module.exports = { arbitrum: { url: "https://arb1.arbitrum.io/rpc", accounts: [`0x${process.env.DEV_PRIVATE_KEY}`], - timeout: 600000, + timeout: 100000, }, hardhat: { mining: { @@ -53,4 +53,7 @@ module.exports = { mocha: { timeout: 100000, }, + etherscan: { + apiKey: process.env.ETHERSCAN_API_KEY + }, }; diff --git a/package.json b/package.json index 57f960d5..b0db929d 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "hardhat-project", "devDependencies": { "@nomiclabs/hardhat-ethers": "^2.0.2", + "@nomiclabs/hardhat-etherscan": "^2.1.7", "@nomiclabs/hardhat-vyper": "^2.0.1", "@nomiclabs/hardhat-waffle": "^2.0.1", "chai": "^4.3.4", diff --git a/scripts/deploy-vault-creation-zap.js b/scripts/deploy-vault-creation-zap.js new file mode 100644 index 00000000..53c04d33 --- /dev/null +++ b/scripts/deploy-vault-creation-zap.js @@ -0,0 +1,32 @@ +const {BigNumber} = require("@ethersproject/bignumber"); +const {ethers, upgrades} = require("hardhat"); + + +async function main() { + const [deployer] = await ethers.getSigners(); + + console.log("Deploying account:", await deployer.getAddress()); + console.log( + "Deploying account balance:", + (await deployer.getBalance()).toString(), + "\n" + ); + + const VaultCreationZap = await ethers.getContractFactory( + "NFTXVaultCreationZap" + ); + const zap = await VaultCreationZap.deploy("0xe01Cf5099e700c282A56E815ABd0C4948298Afae"); + await zap.deployed(); + console.log("Vault Creation Zap:", zap.address); +} + +main() + .then(() => { + console.log("\nDeployment completed successfully ✓"); + process.exit(0); + }) + .catch((error) => { + console.log("\nDeployment failed ✗"); + console.error(error); + process.exit(1); + }); \ No newline at end of file diff --git a/test/zaps/vault-creation-zap.js b/test/zaps/vault-creation-zap.js new file mode 100644 index 00000000..b6c74749 --- /dev/null +++ b/test/zaps/vault-creation-zap.js @@ -0,0 +1,514 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const {deployMockContract} = require('@ethereum-waffle/mock-contract'); + +const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; +const BASE = 1e18; + + +// Store our internal contract references +let weth, erc721, erc1155; +let nftx, vault; +let marketplaceZap, mockSushiSwap; +let inventoryStaking, lpStaking; + +// Store any user addresses we want to interact with +let deployer, alice, bob, users; + + +/** + * Validates our expected NFTX allocator logic. + */ + +describe('Vault Creation Zap', function () { + + before(async function () { + // Set up our deployer / owner address + [deployer, alice, bob, ...users] = await ethers.getSigners() + + // Set up a test ERC721 token + const Erc721 = await ethers.getContractFactory("ERC721"); + erc721 = await Erc721.deploy('SpacePoggers', 'POGGERS'); + await erc721.deployed(); + + // Set up a test ERC1155 token + const Erc1155 = await ethers.getContractFactory("ERC1155"); + erc1155 = await Erc1155.deploy('https://space.poggers/'); + await erc1155.deployed(); + + // Set up our NFTX contracts + await _initialise_nftx_contracts() + + // Set up a test ERC20 token to simulate WETH + const WETH = await ethers.getContractFactory("WETH"); + weth = await WETH.deploy(); + await weth.deployed(); + + // Set up a sushiswap mock + const MockSushiSwap = await ethers.getContractFactory("MockSushiSwap") + mockSushiSwap = await MockSushiSwap.deploy() + await mockSushiSwap.deployed() + + // Set up a mocked sushi helper, which will allow us to repoint our liquidity pool + // token to one that we can control. + const sushiHelperArtifact = await artifacts.readArtifact("SushiHelper"); + const mockSushiHelper = await deployMockContract(deployer, sushiHelperArtifact.abi); + await mockSushiHelper.mock.pairFor.returns(weth.address); + + // Set up vault creation zap + const VaultCreationZap = await ethers.getContractFactory("NFTXVaultCreationZap") + vaultCreationZap = await VaultCreationZap.deploy( + nftx.address, + inventoryStaking.address, + lpStaking.address, + mockSushiSwap.address, + mockSushiHelper.address, + weth.address, + ) + await vaultCreationZap.deployed(); + + // Allow our yield staking zap to exclude fees + await nftx.setZapContract(vaultCreationZap.address); + await nftx.setFeeExclusion(vaultCreationZap.address, true); + }); + + + /** + * Test our vault creation flow + */ + + describe('Create vaults with a variety of configurations', async function () { + + before(async function () { + // .. + }); + + /** + * Confirm that a vault can be created when providing all possible options. + */ + + it("1. Should allow 721 vault to be created in a single call", async function () { + + // Mint the vault asset address to Alice (10 tokens) + for (let i = 0; i < 10; ++i) { + await erc721.publicMint(alice.address, i); + } + + // Approve the Vault Creation Zap to use Alice's ERC721s + await erc721.connect(alice).setApprovalForAll(vaultCreationZap.address, true); + + // Use call static to get the actual return vault from the call + const vaultId = await vaultCreationZap.connect(alice).createVault( + // Vault creation + { + name: 'Space Poggers', + symbol: 'POGGERS', + assetAddress: erc721.address, + is1155: false, + allowAllItems: true + }, + + // Vault features + 21, // 10101 + + // Fee assignment + { + mintFee: 10000000, // 0.10 + randomRedeemFee: 5000000, // 0.05 + targetRedeemFee: 10000000, // 0.10 + randomSwapFee: 5000000, // 0.05 + targetSwapFee: 10000000 // 0.10 + }, + + // Eligibility storage + { + moduleIndex: -1, + initData: 0 + }, + + // Mint and stake + { + assetTokenIds: [1, 2, 3, 4], + assetTokenAmounts: [1, 1, 1, 1], + minTokenIn: 0, + minWethIn: 0, + wethIn: 0 + } + ); + + // Build our NFTXVaultUpgradeable against the newly created vault + const newVault = await ethers.getContractAt( + "NFTXVaultUpgradeable", + await nftx.vault(0) + ); + + // Confirm our general information + expect(await newVault.vaultId()).to.equal(0); + expect(await newVault.name()).to.equal('Space Poggers'); + expect(await newVault.symbol()).to.equal('POGGERS'); + expect(await newVault.assetAddress()).to.equal(erc721.address); + + // Confirm our features + expect(await newVault.enableMint()).to.equal(true); + expect(await newVault.enableRandomRedeem()).to.equal(false); + expect(await newVault.enableTargetRedeem()).to.equal(true); + expect(await newVault.enableRandomSwap()).to.equal(false); + expect(await newVault.enableTargetSwap()).to.equal(true); + + // Confirm our fees + let [mintFee, randomRedeemFee, targetRedeemFee, randomSwapFee, targetSwapFee] = await newVault.vaultFees(); + + expect(mintFee).to.equal("100000000000000000"); + expect(randomRedeemFee).to.equal("50000000000000000"); + expect(targetRedeemFee).to.equal("100000000000000000"); + expect(randomSwapFee).to.equal("50000000000000000"); + expect(targetSwapFee).to.equal("100000000000000000"); + + // Confirm our vault's token holdings + expect(await erc721.balanceOf(newVault.address)).to.equal(4); + + // Confirm our user has the expected balance of inventory staked xToken + let xToken = await inventoryStaking.vaultXToken(0); + const xTokenContract = await ethers.getContractAt('XTokenUpgradeable', xToken); + expect(await xTokenContract.balanceOf(alice.getAddress())).to.equal('4000000000000000000'); + }); + + + /** + * Confirm that a vault can be created when providing all possible options. + */ + + it("2. Should allow 1155 vault to be created in a single call", async function () { + + // Mint 10x the vault asset address to Alice (10 tokens) + for (let i = 0; i < 10; ++i) { + await erc1155.publicMint(alice.address, i, 10); + } + + // Approve the Vault Creation Zap to use Alice's ERC1155s + await erc1155.connect(alice).setApprovalForAll(vaultCreationZap.address, true); + + // Use call static to get the actual return vault from the call + const vaultId = await vaultCreationZap.connect(alice).createVault( + // Vault creation + { + name: 'Pace Spoggers', + symbol: 'SPOGGERS', + assetAddress: erc1155.address, + is1155: true, + allowAllItems: true + }, + + // Vault features + 26, // 11010 + + // Fee assignment + { + mintFee: 20000000, // 0.2 + randomRedeemFee: 12500000, // 0.125 + targetRedeemFee: 20000000, // 0.2 + randomSwapFee: 15000000, // 0.15 + targetSwapFee: 20000000 // 0.2 + }, + + // Eligibility storage + { + moduleIndex: -1, + initData: 0 + }, + + // Mint and stake + { + assetTokenIds: [1, 2, 3], + assetTokenAmounts: [2, 5, 3], + minTokenIn: 0, + minWethIn: 0, + wethIn: 0 + } + ); + + // Build our NFTXVaultUpgradeable against the newly created vault + const newVault = await ethers.getContractAt( + "NFTXVaultUpgradeable", + await nftx.vault(1) + ); + + // Confirm our general information + expect(await newVault.vaultId()).to.equal(1); + expect(await newVault.name()).to.equal('Pace Spoggers'); + expect(await newVault.symbol()).to.equal('SPOGGERS'); + expect(await newVault.assetAddress()).to.equal(erc1155.address); + + // Confirm our features + expect(await newVault.enableMint()).to.equal(true); + expect(await newVault.enableRandomRedeem()).to.equal(true); + expect(await newVault.enableTargetRedeem()).to.equal(false); + expect(await newVault.enableRandomSwap()).to.equal(true); + expect(await newVault.enableTargetSwap()).to.equal(false); + + // Confirm our fees + let [mintFee, randomRedeemFee, targetRedeemFee, randomSwapFee, targetSwapFee] = await newVault.vaultFees(); + + expect(mintFee).to.equal("200000000000000000"); + expect(randomRedeemFee).to.equal("125000000000000000"); + expect(targetRedeemFee).to.equal("200000000000000000"); + expect(randomSwapFee).to.equal("150000000000000000"); + expect(targetSwapFee).to.equal("200000000000000000"); + + // Confirm our vault's token holdings + expect(await erc1155.balanceOf(newVault.address, 1)).to.equal(2); + expect(await erc1155.balanceOf(newVault.address, 2)).to.equal(5); + expect(await erc1155.balanceOf(newVault.address, 3)).to.equal(3); + expect(await erc1155.balanceOf(newVault.address, 4)).to.equal(0); + + // Confirm our user has the expected balance of inventory staked xToken + let xToken = await inventoryStaking.vaultXToken(1); + const xTokenContract = await ethers.getContractAt('XTokenUpgradeable', xToken); + expect(await xTokenContract.balanceOf(alice.getAddress())).to.equal('10000000000000000000'); + }); + + + /** + * Confirm that a vault can be created with both inventory and liquidity staking + * in a single call. + */ + + it("3. Should allow inventory and liquidity staking in a single call", async function () { + // Mint the vault asset address to Alice (10 tokens) + for (let i = 11; i < 20; ++i) { + await erc721.publicMint(alice.address, i); + } + + // Approve the Vault Creation Zap to use Alice's ERC721s + await erc721.connect(alice).setApprovalForAll(vaultCreationZap.address, true); + + // Use call static to get the actual return vault from the call. In this case we additionally + // need to send ETH to the zap in order to fund the liquidity staking. We send 10 ETH but will + // only need to use 7 ETH of this. For this reason, we need to test that we correctly have 3 ETH + // returned after the creation. + const vaultId = await vaultCreationZap.connect(alice).createVault( + // Vault creation + { + name: 'Race Poogers', + symbol: 'POOGERS', + assetAddress: erc721.address, + is1155: false, + allowAllItems: true + }, + + // Vault features + 32, // 11111 + + // Fee assignment + { + mintFee: 10000000, // 0.10 + randomRedeemFee: 5000000, // 0.05 + targetRedeemFee: 10000000, // 0.10 + randomSwapFee: 5000000, // 0.05 + targetSwapFee: 10000000 // 0.10 + }, + + // Eligibility storage + { + moduleIndex: -1, + initData: 0 + }, + + // Mint and stake + { + assetTokenIds: [11, 12, 13, 14, 15], + assetTokenAmounts: [1, 1, 1, 1, 1], + minTokenIn: '350000000000000000', // 3.5 tokens + minWethIn: ethers.utils.parseEther('7'), + wethIn: ethers.utils.parseEther('7') + }, + { value: ethers.utils.parseEther('10') } + ); + + // Build our NFTXVaultUpgradeable against the newly created vault + const newVault = await ethers.getContractAt( + "NFTXVaultUpgradeable", + await nftx.vault(2) + ); + + // Confirm our general information + expect(await newVault.vaultId()).to.equal(2); + expect(await newVault.name()).to.equal('Race Poogers'); + expect(await newVault.symbol()).to.equal('POOGERS'); + expect(await newVault.assetAddress()).to.equal(erc721.address); + + // Confirm our features + expect(await newVault.enableMint()).to.equal(true); + expect(await newVault.enableRandomRedeem()).to.equal(true); + expect(await newVault.enableTargetRedeem()).to.equal(true); + expect(await newVault.enableRandomSwap()).to.equal(true); + expect(await newVault.enableTargetSwap()).to.equal(true); + + // Confirm our fees + let [mintFee, randomRedeemFee, targetRedeemFee, randomSwapFee, targetSwapFee] = await newVault.vaultFees(); + + expect(mintFee).to.equal("100000000000000000"); + expect(randomRedeemFee).to.equal("50000000000000000"); + expect(targetRedeemFee).to.equal("100000000000000000"); + expect(randomSwapFee).to.equal("50000000000000000"); + expect(targetSwapFee).to.equal("100000000000000000"); + + // Confirm our vault's token holdings + expect(await erc721.balanceOf(newVault.address)).to.equal(5); + + // Confirm our user has the expected balance of inventory staked xToken + // let xToken = await inventoryStaking.vaultXToken(2); + // const xTokenContract = await ethers.getContractAt('XTokenUpgradeable', xToken); + // expect(await xTokenContract.balanceOf(alice.getAddress())).to.equal('1500000000000000000'); + + // Confirm our user has the expected balance of liquidity staked xToken + // dev: We can't actually do this as our liquidity staking is mocked + + // Confirm our remaining ETH was returned. Since we can't account for an exact amount + // due to gas, we just want to make sure we have more that: + // 100 (start) - 10 (tx value sent) + 3 (refunded amount) = 93 (with slight gas variance) + expect(await ethers.provider.getBalance(alice.address)).to.gt(ethers.utils.parseEther('90')); + }); + + /** + * Confirm that a vault can be created with both inventory and liquidity staking + * in a single call. + */ + + it("4. Should allow no tokens to be sent when creating", async function () { + const vaultId = await vaultCreationZap.connect(alice).createVault( + // Vault creation + { + name: "fref", + symbol: "ferfre", + assetAddress: "0x3b055557a3d9c638bc32a9cd87dd5f7c740002fa", + is1155: false, + allowAllItems: true + }, + + // Vault features + 31, // 11110 + + // Fee assignment + { + mintFee: 10000000, // 0.10 + randomRedeemFee: 4000000, // 0.04 + targetRedeemFee: 6000000, // 0.10 + randomSwapFee: 4000000, // 0.04 + targetSwapFee: 10000000 // 0.10 + }, + + // Eligibility storage + { + moduleIndex: -1, + initData: 0 + }, + + // Mint and stake + { + assetTokenIds: [], + assetTokenAmounts: [], + minTokenIn: 0, + minWethIn: 0, + wethIn: 0 + }, + + // No msg.value + { } + ); + + // Build our NFTXVaultUpgradeable against the newly created vault + const newVault = await ethers.getContractAt( + "NFTXVaultUpgradeable", + await nftx.vault(3) + ); + + // Confirm our general information + expect(await newVault.vaultId()).to.equal(3); + expect(await newVault.name()).to.equal('fref'); + expect(await newVault.symbol()).to.equal('ferfre'); + expect(await newVault.assetAddress()).to.equal('0x3b055557A3d9c638bC32A9cd87Dd5F7C740002FA'); + + // Confirm our features + expect(await newVault.enableMint()).to.equal(true); + expect(await newVault.enableRandomRedeem()).to.equal(true); + expect(await newVault.enableTargetRedeem()).to.equal(true); + expect(await newVault.enableRandomSwap()).to.equal(true); + expect(await newVault.enableTargetSwap()).to.equal(true); + + // Confirm our fees + let [mintFee, randomRedeemFee, targetRedeemFee, randomSwapFee, targetSwapFee] = await newVault.vaultFees(); + + expect(mintFee).to.equal("100000000000000000"); + expect(randomRedeemFee).to.equal("40000000000000000"); + expect(targetRedeemFee).to.equal("60000000000000000"); + expect(randomSwapFee).to.equal("40000000000000000"); + expect(targetSwapFee).to.equal("100000000000000000"); + + // Confirm our vault's token holdings + expect(await erc721.balanceOf(newVault.address)).to.equal(0); + }); + + }); + +}); + + +async function _initialise_nftx_contracts() { + const StakingProvider = await ethers.getContractFactory("MockStakingProvider"); + const provider = await StakingProvider.deploy(); + await provider.deployed(); + + // We deploy the liquidity pool staking contract as a mock, as we have a conflict in the + // test suite in which the staking reward token doesn't match the sushiswap pair, so we + // never have a sufficient balance to supply. This mock ensures that we just never revert + // on the call. + const lpStakingArtifact = await artifacts.readArtifact("NFTXLPStaking"); + lpStaking = await deployMockContract(deployer, lpStakingArtifact.abi); + await lpStaking.mock.timelockDepositFor.returns(); + await lpStaking.mock.addPoolForVault.returns(); + + const NFTXVault = await ethers.getContractFactory("NFTXVaultUpgradeable"); + const nftxVault = await NFTXVault.deploy(); + await nftxVault.deployed(); + + // Set up a test ERC20 token to simulate WETH + const WETH = await ethers.getContractFactory("WETH"); + weth = await WETH.deploy(); + await weth.deployed(); + + const FeeDistributor = await ethers.getContractFactory( + "NFTXSimpleFeeDistributor" + ); + const feeDistrib = await upgrades.deployProxy( + FeeDistributor, + [lpStaking.address, weth.address], + { + initializer: "__SimpleFeeDistributor__init__", + unsafeAllow: 'delegatecall' + } + ); + await feeDistrib.deployed(); + + const Nftx = await ethers.getContractFactory("NFTXVaultFactoryUpgradeable"); + nftx = await upgrades.deployProxy( + Nftx, + [nftxVault.address, feeDistrib.address], + { + initializer: "__NFTXVaultFactory_init", + unsafeAllow: 'delegatecall' + } + ); + await nftx.deployed(); + + const InventoryStaking = await ethers.getContractFactory("NFTXInventoryStaking"); + inventoryStaking = await upgrades.deployProxy(InventoryStaking, [nftx.address], { + initializer: "__NFTXInventoryStaking_init", + unsafeAllow: 'delegatecall' + }); + await inventoryStaking.deployed(); + + // Connect our contracts to the NFTX Vault Factory + await feeDistrib.connect(deployer).setNFTXVaultFactory(nftx.address); + // await lpStaking.connect(deployer).setNFTXVaultFactory(nftx.address); +}