From 93be32b8ddaccae9d1a8138b901eaf45f1f7853b Mon Sep 17 00:00:00 2001 From: Tom Wade Date: Thu, 1 Sep 2022 14:01:10 +0100 Subject: [PATCH 01/10] Add Yield Staking Zap --- contracts/solidity/NFTXYieldStakingZap.sol | 215 +++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 contracts/solidity/NFTXYieldStakingZap.sol diff --git a/contracts/solidity/NFTXYieldStakingZap.sol b/contracts/solidity/NFTXYieldStakingZap.sol new file mode 100644 index 00000000..c15ee489 --- /dev/null +++ b/contracts/solidity/NFTXYieldStakingZap.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./interface/INFTXInventoryStaking.sol"; +import "./interface/INFTXLPStaking.sol"; +import "./interface/INFTXVaultFactory.sol"; +import "./interface/IRewardDistributionToken.sol"; +import "./token/XTokenUpgradeable.sol"; +import "./util/OwnableUpgradeable.sol"; +import "./util/ReentrancyGuardUpgradeable.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 + * + * @author Twade + */ + +contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { + + using SafeERC20Upgradeable for IERC20Upgradeable; + + /// @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. + */ + + constructor(address _nftxFactory, address _inventoryStaking, address _lpStaking, address _weth) { + // Set our staking contracts + inventoryStaking = INFTXInventoryStaking(_inventoryStaking); + lpStaking = INFTXLPStaking(_lpStaking); + + // Set our NFTX factory contract + nftxFactory = INFTXVaultFactory(_nftxFactory); + + // Set our chain's WETH contract + WETH = IWETH(_weth); + } + + + /** + * @notice .. + */ + + function buyAndStakeInventory( + uint256 vaultId, + address payable swapTarget, + bytes calldata swapCallData + ) external payable nonReentrant { + // Ensure we have tx value + require(msg.value > 0); + + // 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); + + // Convert WETH to vault token + uint256 vaultTokenAmount = _fillQuote(baseToken, swapTarget, swapCallData); + + // Get the starting balance of xTokens + XTokenUpgradeable xToken = XTokenUpgradeable(inventoryStaking.xTokenAddr(baseToken)); + uint256 xTokenBalance = xToken.balanceOf(address(this)); + + // Deposit vault token and receive xToken into the zap + inventoryStaking.deposit(vaultId, vaultTokenAmount); + + // transfer xToken to caller + xToken.transferFrom( + address(this), + msg.sender, + xToken.balanceOf(address(this)) - xTokenBalance + ); + } + + + /** + * @notice .. + */ + + function buyAndStakeLiquidity( + uint256 vaultId, + address payable swapTarget, + bytes calldata swapCallData + ) external payable nonReentrant { + // Ensure we have tx value + require(msg.value > 0); + + // 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); + + // Convert WETH to vault token + uint256 vaultTokenAmount = _fillQuote(baseToken, swapTarget, swapCallData); + + // Check that we have a liquidity pool for our vault + address stakingToken = lpStaking.stakingToken(baseToken); + require(stakingToken != address(0), "LPStaking: Nonexistent pool"); + + // Get the starting balance of xTokens + IERC20Upgradeable xToken = IERC20Upgradeable(lpStaking.rewardDistributionToken(vaultId)); + uint256 xTokenBalance = xToken.balanceOf(address(this)); + + // Deposit vault token and receive xToken into the zap + lpStaking.deposit(vaultId, vaultTokenAmount); + + // transfer xToken to caller + xToken.transferFrom( + address(this), + msg.sender, + xToken.balanceOf(address(this)) - xTokenBalance + ); + } + + + /** + * @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 swapTarget The `to` field from the API response + * @param swapCallData The `data` field from the API response + */ + + function _fillQuote( + address buyToken, + address payable swapTarget, + 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)); + + // 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. + * + * @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 contract to receive any assets. + */ + + receive() external payable { + // + } + +} From 6ace6d67714b5932e15c599d3b832fb1e15d69b6 Mon Sep 17 00:00:00 2001 From: Tom Wade Date: Fri, 2 Sep 2022 15:02:48 +0100 Subject: [PATCH 02/10] Add test and deploy script --- contracts/solidity/NFTXInventoryStaking.sol | 1 - contracts/solidity/NFTXYieldStakingZap.sol | 73 +++-- scripts/deploy-yield-staking-zap.js | 42 +++ test/yield-staking-zap.js | 291 ++++++++++++++++++++ 4 files changed, 376 insertions(+), 31 deletions(-) create mode 100644 scripts/deploy-yield-staking-zap.js create mode 100644 test/yield-staking-zap.js diff --git a/contracts/solidity/NFTXInventoryStaking.sol b/contracts/solidity/NFTXInventoryStaking.sol index bef67a70..d529d9a0 100644 --- a/contracts/solidity/NFTXInventoryStaking.sol +++ b/contracts/solidity/NFTXInventoryStaking.sol @@ -86,7 +86,6 @@ 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 a zap"); require(nftxVaultFactory.excludedFromFees(msg.sender), "Not fee excluded"); (, , uint256 xTokensMinted) = _timelockMintFor(vaultId, to, amount, timelockLength); diff --git a/contracts/solidity/NFTXYieldStakingZap.sol b/contracts/solidity/NFTXYieldStakingZap.sol index c15ee489..fa570dee 100644 --- a/contracts/solidity/NFTXYieldStakingZap.sol +++ b/contracts/solidity/NFTXYieldStakingZap.sol @@ -75,7 +75,10 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { bytes calldata swapCallData ) external payable nonReentrant { // Ensure we have tx value - require(msg.value > 0); + require(msg.value > 0, 'Invalid value 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) { @@ -84,23 +87,24 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { // 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, swapTarget, swapCallData); - // Get the starting balance of xTokens - XTokenUpgradeable xToken = XTokenUpgradeable(inventoryStaking.xTokenAddr(baseToken)); - uint256 xTokenBalance = xToken.balanceOf(address(this)); - - // Deposit vault token and receive xToken into the zap - inventoryStaking.deposit(vaultId, vaultTokenAmount); - - // transfer xToken to caller - xToken.transferFrom( - address(this), - msg.sender, - xToken.balanceOf(address(this)) - xTokenBalance - ); + // 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"); + } } @@ -114,7 +118,10 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { bytes calldata swapCallData ) external payable nonReentrant { // Ensure we have tx value - require(msg.value > 0); + require(msg.value > 0, 'Invalid value 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) { @@ -123,27 +130,28 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { // 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, swapTarget, swapCallData); - // Check that we have a liquidity pool for our vault - address stakingToken = lpStaking.stakingToken(baseToken); - require(stakingToken != address(0), "LPStaking: Nonexistent pool"); - - // Get the starting balance of xTokens - IERC20Upgradeable xToken = IERC20Upgradeable(lpStaking.rewardDistributionToken(vaultId)); - uint256 xTokenBalance = xToken.balanceOf(address(this)); + // Allow our filled base token to be handled by our inventory stake + require( + IERC20Upgradeable(baseToken).approve(address(lpStaking), vaultTokenAmount), + 'Unable to approve contract' + ); - // Deposit vault token and receive xToken into the zap - lpStaking.deposit(vaultId, vaultTokenAmount); + // Deposit vault token and send our xtoken to the sender + lpStaking.timelockDepositFor(vaultId, msg.sender, vaultTokenAmount, 2); - // transfer xToken to caller - xToken.transferFrom( - address(this), - msg.sender, - xToken.balanceOf(address(this)) - xTokenBalance - ); + // 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"); + } } @@ -179,6 +187,11 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { // 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'); diff --git a/scripts/deploy-yield-staking-zap.js b/scripts/deploy-yield-staking-zap.js new file mode 100644 index 00000000..7def7a50 --- /dev/null +++ b/scripts/deploy-yield-staking-zap.js @@ -0,0 +1,42 @@ +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 + "0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6", // WETH + ); + // Mainnet + // const zap = await YieldStakingZap.deploy( + // "0xBE86f647b167567525cCAAfcd6f881F1Ee558216", // Vault Factory + // "0x5fAD0e4cc9925365b9B0bbEc9e0C3536c0B1a5C7", // Inventory Staking + // "0x688c3E4658B5367da06fd629E41879beaB538E37", // LP Staking + // "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH + // ); + 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..4088f009 --- /dev/null +++ b/test/yield-staking-zap.js @@ -0,0 +1,291 @@ +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 our NFTX Marketplace 0x Zap + const YieldStakingZap = await ethers.getContractFactory('NFTXYieldStakingZap') + yieldStakingZap = await YieldStakingZap.deploy(nftx.address, inventoryStaking.address, lpStaking.address, weth.address); + await yieldStakingZap.deployed() + + // Allow our yield staking zap to exclude fees + await nftx.setZapContract(yieldStakingZap.address); + 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(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).buyAndStakeLiquidity( + 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).buyAndStakeLiquidity( + await vault.vaultId(), + swapTarget, + swapCallData, + {value: ethers.utils.parseEther("1")} + ); + + // Our sender should now have 2 xToken + let vaultXTokenAddress = await lpStaking.newRewardDistributionToken(await vault.vaultId()); + console.log(vaultXTokenAddress); + + const xToken = await ethers.getContractAt("IERC20Upgradeable", vaultXTokenAddress); + console.log(xToken); + + 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( + 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); + }); + + }); + +}); + + +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]) +} From f31ec58c880dd9e0da425a7663139d69e2ec5431 Mon Sep 17 00:00:00 2001 From: Tom Wade Date: Wed, 7 Sep 2022 10:10:38 +0100 Subject: [PATCH 03/10] Update tests --- contracts/solidity/NFTXYieldStakingZap.sol | 75 +++++++++++++++++--- contracts/solidity/testing/MockSushiSwap.sol | 24 +++++++ test/yield-staking-zap.js | 50 +++++++++++-- 3 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 contracts/solidity/testing/MockSushiSwap.sol diff --git a/contracts/solidity/NFTXYieldStakingZap.sol b/contracts/solidity/NFTXYieldStakingZap.sol index fa570dee..017f2212 100644 --- a/contracts/solidity/NFTXYieldStakingZap.sol +++ b/contracts/solidity/NFTXYieldStakingZap.sol @@ -6,6 +6,7 @@ import "./interface/INFTXInventoryStaking.sol"; import "./interface/INFTXLPStaking.sol"; import "./interface/INFTXVaultFactory.sol"; import "./interface/IRewardDistributionToken.sol"; +import "./interface/IUniswapV2Router01.sol"; import "./token/XTokenUpgradeable.sol"; import "./util/OwnableUpgradeable.sol"; import "./util/ReentrancyGuardUpgradeable.sol"; @@ -34,6 +35,9 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { using SafeERC20Upgradeable for IERC20Upgradeable; + /// @notice .. + IUniswapV2Router01 public immutable sushiRouter; + /// @notice An interface for the WETH contract IWETH public immutable WETH; @@ -52,7 +56,13 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { * @notice Initialises our zap. */ - constructor(address _nftxFactory, address _inventoryStaking, address _lpStaking, address _weth) { + constructor( + address _nftxFactory, + address _inventoryStaking, + address _lpStaking, + address _sushiRouter, + address _weth + ) { // Set our staking contracts inventoryStaking = INFTXInventoryStaking(_inventoryStaking); lpStaking = INFTXLPStaking(_lpStaking); @@ -60,6 +70,9 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { // 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); } @@ -113,12 +126,22 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { */ function buyAndStakeLiquidity( + // Base data uint256 vaultId, + + // 0x integration address payable swapTarget, - bytes calldata swapCallData + 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)); @@ -135,14 +158,30 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { // Convert WETH to vault token uint256 vaultTokenAmount = _fillQuote(baseToken, swapTarget, swapCallData); - // Allow our filled base token to be handled by our inventory stake - require( - IERC20Upgradeable(baseToken).approve(address(lpStaking), vaultTokenAmount), - 'Unable to approve contract' + // 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 amountEth, uint256 liquidity) = sushiRouter.addLiquidity( + baseToken, + address(WETH), + minTokenIn, + wethIn, + minTokenIn, + minWethIn, + address(this), + block.timestamp ); - // Deposit vault token and send our xtoken to the sender - lpStaking.timelockDepositFor(vaultId, msg.sender, vaultTokenAmount, 2); + // 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 = minTokenIn - 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; @@ -155,6 +194,26 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { } + // calculates the CREATE2 address for a pair without making any external calls + 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 + ))))); + } + + + // 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. * diff --git a/contracts/solidity/testing/MockSushiSwap.sol b/contracts/solidity/testing/MockSushiSwap.sol new file mode 100644 index 00000000..7a96009c --- /dev/null +++ b/contracts/solidity/testing/MockSushiSwap.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./interface/IUniswapV2Router01.sol"; + + +contract MockSushiSwap is IUniswapV2Router01 { + + 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/test/yield-staking-zap.js b/test/yield-staking-zap.js index 4088f009..a1793a33 100644 --- a/test/yield-staking-zap.js +++ b/test/yield-staking-zap.js @@ -62,9 +62,20 @@ describe('Yield Staking Zap', function () { 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, weth.address); + yieldStakingZap = await YieldStakingZap.deploy( + nftx.address, + inventoryStaking.address, + lpStaking.address, + mockSushiSwap.address, // Sushi router + weth.address, + ); await yieldStakingZap.deployed() // Allow our yield staking zap to exclude fees @@ -163,7 +174,17 @@ describe('Yield Staking Zap', function () { it("Should ensure a tx value is sent", async function () { await expect( - yieldStakingZap.connect(alice).buyAndStakeLiquidity(await vault.vaultId(), swapTarget, swapCallData) + 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'); }); @@ -172,9 +193,16 @@ describe('Yield Staking Zap', function () { // 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; @@ -182,19 +210,22 @@ describe('Yield Staking Zap', function () { 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()); - console.log(vaultXTokenAddress); - const xToken = await ethers.getContractAt("IERC20Upgradeable", vaultXTokenAddress); - console.log(xToken); - expect(await xToken.balanceOf(alice.address)).to.equal('2000000000000000000'); }); @@ -202,9 +233,16 @@ describe('Yield Staking Zap', 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")} ); From 79a837bb832a565c2604ebaefab9961d450b31fd Mon Sep 17 00:00:00 2001 From: Tom Wade Date: Tue, 13 Sep 2022 19:56:05 +0100 Subject: [PATCH 04/10] Staking updates --- contracts/solidity/NFTXYieldStakingZap.sol | 49 +++++++++++++++----- contracts/solidity/testing/MockSushiSwap.sol | 4 +- scripts/deploy-yield-staking-zap.js | 2 + 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/contracts/solidity/NFTXYieldStakingZap.sol b/contracts/solidity/NFTXYieldStakingZap.sol index 017f2212..901853ec 100644 --- a/contracts/solidity/NFTXYieldStakingZap.sol +++ b/contracts/solidity/NFTXYieldStakingZap.sol @@ -26,7 +26,8 @@ interface IWETH { /** - * @notice + * @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 */ @@ -35,7 +36,7 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { using SafeERC20Upgradeable for IERC20Upgradeable; - /// @notice .. + /// @notice Holds the mapping of our sushi router IUniswapV2Router01 public immutable sushiRouter; /// @notice An interface for the WETH contract @@ -48,12 +49,14 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { /// @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 + /// @notice A mapping of NFTX Vault IDs to their address corresponding + /// vault contract address mapping(uint256 => address) public nftxVaultAddresses; /** - * @notice Initialises our zap. + * @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( @@ -79,7 +82,13 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { /** - * @notice .. + * @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 swapTarget The `to` field from the 0x API response + * @param swapCallData The `data` field from the 0x API response */ function buyAndStakeInventory( @@ -122,7 +131,16 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { /** - * @notice .. + * @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 swapTarget The `to` field from the 0x API response + * @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( @@ -161,10 +179,10 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { // 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 amountEth, uint256 liquidity) = sushiRouter.addLiquidity( + (uint256 amountToken, , uint256 liquidity) = sushiRouter.addLiquidity( baseToken, address(WETH), - minTokenIn, + vaultTokenAmount, wethIn, minTokenIn, minWethIn, @@ -178,7 +196,7 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { lpStaking.timelockDepositFor(vaultId, msg.sender, liquidity, 48 hours); // Return any token dust to the caller - uint256 remainingTokens = minTokenIn - amountToken; + uint256 remainingTokens = vaultTokenAmount - amountToken; if (remainingTokens != 0) { IERC20Upgradeable(baseToken).transfer(msg.sender, remainingTokens); } @@ -194,7 +212,12 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { } - // calculates the CREATE2 address for a pair without making any external calls + /** + * @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( @@ -206,7 +229,11 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { } - // returns sorted token addresses, used to handle return values from pairs sorted in this order + /** + * @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); diff --git a/contracts/solidity/testing/MockSushiSwap.sol b/contracts/solidity/testing/MockSushiSwap.sol index 7a96009c..6d888c44 100644 --- a/contracts/solidity/testing/MockSushiSwap.sol +++ b/contracts/solidity/testing/MockSushiSwap.sol @@ -2,10 +2,8 @@ pragma solidity ^0.8.0; -import "./interface/IUniswapV2Router01.sol"; - -contract MockSushiSwap is IUniswapV2Router01 { +contract MockSushiSwap { function addLiquidity( address tokenA, diff --git a/scripts/deploy-yield-staking-zap.js b/scripts/deploy-yield-staking-zap.js index 7def7a50..e4ab826f 100644 --- a/scripts/deploy-yield-staking-zap.js +++ b/scripts/deploy-yield-staking-zap.js @@ -17,6 +17,7 @@ async function main() { "0xe01Cf5099e700c282A56E815ABd0C4948298Afae", // Vault Factory "0xe5AB394e284d095aDacff8A0fb486cb5a24b0b7a", // Inventory Staking "0x33b381E2e0c4adC1dbd388888e9A29079e5b6702", // LP Staking + "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", // SushiSwapRouter "0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6", // WETH ); // Mainnet @@ -24,6 +25,7 @@ async function main() { // "0xBE86f647b167567525cCAAfcd6f881F1Ee558216", // Vault Factory // "0x5fAD0e4cc9925365b9B0bbEc9e0C3536c0B1a5C7", // Inventory Staking // "0x688c3E4658B5367da06fd629E41879beaB538E37", // LP Staking + // "0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F", // SushiSwapRouter // "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH // ); await zap.deployed(); From 02d77520fcf3ed9681343ab17c998181619f6fe3 Mon Sep 17 00:00:00 2001 From: Tom Wade Date: Tue, 13 Sep 2022 20:47:04 +0100 Subject: [PATCH 05/10] Updates --- contracts/solidity/NFTXYieldStakingZap.sol | 42 ++++++++++++++++------ 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/contracts/solidity/NFTXYieldStakingZap.sol b/contracts/solidity/NFTXYieldStakingZap.sol index 901853ec..7d87dd01 100644 --- a/contracts/solidity/NFTXYieldStakingZap.sol +++ b/contracts/solidity/NFTXYieldStakingZap.sol @@ -5,9 +5,7 @@ pragma solidity ^0.8.0; import "./interface/INFTXInventoryStaking.sol"; import "./interface/INFTXLPStaking.sol"; import "./interface/INFTXVaultFactory.sol"; -import "./interface/IRewardDistributionToken.sol"; import "./interface/IUniswapV2Router01.sol"; -import "./token/XTokenUpgradeable.sol"; import "./util/OwnableUpgradeable.sol"; import "./util/ReentrancyGuardUpgradeable.sol"; import "./util/SafeERC20Upgradeable.sol"; @@ -36,6 +34,12 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { using SafeERC20Upgradeable for IERC20Upgradeable; + /// @notice Allows zap to be paused + bool public paused = false; + + /// @notice Sets our 0x swap target + address payable private swapTarget; + /// @notice Holds the mapping of our sushi router IUniswapV2Router01 public immutable sushiRouter; @@ -65,7 +69,7 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { address _lpStaking, address _sushiRouter, address _weth - ) { + ) Ownable() ReentrancyGuard() { // Set our staking contracts inventoryStaking = INFTXInventoryStaking(_inventoryStaking); lpStaking = INFTXLPStaking(_lpStaking); @@ -87,13 +91,11 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { * against the vault and timelock them. * * @param vaultId The ID of the NFTX vault - * @param swapTarget The `to` field from the 0x API response * @param swapCallData The `data` field from the 0x API response */ function buyAndStakeInventory( uint256 vaultId, - address payable swapTarget, bytes calldata swapCallData ) external payable nonReentrant { // Ensure we have tx value @@ -112,7 +114,7 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { require(baseToken != address(0), 'Invalid vault provided'); // Convert WETH to vault token - uint256 vaultTokenAmount = _fillQuote(baseToken, swapTarget, swapCallData); + 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. @@ -136,7 +138,6 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { * sushi and then the timelocking against our LP token. * * @param vaultId The ID of the NFTX vault - * @param swapTarget The `to` field from the 0x API response * @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 @@ -148,7 +149,6 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { uint256 vaultId, // 0x integration - address payable swapTarget, bytes calldata swapCallData, // Sushiswap integration @@ -174,7 +174,7 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { require(baseToken != address(0), 'Invalid vault provided'); // Convert WETH to vault token - uint256 vaultTokenAmount = _fillQuote(baseToken, swapTarget, swapCallData); + uint256 vaultTokenAmount = _fillQuote(baseToken, swapCallData); // Provide liquidity to sushiswap, using the vault token that we acquired from 0x and // pairing it with the liquidity amount specified in the call. @@ -261,13 +261,11 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { * @notice Swaps ERC20->ERC20 tokens held by this contract using a 0x-API quote. * * @param buyToken The `buyTokenAddress` field from the API response - * @param swapTarget The `to` field from the API response * @param swapCallData The `data` field from the API response */ function _fillQuote( address buyToken, - address payable swapTarget, bytes calldata swapCallData ) internal returns (uint256) { // Track our balance of the buyToken to determine how much we've bought. @@ -303,6 +301,28 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { } + /** + * @notice Allows our zap to be paused to prevent any processing. + * + * @param _paused New pause state + */ + + function pause(bool _paused) external onlyOnwer { + paused = _paused + } + + + /** + * @notice Allows our zap to be paused to prevent any processing. + * + * @param _swapTarget The new swap target to used + */ + + function setSwapTarget(address payable _swapTarget) external onlyOnwer { + swapTarget = _swapTarget + } + + /** * @notice Allows our contract to receive any assets. */ From f5cfa4809981ba07e0b0f7c2df0f50561b7e9070 Mon Sep 17 00:00:00 2001 From: Tom Wade Date: Wed, 14 Sep 2022 11:25:31 +0100 Subject: [PATCH 06/10] Fix typos --- contracts/solidity/NFTXYieldStakingZap.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/solidity/NFTXYieldStakingZap.sol b/contracts/solidity/NFTXYieldStakingZap.sol index 7d87dd01..83dfee8b 100644 --- a/contracts/solidity/NFTXYieldStakingZap.sol +++ b/contracts/solidity/NFTXYieldStakingZap.sol @@ -6,8 +6,8 @@ import "./interface/INFTXInventoryStaking.sol"; import "./interface/INFTXLPStaking.sol"; import "./interface/INFTXVaultFactory.sol"; import "./interface/IUniswapV2Router01.sol"; -import "./util/OwnableUpgradeable.sol"; -import "./util/ReentrancyGuardUpgradeable.sol"; +import "./util/Ownable.sol"; +import "./util/ReentrancyGuard.sol"; import "./util/SafeERC20Upgradeable.sol"; @@ -30,7 +30,7 @@ interface IWETH { * @author Twade */ -contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { +contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { using SafeERC20Upgradeable for IERC20Upgradeable; @@ -307,19 +307,19 @@ contract NFTXYieldStakingZap is OwnableUpgradeable, ReentrancyGuardUpgradeable { * @param _paused New pause state */ - function pause(bool _paused) external onlyOnwer { - paused = _paused + function pause(bool _paused) external onlyOwner { + paused = _paused; } /** - * @notice Allows our zap to be paused to prevent any processing. + * @notice Allows our zap to set the swap target for 0x. * * @param _swapTarget The new swap target to used */ - function setSwapTarget(address payable _swapTarget) external onlyOnwer { - swapTarget = _swapTarget + function setSwapTarget(address payable _swapTarget) external onlyOwner { + swapTarget = _swapTarget; } From 456c6d8c13bb761556dc88a3348ac078d5be1d43 Mon Sep 17 00:00:00 2001 From: Tom Wade Date: Tue, 20 Sep 2022 12:14:53 +0100 Subject: [PATCH 07/10] Update to use minTokenIn --- contracts/solidity/NFTXYieldStakingZap.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/solidity/NFTXYieldStakingZap.sol b/contracts/solidity/NFTXYieldStakingZap.sol index 83dfee8b..65047674 100644 --- a/contracts/solidity/NFTXYieldStakingZap.sol +++ b/contracts/solidity/NFTXYieldStakingZap.sol @@ -175,6 +175,7 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { // 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. @@ -182,7 +183,7 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { (uint256 amountToken, , uint256 liquidity) = sushiRouter.addLiquidity( baseToken, address(WETH), - vaultTokenAmount, + minTokenIn, wethIn, minTokenIn, minWethIn, @@ -218,6 +219,7 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { * * @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( From edd0dcfc83971f2c204ff13c182b361563edd28b Mon Sep 17 00:00:00 2001 From: Tom Wade Date: Fri, 23 Sep 2022 15:47:53 +0100 Subject: [PATCH 08/10] Make swap target immutable on deployment --- contracts/solidity/NFTXYieldStakingZap.sol | 34 +++++++++------------- scripts/deploy-yield-staking-zap.js | 2 ++ 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/contracts/solidity/NFTXYieldStakingZap.sol b/contracts/solidity/NFTXYieldStakingZap.sol index 65047674..a4a16ee8 100644 --- a/contracts/solidity/NFTXYieldStakingZap.sol +++ b/contracts/solidity/NFTXYieldStakingZap.sol @@ -38,7 +38,7 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { bool public paused = false; /// @notice Sets our 0x swap target - address payable private swapTarget; + address payable private immutable swapTarget; /// @notice Holds the mapping of our sushi router IUniswapV2Router01 public immutable sushiRouter; @@ -68,7 +68,8 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { address _inventoryStaking, address _lpStaking, address _sushiRouter, - address _weth + address _weth, + address payable _swapTarget ) Ownable() ReentrancyGuard() { // Set our staking contracts inventoryStaking = INFTXInventoryStaking(_inventoryStaking); @@ -82,6 +83,9 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { // Set our chain's WETH contract WETH = IWETH(_weth); + + // Set our 0x Swap Target + swapTarget = _swapTarget; } @@ -101,6 +105,10 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { // 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)); @@ -109,10 +117,6 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { 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); @@ -288,7 +292,8 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { /** - * @notice Maps a cached NFTX vault address against a vault ID. + * @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 */ @@ -315,22 +320,11 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { /** - * @notice Allows our zap to set the swap target for 0x. - * - * @param _swapTarget The new swap target to used - */ - - function setSwapTarget(address payable _swapTarget) external onlyOwner { - swapTarget = _swapTarget; - } - - - /** - * @notice Allows our contract to receive any assets. + * @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/scripts/deploy-yield-staking-zap.js b/scripts/deploy-yield-staking-zap.js index e4ab826f..3a3ba578 100644 --- a/scripts/deploy-yield-staking-zap.js +++ b/scripts/deploy-yield-staking-zap.js @@ -19,6 +19,7 @@ async function main() { "0x33b381E2e0c4adC1dbd388888e9A29079e5b6702", // LP Staking "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", // SushiSwapRouter "0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6", // WETH + "0xf91bb752490473b8342a3e964e855b9f9a2a668e", // 0x Swap Target ); // Mainnet // const zap = await YieldStakingZap.deploy( @@ -27,6 +28,7 @@ async function main() { // "0x688c3E4658B5367da06fd629E41879beaB538E37", // LP Staking // "0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F", // SushiSwapRouter // "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH + // "0xdef1c0ded9bec7f1a1670819833240f027b25eff", // 0x Swap Target // ); await zap.deployed(); console.log("Yield Staking Zap:", zap.address); From cbc1ecf5c3fb024d254354a397a46e246b2d75c0 Mon Sep 17 00:00:00 2001 From: Tom Wade Date: Fri, 23 Sep 2022 15:48:01 +0100 Subject: [PATCH 09/10] Use a mapping for zap contracts --- contracts/solidity/NFTXInventoryStaking.sol | 1 + contracts/solidity/NFTXVaultFactoryUpgradeable.sol | 9 ++++++--- contracts/solidity/interface/INFTXVaultFactory.sol | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/contracts/solidity/NFTXInventoryStaking.sol b/contracts/solidity/NFTXInventoryStaking.sol index 241e949d..53545067 100644 --- a/contracts/solidity/NFTXInventoryStaking.sol +++ b/contracts/solidity/NFTXInventoryStaking.sol @@ -110,6 +110,7 @@ contract NFTXInventoryStaking is PausableUpgradeable, UpgradeableBeacon, INFTXIn function timelockMintFor(uint256 vaultId, uint256 amount, address to, uint256 timelockLength) external virtual override returns (uint256) { onlyOwnerIfPaused(10); + require(nftxVaultFactory.zapContracts(msg.sender), "Not staking zap"); require(nftxVaultFactory.excludedFromFees(msg.sender), "Not fee excluded"); (, , uint256 xTokensMinted) = _timelockMintFor(vaultId, to, amount, timelockLength); 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/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( From a940d493b466d4d25a3e5bb91f0c459a3f3c018a Mon Sep 17 00:00:00 2001 From: Tom Wade Date: Fri, 30 Sep 2022 11:01:35 +0100 Subject: [PATCH 10/10] Update test constructors --- test/yield-staking-zap.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/yield-staking-zap.js b/test/yield-staking-zap.js index a1793a33..10bf94a8 100644 --- a/test/yield-staking-zap.js +++ b/test/yield-staking-zap.js @@ -75,11 +75,12 @@ describe('Yield Staking Zap', function () { 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); + await nftx.setZapContract(yieldStakingZap.address, true); await nftx.setFeeExclusion(yieldStakingZap.address, true); // Add WETH to the 0x pool