diff --git a/contracts/polygon/common/basic.sol b/contracts/polygon/common/basic.sol new file mode 100644 index 0000000..2929933 --- /dev/null +++ b/contracts/polygon/common/basic.sol @@ -0,0 +1,55 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import { TokenInterface } from "./interfaces.sol"; +import { Stores } from "./stores.sol"; +import { DSMath } from "./math.sol"; + +abstract contract Basic is DSMath, Stores { + + function convert18ToDec(uint _dec, uint256 _amt) internal pure returns (uint256 amt) { + amt = (_amt / 10 ** (18 - _dec)); + } + + function convertTo18(uint _dec, uint256 _amt) internal pure returns (uint256 amt) { + amt = mul(_amt, 10 ** (18 - _dec)); + } + + function getTokenBal(TokenInterface token) internal view returns(uint _amt) { + _amt = address(token) == maticAddr ? address(this).balance : token.balanceOf(address(this)); + } + + function getTokensDec(TokenInterface buyAddr, TokenInterface sellAddr) internal view returns(uint buyDec, uint sellDec) { + buyDec = address(buyAddr) == maticAddr ? 18 : buyAddr.decimals(); + sellDec = address(sellAddr) == maticAddr ? 18 : sellAddr.decimals(); + } + + function encodeEvent(string memory eventName, bytes memory eventParam) internal pure returns (bytes memory) { + return abi.encode(eventName, eventParam); + } + + function changeMaticAddress(address buy, address sell) internal pure returns(TokenInterface _buy, TokenInterface _sell){ + _buy = buy == maticAddr ? TokenInterface(wmaticAddr) : TokenInterface(buy); + _sell = sell == maticAddr ? TokenInterface(wmaticAddr) : TokenInterface(sell); + } + + function approve(TokenInterface token, address spender, uint256 amount) internal { + try token.approve(spender, amount) { + + } catch { + token.approve(spender, 0); + token.approve(spender, amount); + } + } + + function convertMaticToWmatic(bool isMatic, TokenInterface token, uint amount) internal { + if(isMatic) token.deposit{value: amount}(); + } + + function convertWmaticToMatic(bool isMatic, TokenInterface token, uint amount) internal { + if(isMatic) { + approve(token, address(token), amount); + token.withdraw(amount); + } + } +} diff --git a/contracts/polygon/common/interfaces.sol b/contracts/polygon/common/interfaces.sol new file mode 100644 index 0000000..4b2e6eb --- /dev/null +++ b/contracts/polygon/common/interfaces.sol @@ -0,0 +1,43 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +interface TokenInterface { + function approve(address, uint256) external; + function transfer(address, uint) external; + function transferFrom(address, address, uint) external; + function deposit() external payable; + function withdraw(uint) external; + function balanceOf(address) external view returns (uint); + function decimals() external view returns (uint); + function allowance(address owner, address spender) external view returns (uint256); +} + +interface MemoryInterface { + function getUint(uint id) external returns (uint num); + function setUint(uint id, uint val) external; +} + +interface InstaMapping { + function cTokenMapping(address) external view returns (address); + function gemJoinMapping(bytes32) external view returns (address); +} + +interface AccountInterface { + function enable(address) external; + function disable(address) external; + function isAuth(address) external view returns (bool); + function cast( + string[] calldata _targetNames, + bytes[] calldata _datas, + address _origin + ) external payable returns (bytes32[] memory responses); +} + +interface ListInterface { + function accountID(address) external returns (uint64); +} + +interface InstaConnectors { + function isConnectors(string[] calldata) external returns (bool, address[] memory); +} + diff --git a/contracts/polygon/common/math.sol b/contracts/polygon/common/math.sol new file mode 100644 index 0000000..a61586b --- /dev/null +++ b/contracts/polygon/common/math.sol @@ -0,0 +1,51 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; + +contract DSMath { + uint constant WAD = 10 ** 18; + uint constant RAY = 10 ** 27; + + function add(uint x, uint y) internal pure returns (uint z) { + z = SafeMath.add(x, y); + } + + function sub(uint x, uint y) internal virtual pure returns (uint z) { + z = SafeMath.sub(x, y); + } + + function mul(uint x, uint y) internal pure returns (uint z) { + z = SafeMath.mul(x, y); + } + + function div(uint x, uint y) internal pure returns (uint z) { + z = SafeMath.div(x, y); + } + + function wmul(uint x, uint y) internal pure returns (uint z) { + z = SafeMath.add(SafeMath.mul(x, y), WAD / 2) / WAD; + } + + function wdiv(uint x, uint y) internal pure returns (uint z) { + z = SafeMath.add(SafeMath.mul(x, WAD), y / 2) / y; + } + + function rdiv(uint x, uint y) internal pure returns (uint z) { + z = SafeMath.add(SafeMath.mul(x, RAY), y / 2) / y; + } + + function rmul(uint x, uint y) internal pure returns (uint z) { + z = SafeMath.add(SafeMath.mul(x, y), RAY / 2) / RAY; + } + + function toInt(uint x) internal pure returns (int y) { + y = int(x); + require(y >= 0, "int-overflow"); + } + + function toRad(uint wad) internal pure returns (uint rad) { + rad = mul(wad, 10 ** 27); + } + +} diff --git a/contracts/polygon/common/stores.sol b/contracts/polygon/common/stores.sol new file mode 100644 index 0000000..a213010 --- /dev/null +++ b/contracts/polygon/common/stores.sol @@ -0,0 +1,48 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import { MemoryInterface, InstaMapping, ListInterface, InstaConnectors } from "./interfaces.sol"; + + +abstract contract Stores { + + /** + * @dev Return ethereum address + */ + address constant internal maticAddr = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /** + * @dev Return Wrapped ETH address + */ + address constant internal wmaticAddr = 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270; + + /** + * @dev Return memory variable address + */ + MemoryInterface constant internal instaMemory = MemoryInterface(0x6C7256cf7C003dD85683339F75DdE9971f98f2FD); + + /** + * @dev Return InstaList address + */ + ListInterface internal constant instaList = ListInterface(0x839c2D3aDe63DF5b0b8F3E57D5e145057Ab41556); + + /** + * @dev Return connectors registry address + */ + InstaConnectors internal constant instaConnectors = InstaConnectors(0x2A00684bFAb9717C21271E0751BCcb7d2D763c88); + + /** + * @dev Get Uint value from InstaMemory Contract. + */ + function getUint(uint getId, uint val) internal returns (uint returnVal) { + returnVal = getId == 0 ? val : instaMemory.getUint(getId); + } + + /** + * @dev Set Uint value in InstaMemory Contract. + */ + function setUint(uint setId, uint val) virtual internal { + if (setId != 0) instaMemory.setUint(setId, val); + } + +} diff --git a/contracts/polygon/connectors/fluid/events.sol b/contracts/polygon/connectors/fluid/events.sol new file mode 100644 index 0000000..3fcb301 --- /dev/null +++ b/contracts/polygon/connectors/fluid/events.sol @@ -0,0 +1,13 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +contract Events { + event LogOperate( + address vaultAddress, + uint256 nftId, + int256 newCol, + int256 newDebt, + uint256[] getIds, + uint256[] setIds + ); +} diff --git a/contracts/polygon/connectors/fluid/interface.sol b/contracts/polygon/connectors/fluid/interface.sol new file mode 100644 index 0000000..a7eef73 --- /dev/null +++ b/contracts/polygon/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/polygon/connectors/fluid/main.sol b/contracts/polygon/connectors/fluid/main.sol new file mode 100644 index 0000000..181c428 --- /dev/null +++ b/contracts/polygon/connectors/fluid/main.sol @@ -0,0 +1,159 @@ +//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 getMaticAddr() 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. + * @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( + 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; + + if (newCol_ > 0) { + if (vaultDetails_.supplyToken == getMaticAddr()) { + ethAmount_ = isColMax_ + ? address(this).balance + : uint256(newCol_); + } else { + if (isColMax_) { + newCol_ = int256( + TokenInterface(vaultDetails_.supplyToken).balanceOf( + address(this) + ) + ); + } + + TokenInterface(vaultDetails_.supplyToken).approve( + vaultAddress_, + uint256(newCol_) + ); + } + } + + bool isPaybackMax_ = newDebt_ == type(int256).min; + + if (newDebt_ < 0) { + if (vaultDetails_.borrowToken == getMaticAddr()) { + ethAmount_ = isPaybackMax_ + ? repayApproveAmt_ + : uint256(-1 * newDebt_); + } else { + isPaybackMax_ + ? TokenInterface(vaultDetails_.borrowToken).approve( + vaultAddress_, + repayApproveAmt_ + ) + : TokenInterface(vaultDetails_.borrowToken).approve( + vaultAddress_, + uint256(-1 * newDebt_) + ); + } + } + + (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 = "LogOperate(address,uint256,int256,int256,uint256[],uint256[])"; + _eventParam = abi.encode( + vaultAddress_, + nftId_, + newCol_, + newDebt_, + getIds_, + setIds_ + ); + } +} + +contract ConnectV2FluidPolygon is FluidConnector { + string public constant name = "Fluid-v1.0"; +} diff --git a/test/mainnet/fluid/fluid.test.ts b/test/mainnet/fluid/fluid.test.ts new file mode 100644 index 0000000..d8058b8 --- /dev/null +++ b/test/mainnet/fluid/fluid.test.ts @@ -0,0 +1,226 @@ +import { expect } from "chai"; +import hre from "hardhat"; +import { abis } from "../../../scripts/constant/abis"; +import { addresses } from "../../../scripts/tests/mainnet/addresses"; +import { deployAndEnableConnector } from "../../../scripts/tests/deployAndEnableConnector"; +import { getMasterSigner } from "../../../scripts/tests/getMasterSigner"; +import { buildDSAv2 } from "../../../scripts/tests/buildDSAv2"; +import { ConnectV2Fluid, ConnectV2Fluid__factory } from "../../../typechain"; +import { parseEther } from "@ethersproject/units"; +import { encodeSpells } from "../../../scripts/tests/encodeSpells"; +import { tokens } from "../../../scripts/tests/mainnet/tokens"; +import { constants } from "../../../scripts/constant/constant"; +import { addLiquidity } from "../../../scripts/tests/addLiquidity"; +const { ethers } = hre; +import type { Signer, Contract } from "ethers"; + +describe("Fluid", function () { + const connectorName = "FLUID"; + let connector: any; + + let wallet0: Signer, wallet1:Signer; + let dsaWallet0: any; + let instaConnectorsV2: Contract; + let masterSigner: Signer; + before(async () => { + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + // @ts-ignore + jsonRpcUrl: hre.config.networks.hardhat.forking.url, + blockNumber: 12796965, + }, + }, + ], + }); + [wallet0, wallet1] = await ethers.getSigners(); + masterSigner = await getMasterSigner(); + instaConnectorsV2 = await ethers.getContractAt( + abis.core.connectorsV2, + addresses.core.connectorsV2 + ); + connector = await deployAndEnableConnector({ + connectorName, + contractArtifact: ConnectV2Fluid__factory, + signer: masterSigner, + connectors: instaConnectorsV2, + }); + console.log("Connector address", connector.address); + }); + + it("should have contracts deployed", async () => { + expect(!!instaConnectorsV2.address).to.be.true; + expect(!!connector.address).to.be.true; + expect(!!(await masterSigner.getAddress())).to.be.true; + }); + + describe("DSA wallet setup", function () { + it("Should build DSA v2", async function () { + dsaWallet0 = await buildDSAv2(wallet0.getAddress()); + expect(!!dsaWallet0.address).to.be.true; + }); + + it("Deposit ETH into DSA wallet", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: parseEther("10"), + }); + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.gte( + parseEther("10") + ); + }); + }); + + describe("Main", function () { + it("should deposit ETH in Aave V2", async function () { + const amt = parseEther("1"); + const spells = [ + { + connector: connectorName, + method: "deposit", + args: [tokens.eth.address, amt, 0, 0], + }, + ]; + + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); + + await tx.wait(); + + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.eq( + parseEther("9") + ); + }); + + it("Should borrow and payback DAI from Aave V2", async function () { + const amt = parseEther("100"); // 100 DAI + const setId = "83478237"; + const spells = [ + { + connector: connectorName, + method: "borrow", + args: [tokens.dai.address, amt, 2, 0, setId], + }, + { + connector: connectorName, + method: "payback", + args: [tokens.dai.address, amt, 2, setId, 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("9") + ); + }); + + it("Should borrow and payback half DAI from Aave V2", async function () { + const amt = parseEther("100"); // 100 DAI + // const setId = "83478237"; + await addLiquidity("dai", dsaWallet0.address, parseEther("1")); + let spells = [ + { + connector: connectorName, + method: "borrow", + args: [tokens.dai.address, amt, 2, 0, 0], + }, + { + connector: connectorName, + method: "payback", + args: [tokens.dai.address, amt.div(2), 2, 0, 0], + }, + ]; + + let 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("9") + ); + + spells = [ + { + connector: connectorName, + method: "payback", + args: [tokens.dai.address, constants.max_value, 2, 0, 0], + }, + ]; + + 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("9") + ); + }); + + it("Should deposit all ETH in Aave V2", async function () { + const spells = [ + { + connector: connectorName, + method: "deposit", + args: [tokens.eth.address, constants.max_value, 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("0") + ); + }); + + it("Should withdraw all ETH from Aave V2", async function () { + const spells = [ + { + connector: connectorName, + method: "withdraw", + args: [tokens.eth.address, constants.max_value, 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( + ethers.utils.parseEther("10") + ); + }); + + it("should deposit and withdraw", async () => { + const amt = parseEther("1"); // 1 eth + const setId = "834782373"; + const spells = [ + { + connector: connectorName, + method: "deposit", + args: [tokens.eth.address, amt, 0, setId], + }, + { + connector: connectorName, + method: "withdraw", + args: [tokens.eth.address, amt, setId, 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( + ethers.utils.parseEther("10") + ); + }); + }); +}); diff --git a/test/polygon/fluid/fluid.test.ts b/test/polygon/fluid/fluid.test.ts new file mode 100644 index 0000000..d09deeb --- /dev/null +++ b/test/polygon/fluid/fluid.test.ts @@ -0,0 +1,652 @@ +import { expect } from "chai"; +import hre from "hardhat"; +import { abis } from "../../../scripts/constant/abis"; +import { addresses } from "../../../scripts/tests/mainnet/addresses"; +import { deployAndEnableConnector } from "../../../scripts/tests/deployAndEnableConnector"; +import { getMasterSigner } from "../../../scripts/tests/getMasterSigner"; +import { buildDSAv2 } from "../../../scripts/tests/buildDSAv2"; +import { ConnectV2FluidPolygon, ConnectV2FluidPolygon__factory } from "../../../typechain"; +import { parseEther, parseUnits } from "@ethersproject/units"; +import { encodeSpells } from "../../../scripts/tests/encodeSpells"; +import { constants } from "../../../scripts/constant/constant"; +import { network, ethers } from "hardhat"; +import type { Signer, Contract } from "ethers"; + +describe("Fluid", function () { + const connectorName = "FLUID"; + let connector: any; + + let wallet0: Signer, wallet1: Signer, wethHolderSigner: Signer, usdcHolderSigner: Signer; + let dsaWallet0: any; + let instaConnectorsV2: any; + let masterSigner: Signer; + const setIdMaticUsdc = "83478237"; + const setIdWethUsdc = "83478249"; + const setId3 = "85478249"; + + const vaultMaticUsdc = "0x2226FFAE044B9fd4ED991aDf20CAACF8E8302510"; + const vaultWethUsdc = "0x10D97a8236624222F681C12Eea4Ddac2BDD0471B"; + const vaultWethMatic = "0x553437CB882E3aFbB67Abd135E067AFB0721fbf1"; + + const wethHolder = "0xdeD8C5159CA3673f543D0F72043E4c655b35b96A"; + const usdcHolder = "0xEc4Db437ADEE0420A28Afa8B87c74D3901F56AC4"; + + const WETH = "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619"; + const USDC = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; + + const erc20Abi = [ + { + constant: false, + inputs: [ + { + name: "_spender", + type: "address" + }, + { + name: "_value", + type: "uint256" + } + ], + name: "approve", + outputs: [ + { + name: "", + type: "bool" + } + ], + payable: false, + stateMutability: "nonpayable", + type: "function" + }, + { + constant: true, + inputs: [], + name: "totalSupply", + outputs: [ + { + name: "", + type: "uint256" + } + ], + payable: false, + stateMutability: "view", + type: "function" + }, + { + constant: true, + inputs: [ + { + name: "_owner", + type: "address" + } + ], + name: "balanceOf", + outputs: [ + { + name: "balance", + type: "uint256" + } + ], + payable: false, + stateMutability: "view", + type: "function" + }, + { + constant: false, + inputs: [ + { + name: "_to", + type: "address" + }, + { + name: "_value", + type: "uint256" + } + ], + name: "transfer", + outputs: [ + { + name: "", + type: "bool" + } + ], + payable: false, + stateMutability: "nonpayable", + type: "function" + } + ]; + + const wethToken = new ethers.Contract(WETH, erc20Abi); + const usdcToken = new ethers.Contract(USDC, erc20Abi); + + before(async () => { + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + // @ts-ignore + jsonRpcUrl: hre.config.networks.hardhat.forking.url, + blockNumber: 12796965, + }, + }, + ], + }); + + [wallet0, wallet1] = await ethers.getSigners(); + masterSigner = await getMasterSigner(); + instaConnectorsV2 = await ethers.getContractAt( + abis.core.connectorsV2, + addresses.core.connectorsV2, + masterSigner + ); + connector = await deployAndEnableConnector({ + connectorName, + contractArtifact: ConnectV2FluidPolygon__factory, + signer: masterSigner, + connectors: instaConnectorsV2, + }); + console.log("Connector address", connector.address); + }); + + it("should have contracts deployed", async () => { + expect(!!instaConnectorsV2.address).to.be.true; + expect(!!connector.address).to.be.true; + expect(!!(await masterSigner.getAddress())).to.be.true; + }); + + describe("DSA wallet setup", function () { + it("Should build DSA v2", async function () { + dsaWallet0 = await buildDSAv2(wallet0.getAddress()); + expect(!!dsaWallet0.address).to.be.true; + }); + + it("Deposit 2000 Matic into DSA wallet", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: parseEther("2000"), + }); + + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.gte(ethers.utils.parseEther("2000")); + + }); + + it("Deposit 20 Weth into DSA wallet", async function () { + await wallet0.sendTransaction({ + to: wethHolder, + value: parseEther("200"), + }); + + 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("20")); + + expect(await wethToken.balanceOf(dsaWallet0.address)).to.be.gte(ethers.utils.parseEther("20")); + + }); + + it("Deposit 20 Usdc into DSA wallet", async function () { + await wallet0.sendTransaction({ + to: usdcHolder, + value: parseEther("200"), + }); + + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [usdcHolder] + }); + + usdcHolderSigner = await ethers.getSigner(usdcHolder); + + await usdcToken.connect(usdcHolderSigner).transfer(dsaWallet0.address, parseUnits("20", 6)); + + expect(await usdcToken.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 () { + const amtDeposit = parseEther("1000"); + + const spells = [ + { + connector: connectorName, + method: "operate", + args: [ + vaultMaticUsdc, + 0, // new nft + amtDeposit, // +1000 collateral + 0, // 0 debt + dsaWallet0.address, + 0, + setIdMaticUsdc + ], + }, + ]; + + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); + + await tx.wait(); + + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.gte( + parseEther("1000") + ); + }); + // 1000 matic + + it("should deposit max Matic in Fluid", async function () { + const spells = [ + { + connector: connectorName, + method: "operate", + args: [ + vaultMaticUsdc, // matic-usdc vault + 0, // setIdMaticUsdc + ethers.constants.MaxUint256, // + max collateral + 0, // 0 debt + dsaWallet0.address, + setIdMaticUsdc, + 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 matic + + it("should deposit 10 Weth in Fluid", async function () { + const amtDeposit = parseEther("10"); + + const spells = [ + { + connector: connectorName, + method: "operate", + args: [ + vaultWethUsdc, + 0, // new nft + amtDeposit, // +10 collateral + 0, // 0 debt + dsaWallet0.address, + 0, + setIdWethUsdc + ], + }, + ]; + + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); + + await tx.wait(); + + expect(await wethToken.balanceOf(dsaWallet0.address)).to.lte( + parseEther("10") + ); + }); + + // 10 weth + + it("should deposit max Weth in Fluid", async function () { + const spells = [ + { + connector: connectorName, + method: "operate", + 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, // 0 collateral + amtBorrow, // +100 debt + 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.gte( + parseUnits("120", 6) + ); + }); + + // 120 usdc + + it("Should deposit WETH and borrow MATIC from Fluid", 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("11")); + + const amtDeposit = parseEther("10"); // 10 Weth + const amtBorrow = parseEther("100"); // 100 Matic + + const spells = [ + { + connector: connectorName, + method: "operate", + args: [ + vaultWethMatic, + 0, // new nft id + amtDeposit, // 10 collateral + amtBorrow, // +100 debt + dsaWallet0.address, + 0, + setId3 + ], + }, + ]; + + 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("100") + ); + }); + + // 120 usdc, 100 matic + + it("Should payback Matic in Fluid", async function () { + const amtPayback = ethers.BigNumber.from(parseEther("50")).mul(-1); // 50 Matic + + const spells = [ + { + connector: connectorName, + method: "operate", + args: [ + vaultWethMatic, + 0, // nft id from setId3 + 0, // 0 collateral + amtPayback, // - 50 debt + dsaWallet0.address, + setId3, + 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("50") + ); + }); + + // 120 usdc, 50 matic + + 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 + ], + }, + ]; + + 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") + ); + }); + + // 120 usdc, 0 matic + + it("Should payback Usdc in Fluid", async function () { + const amtPayback = ethers.BigNumber.from(parseUnits("60", 6)).mul(-1); // 60 usdc + + const spells = [ + { + connector: connectorName, + method: "operate", + args: [ + vaultMaticUsdc, + 0, // nft id from setIdWethUsdc + 0, // 0 collateral + amtPayback, // - 60 debt + 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("60", 6)); + }); + + // 60 usdc, 0 matic + + 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 + + const spells = [ + { + connector: connectorName, + method: "operate", + args: [ + vaultMaticUsdc, + 0, // nft id from setIdMaticUsdc + amt, // - 100 collateral + 0, // 0 debt + dsaWallet0.address, + setIdMaticUsdc, + 0 + ], + }, + ]; + + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); + + await tx.wait(); + + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.eq( + parseEther("100") + ); + }); + + // 0 usdc, 100 matic + + 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 + ], + }, + ]; + + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); + + await tx.wait(); + + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.eq( + parseEther("1000") + ); + }); + + // 0 usdc, 1000 matic + + it("Should withdraw WETH from Fluid", async function () { + const amt = ethers.BigNumber.from(parseEther("10")).mul(-1); // 10 Weth + + const spells = [ + { + connector: connectorName, + method: "operate", + args: [ + vaultWethUsdc, + 0, // nft id from setIdWethUsdc + amt, // -10 collateral + 0, // 0 debt + dsaWallet0.address, + setIdWethUsdc, + 0 + ], + }, + ]; + + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); + + await tx.wait(); + + expect(await wethToken.balanceOf(dsaWallet0.address)).to.eq( + parseEther("10") + ); + }); + + // 0 usdc, 1000 matic, 10 weth + + it("Should withdraw max WETH from Fluid", async function () { + const spells = [ + { + connector: connectorName, + method: "operate", + args: [ + vaultWethUsdc, + 0, // nft id from setIdWethUsdc + ethers.constants.MinInt256, // min integer value + 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.eq( + parseEther("29") + ); + }); + + // 0 usdc, 1000 matic, 30 weth + + // todo: add a (payback and withdraw case) + }); +});