diff --git a/contracts/solidity/NFTXMarketplace0xZap.sol b/contracts/solidity/NFTXMarketplace0xZap.sol index 549814d..13054ea 100644 --- a/contracts/solidity/NFTXMarketplace0xZap.sol +++ b/contracts/solidity/NFTXMarketplace0xZap.sol @@ -4,24 +4,18 @@ pragma solidity ^0.8.0; import "./interface/INFTXVault.sol"; import "./interface/INFTXVaultFactory.sol"; -import "./token/IERC1155Upgradeable.sol"; -import "./token/ERC721HolderUpgradeable.sol"; -import "./token/ERC1155HolderUpgradeable.sol"; +import "./testing/IERC1155.sol"; +import "./testing/ERC721Holder.sol"; +import "./testing/ERC1155Holder.sol"; import "./util/Ownable.sol"; import "./util/ReentrancyGuard.sol"; -import "./util/SafeERC20Upgradeable.sol"; +import "./util/SafeERC20.sol"; /** * @notice A partial ERC20 interface. */ -interface IERC20 { - function balanceOf(address owner) external view returns (uint256); - function approve(address spender, uint256 amount) external returns (bool); - function transfer(address to, uint256 amount) external returns (bool); -} - /** * @notice A partial WETH interface. @@ -43,9 +37,9 @@ interface IWETH { * @author Twade */ -contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeable, ERC1155HolderUpgradeable { +contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721Holder, ERC1155Holder { - using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeERC20 for IERC20; /// @notice Allows zap to be paused bool public paused = false; @@ -111,15 +105,13 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab * * @param vaultId The ID of the NFTX vault * @param ids An array of token IDs to be minted - * @param spender The `allowanceTarget` field from the API response * @param swapCallData The `data` field from the API response - * @param to The recipient of the WETH from the tx + * @param to The recipient of ETH from the tx */ function mintAndSell721( uint256 vaultId, uint256[] calldata ids, - address spender, bytes calldata swapCallData, address payable to ) external nonReentrant onlyOwnerIfPaused { @@ -135,11 +127,14 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab // Sell our vault token for WETH uint256 amount = _fillQuote(vault, address(WETH), swapCallData); + // convert WETH to ETH and send to `to` + _transferAllWETH(to); + // Emit our sale event emit Sell(ids.length, amount, to); - // Transfer dust back to the spender - _transferDust(spender, vault); + // Transfer dust back to the sender + _transferDust(msg.sender, vault, false); } @@ -151,16 +146,14 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab * @param vaultId The ID of the NFTX vault * @param idsIn An array of random token IDs to be minted * @param specificIds An array of any specific token IDs to be minted - * @param spender The `allowanceTarget` field from the API response * @param swapCallData The `data` field from the API response - * @param to The recipient of the WETH from the tx + * @param to The recipient of the token IDs from the tx */ function buyAndSwap721( uint256 vaultId, uint256[] calldata idsIn, - uint256[] calldata specificIds, - address spender, + uint256[] calldata specificIds, bytes calldata swapCallData, address payable to ) external payable nonReentrant onlyOwnerIfPaused { @@ -186,8 +179,8 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab _swap721(vaultId, idsIn, specificIds, to); emit Swap(idsIn.length, amount, to); - // Transfer dust back to the spender - _transferDust(spender, vault); + // Transfer dust back to the sender + _transferDust(msg.sender, vault, true); } @@ -199,16 +192,14 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab * @param vaultId The ID of the NFTX vault * @param amount The number of tokens to buy * @param specificIds An array of any specific token IDs to be minted - * @param spender The `allowanceTarget` field from the API response * @param swapCallData The `data` field from the API response - * @param to The recipient of the WETH from the tx + * @param to The recipient of the token IDs from the tx */ function buyAndRedeem( uint256 vaultId, uint256 amount, uint256[] calldata specificIds, - address spender, bytes calldata swapCallData, address payable to ) external payable nonReentrant onlyOwnerIfPaused { @@ -218,9 +209,6 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab // Check that we have an amount specified require(amount > 0, 'Must send amount'); - // Check that we have a message value sent - require(msg.value >= amount, 'Invalid amount'); - // Wrap ETH into WETH for our contract from the sender WETH.deposit{value: msg.value}(); @@ -230,12 +218,15 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab // Buy vault tokens that will cover our transaction uint256 quoteAmount = _fillQuote(address(WETH), vault, swapCallData); + // check if received sufficient vault tokens + require(quoteAmount >= amount * 1e18, 'Insufficient vault tokens'); + // Redeem token IDs from the vault _redeem(vaultId, amount, specificIds, to); emit Buy(amount, quoteAmount, to); - // Transfer dust back to the spender - _transferDust(spender, vault); + // Transfer dust back to the sender + _transferDust(msg.sender, vault, true); } @@ -245,16 +236,14 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab * @param vaultId The ID of the NFTX vault * @param ids An array of token IDs to be minted * @param amounts The number of the corresponding ID to be minted - * @param spender The `allowanceTarget` field from the API response * @param swapCallData The `data` field from the API response - * @param to The recipient of the WETH from the tx + * @param to The recipient of ETH from the tx */ function mintAndSell1155( uint256 vaultId, uint256[] calldata ids, uint256[] calldata amounts, - address spender, bytes calldata swapCallData, address payable to ) external nonReentrant onlyOwnerIfPaused { @@ -271,25 +260,27 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab // Sell our vault token for WETH uint256 amount = _fillQuote(vault, address(WETH), swapCallData); + // convert WETH to ETH and send to `to` + _transferAllWETH(to); + // Emit our sale event emit Sell(totalAmount, amount, to); - // Transfer dust back to the spender - _transferDust(spender, vault); + // Transfer dust back to the sender + _transferDust(msg.sender, vault, false); } /** * @notice Purchases vault tokens from 0x with WETH and then swaps the tokens for * either random or specific token IDs from the vault. The specified recipient will - * receive the ERC721 tokens, as well as any WETH dust that is left over from the tx. + * receive the ERC1155 tokens, as well as any WETH dust that is left over from the tx. * * @param vaultId The ID of the NFTX vault * @param idsIn An array of random token IDs to be minted * @param specificIds An array of any specific token IDs to be minted - * @param spender The `allowanceTarget` field from the API response * @param swapCallData The `data` field from the API response - * @param to The recipient of the WETH from the tx + * @param to The recipient of token IDs from the tx */ function buyAndSwap1155( @@ -297,7 +288,6 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab uint256[] calldata idsIn, uint256[] calldata amounts, uint256[] calldata specificIds, - address spender, bytes calldata swapCallData, address payable to ) external payable nonReentrant onlyOwnerIfPaused { @@ -324,8 +314,8 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab _swap1155(vaultId, idsIn, amounts, specificIds, to); emit Swap(totalAmount, amount, to); - // Transfer dust back to the spender - _transferDust(spender, vault); + // Transfer dust back to the sender + _transferDust(msg.sender, vault, true); } @@ -372,8 +362,8 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab // Transfer tokens from the message sender to the vault address assetAddress = INFTXVault(vault).assetAddress(); - IERC1155Upgradeable(assetAddress).safeBatchTransferFrom(msg.sender, address(this), ids, amounts, ""); - IERC1155Upgradeable(assetAddress).setApprovalForAll(vault, true); + IERC1155(assetAddress).safeBatchTransferFrom(msg.sender, address(this), ids, amounts, ""); + IERC1155(assetAddress).setApprovalForAll(vault, true); // Mint our tokens from the vault to this contract INFTXVault(vault).mint(ids, amounts); @@ -446,8 +436,8 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab // Transfer tokens to zap and mint to NFTX. address assetAddress = INFTXVault(vault).assetAddress(); - IERC1155Upgradeable(assetAddress).safeBatchTransferFrom(msg.sender, address(this), idsIn, amounts, ""); - IERC1155Upgradeable(assetAddress).setApprovalForAll(vault, true); + IERC1155(assetAddress).safeBatchTransferFrom(msg.sender, address(this), idsIn, amounts, ""); + IERC1155(assetAddress).setApprovalForAll(vault, true); INFTXVault(vault).swapTo(idsIn, amounts, idsOut, to); return vault; @@ -552,25 +542,33 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab /** * @notice Transfers remaining ETH and vault token dust to a recipient. * - * @param spender Address of the dust recipient + * @param recipient Address of the dust recipient * @param vault Address of the vault token + * @param isWETHDust Checks and transfers WETH dust if boolean is true */ - function _transferDust(address spender, address vault) internal { - uint256 remaining = WETH.balanceOf(address(this)); - if (remaining > 0) { - // Unwrap our WETH into ETH and transfer it to the recipient - WETH.withdraw(remaining); - (bool success, ) = payable(spender).call{value: remaining}(""); - require(success, "Unable to send unwrapped WETH"); + function _transferDust(address recipient, address vault, bool isWETHDust) internal { + uint256 remaining; + if(isWETHDust) { + remaining = _transferAllWETH(recipient); } - uint dustBalance = IERC20Upgradeable(vault).balanceOf(address(this)); + uint dustBalance = IERC20(vault).balanceOf(address(this)); if (dustBalance > 0) { - IERC20Upgradeable(vault).transfer(spender, dustBalance); + IERC20(vault).transfer(recipient, dustBalance); } - emit DustReturned(remaining, dustBalance, spender); + emit DustReturned(remaining, dustBalance, recipient); + } + + function _transferAllWETH(address recipient) internal returns(uint256 amount) { + amount = WETH.balanceOf(address(this)); + if (amount > 0) { + // Unwrap our WETH into ETH and transfer it to the recipient + WETH.withdraw(amount); + (bool success, ) = payable(recipient).call{value: amount}(""); + require(success, "Unable to send unwrapped WETH"); + } } @@ -648,7 +646,7 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab (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))); + IERC20(token).safeTransfer(msg.sender, IERC20(token).balanceOf(address(this))); } } @@ -671,4 +669,4 @@ contract NFTXMarketplace0xZap is Ownable, ReentrancyGuard, ERC721HolderUpgradeab receive() external payable {} -} +} \ No newline at end of file diff --git a/contracts/solidity/NFTXYieldStakingZap.sol b/contracts/solidity/NFTXYieldStakingZap.sol index a4a16ee..c36a2e3 100644 --- a/contracts/solidity/NFTXYieldStakingZap.sol +++ b/contracts/solidity/NFTXYieldStakingZap.sol @@ -8,7 +8,7 @@ import "./interface/INFTXVaultFactory.sol"; import "./interface/IUniswapV2Router01.sol"; import "./util/Ownable.sol"; import "./util/ReentrancyGuard.sol"; -import "./util/SafeERC20Upgradeable.sol"; +import "./util/SafeERC20.sol"; /** @@ -20,6 +20,7 @@ interface IWETH { function transfer(address to, uint value) external returns (bool); function withdraw(uint) external; function balanceOf(address to) external view returns (uint256); + function approve(address guy, uint wad) external returns (bool); } @@ -32,7 +33,7 @@ interface IWETH { contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { - using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeERC20 for IERC20; /// @notice Allows zap to be paused bool public paused = false; @@ -83,6 +84,9 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { // Set our chain's WETH contract WETH = IWETH(_weth); + // setting infinite approval here to save on subsequent gas costs + IWETH(_weth).approve(_sushiRouter, type(uint256).max); + IWETH(_weth).approve(_swapTarget, type(uint256).max); // Set our 0x Swap Target swapTarget = _swapTarget; @@ -101,7 +105,7 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { function buyAndStakeInventory( uint256 vaultId, bytes calldata swapCallData - ) external payable nonReentrant { + ) external payable nonReentrant onlyOwnerIfPaused { // Ensure we have tx value require(msg.value > 0, 'Invalid value provided'); @@ -122,7 +126,7 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { // 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); + IERC20(baseToken).transfer(inventoryStaking.vaultXToken(vaultId), vaultTokenAmount); inventoryStaking.timelockMintFor(vaultId, vaultTokenAmount, msg.sender, 2); // Return any left of WETH to the user as ETH @@ -160,7 +164,7 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { uint256 minWethIn, uint256 wethIn - ) external payable nonReentrant { + ) external payable nonReentrant onlyOwnerIfPaused { // Ensure we have tx value require(msg.value > 0, 'Invalid value provided'); require(msg.value > wethIn, 'Insufficient vault sent for pairing'); @@ -181,13 +185,17 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { uint256 vaultTokenAmount = _fillQuote(baseToken, swapCallData); require(vaultTokenAmount > minTokenIn, 'Insufficient tokens acquired'); + // Check WETH balance + uint256 WETHAmount = WETH.balanceOf(address(this)) - wethBalance; + require(WETHAmount >= wethIn, 'Insufficient WETH remaining'); + // 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); + IERC20(baseToken).safeApprove(address(sushiRouter), vaultTokenAmount); (uint256 amountToken, , uint256 liquidity) = sushiRouter.addLiquidity( baseToken, address(WETH), - minTokenIn, + vaultTokenAmount, wethIn, minTokenIn, minWethIn, @@ -197,13 +205,13 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { // Stake in LP rewards contract address lpToken = pairFor(baseToken, address(WETH)); - IERC20Upgradeable(lpToken).safeApprove(address(lpStaking), liquidity); + IERC20(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); + IERC20(baseToken).transfer(msg.sender, remainingTokens); } // Return any left of WETH to the user as ETH @@ -258,7 +266,7 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { (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))); + IERC20(token).safeTransfer(msg.sender, IERC20(token).balanceOf(address(this))); } } @@ -275,19 +283,14 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { 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'); + uint256 boughtAmount = IERC20(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; + return IERC20(buyToken).balanceOf(address(this)) - boughtAmount; } @@ -318,6 +321,17 @@ contract NFTXYieldStakingZap is Ownable, ReentrancyGuard { paused = _paused; } + /** + * @notice A modifier that only allows the owner to interact with the function + * if the contract is paused. If the contract is not paused then anyone can + * interact with the function. + */ + + modifier onlyOwnerIfPaused() { + require(!paused || msg.sender == owner(), "Zap is paused"); + _; + } + /** * @notice Allows our contract to only receive WETH and reject everything else. diff --git a/contracts/solidity/testing/ERC1155Holder.sol b/contracts/solidity/testing/ERC1155Holder.sol new file mode 100644 index 0000000..7249de8 --- /dev/null +++ b/contracts/solidity/testing/ERC1155Holder.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/utils/ERC1155Holder.sol) + +pragma solidity ^0.8.0; + +import "./ERC1155Receiver.sol"; + +/** + * Simple implementation of `ERC1155Receiver` that will allow a contract to hold ERC1155 tokens. + * + * IMPORTANT: When inheriting this contract, you must include a way to use the received tokens, otherwise they will be + * stuck. + * + * @dev _Available since v3.1._ + */ +contract ERC1155Holder is ERC1155Receiver { + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } +} diff --git a/contracts/solidity/testing/ERC721Holder.sol b/contracts/solidity/testing/ERC721Holder.sol new file mode 100644 index 0000000..1093853 --- /dev/null +++ b/contracts/solidity/testing/ERC721Holder.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC721/utils/ERC721Holder.sol) + +pragma solidity ^0.8.0; + +import "./IERC721Receiver.sol"; + +/** + * @dev Implementation of the {IERC721Receiver} interface. + * + * Accepts all token transfers. + * Make sure the contract is able to use its token with {IERC721-safeTransferFrom}, {IERC721-approve} or {IERC721-setApprovalForAll}. + */ +contract ERC721Holder is IERC721Receiver { + /** + * @dev See {IERC721Receiver-onERC721Received}. + * + * Always returns `IERC721Receiver.onERC721Received.selector`. + */ + function onERC721Received( + address, + address, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC721Received.selector; + } +} diff --git a/contracts/solidity/testing/IERC20.sol b/contracts/solidity/testing/IERC20.sol new file mode 100644 index 0000000..82a00e4 --- /dev/null +++ b/contracts/solidity/testing/IERC20.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) + external + view + returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} diff --git a/contracts/solidity/testing/IERC20Permit.sol b/contracts/solidity/testing/IERC20Permit.sol new file mode 100644 index 0000000..bb43e53 --- /dev/null +++ b/contracts/solidity/testing/IERC20Permit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Permit.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC20Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/contracts/solidity/util/SafeERC20.sol b/contracts/solidity/util/SafeERC20.sol new file mode 100644 index 0000000..7e99227 --- /dev/null +++ b/contracts/solidity/util/SafeERC20.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/utils/SafeERC20.sol) + +pragma solidity ^0.8.0; + +import "../testing/IERC20.sol"; +import "../testing/IERC20Permit.sol"; +import "./Address.sol"; + +/** + * @title SafeERC20 + * @dev Wrappers around ERC20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20 { + using Address for address; + + function safeTransfer( + IERC20 token, + address to, + uint256 value + ) internal { + _callOptionalReturn( + token, + abi.encodeWithSelector(token.transfer.selector, to, value) + ); + } + + function safeTransferFrom( + IERC20 token, + address from, + address to, + uint256 value + ) internal { + _callOptionalReturn( + token, + abi.encodeWithSelector(token.transferFrom.selector, from, to, value) + ); + } + + /** + * @dev Deprecated. This function has issues similar to the ones found in + * {IERC20-approve}, and its usage is discouraged. + * + * Whenever possible, use {safeIncreaseAllowance} and + * {safeDecreaseAllowance} instead. + */ + function safeApprove( + IERC20 token, + address spender, + uint256 value + ) internal { + // safeApprove should only be called when setting an initial allowance, + // or when resetting it to zero. To increase and decrease it, use + // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' + require( + (value == 0) || (token.allowance(address(this), spender) == 0), + "SafeERC20: approve from non-zero to non-zero allowance" + ); + _callOptionalReturn( + token, + abi.encodeWithSelector(token.approve.selector, spender, value) + ); + } + + function safeIncreaseAllowance( + IERC20 token, + address spender, + uint256 value + ) internal { + uint256 newAllowance = token.allowance(address(this), spender) + value; + _callOptionalReturn( + token, + abi.encodeWithSelector( + token.approve.selector, + spender, + newAllowance + ) + ); + } + + function safeDecreaseAllowance( + IERC20 token, + address spender, + uint256 value + ) internal { + unchecked { + uint256 oldAllowance = token.allowance(address(this), spender); + require( + oldAllowance >= value, + "SafeERC20: decreased allowance below zero" + ); + uint256 newAllowance = oldAllowance - value; + _callOptionalReturn( + token, + abi.encodeWithSelector( + token.approve.selector, + spender, + newAllowance + ) + ); + } + } + + function safePermit( + IERC20Permit token, + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + uint256 nonceBefore = token.nonces(owner); + token.permit(owner, spender, value, deadline, v, r, s); + uint256 nonceAfter = token.nonces(owner); + require( + nonceAfter == nonceBefore + 1, + "SafeERC20: permit did not succeed" + ); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall( + data, + "SafeERC20: low-level call failed" + ); + if (returndata.length > 0) { + // Return data is optional + require( + abi.decode(returndata, (bool)), + "SafeERC20: ERC20 operation did not succeed" + ); + } + } +} diff --git a/contracts/solidity/zaps/VaultCreationZap.sol b/contracts/solidity/zaps/VaultCreationZap.sol index 7adaf20..f434394 100644 --- a/contracts/solidity/zaps/VaultCreationZap.sol +++ b/contracts/solidity/zaps/VaultCreationZap.sol @@ -7,11 +7,11 @@ 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 "../testing/IERC1155.sol"; +import "../testing/ERC1155Holder.sol"; import "../util/Ownable.sol"; import "../util/ReentrancyGuard.sol"; -import "../util/SafeERC20Upgradeable.sol"; +import "../util/SafeERC20.sol"; import "../util/SushiHelper.sol"; @@ -24,6 +24,7 @@ interface IWETH { function transfer(address to, uint value) external returns (bool); function withdraw(uint) external; function balanceOf(address to) external view returns (uint256); + function approve(address guy, uint wad) external returns (bool); } @@ -34,9 +35,9 @@ interface IWETH { * @author Twade */ -contract NFTXVaultCreationZap is Ownable, ReentrancyGuard, ERC1155SafeHolderUpgradeable { +contract NFTXVaultCreationZap is Ownable, ReentrancyGuard, ERC1155Holder { - using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeERC20 for IERC20; /// @notice Allows zap to be paused bool public paused = false; @@ -55,6 +56,9 @@ contract NFTXVaultCreationZap is Ownable, ReentrancyGuard, ERC1155SafeHolderUpgr INFTXInventoryStaking public immutable inventoryStaking; INFTXLPStaking public immutable lpStaking; + // Set a constant address for specific contracts that need special logic + address constant CRYPTO_PUNKS = 0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB; + /// @notice Basic information pertaining to the vault struct vaultInfo { address assetAddress; // 20/32 @@ -117,6 +121,8 @@ contract NFTXVaultCreationZap is Ownable, ReentrancyGuard, ERC1155SafeHolderUpgr // Set our chain's WETH contract WETH = IWETH(_weth); + // setting infinite approval here to save on subsequent gas costs + IWETH(_weth).approve(_sushiRouter, type(uint256).max); } @@ -180,12 +186,24 @@ contract NFTXVaultCreationZap is Ownable, ReentrancyGuard, ERC1155SafeHolderUpgr // 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)); + + if(vaultData.assetAddress == CRYPTO_PUNKS) { + bytes memory data = abi.encodeWithSignature( + "offerPunkForSaleToAddress(uint256,uint256,address)", + assetTokens.assetTokenIds[i], + 0, + address(vault) + ); + (bool success, bytes memory resultData) = vaultData.assetAddress.call(data); + require(success, string(resultData)); + } + 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( + IERC1155(vaultData.assetAddress).safeBatchTransferFrom( msg.sender, address(this), assetTokens.assetTokenIds, @@ -194,7 +212,7 @@ contract NFTXVaultCreationZap is Ownable, ReentrancyGuard, ERC1155SafeHolderUpgr ); // Approve our vault to play with our 1155 tokens - IERC1155Upgradeable(vaultData.assetAddress).setApprovalForAll(address(vault), true); + IERC1155(vaultData.assetAddress).setApprovalForAll(address(vault), true); } // We can now mint our asset tokens, giving the vault our tokens and storing them @@ -210,17 +228,17 @@ contract NFTXVaultCreationZap is Ownable, ReentrancyGuard, ERC1155SafeHolderUpgr // 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'); + require(msg.value >= assetTokens.wethIn, 'Insufficient msg.value 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'); + require(IERC20(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); + IERC20(baseToken).safeApprove(address(sushiRouter), assetTokens.minTokenIn); (,, uint256 liquidity) = sushiRouter.addLiquidity( baseToken, address(WETH), @@ -231,22 +249,23 @@ contract NFTXVaultCreationZap is Ownable, ReentrancyGuard, ERC1155SafeHolderUpgr address(this), block.timestamp ); + IERC20(baseToken).safeApprove(address(sushiRouter), 0); // Stake in LP rewards contract address lpToken = sushiHelper.pairFor(sushiRouter.factory(), baseToken, address(WETH)); - IERC20Upgradeable(lpToken).safeApprove(address(lpStaking), liquidity); + IERC20(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)); + uint256 remainingTokens = IERC20(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); + IERC20(baseToken).transfer(inventoryStaking.vaultXToken(vaultId_), remainingTokens); inventoryStaking.timelockMintFor(vaultId_, remainingTokens, msg.sender, 2); } } @@ -297,7 +316,7 @@ contract NFTXVaultCreationZap is Ownable, ReentrancyGuard, ERC1155SafeHolderUpgr function _transferFromERC721(address assetAddr, uint256 tokenId, address to) internal virtual { bytes memory data; - if (assetAddr == 0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB) { + if (assetAddr == CRYPTO_PUNKS) { // Fix here for frontrun attack. bytes memory punkIndexToAddress = abi.encodeWithSignature("punkIndexToAddress(uint256)", tokenId); (bool checkSuccess, bytes memory result) = address(assetAddr).staticcall(punkIndexToAddress);