From a10342d5c6379635411d82bd2e342a023679d77e Mon Sep 17 00:00:00 2001 From: Cela Pablo Date: Fri, 22 Mar 2024 13:15:01 -0300 Subject: [PATCH] adds Agave asset for Gnosis chain --- contracts/Assets/AgaveAsset.sol | 114 ++++++++ contracts/Assets/Interfaces/Agave/IAgave.sol | 266 +++++++++++++++++++ hardhat.config.js | 8 + scripts/deploy/assets/agave.js | 42 +++ test/assets/agave.js | 72 +++++ utils/networks/gnosis.js | 60 +++++ 6 files changed, 562 insertions(+) create mode 100644 contracts/Assets/AgaveAsset.sol create mode 100644 contracts/Assets/Interfaces/Agave/IAgave.sol create mode 100644 scripts/deploy/assets/agave.js create mode 100644 test/assets/agave.js create mode 100644 utils/networks/gnosis.js diff --git a/contracts/Assets/AgaveAsset.sol b/contracts/Assets/AgaveAsset.sol new file mode 100644 index 0000000..2935838 --- /dev/null +++ b/contracts/Assets/AgaveAsset.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +// ==================================================================== +// ========================= AgaveAsset.sol =========================== +// ==================================================================== + +/** + * @title Agave Asset + * @dev Representation of an on-chain investment on a Agave pool + */ + +import { IERC20Metadata } from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; +import { TransferHelper } from "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; +import { Stabilizer, OvnMath } from "../Stabilizer/Stabilizer.sol"; +import { ILendingPool, IAgaveOracle } from "./Interfaces/Agave/IAgave.sol"; + +contract AgaveAsset is Stabilizer { + // Variables + IERC20Metadata public immutable aToken; + ILendingPool public immutable agavePool; + + uint16 private constant DEADLINE_GAP = 15 minutes; + + // Events + event Invested(uint256 indexed usdxAmount); + event Divested(uint256 indexed usdxAmount); + + constructor( + string memory _name, + address _sweep, + address _usdx, // wxDAI + address _aToken, // agwxDAI + address _agavePool, + address _oracle, + address _borrower + ) Stabilizer(_name, _sweep, _usdx, _oracle, _borrower) { + aToken = IERC20Metadata(_aToken); + agavePool = ILendingPool(_agavePool); + } + + /* ========== Views ========== */ + + /** + * @notice Get Asset Value + * @return uint256 Asset Amount. + */ + function assetValue() public view override returns (uint256) { + uint256 aTokenBalance = aToken.balanceOf(address(this)); + + return _oracleUsdxToUsd(aTokenBalance); + } + + /* ========== Actions ========== */ + + /** + * @notice Invest USDX + * @param usdxAmount USDX Amount to be invested. + */ + function invest(uint256 usdxAmount) + external onlyBorrower whenNotPaused nonReentrant validAmount(usdxAmount) + { + _invest(usdxAmount, 0, 0); + } + + /** + * @notice Divests From Agave. + * @param usdxAmount Amount to be divested. + */ + function divest(uint256 usdxAmount) + external onlyBorrower nonReentrant validAmount(usdxAmount) + { + _divest(usdxAmount, 0); + } + + function liquidate() external nonReentrant { + if(auctionAllowed) revert ActionNotAllowed(); + _liquidate(_getToken(), getDebt()); + } + + /* ========== Internals ========== */ + + function _getToken() internal view override returns (address) { + return address(aToken); + } + + /** + * @notice Invest + * @dev Deposits the amount into the Agave pool. + */ + function _invest(uint256 usdxAmount, uint256, uint256) internal override { + uint256 usdxBalance = usdx.balanceOf(address(this)); + if (usdxBalance == 0) revert NotEnoughBalance(); + if (usdxBalance < usdxAmount) usdxAmount = usdxBalance; + + TransferHelper.safeApprove(address(usdx), address(agavePool), usdxAmount); + agavePool.deposit(address(usdx), usdxAmount, address(this), 0); + + emit Invested(usdxAmount); + } + + /** + * @notice Divest + * @dev Withdraws the amount from the Agave pool. + */ + function _divest(uint256 tokenAmount, uint256) internal override { + if (aToken.balanceOf(address(this)) < tokenAmount) + tokenAmount = type(uint256).max; + + agavePool.withdraw(address(usdx), tokenAmount, address(this)); + + emit Divested(tokenAmount); + } +} diff --git a/contracts/Assets/Interfaces/Agave/IAgave.sol b/contracts/Assets/Interfaces/Agave/IAgave.sol new file mode 100644 index 0000000..e891bce --- /dev/null +++ b/contracts/Assets/Interfaces/Agave/IAgave.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +interface ILendingPool { + /** + * @dev Deposits an `amount` of underlying asset into the reserve, receiving in return overlying aTokens. + * - E.g. User deposits 100 USDC and gets in return 100 aUSDC + * @param asset The address of the underlying asset to deposit + * @param amount The amount to be deposited + * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user + * wants to receive them on his own wallet, or a different address if the beneficiary of aTokens + * is a different wallet + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + **/ + function deposit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external; + + /** + * @dev Withdraws an `amount` of underlying asset from the reserve, burning the equivalent aTokens owned + * E.g. User has 100 aUSDC, calls withdraw() and receives 100 USDC, burning the 100 aUSDC + * @param asset The address of the underlying asset to withdraw + * @param amount The underlying amount to be withdrawn + * - Send the value type(uint256).max in order to withdraw the whole aToken balance + * @param to Address that will receive the underlying, same as msg.sender if the user + * wants to receive it on his own wallet, or a different address if the beneficiary is a + * different wallet + * @return The final amount withdrawn + **/ + function withdraw( + address asset, + uint256 amount, + address to + ) external returns (uint256); + + /** + * @dev Allows users to borrow a specific `amount` of the reserve underlying asset, provided that the borrower + * already deposited enough collateral, or he was given enough allowance by a credit delegator on the + * corresponding debt token (StableDebtToken or VariableDebtToken) + * - E.g. User borrows 100 USDC passing as `onBehalfOf` his own address, receiving the 100 USDC in his wallet + * and 100 stable/variable debt tokens, depending on the `interestRateMode` + * @param asset The address of the underlying asset to borrow + * @param amount The amount to be borrowed + * @param interestRateMode The interest rate mode at which the user wants to borrow: 1 for Stable, 2 for Variable + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + * @param onBehalfOf Address of the user who will receive the debt. Should be the address of the borrower itself + * calling the function if he wants to borrow against his own collateral, or the address of the credit delegator + * if he has been given credit delegation allowance + **/ + function borrow( + address asset, + uint256 amount, + uint256 interestRateMode, + uint16 referralCode, + address onBehalfOf + ) external; + + /** + * @notice Repays a borrowed `amount` on a specific reserve, burning the equivalent debt tokens owned using the reserve token + * - E.g. User repays 100 USDC, burning 100 variable/stable debt tokens of the `onBehalfOf` address + * @param asset The address of the borrowed underlying asset previously borrowed + * @param amount The amount to repay + * - Send the value type(uint256).max in order to repay the whole debt for `asset` on the specific `debtMode` + * @param rateMode The interest rate mode at of the debt the user wants to repay: 1 for Stable, 2 for Variable + * @param onBehalfOf Address of the user who will get his debt reduced/removed. Should be the address of the + * user calling the function if he wants to reduce/remove his own debt, or the address of any other + * other borrower whose debt should be removed + * @return The final amount repaid + **/ + function repay( + address asset, + uint256 amount, + uint256 rateMode, + address onBehalfOf + ) external returns (uint256); + + /** + * @notice Repays a borrowed `amount` on a specific reserve, burning the equivalent debt tokens owned using deposited balance of the same asset + * - E.g. User repays 100 agUSDC, burning 100 variable/stable debt tokens of the `onBehalfOf` address + * @param asset The address of the borrowed underlying asset previously borrowed + * @param amount The amount to repay + * - Send the value type(uint256).max in order to repay the whole debt for `asset` on the specific `debtMode` + * @param rateMode The interest rate mode at of the debt the user wants to repay: 1 for Stable, 2 for Variable + * @param onBehalfOf Address of the user who will get his debt reduced/removed. Should be the address of the + * user calling the function if he wants to reduce/remove his own debt, or the address of any other + * other borrower whose debt should be removed + * @return The final amount repaid + **/ + function repayUsingAgToken( + address asset, + uint256 amount, + uint256 rateMode, + address onBehalfOf + ) external returns (uint256); + + /** + * @dev Allows a borrower to swap his debt between stable and variable mode, or viceversa + * @param asset The address of the underlying asset borrowed + * @param rateMode The rate mode that the user wants to swap to + **/ + function swapBorrowRateMode(address asset, uint256 rateMode) external; + + /** + * @dev Rebalances the stable interest rate of a user to the current stable rate defined on the reserve. + * - Users can be rebalanced if the following conditions are satisfied: + * 1. Usage ratio is above 95% + * 2. the current deposit APY is below REBALANCE_UP_THRESHOLD * maxVariableBorrowRate, which means that too much has been + * borrowed at a stable rate and depositors are not earning enough + * @param asset The address of the underlying asset borrowed + * @param user The address of the user to be rebalanced + **/ + function rebalanceStableBorrowRate(address asset, address user) external; + + /** + * @dev Allows depositors to enable/disable a specific deposited asset as collateral + * @param asset The address of the underlying asset deposited + * @param useAsCollateral `true` if the user wants to use the deposit as collateral, `false` otherwise + **/ + function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external; + + /** + * @dev Function to liquidate a non-healthy position collateral-wise, with Health Factor below 1 + * - The caller (liquidator) covers `debtToCover` amount of debt of the user getting liquidated using the reserve asset, + * and receives a proportionally amount of the `collateralAsset` plus a bonus to cover market risk + * @param collateralAsset The address of the underlying asset used as collateral, to receive as result of the liquidation + * @param debtAsset The address of the underlying borrowed asset to be repaid with the liquidation + * @param user The address of the borrower getting liquidated + * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover + * @param receiveAToken `true` if the liquidators wants to receive the collateral aTokens, `false` if he wants + * to receive the underlying collateral asset directly + **/ + function liquidationCall( + address collateralAsset, + address debtAsset, + address user, + uint256 debtToCover, + bool receiveAToken + ) external; + + /** + * @dev Function to liquidate a non-healthy position collateral-wise, with Health Factor below 1 + * - The caller (liquidator) covers `debtToCover` amount of debt of the user getting liquidated using the corresponding agToken, + * and receives a proportionally amount of the `collateralAsset` plus a bonus to cover market risk + * @param collateralAsset The address of the underlying asset used as collateral, to receive as result of the liquidation + * @param debtAsset The address of the underlying borrowed asset to be repaid with the liquidation + * @param user The address of the borrower getting liquidated + * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover + * @param receiveAToken `true` if the liquidators wants to receive the collateral aTokens, `false` if he wants + * to receive the underlying collateral asset directly + **/ + function liquidationCallUsingAgToken( + address collateralAsset, + address debtAsset, + address user, + uint256 debtToCover, + bool receiveAToken + ) external; + + /** + * @dev Allows smartcontracts to access the liquidity of the pool within one transaction, + * as long as the amount taken plus a fee is returned. + * IMPORTANT There are security concerns for developers of flashloan receiver contracts that must be kept into consideration. + * For further details please visit https://developers.aave.com + * @param receiverAddress The address of the contract receiving the funds, implementing the IFlashLoanReceiver interface + * @param assets The addresses of the assets being flash-borrowed + * @param amounts The amounts amounts being flash-borrowed + * @param modes Types of the debt to open if the flash loan is not returned: + * 0 -> Don't open any debt, just revert if funds can't be transferred from the receiver + * 1 -> Open debt at stable rate for the value of the amount flash-borrowed to the `onBehalfOf` address + * 2 -> Open debt at variable rate for the value of the amount flash-borrowed to the `onBehalfOf` address + * @param onBehalfOf The address that will receive the debt in the case of using on `modes` 1 or 2 + * @param params Variadic packed params to pass to the receiver as extra information + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + **/ + function flashLoan( + address receiverAddress, + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata modes, + address onBehalfOf, + bytes calldata params, + uint16 referralCode + ) external; + + /** + * @dev Returns the user account data across all the reserves + * @param user The address of the user + * @return totalCollateralETH the total collateral in ETH of the user + * @return totalDebtETH the total debt in ETH of the user + * @return availableBorrowsETH the borrowing power left of the user + * @return currentLiquidationThreshold the liquidation threshold of the user + * @return ltv the loan to value of the user + * @return healthFactor the current health factor of the user + **/ + function getUserAccountData(address user) + external + view + returns ( + uint256 totalCollateralETH, + uint256 totalDebtETH, + uint256 availableBorrowsETH, + uint256 currentLiquidationThreshold, + uint256 ltv, + uint256 healthFactor + ); + + function initReserve( + address reserve, + address aTokenAddress, + address stableDebtAddress, + address variableDebtAddress, + address interestRateStrategyAddress + ) external; + + function setReserveInterestRateStrategyAddress(address reserve, address rateStrategyAddress) + external; + + function setReserveLimits( + address asset, + uint256 depositLimit, + uint256 borrowLimit, + uint256 collateralUsageLimit + ) external; + + function setConfiguration(address reserve, uint256 configuration) external; + + /** + * @dev Returns the normalized income normalized income of the reserve + * @param asset The address of the underlying asset of the reserve + * @return The reserve's normalized income + */ + function getReserveNormalizedIncome(address asset) external view returns (uint256); + + /** + * @dev Returns the normalized variable debt per unit of asset + * @param asset The address of the underlying asset of the reserve + * @return The reserve normalized variable debt + */ + function getReserveNormalizedVariableDebt(address asset) external view returns (uint256); + + + function finalizeTransfer( + address asset, + address from, + address to, + uint256 amount, + uint256 balanceFromAfter, + uint256 balanceToBefore + ) external; + + function getReservesList() external view returns (address[] memory); + + function setPause(bool val) external; + + function paused() external view returns (bool); +} + +interface IAgaveOracle { + function getAssetPrice(address asset) external view returns (uint256); +} \ No newline at end of file diff --git a/hardhat.config.js b/hardhat.config.js index 61ca964..6ce0825 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -80,6 +80,14 @@ module.exports = { browserURL: "https://avalanche.routescan.io" } }, + { + network: "gnosis", + chainId: 100, + urls: { + apiURL: "https://api.gnosisscan.io/api", + browserURL: "https://gnosisscan.io" + } + }, ] }, diff --git a/scripts/deploy/assets/agave.js b/scripts/deploy/assets/agave.js new file mode 100644 index 0000000..c38205b --- /dev/null +++ b/scripts/deploy/assets/agave.js @@ -0,0 +1,42 @@ +const { ethers } = require("hardhat"); +const { tokens, wallets, protocols, chainlink, network } = require("../../../utils/constants"); +const { ask } = require("../../../utils/helper_functions"); + +async function main() { + [deployer] = await ethers.getSigners(); + + const name = 'Agave Asset'; + const sweep = tokens.sweep; + const wxdai = tokens.wxdai; + const agwxdai = tokens.agwxdai; + const pool = protocols.agave.pool; + const oracle = chainlink.xdai_usd; + const borrower = wallets.multisig; + + console.log("==========================================="); + console.log("AGAVE ASSET DEPLOY"); + console.log("==========================================="); + console.log("Network:", network.name); + console.log("Deployer:", deployer.address); + console.log("==========================================="); + console.log("Asset Name:", name); + console.log("SWEEP:", sweep); + console.log("wxDAI:", wxdai); + console.log("agwxDAI:", agwxdai); + console.log("Agave POOL:", pool); + console.log("xDAI/USD Chainlink Oracle:", oracle); + console.log("Borrower:", borrower); + console.log("==========================================="); + const answer = (await ask("continue? y/n: ")); + if(answer !== 'y'){ process.exit(); } + console.log("Deploying..."); + + const Asset = await ethers.getContractFactory("AgaveAsset"); + const asset = await Asset.deploy(name, sweep, wxdai, agwxdai, pool, oracle, borrower); + + console.log("Agave Asset deployed to: ", asset.address); + console.log(`\nnpx hardhat verify --network ${network.name} ${asset.address} "${name}" ${sweep} ${wxdai} ${agwxdai} ${pool} ${oracle} ${borrower}`); +} + +main(); + diff --git a/test/assets/agave.js b/test/assets/agave.js new file mode 100644 index 0000000..67ccb86 --- /dev/null +++ b/test/assets/agave.js @@ -0,0 +1,72 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const { network, wallets, tokens, chainlink, protocols } = require("../../utils/constants"); +const { impersonate, sendEth, toBN, increaseTime } = require("../../utils/helper_functions"); + +contract.only("Agave Asset", async function () { + if (Number(network.id) !== 100) return; + + before(async () => { + [borrower, treasury] = await ethers.getSigners(); + BORROWER = borrower.address; + HOLDER = wallets.holder; + POOL = protocols.agave.pool; + AMOUNT = toBN("10000", 18); + + Sweep = await ethers.getContractFactory("SweepCoin"); + const Proxy = await upgrades.deployProxy(Sweep, [treasury.address, BORROWER, 2500]); + sweep = await Proxy.deployed(); + await sweep.setTreasury(treasury.address); + + wxdai = await ethers.getContractAt("ERC20", tokens.wxdai); + atoken = await ethers.getContractAt("ERC20", tokens.agwxdai); + + Asset = await ethers.getContractFactory("AgaveAsset"); + asset = await Asset.deploy( + 'Agave Asset', + sweep.address, + tokens.wxdai, + tokens.agwxdai, + POOL, + chainlink.xdai_usd, + BORROWER + ); + ASSET = asset.address; + + await sendEth(HOLDER); + holder = await impersonate(HOLDER); + await wxdai.connect(holder).transfer(ASSET, AMOUNT); + }); + + describe("main functions", async function () { + it("invests into agave correctly", async function () { + expect(await asset.assetValue()).to.equal(0); + expect(await wxdai.balanceOf(ASSET)).to.equal(AMOUNT); + + investAmount = toBN("6000", 18); + await asset.invest(investAmount); + expect(await asset.assetValue()).to.greaterThan(0); + + await asset.invest(investAmount); + current = await asset.currentValue(); + expect(await asset.assetValue()).to.equal(current); + }); + + it("divests from agave correctly", async function () { + assetValue = await asset.assetValue(); + await increaseTime(60*60*24*365); + expect(await asset.currentValue()).to.be.greaterThan(assetValue); + expect(await wxdai.balanceOf(ASSET)).to.equal(0); + + assetValue = await asset.assetValue(); + divestAmount = toBN("6000", 18); + + await asset.divest(divestAmount); + expect(await asset.assetValue()).to.be.lessThan(assetValue); + + divestAmount = toBN("6000", 18); + await asset.divest(divestAmount); + expect(await asset.assetValue()).to.equal(0); + }); + }); +}); diff --git a/utils/networks/gnosis.js b/utils/networks/gnosis.js new file mode 100644 index 0000000..5fde534 --- /dev/null +++ b/utils/networks/gnosis.js @@ -0,0 +1,60 @@ +module.exports = { + + network: { + id: 100, + name: 'gnosis', + }, + + layerZero: { + id: 145, + endpoint: '0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4', + }, + + alchemyLink: 'https://rpc.gnosischain.com', + scanApiKey: process.env.GNOSISSCAN_API_KEY, + + wallets: { + multisig: '0x837bb49403346a307C449Fe831cCA5C1992C57f5', + owner: '0x7Adc86401f246B87177CEbBEC189dE075b75Af3A', + borrower: '', + holder: '0xba12222222228d8ba445958a75a0704d566bf2c8', // wxDAI + }, + + tokens: { + sweep: '', + sweepr: '', + usdc: '0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83', + wxdai: '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d', + agwxdai: '0xd4e420bBf00b0F409188b338c5D87Df761d6C894', + }, + + protocols: { + agave: { + pool: '0x5E15d5E33d318dCEd84Bfe3F4EACe07909bE6d9c' + } + }, + + chainlink: { + xdai_usd: '0x678df3415fc31947dA4324eC63212874be5a82f8', + sequencer: '0x0000000000000000000000000000000000000000', + }, + + balancer: { + factory: '0x4bdCc2fb18AEb9e2d281b0278D946445070EAda7', + }, + + deployments: { + balancer: '', + treasury: '', + proposal_executor: '', + + balancer_pool: '', + balancer_amm: '', + + assets: { + balancer_market_maker: '', + agave: '', + } + }, + +};