diff --git a/contracts/solidity/NFTXInventoryStaking.sol b/contracts/solidity/NFTXInventoryStaking.sol index e00c7c7e..53545067 100644 --- a/contracts/solidity/NFTXInventoryStaking.sol +++ b/contracts/solidity/NFTXInventoryStaking.sol @@ -110,8 +110,8 @@ contract NFTXInventoryStaking is PausableUpgradeable, UpgradeableBeacon, INFTXIn function timelockMintFor(uint256 vaultId, uint256 amount, address to, uint256 timelockLength) external virtual override returns (uint256) { onlyOwnerIfPaused(10); - require(msg.sender == nftxVaultFactory.zapContract(), "Not staking zap"); - require(nftxVaultFactory.excludedFromFees(msg.sender), "Not fee excluded"); // important for math that staking zap is excluded from fees + require(nftxVaultFactory.zapContracts(msg.sender), "Not staking zap"); + require(nftxVaultFactory.excludedFromFees(msg.sender), "Not fee excluded"); (, , uint256 xTokensMinted) = _timelockMintFor(vaultId, to, amount, timelockLength); emit Deposit(vaultId, amount, xTokensMinted, timelockLength, to); diff --git a/contracts/solidity/NFTXVaultFactoryUpgradeable.sol b/contracts/solidity/NFTXVaultFactoryUpgradeable.sol index da4bcdfa..65b584b1 100644 --- a/contracts/solidity/NFTXVaultFactoryUpgradeable.sol +++ b/contracts/solidity/NFTXVaultFactoryUpgradeable.sol @@ -47,6 +47,9 @@ contract NFTXVaultFactoryUpgradeable is uint64 public override factoryRandomSwapFee; uint64 public override factoryTargetSwapFee; + // v1.0.3 + mapping(address => bool) public override zapContracts; + function __NFTXVaultFactory_init(address _vaultImpl, address _feeDistributor) public override initializer { __Pausable_init(); // We use a beacon proxy so that every child contract follows the same implementation code. @@ -140,9 +143,9 @@ contract NFTXVaultFactoryUpgradeable is feeDistributor = _feeDistributor; } - function setZapContract(address _zapContract) public onlyOwner virtual override { - emit NewZapContract(zapContract, _zapContract); - zapContract = _zapContract; + function setZapContract(address _zapContract, bool _excluded) public onlyOwner virtual override { + emit UpdatedZapContract(_zapContract, _excluded); + zapContracts[_zapContract] = _excluded; } function setFeeExclusion(address _excludedAddr, bool excluded) public onlyOwner virtual override { diff --git a/contracts/solidity/NFTXYieldStakingZap.sol b/contracts/solidity/NFTXYieldStakingZap.sol new file mode 100644 index 00000000..a4a16ee8 --- /dev/null +++ b/contracts/solidity/NFTXYieldStakingZap.sol @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./interface/INFTXInventoryStaking.sol"; +import "./interface/INFTXLPStaking.sol"; +import "./interface/INFTXVaultFactory.sol"; +import "./interface/IUniswapV2Router01.sol"; +import "./util/Ownable.sol"; +import "./util/ReentrancyGuard.sol"; +import "./util/SafeERC20Upgradeable.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 Allows users to buy and stake tokens into either an inventory or liquidity + * pool, handling the steps between buying and staking across 0x and sushi. + * + * @author Twade + */ + +contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { + + using SafeERC20Upgradeable for IERC20Upgradeable; + + /// @notice Allows zap to be paused + bool public paused = false; + + /// @notice Sets our 0x swap target + address payable private immutable swapTarget; + + /// @notice Holds the mapping of our sushi router + IUniswapV2Router01 public immutable sushiRouter; + + /// @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 An interface for the NFTX Vault Factory contract + INFTXVaultFactory public immutable nftxFactory; + + /// @notice A mapping of NFTX Vault IDs to their address corresponding + /// vault contract address + mapping(uint256 => address) public nftxVaultAddresses; + + + /** + * @notice Initialises our zap and sets our internal addresses that will be referenced + * in our contract. This allows for varied addresses based on the network. + */ + + constructor( + address _nftxFactory, + address _inventoryStaking, + address _lpStaking, + address _sushiRouter, + address _weth, + address payable _swapTarget + ) Ownable() ReentrancyGuard() { + // Set our staking contracts + inventoryStaking = INFTXInventoryStaking(_inventoryStaking); + lpStaking = INFTXLPStaking(_lpStaking); + + // Set our NFTX factory contract + nftxFactory = INFTXVaultFactory(_nftxFactory); + + // Set our Sushi Router used for liquidity + sushiRouter = IUniswapV2Router01(_sushiRouter); + + // Set our chain's WETH contract + WETH = IWETH(_weth); + + // Set our 0x Swap Target + swapTarget = _swapTarget; + } + + + /** + * @notice Allows the user to buy and stake tokens against an Inventory. This will + * handle the purchase of the vault tokens against 0x and then generate the xToken + * against the vault and timelock them. + * + * @param vaultId The ID of the NFTX vault + * @param swapCallData The `data` field from the 0x API response + */ + + function buyAndStakeInventory( + uint256 vaultId, + bytes calldata swapCallData + ) external payable nonReentrant { + // Ensure we have tx value + require(msg.value > 0, 'Invalid value provided'); + + // Get our vaults base staking token. This is used to calculate the xToken + address baseToken = _vaultAddress(vaultId); + require(baseToken != address(0), 'Invalid vault provided'); + + // Get our start WETH balance + uint wethBalance = WETH.balanceOf(address(this)); + + // Wrap ETH into WETH for our contract from the sender + if (msg.value > 0) { + WETH.deposit{value: msg.value}(); + } + + // Convert WETH to vault token + uint256 vaultTokenAmount = _fillQuote(baseToken, swapCallData); + + // 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), vaultTokenAmount); + inventoryStaking.timelockMintFor(vaultId, vaultTokenAmount, msg.sender, 2); + + // Return any left of WETH to the user as ETH + uint256 remainingWETH = WETH.balanceOf(address(this)) - wethBalance; + if (remainingWETH > 0) { + // Unwrap our WETH into ETH and transfer it to the recipient + WETH.withdraw(remainingWETH); + (bool success, ) = payable(msg.sender).call{value: remainingWETH}(""); + require(success, "Unable to send unwrapped WETH"); + } + } + + + /** + * @notice Allows the user to buy and stake tokens against a Liquidity pool. This will + * handle the purchase of the vault tokens against 0x, the liquidity pool supplying via + * sushi and then the timelocking against our LP token. + * + * @param vaultId The ID of the NFTX vault + * @param swapCallData The `data` field from the 0x API response + * @param minTokenIn The minimum amount of token to LP + * @param minWethIn The minimum amount of ETH (WETH) to LP + * @param wethIn The amount of ETH (WETH) supplied + */ + + function buyAndStakeLiquidity( + // Base data + uint256 vaultId, + + // 0x integration + bytes calldata swapCallData, + + // Sushiswap integration + uint256 minTokenIn, + uint256 minWethIn, + uint256 wethIn + + ) external payable nonReentrant { + // Ensure we have tx value + require(msg.value > 0, 'Invalid value provided'); + require(msg.value > wethIn, 'Insufficient vault sent for pairing'); + + // Get our start WETH balance + uint wethBalance = WETH.balanceOf(address(this)); + + // Wrap ETH into WETH for our contract from the sender + if (msg.value > 0) { + WETH.deposit{value: msg.value}(); + } + + // Get our vaults base staking token. This is used to calculate the xToken + address baseToken = _vaultAddress(vaultId); + require(baseToken != address(0), 'Invalid vault provided'); + + // Convert WETH to vault token + uint256 vaultTokenAmount = _fillQuote(baseToken, swapCallData); + require(vaultTokenAmount > minTokenIn, 'Insufficient tokens acquired'); + + // Provide liquidity to sushiswap, using the vault token that we acquired from 0x and + // pairing it with the liquidity amount specified in the call. + IERC20Upgradeable(baseToken).safeApprove(address(sushiRouter), minTokenIn); + (uint256 amountToken, , uint256 liquidity) = sushiRouter.addLiquidity( + baseToken, + address(WETH), + minTokenIn, + wethIn, + minTokenIn, + minWethIn, + address(this), + block.timestamp + ); + + // Stake in LP rewards contract + address lpToken = pairFor(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 = vaultTokenAmount - amountToken; + if (remainingTokens != 0) { + IERC20Upgradeable(baseToken).transfer(msg.sender, remainingTokens); + } + + // Return any left of WETH to the user as ETH + uint256 remainingWETH = WETH.balanceOf(address(this)) - wethBalance; + if (remainingWETH > 0) { + // Unwrap our WETH into ETH and transfer it to the recipient + WETH.withdraw(remainingWETH); + (bool success, ) = payable(msg.sender).call{value: remainingWETH}(""); + require(success, "Unable to send unwrapped WETH"); + } + } + + + /** + * @notice Calculates the CREATE2 address for a sushi pair without making any + * external calls. + * + * @return pair Address of our token pair + */ + + function pairFor(address tokenA, address tokenB) internal view returns (address pair) { + (address token0, address token1) = sortTokens(tokenA, tokenB); + pair = address(uint160(uint256(keccak256(abi.encodePacked( + hex'ff', + sushiRouter.factory(), + 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'); + } + + + /** + * @notice Allows our owner to withdraw and tokens in the contract. + * + * @param token The address of the token to be rescued + */ + + function rescue(address token) external onlyOwner { + if (token == address(0)) { + (bool success, ) = payable(msg.sender).call{value: address(this).balance}(""); + require(success, "Address: unable to send value"); + } else { + IERC20Upgradeable(token).safeTransfer(msg.sender, IERC20Upgradeable(token).balanceOf(address(this))); + } + } + + + /** + * @notice Swaps ERC20->ERC20 tokens held by this contract using a 0x-API quote. + * + * @param buyToken The `buyTokenAddress` field from the API response + * @param swapCallData The `data` field from the API response + */ + + function _fillQuote( + address buyToken, + bytes calldata swapCallData + ) internal returns (uint256) { + // Track our balance of the buyToken to determine how much we've bought. + uint256 boughtAmount = IERC20Upgradeable(buyToken).balanceOf(address(this)); + + // Give `swapTarget` an infinite allowance to spend this contract's `sellToken`. + // Note that for some tokens (e.g., USDT, KNC), you must first reset any existing + // allowance to 0 before being able to update it. + require(IERC20Upgradeable(address(WETH)).approve(swapTarget, type(uint256).max), 'Unable to approve contract'); + + // Call the encoded swap function call on the contract at `swapTarget` + (bool success,) = swapTarget.call(swapCallData); + require(success, 'SWAP_CALL_FAILED'); + + // Use our current buyToken balance to determine how much we've bought. + return IERC20Upgradeable(buyToken).balanceOf(address(this)) - boughtAmount; + } + + + /** + * @notice Maps a cached NFTX vault address against a vault ID for gas savings on + * repeat vault address lookups. + * + * @param vaultId The ID of the NFTX vault + */ + + function _vaultAddress(uint256 vaultId) internal returns (address) { + if (nftxVaultAddresses[vaultId] == address(0)) { + nftxVaultAddresses[vaultId] = nftxFactory.vault(vaultId); + } + + require(nftxVaultAddresses[vaultId] != address(0), 'Vault does not exist'); + return nftxVaultAddresses[vaultId]; + } + + + /** + * @notice Allows our zap to be paused to prevent any processing. + * + * @param _paused New pause state + */ + + function pause(bool _paused) external onlyOwner { + paused = _paused; + } + + + /** + * @notice Allows our contract to only receive WETH and reject everything else. + */ + + receive() external payable { + require(msg.sender == address(WETH), "Only WETH"); + } + +} diff --git a/contracts/solidity/interface/INFTXVaultFactory.sol b/contracts/solidity/interface/INFTXVaultFactory.sol index f751aef5..b5462b73 100644 --- a/contracts/solidity/interface/INFTXVaultFactory.sol +++ b/contracts/solidity/interface/INFTXVaultFactory.sol @@ -8,6 +8,7 @@ interface INFTXVaultFactory is IBeacon { // Read functions. function numVaults() external view returns (uint256); function zapContract() external view returns (address); + function zapContracts(address addr) external view returns (bool); function feeDistributor() external view returns (address); function eligibilityManager() external view returns (address); function vault(uint256 vaultId) external view returns (address); @@ -24,6 +25,7 @@ interface INFTXVaultFactory is IBeacon { event NewFeeDistributor(address oldDistributor, address newDistributor); event NewZapContract(address oldZap, address newZap); + event UpdatedZapContract(address zap, bool excluded); event FeeExclusion(address feeExcluded, bool excluded); event NewEligibilityManager(address oldEligManager, address newEligManager); event NewVault(uint256 indexed vaultId, address vaultAddress, address assetAddress); @@ -42,7 +44,7 @@ interface INFTXVaultFactory is IBeacon { ) external returns (uint256); function setFeeDistributor(address _feeDistributor) external; function setEligibilityManager(address _eligibilityManager) external; - function setZapContract(address _zapContract) external; + function setZapContract(address _zapContract, bool _excluded) external; function setFeeExclusion(address _excludedAddr, bool excluded) external; function setFactoryFees( diff --git a/contracts/solidity/testing/MockSushiSwap.sol b/contracts/solidity/testing/MockSushiSwap.sol new file mode 100644 index 00000000..6d888c44 --- /dev/null +++ b/contracts/solidity/testing/MockSushiSwap.sol @@ -0,0 +1,22 @@ +// 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 = 1; + amountB = 1; + liquidity = 1; + } +} diff --git a/scripts/deploy-yield-staking-zap.js b/scripts/deploy-yield-staking-zap.js new file mode 100644 index 00000000..3a3ba578 --- /dev/null +++ b/scripts/deploy-yield-staking-zap.js @@ -0,0 +1,46 @@ +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 YieldStakingZap = await ethers.getContractFactory("NFTXYieldStakingZap"); + // Goerli + const zap = await YieldStakingZap.deploy( + "0xe01Cf5099e700c282A56E815ABd0C4948298Afae", // Vault Factory + "0xe5AB394e284d095aDacff8A0fb486cb5a24b0b7a", // Inventory Staking + "0x33b381E2e0c4adC1dbd388888e9A29079e5b6702", // LP Staking + "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", // SushiSwapRouter + "0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6", // WETH + "0xf91bb752490473b8342a3e964e855b9f9a2a668e", // 0x Swap Target + ); + // Mainnet + // const zap = await YieldStakingZap.deploy( + // "0xBE86f647b167567525cCAAfcd6f881F1Ee558216", // Vault Factory + // "0x5fAD0e4cc9925365b9B0bbEc9e0C3536c0B1a5C7", // Inventory Staking + // "0x688c3E4658B5367da06fd629E41879beaB538E37", // LP Staking + // "0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F", // SushiSwapRouter + // "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH + // "0xdef1c0ded9bec7f1a1670819833240f027b25eff", // 0x Swap Target + // ); + await zap.deployed(); + console.log("Yield Staking 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); + }); diff --git a/test/yield-staking-zap.js b/test/yield-staking-zap.js new file mode 100644 index 00000000..10bf94a8 --- /dev/null +++ b/test/yield-staking-zap.js @@ -0,0 +1,330 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); + +const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; +const BASE = 1e18; +const TOKENS_IN_0X = 500; + + +// Store our internal contract references +let weth, erc721; +let nftx, vault; +let inventoryStaking, lpStaking; +let zap; + +// Store any user addresses we want to interact with +let deployer, alice, bob, users; + +// Set some 0x variables +let swapTarget, swapCallData; + + +/** + * Validates our expected NFTX allocator logic. + */ + +describe('Yield Staking Zap', function () { + + /** + * Confirm that tokens cannot be sent to invalid recipient. + */ + + before(async function () { + // Set up our deployer / owner address + [deployer, alice, bob, carol, ...users] = await ethers.getSigners() + + // Set up a test ERC20 token to simulate WETH + const WETH = await ethers.getContractFactory("WETH"); + weth = await WETH.deploy(); + await weth.deployed(); + + // Fill our WETH contract with Carol's juicy ETH + await carol.sendTransaction({ + to: weth.address, + value: ethers.utils.parseEther("50") + }) + + // Set up a test ERC721 token + const Erc721 = await ethers.getContractFactory("ERC721"); + erc721 = await Erc721.deploy('SpacePoggers', 'POGGERS'); + await erc721.deployed(); + + // Set up our NFTX contracts + await _initialise_nftx_contracts() + + // Deploy our vault's xtoken + await inventoryStaking.deployXTokenForVault(await vault.vaultId()); + + // Set up our 0x mock. Our mock gives us the ability to set a range of parameters + // against the contract that would normally be out of our control. This allows us + // to control expected, external proceedures. + const Mock0xProvider = await ethers.getContractFactory("Mock0xProvider") + mock0xProvider = await Mock0xProvider.deploy() + await mock0xProvider.deployed() + + // Set up a sushiswap mock + const MockSushiSwap = await ethers.getContractFactory("MockSushiSwap") + mockSushiSwap = await MockSushiSwap.deploy() + await mockSushiSwap.deployed() + + // Set up our NFTX Marketplace 0x Zap + const YieldStakingZap = await ethers.getContractFactory('NFTXYieldStakingZap') + yieldStakingZap = await YieldStakingZap.deploy( + nftx.address, + inventoryStaking.address, + lpStaking.address, + mockSushiSwap.address, // Sushi router + weth.address, + mock0xProvider.address // 0x swap target + ); + await yieldStakingZap.deployed() + + // Allow our yield staking zap to exclude fees + await nftx.setZapContract(yieldStakingZap.address, true); + await nftx.setFeeExclusion(yieldStakingZap.address, true); + + // Add WETH to the 0x pool + await weth.mint(mock0xProvider.address, String(BASE * TOKENS_IN_0X)) + expect(await weth.balanceOf(mock0xProvider.address)).to.equal(String(BASE * TOKENS_IN_0X)) + + // Give Alice a bunch of tokens to throw into the vault (20 tokens) + for (let i = 0; i < 20; ++i) { + await erc721.publicMint(alice.address, i); + await erc721.connect(alice).approve(vault.address, i); + } + + // Add vault token to our 0x mock + // Send the tokens to our 0x contract from Alice + await vault.connect(alice).mintTo( + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + mock0xProvider.address + ); + + // Set our 0x data + swapTarget = mock0xProvider.address; + swapCallData = _create_call_data(weth.address, vault.address); + }); + + + describe('buyAndStakeInventory', async function () { + + before(async function () { + // Set the 0x payout token and amount + await mock0xProvider.setPayInAmount(String(BASE)); // 1 + await mock0xProvider.setPayOutAmount(String(BASE * 2)); // 2 + }); + + it("Should ensure a tx value is sent", async function () { + await expect( + yieldStakingZap.connect(alice).buyAndStakeInventory(await vault.vaultId(), swapTarget, swapCallData) + ).to.be.revertedWith('Invalid value provided'); + }); + + it("Should require a valid vault ID", async function () { + // This will be reverted with a panic code, as the internal NFTX vault mapping does + // not check that the ID exists before referencing it. + await expect( + yieldStakingZap.connect(alice).buyAndStakeInventory( + 420, + swapTarget, + swapCallData, + {value: ethers.utils.parseEther("1")} + ) + ).to.be.reverted; + }); + + it("Should allow for ETH to be staked into xToken", async function () { + await yieldStakingZap.connect(alice).buyAndStakeInventory( + await vault.vaultId(), + swapTarget, + swapCallData, + {value: ethers.utils.parseEther("1")} + ); + + // Our sender should now have 2 xToken + let vaultXTokenAddress = await inventoryStaking.vaultXToken(await vault.vaultId()); + const xToken = await ethers.getContractAt("XTokenUpgradeable", vaultXTokenAddress); + expect(await xToken.balanceOf(alice.address)).to.equal('2000000000000000000'); + }); + + it("Should refund any ETH that remains after transaction", async function () { + let startBalance = await ethers.provider.getBalance(bob.address); + + await yieldStakingZap.connect(bob).buyAndStakeInventory( + await vault.vaultId(), + swapTarget, + swapCallData, + {value: ethers.utils.parseEther("2")} + ); + + // User should have had 1 ETH refunded (we use not equal to allow for dust loss) + expect(await ethers.provider.getBalance(bob.address)).to.not.equal(0); + }); + + }); + + + describe('buyAndStakeLiquidity', async function () { + + before(async function () { + // Set the 0x payout token and amount + await mock0xProvider.setPayInAmount(String(BASE)); // 1 + await mock0xProvider.setPayOutAmount(String(BASE * 2)); // 2 + }); + + it("Should ensure a tx value is sent", async function () { + await expect( + yieldStakingZap.connect(alice).buyAndStakeLiquidity( + // Base + await vault.vaultId(), + // 0x + swapTarget, + swapCallData, + // Sushiswap + ethers.utils.parseEther('2'), // minTokenIn + ethers.utils.parseEther('0.5'), // minWethIn + ethers.utils.parseEther('0.5'), + ) + ).to.be.revertedWith('Invalid value provided'); + }); + + it("Should require a valid vault ID", async function () { + // This will be reverted with a panic code, as the internal NFTX vault mapping does + // not check that the ID exists before referencing it. + await expect( + yieldStakingZap.connect(alice).buyAndStakeLiquidity( + // Base + 420, + // 0x + swapTarget, + swapCallData, + // Sushiswap + ethers.utils.parseEther('2'), // minTokenIn + ethers.utils.parseEther('0.5'), // minWethIn + ethers.utils.parseEther('0.5'), + // TX value + {value: ethers.utils.parseEther("1")} + ) + ).to.be.reverted; + }); + + it("Should allow for ETH to be staked into xToken", async function () { + await yieldStakingZap.connect(alice).buyAndStakeLiquidity( + // Base + await vault.vaultId(), + // 0x + swapTarget, + swapCallData, + // Sushiswap + ethers.utils.parseEther('2'), // minTokenIn + ethers.utils.parseEther('0.5'), // minWethIn + ethers.utils.parseEther('0.5'), + // TX value + {value: ethers.utils.parseEther("1")} + ); + + // Our sender should now have 2 xToken + let vaultXTokenAddress = await lpStaking.newRewardDistributionToken(await vault.vaultId()); + const xToken = await ethers.getContractAt("IERC20Upgradeable", vaultXTokenAddress); + expect(await xToken.balanceOf(alice.address)).to.equal('2000000000000000000'); + }); + + it("Should refund any ETH that remains after transaction", async function () { + let startBalance = await ethers.provider.getBalance(bob.address); + + await yieldStakingZap.connect(alice).buyAndStakeLiquidity( + // Base + await vault.vaultId(), + // 0x + swapTarget, + swapCallData, + // Sushiswap + ethers.utils.parseEther('2'), // minTokenIn + ethers.utils.parseEther('0.5'), // minWethIn + ethers.utils.parseEther('0.5'), + // TX value + {value: ethers.utils.parseEther("2")} + ); + + // User should have had 1 ETH refunded (we use not equal to allow for dust loss) + expect(await ethers.provider.getBalance(bob.address)).to.not.equal(0); + }); + + }); + +}); + + +async function _initialise_nftx_contracts() { + const StakingProvider = await ethers.getContractFactory("MockStakingProvider"); + const provider = await StakingProvider.deploy(); + await provider.deployed(); + + const LPStaking = await ethers.getContractFactory("NFTXLPStaking"); + lpStaking = await upgrades.deployProxy(LPStaking, [provider.address], { + initializer: "__NFTXLPStaking__init", + unsafeAllow: 'delegatecall' + }); + await lpStaking.deployed(); + + const NFTXVault = await ethers.getContractFactory("NFTXVaultUpgradeable"); + const nftxVault = await NFTXVault.deploy(); + await nftxVault.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); + + // Register our 721 NFTX vault that we will use for testing + let response = await nftx.createVault( + 'Space Poggers', // name + 'POGGERS', // symbol + erc721.address, // _assetAddress + false, // is1155 + true // allowAllItems + ); + + let receipt = await response.wait(0); + let vaultId = receipt.events.find((elem) => elem.event === "NewVault").args[0].toString(); + let vaultAddr = await nftx.vault(vaultId) + let vaultArtifact = await artifacts.readArtifact("NFTXVaultUpgradeable"); + + vault = new ethers.Contract(vaultAddr, vaultArtifact. abi,ethers.provider); +} + +function _create_call_data(tokenIn, tokenOut) { + let parsedABI = new ethers.utils.Interface(["function transfer(address spender, address tokenIn, address tokenOut) payable"]); + return parsedABI.encodeFunctionData('transfer', [yieldStakingZap.address, tokenIn, tokenOut]) +}