From bc1e4ea97b6fea0af8f582b9bbfce65f2ffe086e Mon Sep 17 00:00:00 2001 From: atozICT20 Date: Tue, 18 Oct 2022 10:11:25 -0400 Subject: [PATCH] Sturdy connector --- .../mainnet/connectors/sturdy/events.sol | 19 ++ .../mainnet/connectors/sturdy/helpers.sol | 59 ++++ .../mainnet/connectors/sturdy/interface.sol | 82 +++++ contracts/mainnet/connectors/sturdy/main.sol | 298 ++++++++++++++++++ test/mainnet/sturdy/sturdy.test.ts | 205 ++++++++++++ 5 files changed, 663 insertions(+) create mode 100644 contracts/mainnet/connectors/sturdy/events.sol create mode 100644 contracts/mainnet/connectors/sturdy/helpers.sol create mode 100644 contracts/mainnet/connectors/sturdy/interface.sol create mode 100644 contracts/mainnet/connectors/sturdy/main.sol create mode 100644 test/mainnet/sturdy/sturdy.test.ts diff --git a/contracts/mainnet/connectors/sturdy/events.sol b/contracts/mainnet/connectors/sturdy/events.sol new file mode 100644 index 00000000..6773234d --- /dev/null +++ b/contracts/mainnet/connectors/sturdy/events.sol @@ -0,0 +1,19 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +contract Events { + event LogDeposit(address indexed token, uint256 tokenAmt, uint256 getId, uint256 setId); + event LogWithdraw(address indexed token, uint256 tokenAmt, uint256 getId, uint256 setId); + event LogBorrow(address indexed token, uint256 tokenAmt, uint256 indexed rateMode, uint256 getId, uint256 setId); + event LogRepay(address indexed token, uint256 tokenAmt, uint256 indexed rateMode, uint256 getId, uint256 setId); + event LogRepayOnBehalfOf( + address token, + uint256 amt, + uint256 rateMode, + address onBehalfOf, + uint256 getId, + uint256 setId + ); + event LogDepositCollateral(address indexed token, uint256 tokenAmt, uint256 getId, uint256 setId); + event LogWithdrawCollateral(address indexed token, uint256 tokenAmt, uint256 getId, uint256 setId); +} diff --git a/contracts/mainnet/connectors/sturdy/helpers.sol b/contracts/mainnet/connectors/sturdy/helpers.sol new file mode 100644 index 00000000..05d4c201 --- /dev/null +++ b/contracts/mainnet/connectors/sturdy/helpers.sol @@ -0,0 +1,59 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +import { DSMath } from "../../common/math.sol"; +import { Basic } from "../../common/basic.sol"; +import { SturdyAddressesProviderInterface, SturdyDataProviderInterface } from "./interface.sol"; + +abstract contract Helpers is DSMath, Basic { + + /** + * @dev Sturdy Address Provider + */ + SturdyAddressesProviderInterface constant internal sturdyAddressesProvider = SturdyAddressesProviderInterface(0xb7499a92fc36e9053a4324aFfae59d333635D9c3); + + /** + * @dev Sturdy Protocol Data Provider + */ + SturdyDataProviderInterface constant internal sturdyData = SturdyDataProviderInterface(0x960993Cb6bA0E8244007a57544A55bDdb52db97e); + + /** + * @dev Sturdy Lido Vault + */ + address constant internal sturdyLidoVault = 0x01c05337354aae5345d27d2A4A70B56a17aF2b4a; + + /** + * @dev Sturdy Referral Code + */ + uint16 constant internal referralCode = 3333; + + /** + * @dev Return Wrapped ETH address + */ + address constant internal stEthAddr = 0xDFe66B14D37C77F4E9b180cEb433d1b164f0281D; + + /** + * @dev Get total debt balance & fee for an asset + * @param token token address of the debt.(For ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param rateMode Borrow rate mode (Stable = 1, Variable = 2) + */ + function getRepayBalance(address token, uint rateMode) internal view returns (uint) { + (, uint stableDebt, uint variableDebt, , , , , , ) = sturdyData.getUserReserveData(token, address(this)); + return rateMode == 1 ? stableDebt : variableDebt; + } + + /** + * @dev Get OnBehalfOf user's total debt balance & fee for an asset + * @param token token address of the debt.(For ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param rateMode Borrow rate mode (Stable = 1, Variable = 2) + */ + function getOnBehalfOfRepayBalance(address token, uint256 rateMode, address onBehalfOf) + internal + view + returns (uint256) + { + (, uint256 stableDebt, uint256 variableDebt, , , , , , ) = sturdyData + .getUserReserveData(token, onBehalfOf); + return rateMode == 1 ? stableDebt : variableDebt; + } +} diff --git a/contracts/mainnet/connectors/sturdy/interface.sol b/contracts/mainnet/connectors/sturdy/interface.sol new file mode 100644 index 00000000..a3f45429 --- /dev/null +++ b/contracts/mainnet/connectors/sturdy/interface.sol @@ -0,0 +1,82 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +interface SturdyAddressesProviderInterface { + function getLendingPool() external view returns (address); + + function getAddress(bytes32 id) external view returns (address); +} + +interface SturdyCollateralAdapterInterface { + function getAcceptableVault(address _externalAsset) + external + view + returns (address); +} + +interface SturdyLendingPoolInterface { + function deposit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external; + + function withdraw( + address asset, + uint256 amount, + address to + ) external returns (uint256); + + function borrow( + address asset, + uint256 amount, + uint256 interestRateMode, + uint16 referralCode, + address onBehalfOf + ) external; + + function repay( + address asset, + uint256 amount, + uint256 rateMode, + address onBehalfOf + ) external returns (uint256); +} + +interface SturdyVaultInterface { + function depositCollateralFrom(address asset, uint256 amount, address onBehalfOf) external payable; + + function withdrawCollateral( + address asset, + uint256 amount, + uint256 slippage, + address to + ) external; +} + +interface SturdyDataProviderInterface { + function getReserveTokensAddresses(address _asset) + external + view + returns ( + address aTokenAddress, + address stableDebtTokenAddress, + address variableDebtTokenAddress + ); + + function getUserReserveData(address _asset, address _user) + external + view + returns ( + uint256 currentATokenBalance, + uint256 currentStableDebt, + uint256 currentVariableDebt, + uint256 principalStableDebt, + uint256 scaledVariableDebt, + uint256 stableBorrowRate, + uint256 liquidityRate, + uint40 stableRateLastUpdated, + bool usageAsCollateralEnabled + ); +} diff --git a/contracts/mainnet/connectors/sturdy/main.sol b/contracts/mainnet/connectors/sturdy/main.sol new file mode 100644 index 00000000..b533a978 --- /dev/null +++ b/contracts/mainnet/connectors/sturdy/main.sol @@ -0,0 +1,298 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +/** + * @title Sturdy + * @dev Lending & Borrowing. + */ + +import { TokenInterface } from "../../common/interfaces.sol"; +import { Stores } from "../../common/stores.sol"; +import { Helpers } from "./helpers.sol"; +import { Events } from "./events.sol"; +import { SturdyLendingPoolInterface, SturdyCollateralAdapterInterface, SturdyVaultInterface } from "./interface.sol"; +import "hardhat/console.sol"; + +abstract contract SturdyResolver is Events, Helpers { + /** + * @dev Deposit Stable Coins + * @notice Deposit a token to Sturdy for farming. + * @param token The address of the token to deposit. + * @param amt The amount of the token to deposit. (For max: `uint256(-1)`) + * @param getId ID to retrieve amt. + * @param setId ID stores the amount of tokens deposited. + */ + function deposit( + address token, + uint256 amt, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + uint256 _amt = getUint(getId, amt); + + SturdyLendingPoolInterface sturdy = SturdyLendingPoolInterface(sturdyAddressesProvider.getLendingPool()); + + TokenInterface tokenContract = TokenInterface(token); + + _amt = _amt == uint256(-1) + ? tokenContract.balanceOf(address(this)) + : _amt; + + approve(tokenContract, address(sturdy), _amt); + + sturdy.deposit(token, _amt, address(this), referralCode); + + setUint(setId, _amt); + + _eventName = "LogDeposit(address,uint256,uint256,uint256)"; + _eventParam = abi.encode(token, _amt, getId, setId); + } + + /** + * @dev Withdraw ETH/ERC20_Token. + * @notice Withdraw deposited token from Sturdy v2 + * @param token The address of the token to withdraw. + * @param amt The amount of the token to withdraw. (For max: `uint256(-1)`) + * @param getId ID to retrieve amt. + * @param setId ID stores the amount of tokens withdrawn. + */ + function withdraw( + address token, + uint256 amt, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + uint256 _amt = getUint(getId, amt); + + SturdyLendingPoolInterface sturdy = SturdyLendingPoolInterface(sturdyAddressesProvider.getLendingPool()); + + TokenInterface tokenContract = TokenInterface(token); + + sturdy.withdraw(token, _amt, address(this)); + + setUint(setId, _amt); + + _eventName = "LogWithdraw(address,uint256,uint256,uint256)"; + _eventParam = abi.encode(token, _amt, getId, setId); + } + + /** + * @dev Borrow ETH/ERC20_Token. + * @notice Borrow a token using Sturdy v2 + * @param token The address of the token to borrow. + * @param amt The amount of the token to borrow. + * @param rateMode The type of borrow debt. (For Stable: 1, Variable: 2) + * @param getId ID to retrieve amt. + * @param setId ID stores the amount of tokens borrowed. + */ + function borrow( + address token, + uint256 amt, + uint256 rateMode, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + uint256 _amt = getUint(getId, amt); + + SturdyLendingPoolInterface sturdy = SturdyLendingPoolInterface(sturdyAddressesProvider.getLendingPool()); + + sturdy.borrow(token, _amt, rateMode, referralCode, address(this)); + + setUint(setId, _amt); + + _eventName = "LogBorrow(address,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(token, _amt, rateMode, getId, setId); + } + + /** + * @dev Repay borrowed ETH/ERC20_Token. + * @notice Repay debt owed. + * @param token The address of the token to payback. + * @param amt The amount of the token to payback. (For max: `uint256(-1)`) + * @param rateMode The type of debt paying back. (For Stable: 1, Variable: 2) + * @param getId ID to retrieve amt. + * @param setId ID stores the amount of tokens paid back. + */ + function repay( + address token, + uint256 amt, + uint256 rateMode, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + uint256 _amt = getUint(getId, amt); + + SturdyLendingPoolInterface sturdy = SturdyLendingPoolInterface(sturdyAddressesProvider.getLendingPool()); + + TokenInterface tokenContract = TokenInterface(token); + + if (_amt == uint256(-1)) { + uint256 _amtDSA = tokenContract.balanceOf(address(this)); + uint256 _amtDebt = getRepayBalance(token, rateMode); + _amt = _amtDSA <= _amtDebt ? _amtDSA : _amtDebt; + } + + approve(tokenContract, address(sturdy), _amt); + + sturdy.repay(token, _amt, rateMode, address(this)); + + setUint(setId, _amt); + + _eventName = "LogRepay(address,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(token, _amt, rateMode, getId, setId); + } + + /** + * @dev Repay borrowed ETH/ERC20_Token on behalf of a user. + * @notice Repay debt owed on behalf os a user. + * @param token The address of the token to payback. + * @param amt The amount of the token to payback. (For max: `uint256(-1)`) + * @param rateMode The type of debt paying back. (For Stable: 1, Variable: 2) + * @param onBehalfOf Address of user who's debt to repay. + * @param getId ID to retrieve amt. + * @param setId ID stores the amount of tokens paid back. + */ + function repayOnBehalfOf( + address token, + uint256 amt, + uint256 rateMode, + address onBehalfOf, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + uint256 _amt = getUint(getId, amt); + + SturdyLendingPoolInterface sturdy = SturdyLendingPoolInterface(sturdyAddressesProvider.getLendingPool()); + + TokenInterface tokenContract = TokenInterface(token); + + if (_amt == uint256(-1)) { + uint256 _amtDSA = tokenContract.balanceOf(address(this)); + uint256 _amtDebt = getOnBehalfOfRepayBalance( + token, + rateMode, + onBehalfOf + ); + _amt = _amtDSA <= _amtDebt ? _amtDSA : _amtDebt; + } + + approve(tokenContract, address(sturdy), _amt); + + sturdy.repay(token, _amt, rateMode, onBehalfOf); + + setUint(setId, _amt); + + _eventName = "LogRepayOnBehalfOf(address,uint256,uint256,address,uint256,uint256)"; + _eventParam = abi.encode( + token, + _amt, + rateMode, + onBehalfOf, + getId, + setId + ); + } + + /** + * @dev Deposit Collateral + * @notice Deposit a token to Sturdy for lending. + * @param token The address of the token to deposit. + * @param amt The amount of the token to deposit. (For max: `uint256(-1)`) + * @param getId ID to retrieve amt. + * @param setId ID stores the amount of tokens deposited. + */ + function depositCollateral( + address token, + uint256 amt, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + uint256 _amt = getUint(getId, amt); + + SturdyCollateralAdapterInterface sturdyCollateralAdapter = SturdyCollateralAdapterInterface(sturdyAddressesProvider.getAddress('COLLATERAL_ADAPTER')); + + address vault = token == stEthAddr ? sturdyLidoVault : sturdyCollateralAdapter.getAcceptableVault(token); + require(vault != address(0), "Collateral is not supported"); + + SturdyVaultInterface sturdyVault = SturdyVaultInterface(vault); + + TokenInterface tokenContract = TokenInterface(token); + _amt = _amt == uint256(-1) + ? tokenContract.balanceOf(address(this)) + : _amt; + + approve(tokenContract, address(sturdyVault), _amt); + + sturdyVault.depositCollateralFrom(token, _amt, address(this)); + + setUint(setId, _amt); + + _eventName = "LogDepositCollateral(address,uint256,uint256,uint256)"; + _eventParam = abi.encode(token, _amt, getId, setId); + } + + /** + * @dev Withdraw ETH/ERC20_Token. + * @notice Withdraw deposited collateral from Sturdy + * @param token The address of the token to withdraw. + * @param amt The amount of the token to withdraw. (For max: `uint256(-1)`) + * @param getId ID to retrieve amt. + * @param setId ID stores the amount of tokens withdrawn. + */ + function withdrawCollateral( + address token, + uint256 amt, + uint256 slippage, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + uint256 _amt = getUint(getId, amt); + + SturdyCollateralAdapterInterface sturdyCollateralAdapter = SturdyCollateralAdapterInterface(sturdyAddressesProvider.getAddress('COLLATERAL_ADAPTER')); + + address vault = token == stEthAddr ? sturdyLidoVault : sturdyCollateralAdapter.getAcceptableVault(token); + require(vault != address(0), "Collateral is not supported"); + + SturdyVaultInterface sturdyVault = SturdyVaultInterface(vault); + + sturdyVault.withdrawCollateral(token, _amt, slippage, address(this)); + + setUint(setId, _amt); + + _eventName = "LogWithdrawCollateral(address,uint256,uint256,uint256)"; + _eventParam = abi.encode(token, _amt, getId, setId); + } + +} + +contract ConnectV2Sturdy is SturdyResolver { + string public constant name = "Sturdy-v1"; +} diff --git a/test/mainnet/sturdy/sturdy.test.ts b/test/mainnet/sturdy/sturdy.test.ts new file mode 100644 index 00000000..9bbc4418 --- /dev/null +++ b/test/mainnet/sturdy/sturdy.test.ts @@ -0,0 +1,205 @@ +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 { ConnectV2Sturdy, ConnectV2Sturdy__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"; +import {impersonateAccounts} from "../../../scripts/tests/impersonate"; +const { ethers } = hre; +import type { Signer, Contract } from "ethers"; + +describe("Sturdy", function () { + const connectorName = "STURDY-TEST-A"; + let connector: any; + + let wallet0: Signer, wallet1:Signer; + let dsaWallet0: any; + let instaConnectorsV2: Contract; + let masterSigner: Signer; + + const stETH = '0xDFe66B14D37C77F4E9b180cEb433d1b164f0281D'; + const crvFRAX = '0x3175Df0976dFA876431C2E9eE6Bc45b65d3473CC'; + + const dsaDeposit = async (tokenName: string, address: string, amt: string) => { + const abi = ["function transfer(address to, uint value)"]; + + const tokenMapping = { + dai: { + impersonateSigner: '0x5d38b4e4783e34e2301a2a36c39a03c45798c4dd', + address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + }, + usdc: { + impersonateSigner: '0x72a53cdbbcc1b9efa39c834a540550e23463aacb', + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + }, + usdt: { + impersonateSigner: '0x5a52e96bacdabb82fd05763e25335261b270efcb', + address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + }, + stETH: { + impersonateSigner: '0x3c79d7e8a5d6b49336f0fd2f3adde745954bc9f7', + address: stETH, + }, + crvFRAX: { + impersonateSigner: '0xfd1d36995d76c0f75bbe4637c84c06e4a68bbb3a', + address: crvFRAX, + } + }; + + const token = tokenMapping[tokenName]; + const [impersonatedSigner] = await impersonateAccounts([token.impersonateSigner]); + const contract = new ethers.Contract(token.address, abi, impersonatedSigner); + + const tx = await contract.transfer(address, amt); + await tx.wait(); + }; + + before(async () => { + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + // @ts-ignore + jsonRpcUrl: hre.config.networks.hardhat.forking.url, + blockNumber: 15454171, + }, + }, + ], + }); + [wallet0, wallet1] = await ethers.getSigners(); + masterSigner = await getMasterSigner(); + instaConnectorsV2 = await ethers.getContractAt( + abis.core.connectorsV2, + addresses.core.connectorsV2 + ); + connector = await deployAndEnableConnector({ + connectorName, + contractArtifact: ConnectV2Sturdy__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 DAI in Sturdy", async function () { + const amt = parseEther("100"); // 100 dai + await dsaDeposit("dai", dsaWallet0.address, amt.toString()); + const spells = [ + { + connector: connectorName, + method: "deposit", + args: [tokens.dai.address, amt, 0, 0], + }, + ]; + + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); + + await tx.wait(); + }); + // it("should provide stETH in Sturdy", async function () { + // const amt = parseEther("100"); + // await dsaDeposit("stETH", dsaWallet0.address, amt.toString()); + // const spells = [ + // { + // connector: connectorName, + // method: "depositCollateral", + // args: [stETH, amt, 0, 0], + // }, + // ]; + + // const tx = await dsaWallet0 + // .connect(wallet0) + // .cast(...encodeSpells(spells), wallet1.getAddress()); + + // await tx.wait(); + // }); + it("should provide crvFRAX in Sturdy", async function () { + const amt = parseEther("200"); + await dsaDeposit("crvFRAX", dsaWallet0.address, amt.toString()); + const spells = [ + { + connector: connectorName, + method: "depositCollateral", + args: [crvFRAX, amt, 0, 0], + }, + ]; + + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); + + await tx.wait(); + }); + it("Should borrow and repay DAI from Sturdy", 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: "repay", + args: [tokens.dai.address, amt, 2, setId, 0], + }, + ]; + + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); + await tx.wait(); + }); + + it("Should withdraw crvFRAX from Sturdy", async function () { + const slippage = '100'; // 1% + const spells = [ + { + connector: connectorName, + method: "withdrawCollateral", + args: [crvFRAX, constants.max_value, slippage, 0, 0], + }, + ]; + + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); + await tx.wait(); + }); + }); +});