diff --git a/contracts/mainnet/connectors/fluid/events.sol b/contracts/mainnet/connectors/fluid/events.sol new file mode 100644 index 0000000..c0055ef --- /dev/null +++ b/contracts/mainnet/connectors/fluid/events.sol @@ -0,0 +1,20 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +contract Events { + event LogOperate( + address vaultAddress, + uint256 nftId, + int256 newCol, + int256 newDebt + ); + + event LogOperateWithIds( + address vaultAddress, + uint256 nftId, + int256 newCol, + int256 newDebt, + uint256[] getIds, + uint256[] setIds + ); +} diff --git a/contracts/mainnet/connectors/fluid/interface.sol b/contracts/mainnet/connectors/fluid/interface.sol new file mode 100644 index 0000000..a7eef73 --- /dev/null +++ b/contracts/mainnet/connectors/fluid/interface.sol @@ -0,0 +1,49 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +interface IVault { + /// @dev Single function which handles supply, withdraw, borrow & payback + /// @param nftId_ NFT ID for interaction. If 0 then create new NFT/position. + /// @param newCol_ new collateral. If positive then deposit, if negative then withdraw, if 0 then do nohing + /// @param newDebt_ new debt. If positive then borrow, if negative then payback, if 0 then do nohing + /// @param to_ address where withdraw or borrow should go. If address(0) then msg.sender + /// @return nftId_ if 0 then this returns the newly created NFT Id else returns the same NFT ID + /// @return final supply amount. Mainly if max withdraw using type(int).min then this is useful to get perfect amount else remain same as newCol_ + /// @return final borrow amount. Mainly if max payback using type(int).min then this is useful to get perfect amount else remain same as newDebt_ + function operate( + uint256 nftId_, // if 0 then new position + int256 newCol_, // if negative then withdraw + int256 newDebt_, // if negative then payback + address to_ // address at which the borrow & withdraw amount should go to. If address(0) then it'll go to msg.sender + ) + external + payable + returns ( + uint256, // nftId_ + int256, // final supply amount if - then withdraw + int256 // final borrow amount if - then payback + ); + + struct ConstantViews { + address liquidity; + address factory; + address adminImplementation; + address secondaryImplementation; + address supplyToken; + address borrowToken; + uint8 supplyDecimals; + uint8 borrowDecimals; + uint vaultId; + bytes32 liquidityTotalSupplySlot; + bytes32 liquidityTotalBorrowSlot; + bytes32 liquiditySupplyExchangePriceSlot; + bytes32 liquidityBorrowExchangePriceSlot; + bytes32 liquidityUserSupplySlot; + bytes32 liquidityUserBorrowSlot; + } + + function constantsView() + external + view + returns (ConstantViews memory constantsView_); +} diff --git a/contracts/mainnet/connectors/fluid/main.sol b/contracts/mainnet/connectors/fluid/main.sol new file mode 100644 index 0000000..265ce2f --- /dev/null +++ b/contracts/mainnet/connectors/fluid/main.sol @@ -0,0 +1,260 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +/** + * @title Fluid. + * @dev Lending & Borrowing. + */ + +import {Stores} from "../../common/stores.sol"; +import {TokenInterface} from "../../common/interfaces.sol"; +import {Events} from "./events.sol"; +import {IVault} from "./interface.sol"; + +abstract contract FluidConnector is Events, Stores { + /** + * @dev Returns Eth address + */ + function getEthAddr() internal pure returns (address) { + return 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + } + + /** + * @dev Deposit, borrow, payback and withdraw asset from the vault. + * @notice Single function which handles supply, withdraw, borrow & payback + * @param vaultAddress_ Vault address. + * @param nftId_ NFT ID for interaction. If 0 then create new NFT/position. + * @param newCol_ New collateral. If positive then deposit, if negative then withdraw, if 0 then do nothing. + * For max deposit use type(uint25).max, for max withdraw use type(uint25).min. + * @param newDebt_ New debt. If positive then borrow, if negative then payback, if 0 then do nothing + * For max payback use type(uint25).min. + * @param repayApproveAmt_ In case of max amount for payback, this amount will be approved for spending. + * Should always be positive. + * @param getIds_ Array of 5 elements to retrieve IDs: + * Nft Id, Supply amount, Withdraw amount, Borrow Amount, Payback Amount + * @param setIds_ Array of 5 elements to store IDs generated: + * Nft Id, Supply amount, Withdraw amount, Borrow Amount, Payback Amount + */ + function operateWithIds( + address vaultAddress_, + uint256 nftId_, + int256 newCol_, + int256 newDebt_, + uint256 repayApproveAmt_, + uint256[] memory getIds_, + uint256[] memory setIds_ + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + if (getIds_[1] > 0 && getIds_[2] > 0) { + revert("Supply and withdraw get IDs cannot both be > 0."); + } + + if (getIds_[3] > 0 && getIds_[4] > 0) { + revert("Borrow and payback get IDs cannot both be > 0."); + } + + if (setIds_[1] > 0 && setIds_[2] > 0) { + revert("Supply and withdraw set IDs cannot both be > 0."); + } + + if (setIds_[3] > 0 && setIds_[4] > 0) { + revert("Borrow and payback set IDs cannot both be > 0."); + } + + nftId_ = getUint(getIds_[0], nftId_); + + newCol_ = getIds_[1] > 0 + ? int256(getUint(getIds_[1], uint256(newCol_))) + : getIds_[2] > 0 + ? -int256(getUint(getIds_[2], uint256(newCol_))) + : newCol_; + + newDebt_ = getIds_[3] > 0 + ? int256(getUint(getIds_[3], uint256(newDebt_))) + : getIds_[4] > 0 + ? -int256(getUint(getIds_[4], uint256(newDebt_))) + : newDebt_; + + IVault vault_ = IVault(vaultAddress_); + + IVault.ConstantViews memory vaultDetails_ = vault_.constantsView(); + + uint256 ethAmount_; + + bool isColMax_ = newCol_ == type(int256).max; + + // Deposit + if (newCol_ > 0) { + if (vaultDetails_.supplyToken == getEthAddr()) { + ethAmount_ = isColMax_ + ? address(this).balance + : uint256(newCol_); + + newCol_ = int256(ethAmount_); + } else { + if (isColMax_) { + newCol_ = int256( + TokenInterface(vaultDetails_.supplyToken).balanceOf( + address(this) + ) + ); + } + + TokenInterface(vaultDetails_.supplyToken).approve( + vaultAddress_, + uint256(newCol_) + ); + } + } + + bool isPaybackMin_ = newDebt_ == type(int256).min; + + // Payback + if (newDebt_ < 0) { + if (vaultDetails_.borrowToken == getEthAddr()) { + // Needs to be positive as it will be send in msg.value + ethAmount_ = isPaybackMin_ + ? repayApproveAmt_ + : uint256(-newDebt_); + } else { + isPaybackMin_ + ? TokenInterface(vaultDetails_.borrowToken).approve( + vaultAddress_, + repayApproveAmt_ + ) + : TokenInterface(vaultDetails_.borrowToken).approve( + vaultAddress_, + uint256(-newDebt_) + ); + } + } + + // Note max withdraw will be handled by Fluid contract + (nftId_, newCol_, newDebt_) = vault_.operate{value: ethAmount_}( + nftId_, + newCol_, + newDebt_, + address(this) + ); + + setUint(setIds_[0], nftId_); + + setIds_[1] > 0 + ? setUint(setIds_[1], uint256(newCol_)) + : setUint(setIds_[2], uint256(newCol_)); // If setIds_[2] != 0, it will set the ID. + setIds_[3] > 0 + ? setUint(setIds_[3], uint256(newDebt_)) + : setUint(setIds_[4], uint256(newDebt_)); // If setIds_[4] != 0, it will set the ID. + + _eventName = "LogOperateWithIds(address,uint256,int256,int256,uint256[],uint256[])"; + _eventParam = abi.encode( + vaultAddress_, + nftId_, + newCol_, + newDebt_, + getIds_, + setIds_ + ); + } + + /** + * @dev Deposit, borrow, payback and withdraw asset from the vault. + * @notice Single function which handles supply, withdraw, borrow & payback + * @param vaultAddress_ Vault address. + * @param nftId_ NFT ID for interaction. If 0 then create new NFT/position. + * @param newCol_ New collateral. If positive then deposit, if negative then withdraw, if 0 then do nothing. + * For max deposit use type(uint25).max, for max withdraw use type(uint25).min. + * @param newDebt_ New debt. If positive then borrow, if negative then payback, if 0 then do nothing + * For max payback use type(uint25).min. + * @param repayApproveAmt_ In case of max amount for payback, this amount will be approved for spending. + * Should always be positive. + */ + function operate( + address vaultAddress_, + uint256 nftId_, + int256 newCol_, + int256 newDebt_, + uint256 repayApproveAmt_ + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + IVault vault_ = IVault(vaultAddress_); + + IVault.ConstantViews memory vaultDetails_ = vault_.constantsView(); + + uint256 ethAmount_; + + bool isColMax_ = newCol_ == type(int256).max; + + // Deposit + if (newCol_ > 0) { + if (vaultDetails_.supplyToken == getEthAddr()) { + ethAmount_ = isColMax_ + ? address(this).balance + : uint256(newCol_); + + newCol_ = int256(ethAmount_); + } else { + if (isColMax_) { + newCol_ = int256( + TokenInterface(vaultDetails_.supplyToken).balanceOf( + address(this) + ) + ); + } + + TokenInterface(vaultDetails_.supplyToken).approve( + vaultAddress_, + uint256(newCol_) + ); + } + } + + bool isPaybackMin_ = newDebt_ == type(int256).min; + + // Payback + if (newDebt_ < 0) { + if (vaultDetails_.borrowToken == getEthAddr()) { + // Needs to be positive as it will be send in msg.value + ethAmount_ = isPaybackMin_ + ? repayApproveAmt_ + : uint256(-newDebt_); + } else { + isPaybackMin_ + ? TokenInterface(vaultDetails_.borrowToken).approve( + vaultAddress_, + repayApproveAmt_ + ) + : TokenInterface(vaultDetails_.borrowToken).approve( + vaultAddress_, + uint256(-newDebt_) + ); + } + } + + // Note max withdraw will be handled by Fluid contract + (nftId_, newCol_, newDebt_) = vault_.operate{value: ethAmount_}( + nftId_, + newCol_, + newDebt_, + address(this) + ); + + _eventName = "LogOperate(address,uint256,int256,int256)"; + _eventParam = abi.encode( + vaultAddress_, + nftId_, + newCol_, + newDebt_ + ); + } +} + +contract ConnectV2Fluid is FluidConnector { + string public constant name = "Fluid-v1.0"; +} diff --git a/contracts/mainnet/connectors/instapool_v5/events.sol b/contracts/mainnet/connectors/instapool_v5/events.sol new file mode 100644 index 0000000..5d5bd77 --- /dev/null +++ b/contracts/mainnet/connectors/instapool_v5/events.sol @@ -0,0 +1,10 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +contract Events { + event LogFlashBorrow(address token, uint256 tokenAmt); + event LogFlashPayback(address token, uint256 tokenAmt); + + event LogFlashMultiBorrow(address[] token, uint256[] tokenAmts); + event LogFlashMultiPayback(address[] token, uint256[] tokenAmts); +} \ No newline at end of file diff --git a/contracts/mainnet/connectors/instapool_v5/interfaces.sol b/contracts/mainnet/connectors/instapool_v5/interfaces.sol new file mode 100644 index 0000000..58c7a98 --- /dev/null +++ b/contracts/mainnet/connectors/instapool_v5/interfaces.sol @@ -0,0 +1,11 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +interface InstaFlashV5Interface { + function flashLoan(address[] memory tokens, uint256[] memory amts, uint route, bytes memory data, bytes memory extraData) external; +} + +interface AccountInterface { + function enable(address) external; + function disable(address) external; +} diff --git a/contracts/mainnet/connectors/instapool_v5/main.sol b/contracts/mainnet/connectors/instapool_v5/main.sol new file mode 100644 index 0000000..8fce0d1 --- /dev/null +++ b/contracts/mainnet/connectors/instapool_v5/main.sol @@ -0,0 +1,136 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title Instapool. + * @dev Inbuilt Flash Loan in DSA + */ + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { AccountInterface } from "./interfaces.sol"; +import { Stores } from "../../common/stores.sol"; +import { Variables } from "./variables.sol"; +import { Events } from "./events.sol"; + +contract LiquidityResolver is Stores, Variables, Events { + using SafeERC20 for IERC20; + + /** + * @dev Borrow Flashloan and Cast spells. + * @notice Borrow Flashloan and Cast spells. + * @param token Token Address. + * @param amt Token Amount. + * @param route Flashloan source route. + * @param data targets & data for cast. + * @param extraData to be kept bytes(0) in most cases. Can be useful to decide data for some particular routes + */ + function flashBorrowAndCast( + address token, + uint amt, + uint route, + bytes memory data, + bytes memory extraData + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + AccountInterface(address(this)).enable(address(instaPool)); + (string[] memory _targets, bytes[] memory callDatas) = abi.decode(data, (string[], bytes[])); + + bytes memory callData_ = abi.encodeWithSignature("cast(string[],bytes[],address)", _targets, callDatas, address(instaPool)); + + address[] memory tokens_ = new address[](1); + tokens_[0] = token; + uint[] memory amts_ = new uint[](1); + amts_[0] = amt; + instaPool.flashLoan(tokens_, amts_, route, callData_, extraData); + + AccountInterface(address(this)).disable(address(instaPool)); + + _eventName = "LogFlashBorrow(address,uint256)"; + _eventParam = abi.encode(token, amt); + } + + /** + * @dev Return token to InstaPool. + * @notice Return token to InstaPool. + * @param token Token Address. + * @param amt Token Amount. + * @param getId Get token amount at this ID from `InstaMemory` Contract. + * @param setId Set token amount at this ID in `InstaMemory` Contract. + */ + function flashPayback( + address token, + uint amt, + uint getId, + uint setId + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, amt); + + IERC20 tokenContract = IERC20(token); + + tokenContract.safeTransfer(address(instaPool), _amt); + + setUint(setId, _amt); + + _eventName = "LogFlashPayback(address,uint256)"; + _eventParam = abi.encode(token, amt); + } + + /** + * @dev Borrow multi-tokens Flashloan and Cast spells. + * @notice Borrow multi-tokens Flashloan and Cast spells. + * @param tokens_ Array of Token Addresses. + * @param amts_ Array of Token Amounts. + * @param route Flashloan source route. + * @param data targets & data for cast. + * @param extraData to be kept bytes(0) in most cases. Can be useful to decide data for some particular routes + */ + function flashMultiBorrowAndCast( + address[] memory tokens_, + uint[] memory amts_, + uint route, + bytes memory data, + bytes memory extraData + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + AccountInterface(address(this)).enable(address(instaPool)); + (string[] memory _targets, bytes[] memory callDatas) = abi.decode(data, (string[], bytes[])); + + bytes memory callData_ = abi.encodeWithSignature("cast(string[],bytes[],address)", _targets, callDatas, address(instaPool)); + + instaPool.flashLoan(tokens_, amts_, route, callData_, extraData); + + AccountInterface(address(this)).disable(address(instaPool)); + _eventName = "LogFlashMultiBorrow(address[],uint256[])"; + _eventParam = abi.encode(tokens_, amts_); + } + + /** + * @dev Return multi-tokens to InstaPool. + * @notice Return multi-tokens to InstaPool. + * @param tokens_ Array of Token Addresses. + * @param amts_ Array of Token Amounts. + * @param getIds Array of getId token amounts. + * @param setIds Array of setId token amounts. + */ + function flashMultiPayback( + address[] memory tokens_, + uint[] memory amts_, + uint[] memory getIds, + uint[] memory setIds + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + for (uint i = 0; i < tokens_.length; i++) { + amts_[i] = getUint(getIds[i], amts_[i]); + + IERC20(tokens_[i]).safeTransfer(address(instaPool), amts_[i]); + + setUint(setIds[i], amts_[i]); + } + + _eventName = "LogFlashMultiPayback(address[],uint256[])"; + _eventParam = abi.encode(tokens_, amts_); + } + +} + +contract ConnectV2InstaPoolV5 is LiquidityResolver { + string public name = "Instapool-v5"; +} diff --git a/contracts/mainnet/connectors/instapool_v5/variables.sol b/contracts/mainnet/connectors/instapool_v5/variables.sol new file mode 100644 index 0000000..f2182a4 --- /dev/null +++ b/contracts/mainnet/connectors/instapool_v5/variables.sol @@ -0,0 +1,14 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { InstaFlashV5Interface } from "./interfaces.sol"; + +contract Variables { + + /** + * @dev Instapool contract proxy + */ + InstaFlashV5Interface public constant instaPool = + InstaFlashV5Interface(0xAB50Dd1C57938218627Df2311ef65b4e2e84aF48); + +} \ No newline at end of file diff --git a/contracts/polygon/connectors/fluid/events.sol b/contracts/polygon/connectors/fluid/events.sol index 3fcb301..c0055ef 100644 --- a/contracts/polygon/connectors/fluid/events.sol +++ b/contracts/polygon/connectors/fluid/events.sol @@ -3,6 +3,13 @@ pragma solidity ^0.8.2; contract Events { event LogOperate( + address vaultAddress, + uint256 nftId, + int256 newCol, + int256 newDebt + ); + + event LogOperateWithIds( address vaultAddress, uint256 nftId, int256 newCol, diff --git a/contracts/polygon/connectors/fluid/main.sol b/contracts/polygon/connectors/fluid/main.sol index 181c428..64120f9 100644 --- a/contracts/polygon/connectors/fluid/main.sol +++ b/contracts/polygon/connectors/fluid/main.sol @@ -29,12 +29,13 @@ abstract contract FluidConnector is Events, Stores { * @param newDebt_ New debt. If positive then borrow, if negative then payback, if 0 then do nothing * For max payback use type(uint25).min. * @param repayApproveAmt_ In case of max amount for payback, this amount will be approved for spending. + * Should always be positive. * @param getIds_ Array of 5 elements to retrieve IDs: * Nft Id, Supply amount, Withdraw amount, Borrow Amount, Payback Amount * @param setIds_ Array of 5 elements to store IDs generated: * Nft Id, Supply amount, Withdraw amount, Borrow Amount, Payback Amount */ - function operate( + function operateWithIds( address vaultAddress_, uint256 nftId_, int256 newCol_, @@ -81,15 +82,18 @@ abstract contract FluidConnector is Events, Stores { IVault.ConstantViews memory vaultDetails_ = vault_.constantsView(); - uint256 ethAmount_; + uint256 maticAmount_; bool isColMax_ = newCol_ == type(int256).max; + // Deposit if (newCol_ > 0) { if (vaultDetails_.supplyToken == getMaticAddr()) { - ethAmount_ = isColMax_ + maticAmount_ = isColMax_ ? address(this).balance : uint256(newCol_); + + newCol_ = int256(maticAmount_); } else { if (isColMax_) { newCol_ = int256( @@ -106,27 +110,30 @@ abstract contract FluidConnector is Events, Stores { } } - bool isPaybackMax_ = newDebt_ == type(int256).min; + bool isPaybackMin_ = newDebt_ == type(int256).min; + // Payback if (newDebt_ < 0) { if (vaultDetails_.borrowToken == getMaticAddr()) { - ethAmount_ = isPaybackMax_ + // Needs to be positive as it will be send in msg.value + maticAmount_ = isPaybackMin_ ? repayApproveAmt_ - : uint256(-1 * newDebt_); + : uint256(-newDebt_); } else { - isPaybackMax_ + isPaybackMin_ ? TokenInterface(vaultDetails_.borrowToken).approve( vaultAddress_, repayApproveAmt_ ) : TokenInterface(vaultDetails_.borrowToken).approve( vaultAddress_, - uint256(-1 * newDebt_) + uint256(-newDebt_) ); } } - (nftId_, newCol_, newDebt_) = vault_.operate{value: ethAmount_}( + // Note max withdraw will be handled by Fluid contract + (nftId_, newCol_, newDebt_) = vault_.operate{value: maticAmount_}( nftId_, newCol_, newDebt_, @@ -142,7 +149,7 @@ abstract contract FluidConnector is Events, Stores { ? setUint(setIds_[3], uint256(newDebt_)) : setUint(setIds_[4], uint256(newDebt_)); // If setIds_[4] != 0, it will set the ID. - _eventName = "LogOperate(address,uint256,int256,int256,uint256[],uint256[])"; + _eventName = "LogOperateWithIds(address,uint256,int256,int256,uint256[],uint256[])"; _eventParam = abi.encode( vaultAddress_, nftId_, @@ -152,6 +159,100 @@ abstract contract FluidConnector is Events, Stores { setIds_ ); } + + /** + * @dev Deposit, borrow, payback and withdraw asset from the vault. + * @notice Single function which handles supply, withdraw, borrow & payback + * @param vaultAddress_ Vault address. + * @param nftId_ NFT ID for interaction. If 0 then create new NFT/position. + * @param newCol_ New collateral. If positive then deposit, if negative then withdraw, if 0 then do nothing. + * For max deposit use type(uint25).max, for max withdraw use type(uint25).min. + * @param newDebt_ New debt. If positive then borrow, if negative then payback, if 0 then do nothing + * For max payback use type(uint25).min. + * @param repayApproveAmt_ In case of max amount for payback, this amount will be approved for spending. + * Should always be positive. + */ + function operate( + address vaultAddress_, + uint256 nftId_, + int256 newCol_, + int256 newDebt_, + uint256 repayApproveAmt_ + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + IVault vault_ = IVault(vaultAddress_); + + IVault.ConstantViews memory vaultDetails_ = vault_.constantsView(); + + uint256 maticAmount_; + + bool isColMax_ = newCol_ == type(int256).max; + + // Deposit + if (newCol_ > 0) { + if (vaultDetails_.supplyToken == getMaticAddr()) { + maticAmount_ = isColMax_ + ? address(this).balance + : uint256(newCol_); + + newCol_ = int256(maticAmount_); + } else { + if (isColMax_) { + newCol_ = int256( + TokenInterface(vaultDetails_.supplyToken).balanceOf( + address(this) + ) + ); + } + + TokenInterface(vaultDetails_.supplyToken).approve( + vaultAddress_, + uint256(newCol_) + ); + } + } + + bool isPaybackMin_ = newDebt_ == type(int256).min; + + // Payback + if (newDebt_ < 0) { + if (vaultDetails_.borrowToken == getMaticAddr()) { + // Needs to be positive as it will be send in msg.value + maticAmount_ = isPaybackMin_ + ? repayApproveAmt_ + : uint256(-1 * newDebt_); + } else { + isPaybackMin_ + ? TokenInterface(vaultDetails_.borrowToken).approve( + vaultAddress_, + repayApproveAmt_ + ) + : TokenInterface(vaultDetails_.borrowToken).approve( + vaultAddress_, + uint256(-1 * newDebt_) + ); + } + } + + // Note max withdraw will be handled by Fluid contract + (nftId_, newCol_, newDebt_) = vault_.operate{value: maticAmount_}( + nftId_, + newCol_, + newDebt_, + address(this) + ); + + _eventName = "LogOperate(address,uint256,int256,int256)"; + _eventParam = abi.encode( + vaultAddress_, + nftId_, + newCol_, + newDebt_ + ); + } } contract ConnectV2FluidPolygon is FluidConnector { diff --git a/test/polygon/fluid/fluid.test.ts b/test/polygon/fluid/fluid.test.ts index d09deeb..43aebc4 100644 --- a/test/polygon/fluid/fluid.test.ts +++ b/test/polygon/fluid/fluid.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import hre from "hardhat"; import { abis } from "../../../scripts/constant/abis"; -import { addresses } from "../../../scripts/tests/mainnet/addresses"; +import { addresses } from "../../../scripts/tests/polygon/addresses"; import { deployAndEnableConnector } from "../../../scripts/tests/deployAndEnableConnector"; import { getMasterSigner } from "../../../scripts/tests/getMasterSigner"; import { buildDSAv2 } from "../../../scripts/tests/buildDSAv2"; @@ -11,6 +11,7 @@ import { encodeSpells } from "../../../scripts/tests/encodeSpells"; import { constants } from "../../../scripts/constant/constant"; import { network, ethers } from "hardhat"; import type { Signer, Contract } from "ethers"; +import { BigNumber } from "bignumber.js"; describe("Fluid", function () { const connectorName = "FLUID"; @@ -23,13 +24,15 @@ describe("Fluid", function () { const setIdMaticUsdc = "83478237"; const setIdWethUsdc = "83478249"; const setId3 = "85478249"; + const setId4 = "55478249"; - const vaultMaticUsdc = "0x2226FFAE044B9fd4ED991aDf20CAACF8E8302510"; - const vaultWethUsdc = "0x10D97a8236624222F681C12Eea4Ddac2BDD0471B"; - const vaultWethMatic = "0x553437CB882E3aFbB67Abd135E067AFB0721fbf1"; + const vaultMaticUsdc = "0xAf047A21CE590B36FE894dd6fa350b57Ea5Cb0aa"; + const vaultWethUsdc = "0xEad5D80db075a905c141b37cE903d621952eA3f6"; + const vaultWethMatic = "0x23918014AF7610e31e58A9DC9f9A7DdbfcA4087e"; + const vaultUsdcMatic = "0x6395Ddb6161CeF6e64D4c027fbBa26CC76F18148"; const wethHolder = "0xdeD8C5159CA3673f543D0F72043E4c655b35b96A"; - const usdcHolder = "0xEc4Db437ADEE0420A28Afa8B87c74D3901F56AC4"; + const usdcHolder = "0xA67EFB69A4f58F568aAB1b9d51110102985835b0"; const WETH = "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619"; const USDC = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; @@ -127,7 +130,7 @@ describe("Fluid", function () { forking: { // @ts-ignore jsonRpcUrl: hre.config.networks.hardhat.forking.url, - blockNumber: 12796965, + blockNumber: 53327050, }, }, ], @@ -140,6 +143,7 @@ describe("Fluid", function () { addresses.core.connectorsV2, masterSigner ); + connector = await deployAndEnableConnector({ connectorName, contractArtifact: ConnectV2FluidPolygon__factory, @@ -186,8 +190,7 @@ describe("Fluid", function () { await wethToken.connect(wethHolderSigner).transfer(dsaWallet0.address, ethers.utils.parseEther("20")); - expect(await wethToken.balanceOf(dsaWallet0.address)).to.be.gte(ethers.utils.parseEther("20")); - + expect(await wethToken.connect(wethHolderSigner).balanceOf(dsaWallet0.address)).to.be.gte(ethers.utils.parseEther("20")); }); it("Deposit 20 Usdc into DSA wallet", async function () { @@ -205,28 +208,28 @@ describe("Fluid", function () { await usdcToken.connect(usdcHolderSigner).transfer(dsaWallet0.address, parseUnits("20", 6)); - expect(await usdcToken.balanceOf(dsaWallet0.address)).to.be.gte(parseUnits("20", 6)); + expect(await usdcToken.connect(usdcHolderSigner).balanceOf(dsaWallet0.address)).to.be.gte(parseUnits("20", 6)); }); }); // 2000 matic, 20 weth, 20 usdc describe("Main", function () { - it("should deposit 1000 Matic in Fluid", async function () { + it("should deposit 1000 Matic in Fluid in MATIC-USDC", async function () { const amtDeposit = parseEther("1000"); const spells = [ { connector: connectorName, - method: "operate", + method: "operateWithIds", args: [ vaultMaticUsdc, - 0, // new nft + '0', // new nft amtDeposit, // +1000 collateral - 0, // 0 debt - dsaWallet0.address, - 0, - setIdMaticUsdc + '0', // 0 debt + '0', + ['0', '0', '0', '0', '0'], + [setIdMaticUsdc, '0', '0', '0', '0'] // set NFT ID of position ], }, ]; @@ -241,21 +244,21 @@ describe("Fluid", function () { parseEther("1000") ); }); - // 1000 matic + // 1000 matic, 20 weth, 20 usdc - it("should deposit max Matic in Fluid", async function () { + it("should deposit max Matic in Fluid in MATIC-USDC", async function () { const spells = [ { connector: connectorName, - method: "operate", + method: "operateWithIds", args: [ vaultMaticUsdc, // matic-usdc vault - 0, // setIdMaticUsdc - ethers.constants.MaxUint256, // + max collateral + 0, // NFT ID from setIdMaticUsdc + ethers.constants.MaxInt256, // + max collateral 0, // 0 debt - dsaWallet0.address, - setIdMaticUsdc, - 0 + 0, + [setIdMaticUsdc, '0', '0', '0', '0'], + [setIdMaticUsdc, '0', '0', '0', '0'] ], }, ]; @@ -271,23 +274,23 @@ describe("Fluid", function () { ); }); - // 0 matic + // // 0 matic, 20 weth, 20 usdc - it("should deposit 10 Weth in Fluid", async function () { - const amtDeposit = parseEther("10"); + it("should deposit 9 Weth in Fluid in WETH-USDC", async function () { + const amtDeposit = parseEther("9"); const spells = [ { connector: connectorName, - method: "operate", + method: "operateWithIds", args: [ vaultWethUsdc, - 0, // new nft + 0, // New nft for WETH-USDC market amtDeposit, // +10 collateral 0, // 0 debt - dsaWallet0.address, - 0, - setIdWethUsdc + 0, // 0 repay + ['0', '0', '0', '0', '0'], + [setIdWethUsdc, '0', '0', '0', '0'] ], }, ]; @@ -298,58 +301,86 @@ describe("Fluid", function () { await tx.wait(); - expect(await wethToken.balanceOf(dsaWallet0.address)).to.lte( + expect(await wethToken.connect(wallet0).balanceOf(dsaWallet0.address)).to.lte( + parseEther("11") + ); + }); + + // // 0 matic, 11 weth, 20 usdc + + it("should deposit max Weth in Fluid in WETH-USDC", async function () { + const spells = [ + { + connector: connectorName, + method: "operateWithIds", + args: [ + vaultWethUsdc, + 0, // get nft id + ethers.constants.MaxInt256, // + max collateral + 0, // 0 debt + 0, + [setIdWethUsdc, '0', '0', '0', '0'], + [setIdWethUsdc, '0', '0', '0', '0'] + ], + }, + ]; + + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); + + await tx.wait(); + + expect(await wethToken.connect(wallet0).balanceOf(dsaWallet0.address)).to.lte( + parseEther("1") + ); + }); + + // // 0 matic, 0 weth, 20 usdc + + it("should deposit 10 USDC in Fluid in USDC-MATIC", async function () { + const spells = [ + { + connector: connectorName, + method: "operate", + args: [ + vaultUsdcMatic, + 0, // get nft id + parseUnits("10", 6), // + max collateral + 0, // 0 debt + 0 + ], + }, + ]; + + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); + + await tx.wait(); + + expect(await usdcToken.connect(wallet0).balanceOf(dsaWallet0.address)).to.lte( parseEther("10") ); }); - // 10 weth + // // 0 matic, 0 weth, 10 usdc + + it("Should borrow 0.1 USDC from Fluid in WETH-USDC", async function () { + const amtBorrow = parseUnits("0.1", 6); // 0.1 USDC - it("should deposit max Weth in Fluid", async function () { const spells = [ { connector: connectorName, - method: "operate", + method: "operateWithIds", args: [ vaultWethUsdc, - 0, // get id nft - ethers.constants.MaxUint256, // + max collateral - 0, // 0 debt - dsaWallet0.address, - setIdWethUsdc, - 0 - ], - }, - ]; - - const tx = await dsaWallet0 - .connect(wallet0) - .cast(...encodeSpells(spells), wallet1.getAddress()); - - await tx.wait(); - - expect(await ethers.provider.getBalance(dsaWallet0.address)).to.lte( - parseEther("1") - ); - }); - - // 0 weth - - it("Should borrow USDC from Fluid", async function () { - const amtBorrow = parseUnits("100", 6); // 100 USDC - - const spells = [ - { - connector: connectorName, - method: "operate", - args: [ - vaultMaticUsdc, - 0, // nft id from getID + 0, // nft ID from getID 0, // 0 collateral - amtBorrow, // +100 debt - dsaWallet0.address, - setIdMaticUsdc, - 0 + amtBorrow, // +0.1 debt + 0, + [setIdWethUsdc, '0', '0', '0', '0'], + [setIdWethUsdc, '0', '0', '0', '0'] ], }, ]; @@ -359,14 +390,14 @@ describe("Fluid", function () { .cast(...encodeSpells(spells), wallet1.getAddress()); await tx.wait(); - expect(await usdcToken.balanceOf(dsaWallet0.address)).to.be.gte( - parseUnits("120", 6) + expect(await usdcToken.connect(wallet0).balanceOf(dsaWallet0.address)).to.gte( + parseUnits("10", 6) ); }); - // 120 usdc + // // 0 matic, 0 weth, 10.1 usdc - it("Should deposit WETH and borrow MATIC from Fluid", async function () { + it("Should deposit 5 WETH and borrow 0.1 MATIC from Fluid in WETH-MATIC", async function () { await hre.network.provider.request({ method: "hardhat_impersonateAccount", params: [wethHolder] @@ -374,23 +405,23 @@ describe("Fluid", function () { wethHolderSigner = await ethers.getSigner(wethHolder); - await wethToken.connect(wethHolderSigner).transfer(dsaWallet0.address, ethers.utils.parseEther("11")); + await wethToken.connect(wethHolderSigner).transfer(dsaWallet0.address, ethers.utils.parseEther("10")); - const amtDeposit = parseEther("10"); // 10 Weth - const amtBorrow = parseEther("100"); // 100 Matic + const amtDeposit = parseEther("5"); // 5 Weth + const amtBorrow = parseEther("0.1"); // 100 Matic const spells = [ { connector: connectorName, - method: "operate", + method: "operateWithIds", args: [ vaultWethMatic, 0, // new nft id amtDeposit, // 10 collateral amtBorrow, // +100 debt - dsaWallet0.address, 0, - setId3 + ['0', '0', '0', '0', '0'], + [setId3, '0', '0', '0', '0'] ], }, ]; @@ -401,27 +432,76 @@ describe("Fluid", function () { await tx.wait(); expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.gte( - parseEther("100") + parseEther("0.099") + ); + + expect(await wethToken.connect(wallet0).balanceOf(dsaWallet0.address)).to.lte( + parseEther("6") ); }); - // 120 usdc, 100 matic + // // 0.1 matic, 5 weth, 10.1 usdc - it("Should payback Matic in Fluid", async function () { - const amtPayback = ethers.BigNumber.from(parseEther("50")).mul(-1); // 50 Matic + it("Should deposit 5 WETH and borrow 0.1 MATIC from Fluid in WETH-MATIC", async function () { + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [wethHolder] + }); + + wethHolderSigner = await ethers.getSigner(wethHolder); + + await wethToken.connect(wethHolderSigner).transfer(dsaWallet0.address, ethers.utils.parseEther("10")); + + const amtDeposit = parseEther("5"); // 5 Weth + const amtBorrow = parseEther("0.1"); // 0.1 Matic + + const spells = [ + { + connector: connectorName, + method: "operateWithIds", + args: [ + vaultWethMatic, + 0, // new nft id + amtDeposit, // 10 collateral + amtBorrow, // +0.1 debt + 0, + ['0', '0', '0', '0', '0'], + [setId4, '0', '0', '0', '0'] + ], + }, + ]; + + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); + await tx.wait(); + + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.gte( + parseEther("0.19") + ); + + expect(await wethToken.connect(wallet0).balanceOf(dsaWallet0.address)).to.lte( + parseEther("11") + ); + }); + + // // 0.2 matic, 10 weth, 10.1 usdc + + it("Should payback 0.04 Matic in Fluid in WETH-MATIC", async function () { + const amtPayback = new BigNumber(parseEther("0.04").toString()).multipliedBy(-1); // 0.04 Matic const spells = [ { connector: connectorName, - method: "operate", + method: "operateWithIds", args: [ vaultWethMatic, 0, // nft id from setId3 0, // 0 collateral - amtPayback, // - 50 debt - dsaWallet0.address, - setId3, - 0 + amtPayback, // - 0.04 debt + new BigNumber(parseEther("0.04").toString()), + [setId3, '0', '0', '0', '0'], + [setId3, '0', '0', '0', '0'] ], }, ]; @@ -432,56 +512,61 @@ describe("Fluid", function () { await tx.wait(); expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.lte( - ethers.utils.parseEther("50") + ethers.utils.parseEther("0.2") ); }); - // 120 usdc, 50 matic + // // 0.16 matic, 10 weth, 10.1 usdc - it("Should payback max Matic in Fluid", async function () { - const spells = [ - { - connector: connectorName, - method: "operate", - args: [ - vaultWethMatic, - 0, // nft id from setId3 - 0, // 0 collateral - ethers.constants.MinInt256, // min Int - dsaWallet0.address, - setId3, - 0 - ], - }, - ]; + // it("Should payback max Matic in Fluid in WETH-MATIC", async function () { + // await wallet0.sendTransaction({ + // to: dsaWallet0.address, + // value: parseEther("1"), + // }); - const tx = await dsaWallet0 - .connect(wallet0) - .cast(...encodeSpells(spells), wallet1.getAddress()); - await tx.wait(); + // const spells = [ + // { + // connector: connectorName, + // method: "operateWithIds", + // args: [ + // vaultWethMatic, + // 0, // nft id from setId3 + // 0, // 0 collateral + // ethers.constants.MinInt256, // min Int + // new BigNumber(parseEther("0.7").toString()), + // [setId3, '0', '0', '0', '0'], + // ['0', '0', '0', '0', '0'] + // ], + // }, + // ]; + + // const tx = await dsaWallet0 + // .connect(wallet0) + // .cast(...encodeSpells(spells), wallet1.getAddress()); + // await tx.wait(); - expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.lte( - ethers.utils.parseEther("1") - ); - }); + // // expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.lte( + // // ethers.utils.parseEther("0.97") + // // ); + // }); - // 120 usdc, 0 matic + // // 0.96 matic, 10 weth, 10.1 usdc - it("Should payback Usdc in Fluid", async function () { - const amtPayback = ethers.BigNumber.from(parseUnits("60", 6)).mul(-1); // 60 usdc + it("Should payback 0.05 Usdc in Fluid in WETH-USDC", async function () { + const amtPayback = new BigNumber(parseUnits("0.05", 6).toString()).multipliedBy(-1); // 0.05 usdc const spells = [ { connector: connectorName, - method: "operate", + method: "operateWithIds", args: [ - vaultMaticUsdc, + vaultWethUsdc, 0, // nft id from setIdWethUsdc 0, // 0 collateral - amtPayback, // - 60 debt - dsaWallet0.address, - setIdMaticUsdc, - 0 + amtPayback, // - 0.05 debt + new BigNumber(parseUnits("0.05", 6).toString()), + [setIdWethUsdc, '0', '0', '0', '0'], + [setIdWethUsdc, '0', '0', '0', '0'] ], }, ]; @@ -491,53 +576,26 @@ describe("Fluid", function () { .cast(...encodeSpells(spells), wallet1.getAddress()); await tx.wait(); - expect(await usdcToken.balanceOf(dsaWallet0.address)).to.be.lte(parseUnits("60", 6)); + expect(await usdcToken.connect(wallet0).balanceOf(dsaWallet0.address)).to.be.lte(parseUnits("10.05", 6)); }); - // 60 usdc, 0 matic + // // 0.96 matic, 10 weth, 10.05 usdc - it("Should payback max Matic in Fluid", async function () { - const spells = [ - { - connector: connectorName, - method: "operate", - args: [ - vaultMaticUsdc, - 0, // nft id from setIdWethUsdc - 0, // 0 collateral - ethers.constants.MinInt256, // min Int - dsaWallet0.address, - setIdMaticUsdc, - 0 - ], - }, - ]; - - const tx = await dsaWallet0 - .connect(wallet0) - .cast(...encodeSpells(spells), wallet1.getAddress()); - await tx.wait(); - - expect(await usdcToken.balanceOf(dsaWallet0.address)).to.be.lte(parseUnits("1", 6)); - }); - - // 0 usdc, 0 matic - - it("Should withdraw Matic from Fluid", async function () { - const amt = ethers.BigNumber.from(parseEther("100")).mul(-1); // 100 Matic + it("Should withdraw 100 Matic from Fluid in MATIC-USDC", async function () { + const amt = new BigNumber(parseEther("100").toString()).multipliedBy(-1); // 100 Matic const spells = [ { connector: connectorName, - method: "operate", + method: "operateWithIds", args: [ vaultMaticUsdc, 0, // nft id from setIdMaticUsdc amt, // - 100 collateral 0, // 0 debt - dsaWallet0.address, - setIdMaticUsdc, - 0 + 0, + [setIdMaticUsdc, '0', '0', '0', '0'], + [setIdMaticUsdc, '0', '0', '0', '0'] ], }, ]; @@ -548,58 +606,58 @@ describe("Fluid", function () { await tx.wait(); - expect(await ethers.provider.getBalance(dsaWallet0.address)).to.eq( + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.gte( parseEther("100") ); }); - // 0 usdc, 100 matic + // // 100.96 matic, 10 weth, 10.05 usdc - it("Should withdraw max Matic from Fluid", async function () { - const spells = [ - { - connector: connectorName, - method: "operate", - args: [ - vaultMaticUsdc, - 0, // nft id from setIdMaticUsdc - ethers.constants.MinInt256, // min integer value - 0, // 0 debt - dsaWallet0.address, - setIdMaticUsdc, - 0 - ], - }, - ]; + // it("Should withdraw max Matic from Fluid in MATIC-USDC", async function () { + // const spells = [ + // { + // connector: connectorName, + // method: "operateWithIds", + // args: [ + // vaultMaticUsdc, + // 0, // nft id from setIdMaticUsdc + // ethers.constants.MinInt256, // min integer value + // 0, // 0 debt + // 0, + // [setIdMaticUsdc, '0', '0', '0', '0'], + // ['0', '0', '0', '0', '0'] + // ], + // }, + // ]; - const tx = await dsaWallet0 - .connect(wallet0) - .cast(...encodeSpells(spells), wallet1.getAddress()); + // const tx = await dsaWallet0 + // .connect(wallet0) + // .cast(...encodeSpells(spells), wallet1.getAddress()); - await tx.wait(); + // await tx.wait(); - expect(await ethers.provider.getBalance(dsaWallet0.address)).to.eq( - parseEther("1000") - ); - }); + // expect(await ethers.provider.getBalance(dsaWallet0.address)).to.eq( + // parseEther("2000") + // ); + // }); - // 0 usdc, 1000 matic + // // 2000.96 matic, 10 weth, 10.05 usdc - it("Should withdraw WETH from Fluid", async function () { - const amt = ethers.BigNumber.from(parseEther("10")).mul(-1); // 10 Weth + it("Should withdraw 0.4 WETH from Fluid in WETH-USDC", async function () { + const amt = new BigNumber(parseEther("0.4").toString()).multipliedBy(-1); // 1 Weth const spells = [ { connector: connectorName, - method: "operate", + method: "operateWithIds", args: [ vaultWethUsdc, 0, // nft id from setIdWethUsdc - amt, // -10 collateral + amt, // -1 collateral 0, // 0 debt - dsaWallet0.address, - setIdWethUsdc, - 0 + 0, + [setIdWethUsdc, '0', '0', '0', '0'], + [setIdWethUsdc, '0', '0', '0', '0'] ], }, ]; @@ -610,26 +668,29 @@ describe("Fluid", function () { await tx.wait(); - expect(await wethToken.balanceOf(dsaWallet0.address)).to.eq( - parseEther("10") - ); + // expect(await wethToken.connect(wallet0).balanceOf(dsaWallet0.address)).to.gte( + // parseEther("11") + // ); }); - // 0 usdc, 1000 matic, 10 weth + // // 2000.96 matic, 11 weth, 10.05 usdc + + it("Should payback 0.02 and withdraw 0.5 WETH from Fluid in WETH-USDC", async function () { + const amt = new BigNumber(parseEther("0.5").toString()).multipliedBy(-1); // 1 Weth + const paybackAmt = new BigNumber(parseUnits("0.02", 6).toString()).multipliedBy(-1); // 1 Weth - it("Should withdraw max WETH from Fluid", async function () { const spells = [ { connector: connectorName, - method: "operate", + method: "operateWithIds", args: [ vaultWethUsdc, 0, // nft id from setIdWethUsdc - ethers.constants.MinInt256, // min integer value - 0, // 0 debt - dsaWallet0.address, - setIdWethUsdc, - 0 + amt, // -1 collateral + paybackAmt, // 0 debt + new BigNumber(parseUnits("0.03", 6).toString()), + [setIdWethUsdc, '0', '0', '0', '0'], + [setIdWethUsdc, '0', '0', '0', '0'] ], }, ]; @@ -640,12 +701,14 @@ describe("Fluid", function () { await tx.wait(); - expect(await ethers.provider.getBalance(dsaWallet0.address)).to.eq( - parseEther("29") - ); - }); + // expect(await wethToken.connect(wallet0).balanceOf(dsaWallet0.address)).to.gte( + // parseEther("12") + // ); - // 0 usdc, 1000 matic, 30 weth + // expect(await usdcToken.connect(wallet0).balanceOf(dsaWallet0.address)).to.gte( + // parseEther("11") + // ); + }); // todo: add a (payback and withdraw case) });