From d9d3c1355af57cf7e1b09594e929955973509bc3 Mon Sep 17 00:00:00 2001 From: maxcoto Date: Tue, 16 Apr 2024 12:40:38 -0300 Subject: [PATCH 1/2] adds apollox asset --- contracts/Assets/ApolloAsset.sol | 123 ++++++++++++++++++ .../Assets/Interfaces/ApolloX/IApolloX.sol | 20 +++ hardhat.config.js | 1 + test/assets/apollo.js | 73 +++++++++++ utils/networks/bsc.js | 8 ++ 5 files changed, 225 insertions(+) create mode 100644 contracts/Assets/ApolloAsset.sol create mode 100644 contracts/Assets/Interfaces/ApolloX/IApolloX.sol create mode 100644 test/assets/apollo.js diff --git a/contracts/Assets/ApolloAsset.sol b/contracts/Assets/ApolloAsset.sol new file mode 100644 index 0000000..f0cdfbb --- /dev/null +++ b/contracts/Assets/ApolloAsset.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +// ==================================================================== +// ========================== ApolloAsset.sol =========================== +// ==================================================================== + +/** + * @title Apollo Asset + * @dev Representation of an on-chain investment on ApolloX finance. + */ + +import { Stabilizer } from "../Stabilizer/Stabilizer.sol"; +import { IApolloX } from "./Interfaces/ApolloX/IApolloX.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; + +contract ApolloAsset is Stabilizer { + error UnexpectedAmount(); + error CooldownError(); + + // Variables + IApolloX private constant apollo = IApolloX(0x1b6F2d3844C6ae7D56ceb3C3643b9060ba28FEb0); + IERC20Metadata private constant alp = IERC20Metadata(0x4E47057f45adF24ba41375a175dA0357cB3480E5); + uint256 public investedAt; + + // Events + event Invested(uint256 indexed usdxAmount); + event Divested(uint256 indexed usdxAmount); + + constructor( + string memory _name, + address _sweep, + address _usdx, + address _oracleUsdx, + address _borrower + ) Stabilizer(_name, _sweep, _usdx, _oracleUsdx, _borrower) { + alp.approve(address(apollo), type(uint256).max); + usdx.approve(address(apollo), type(uint256).max); + } + + /* ========== Views ========== */ + + /** + * @notice Asset Value of investment. + * @return the Returns the value of the investment in the USD coin + */ + function assetValue() public view override returns (uint256) { + return _oracleUsdxToUsd(getDepositAmount()); + } + + function getDepositAmount() public view returns (uint256) { + return apollo.stakeOf(address(this)) * apollo.alpPrice() / 1e8; + } + + /* ========== Actions ========== */ + + /** + * @notice Invest. + * @param usdxAmount Amount of usdx to be swapped for token. + * @param alpAmountOut Minimum amount out of ALP. + */ + function invest(uint256 usdxAmount, uint256 alpAmountOut) + external onlyBorrower whenNotPaused nonReentrant validAmount(usdxAmount) + { + _invest(usdxAmount, 0, alpAmountOut); + } + + /** + * @notice Divest. + * @param alpAmount Amount to be divested. + * @param usdxMinOut Minimum amount out of usdx. + */ + function divest(uint256 alpAmount, uint256 usdxMinOut) + external onlyBorrower nonReentrant validAmount(alpAmount) + { + _divest(alpAmount, usdxMinOut); + } + + /** + * @notice Liquidate + */ + function liquidate() external nonReentrant { + if(auctionAllowed) revert ActionNotAllowed(); + apollo.unStake(apollo.stakeOf(address(this))); + _liquidate(_getToken(), getDebt()); + } + + function collect() external nonReentrant onlyBorrower { + apollo.claimAllReward(); + } + + /* ========== Internals ========== */ + + function _getToken() internal pure override returns (address) { + return address(alp); + } + + function _invest(uint256 usdxAmount, uint256, uint256 minAlpOut) + internal override + { + investedAt = block.timestamp; + + uint256 usdxBalance = usdx.balanceOf(address(this)); + if (usdxBalance < usdxAmount) usdxAmount = usdxBalance; + + apollo.mintAlp(address(usdx), usdxAmount, minAlpOut, true); + + emit Invested(usdxAmount); + } + + function _divest(uint256 alpAmount, uint256 usdxMinOut) internal override { + if(block.timestamp - investedAt < apollo.coolingDuration()) revert CooldownError(); + + uint256 alpBalance = apollo.stakeOf(address(this)); + if(alpAmount > alpBalance) alpAmount = alpBalance; + + apollo.unStake(alpAmount); + apollo.burnAlp(address(usdx), alpAmount, usdxMinOut, address(this)); + + emit Divested(usdxMinOut); + } + +} diff --git a/contracts/Assets/Interfaces/ApolloX/IApolloX.sol b/contracts/Assets/Interfaces/ApolloX/IApolloX.sol new file mode 100644 index 0000000..8c0c5a5 --- /dev/null +++ b/contracts/Assets/Interfaces/ApolloX/IApolloX.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.19; + +// import "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +interface IApolloX { // } is IERC4626 { + + function coolingDuration() external view returns (uint256); + + function mintAlp(address tokenIn, uint256 amountIn, uint256 minAlp, bool stake) external; + function burnAlp(address tokenOut, uint256 alpAmount, uint256 minOut, address receiver) external; + + function unStake(uint256 _amount) external; + function claimAllReward() external; + + function alpPrice() external view returns (uint256); + function stakeOf(address _user) external view returns (uint256); + + function pendingApx(address _account) external view returns (uint256); +} diff --git a/hardhat.config.js b/hardhat.config.js index 6ce0825..fe973ca 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -20,6 +20,7 @@ const networks = { url: alchemyLink, // blockNumber: 20005467 }, + chainId: network.id, }, localhost: { allowUnlimitedContractSize: true, diff --git a/test/assets/apollo.js b/test/assets/apollo.js new file mode 100644 index 0000000..c333bdd --- /dev/null +++ b/test/assets/apollo.js @@ -0,0 +1,73 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const { wallets, tokens, chainlink, protocols } = require("../../utils/constants"); +const { impersonate, sendEth, toBN, increaseTime } = require("../../utils/helper_functions"); + +contract("Apollo Asset", async function () { + before(async () => { + [borrower] = await ethers.getSigners(); + + depositAmount = toBN("7000", 18); + investAmount = toBN("7000", 18); + divestAmount = toBN("7000", 18); + + Token = await ethers.getContractFactory("ERC20"); + usdt = await Token.attach(tokens.usdt); + + apollo = await ethers.getContractAt("IApolloX", protocols.apollo.apollo); + apx = await Token.attach(protocols.apollo.apx); + + ApolloAsset = await ethers.getContractFactory("ApolloAsset"); + apollo_asset = await ApolloAsset.deploy( + 'Apollo Asset', + tokens.sweep, + tokens.usdt, + chainlink.usdt_usd, + borrower.address, + ); + + await sendEth(wallets.usdt_holder); + const usdHolder = await impersonate(wallets.usdt_holder); + await usdt.connect(usdHolder).transfer(apollo_asset.address, depositAmount); + }); + + describe("apollo asset functions", async function () { + it("invests into apollo correctly", async function () { + expect(await apollo_asset.assetValue()).to.equal(0); + expect(await usdt.balanceOf(apollo_asset.address)).to.equal(depositAmount); + + const minAlpOut = investAmount.mul(toBN("98",6)).div(await apollo.alpPrice()); + await apollo_asset.invest(investAmount, minAlpOut); + + const assetValue = await apollo_asset.assetValue(); + expect(assetValue).to.greaterThan(7000e6 * 0.98); + + expect(await usdt.balanceOf(apollo_asset.address)).to.equal(0); + }); + + it("collects APX rewards", async function () { + await increaseTime(259200); + + expect(await apx.balanceOf(apollo_asset.address)).to.equal(0); + + await apollo_asset.collect(); + + const apxBalance = await apx.balanceOf(apollo_asset.address); + expect(apxBalance).to.be.greaterThan(0); + }); + + it("divests by reedeming the staked tokens and exiting the pool", async function () { + expect(await usdt.balanceOf(apollo_asset.address)).to.eq(0); + expect(await apollo_asset.currentValue()).to.equal(await apollo_asset.assetValue()); + + const minUsdtOut = toBN("6800", 18); + const alpAmount = toBN("99999", 18); // big int value + await apollo_asset.divest(alpAmount, minUsdtOut); + + expect(await apollo_asset.currentValue()).to.be.greaterThan(await apollo_asset.assetValue()); + + const usdtValue = await usdt.balanceOf(apollo_asset.address); + expect(usdtValue).to.greaterThan(minUsdtOut); + }); + }); +}); diff --git a/utils/networks/bsc.js b/utils/networks/bsc.js index 2dbf43e..6abe742 100644 --- a/utils/networks/bsc.js +++ b/utils/networks/bsc.js @@ -49,6 +49,14 @@ module.exports = { observationCardinality: 480, }, + protocols: { + apollo: { + apollo: "0x1b6F2d3844C6ae7D56ceb3C3643b9060ba28FEb0", + apx: "0x78f5d389f5cdccfc41594abab4b0ed02f31398b3", + alp: "0x4E47057f45adF24ba41375a175dA0357cB3480E5", + } + }, + deployments: { balancer: '0xa884970F06Dda7BedD86829E14BeCa2c8fEd5220', treasury: '0x7c9131d7E2bEdb29dA39503DD8Cf809739f047B3', From a3eb05885357e5a6a9b2b0f404cc10c34d0571bd Mon Sep 17 00:00:00 2001 From: maxcoto Date: Tue, 16 Apr 2024 18:14:49 -0300 Subject: [PATCH 2/2] adds the deploy script --- scripts/deploy/assets/apollo.js | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 scripts/deploy/assets/apollo.js diff --git a/scripts/deploy/assets/apollo.js b/scripts/deploy/assets/apollo.js new file mode 100644 index 0000000..3208f9f --- /dev/null +++ b/scripts/deploy/assets/apollo.js @@ -0,0 +1,45 @@ +const { ethers } = require("hardhat"); +const { tokens, network, wallets, protocols, chainlink } = require("../../../utils/constants"); +const { ask } = require("../../../utils/helper_functions"); + +async function main() { + [deployer] = await ethers.getSigners(); + + const name = 'ApolloX Asset'; + const sweep = tokens.sweep; + const usdc = tokens.usdc; + const oracleUsdc = chainlink.usdc_usd; + const borrower = wallets.borrower; + + console.log("==========================================="); + console.log("APOLLO 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("USDC:", usdc); + console.log("USDC/USD Chainlink Oracle:", oracleUsdc); + 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("ApolloAsset"); + const asset = await Asset.deploy( + name, + sweep, + usdc, + oracleUsdc, + borrower, + ); + + console.log("Apollo Asset deployed to:", asset.address); + console.log(`\nnpx hardhat verify --network ${network.name} ${asset.address} "${name}" ${sweep} ${usdc} ${oracleUsdc} ${borrower}`) + +} + +main(); +