Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds yearn v2 asset for optimism #165

Merged
merged 1 commit into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions contracts/Assets/Interfaces/Yearn/IYearnVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.19;

interface IYearnVault {
function balanceOf(address) external view returns(uint256);
function deposit(uint256 _amount) external;
function withdraw(uint256 _amount) external;
function pricePerShare() external view returns(uint256);
}

interface IYearnStaking {
function withdraw(uint256 _amount) external;
function stake(uint256 _amount) external;
function getReward() external;
function balanceOf(address) external view returns(uint256);
}
167 changes: 167 additions & 0 deletions contracts/Assets/YearnV2Asset.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.19;

// ====================================================================
// ========================== YearnV2Asset.sol ========================
// ====================================================================

/**
* @title YearnV2 Asset
* @dev Representation of an on-chain investment
*/
import { Stabilizer, IERC20Metadata, IAMM, TransferHelper, OvnMath } from "../Stabilizer/Stabilizer.sol";
import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import { IYearnVault, IYearnStaking } from "./Interfaces/Yearn/IYearnVault.sol";

contract YearnV2Asset is Stabilizer {
// Variables
IYearnVault public immutable asset;
IYearnStaking public immutable stake;
IERC20Metadata private immutable dai;
ISwapRouter private immutable router;

// Events
event Invested(uint256 indexed tokenAmount);
event Divested(uint256 indexed usdxAmount);

error UnexpectedAmount();
uint16 private constant DEADLINE_GAP = 15 minutes;

constructor(
string memory _name,
address _sweep,
address _usdx,
address _dai,
address _asset,
address _stake,
address _oracleUsdx,
address _borrower,
address _router
) Stabilizer(_name, _sweep, _usdx, _oracleUsdx, _borrower) {
asset = IYearnVault(_asset);
dai = IERC20Metadata(_dai);
router = ISwapRouter(_router);
stake = IYearnStaking(_stake);
}

/* ========== Views ========== */

/**
* @notice Asset Value of investment.
* @return the Returns the value of the investment in the USD coin
* @dev the price is obtained from the target asset
*/
function assetValue() public view virtual override returns (uint256) {
uint256 sharesBalance = stake.balanceOf(address(this));
uint256 usdxAmount = sharesBalance * asset.pricePerShare() / 1e30;

return _oracleUsdxToUsd(usdxAmount);
}

/* ========== Actions ========== */

/**
* @notice Invest.
* @param usdxAmount Amount to be invested
* @dev Sends usdx to the target asset to get shares.
*/
function invest(uint256 usdxAmount, uint256 slippage)
external onlyBorrower whenNotPaused nonReentrant validAmount(usdxAmount)
{
_invest(usdxAmount, 0, slippage);
}

/**
* @notice Divest.
* @param usdxAmount Amount to be divested.
* @dev Gets usdx back by redeeming shares.
*/
function divest(uint256 usdxAmount, uint256 slippage)
external onlyBorrower nonReentrant validAmount(usdxAmount)
{
_divest(usdxAmount, slippage);
}

/**
* @notice Liquidate
*/
function liquidate() external nonReentrant {
if(auctionAllowed) revert ActionNotAllowed();
_liquidate(_getToken(), getDebt());
}

function collect() external onlyBorrower nonReentrant {
stake.getReward();
}

function _getToken() internal view override returns (address) {
return address(asset);
}

/* ========== Internals ========== */

function _invest(uint256 usdxAmount, uint256, uint256 slippage) internal override {
uint256 usdxBalance = usdx.balanceOf(address(this));
if (usdxBalance == 0) revert NotEnoughBalance();
if (usdxBalance < usdxAmount) usdxAmount = usdxBalance;

uint256 amountOutMin = OvnMath.subBasisPoints(usdxAmount, slippage);
uint256 daiAmount = swap(address(usdx), address(dai), usdxAmount, amountOutMin);

// deposit into yearn vault
TransferHelper.safeApprove(address(dai), address(asset), daiAmount);
asset.deposit(daiAmount);

// stake the shares
uint256 sharesAmount = asset.balanceOf(address(this));
TransferHelper.safeApprove(address(asset), address(stake), sharesAmount);
stake.stake(sharesAmount);

emit Invested(usdxAmount);
}

function _divest(uint256 usdxAmount, uint256 slippage) internal override {
uint256 sharesBalance = stake.balanceOf(address(this));
if (sharesBalance == 0) revert NotEnoughBalance();
uint256 sharesAmount = 1e30 * usdxAmount / asset.pricePerShare();
if (sharesBalance < sharesAmount) sharesAmount = sharesBalance;

stake.withdraw(sharesAmount);
asset.withdraw(sharesAmount);

uint256 daiAmount = dai.balanceOf(address(this));
uint256 amountOutMin = OvnMath.subBasisPoints(daiAmount/1e12, slippage);
uint256 usdcAmount = swap(address(dai), address(usdx), daiAmount, amountOutMin);

emit Divested(usdcAmount);
}

/**
* @notice Swap tokenA into tokenB using uniV3Router.ExactInputSingle()
* @param tokenA Address to in
* @param tokenB Address to out
* @param amountIn Amount of _tokenA
* @param amountOutMin Minimum amount out.
*/
function swap(address tokenA, address tokenB, uint256 amountIn, uint256 amountOutMin)
private returns (uint256 amountOut)
{
TransferHelper.safeApprove(tokenA, address(router), amountIn);

ISwapRouter.ExactInputSingleParams memory swapParams = ISwapRouter
.ExactInputSingleParams({
tokenIn: tokenA,
tokenOut: tokenB,
fee: 100,
recipient: address(this),
deadline: block.timestamp + DEADLINE_GAP,
amountIn: amountIn,
amountOutMinimum: amountOutMin,
sqrtPriceLimitX96: 0
});

amountOut = router.exactInputSingle(swapParams);
}

}
58 changes: 58 additions & 0 deletions scripts/deploy/assets/yearn_v2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const { ethers } = require("hardhat");
const { tokens, chainlink, protocols, wallets, network, uniswap } = require("../../../utils/constants");
const { ask } = require("../../../utils/helper_functions");

async function main() {
[deployer] = await ethers.getSigners();

const assetName = "Yearn V2 Asset";
const sweep = tokens.sweep;
const usdc = tokens.usdc;
const dai = tokens.dai;
const oracleUsdc = chainlink.usdc_usd;
const vault = protocols.yearn.vault;
const stake = protocols.yearn.stake;
const borrower = wallets.multisig;
const router = uniswap.router;

console.log("===========================================");
console.log("YEARN V3 ASSET DEPLOY");
console.log("===========================================");
console.log("Network:", network.name);
console.log("Deployer:", deployer.address);
console.log("===========================================");
console.log("Asset Name:", assetName);
console.log("SWEEP:", sweep);
console.log("USDC:", usdc);
console.log("DAI:", dai);
console.log("USDC/USD Chainlink Oracle:", oracleUsdc);
console.log("Yearn Vault:", vault);
console.log("Yearn Stake:", stake);
console.log("Borrower:", borrower);
console.log("Router:", router);
console.log("===========================================");
const answer = (await ask("continue? y/n: "));
if(answer !== 'y'){ process.exit(); }
console.log("Deploying...");


const Asset = await ethers.getContractFactory("YearnV3Asset");
const asset = await Asset.deploy(
assetName,
sweep, // SWEEP
usdc, // USDC
usdc_e, // USDC.e
vault, // YEARN VAULT
stake, // YEARN STAKING CONTRACT
oracleUsdc, // ORACLE USDC/USD
borrower,
router,
);

console.log("YearnV3 asset deployed to: ", asset.address);
console.log(`\nnpx hardhat verify --network ${network.name} ${asset.address} "${assetName}" ${sweep} ${usdc} ${usdc_e} ${vault} ${stake} ${oracleUsdc} ${borrower} ${router}`);
}

main();


File renamed without changes.
76 changes: 76 additions & 0 deletions test/assets/yearn_v2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { wallets, tokens, chainlink, protocols, network, uniswap } = require("../../utils/constants");
const { impersonate, sendEth, increaseTime } = require("../../utils/helper_functions");

contract("Yearn V2 Asset", async function () {
if (Number(network.id) !== 10) return;

before(async () => {
[borrower] = await ethers.getSigners();

depositAmount = 7000e6;
investAmount = 7000e6;
divestAmount = 7000e6;

Token = await ethers.getContractFactory("ERC20");
usdc = await Token.attach(tokens.usdc);
yvMAI = await Token.attach(tokens.yvMAI);

Asset = await ethers.getContractFactory("YearnV2Asset");
asset = await Asset.deploy(
'Yearn V2 Asset',
tokens.sweep,
tokens.usdc,
tokens.dai,
protocols.yearn.vault,
protocols.yearn.stake,
chainlink.usdc_usd,
borrower.address,
uniswap.router,
);

await sendEth(wallets.usdc_holder);
const usdHolder = await impersonate(wallets.usdc_holder);
await usdc.connect(usdHolder).transfer(asset.address, depositAmount);
});

describe("yearn asset functions", async function () {
it("invests into yearn correctly", async function () {
expect(await asset.assetValue()).to.equal(0);
expect(await usdc.balanceOf(asset.address)).to.equal(depositAmount);
await asset.invest(investAmount, 2000);

const assetValue = await asset.assetValue();
// give a 4% error margin because of oracles
expect(assetValue).to.greaterThan(investAmount * 0.98);
expect(assetValue).to.lessThan(investAmount * 1.02);

expect(await usdc.balanceOf(asset.address)).to.equal(0);
});

it("collects yvMAI rewards", async function () {
expect(await yvMAI.balanceOf(asset.address)).to.equal(0);

await increaseTime(86400*365);
await asset.collect();

// TODO cannot verify that we will get yvMAI rewards
// const maiBalance = await yvMAI.balanceOf(asset.address);
// expect(maiBalance).to.be.greaterThan(0);
});

it("divests from yearn correctly", async function () {
expect(await usdc.balanceOf(asset.address)).to.eq(0);
expect(await asset.currentValue()).to.equal(await asset.assetValue());
await asset.divest(divestAmount, 2000);

expect(await asset.currentValue()).to.be.greaterThan(await asset.assetValue());

const usdcValue = await usdc.balanceOf(asset.address);

expect(usdcValue).to.greaterThan(divestAmount * 0.99);
expect(usdcValue).to.lessThan(divestAmount * 1.01);
});
});
});
2 changes: 1 addition & 1 deletion test/assets/yearn_v3.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { ethers } = require("hardhat");
const { wallets, tokens, chainlink, protocols, network, uniswap } = require("../../utils/constants");
const { impersonate, sendEth } = require("../../utils/helper_functions");

contract.only("Yearn V3 Asset", async function () {
contract("Yearn V3 Asset", async function () {
if (Number(network.id) !== 137) return;

before(async () => {
Expand Down
7 changes: 7 additions & 0 deletions utils/networks/optimism.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ module.exports = {
multisig: '0xE0585bDaee364deAd2683c5Aa1520B87F1d2FBAD',
owner: '0x7Adc86401f246B87177CEbBEC189dE075b75Af3A',
borrower: '0xF2D3Ba4Ad843Ac0842Baf487660FCb3B208c988c',
usdc_holder: '0xa1549a810b88313585e140e13081e1b7c2fe74a1',
},

tokens: {
sweep: '0xB88a5Ac00917a02d82c7cd6CEBd73E2852d43574',
sweepr: '0x89B1e7068bF8E3232dD8f16c35cAc45bDA584f4E',
usdc: '0x0b2c639c533813f4aa9d7837caf62653d097ff85',
usdc_e: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607',
dai: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1',
yvMAI: '0x6759574667896a453304897993d61f00fdc7214d',
},

protocols: {
Expand All @@ -33,6 +36,10 @@ module.exports = {
overnight: {
usd_plus: '0x73cb180bf0521828d8849bc8CF2B920918e23032',
exchange: '0xe80772Eaf6e2E18B651F160Bc9158b2A5caFCA65',
},
yearn: {
vault: '0x65343F414FFD6c97b0f6add33d16F6845Ac22BAc',
stake: '0xf8126EF025651E1B313a6893Fcf4034F4F4bD2aA'
}
},

Expand Down
Loading