Skip to content

Commit

Permalink
Merge pull request #163 from SweeprFi/yearn
Browse files Browse the repository at this point in the history
Yearn V3 Asset for Polygon
  • Loading branch information
maxcoto authored Mar 5, 2024
2 parents cd643e2 + 7a23fd8 commit 7fa68e2
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 5 deletions.
6 changes: 3 additions & 3 deletions contracts/Assets/ERC4626Asset.sol
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ contract ERC4626Asset is Stabilizer {
uint256 sharesBalance = asset.balanceOf(address(this));
if (sharesBalance == 0) revert NotEnoughBalance();
uint256 sharesAmount = asset.convertToShares(usdxAmount);
if (sharesBalance > sharesAmount) sharesAmount = sharesBalance;
if (sharesBalance < sharesAmount) sharesAmount = sharesBalance;

uint256 divestedAmount = asset.convertToAssets(sharesAmount);
asset.withdraw(divestedAmount, address(this), address(this));

Expand All @@ -180,7 +180,7 @@ contract ERC4626Asset is Stabilizer {
function _divestRedeem(uint256 sharesAmount) internal virtual {
uint256 sharesBalance = asset.balanceOf(address(this));
if (sharesBalance == 0) revert NotEnoughBalance();
if (sharesBalance > sharesAmount) sharesAmount = sharesBalance;
if (sharesBalance < sharesAmount) sharesAmount = sharesBalance;

uint256 divestedAmount = asset.convertToAssets(sharesAmount);
asset.redeem(divestedAmount, address(this), address(this));
Expand Down
155 changes: 155 additions & 0 deletions contracts/Assets/YearnV3Asset.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.19;

// ====================================================================
// ========================== YearnV3Asset.sol ========================
// ====================================================================

/**
* @title YearnV3 Asset
* @dev Representation of an on-chain investment
*/
import "@openzeppelin/contracts/interfaces/IERC4626.sol";
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";

contract YearnV3Asset is Stabilizer {
// Variables
IERC4626 public immutable asset;
IERC20Metadata private immutable usdc_e;
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 _usdc_e,
address _asset,
address _oracleUsdx,
address _borrower,
address _router
) Stabilizer(_name, _sweep, _usdx, _oracleUsdx, _borrower) {
asset = IERC4626(_asset);
usdc_e = IERC20Metadata(_usdc_e);
router = ISwapRouter(_router);
}

/* ========== 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 = asset.balanceOf(address(this));
uint256 assetsBalance = asset.convertToAssets(sharesBalance);

return _oracleUsdxToUsd(assetsBalance);
}

/* ========== 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 _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 usdceAmount = swap(address(usdx), address(usdc_e), usdxAmount, amountOutMin);

TransferHelper.safeApprove(address(usdc_e), address(asset), usdceAmount);
uint256 shares = asset.deposit(usdceAmount, address(this));

if(shares < asset.convertToShares(usdceAmount)) revert UnexpectedAmount();
emit Invested(usdceAmount);
}

function _divest(uint256 usdxAmount, uint256 slippage) internal override {
uint256 sharesBalance = asset.balanceOf(address(this));
if (sharesBalance == 0) revert NotEnoughBalance();
uint256 sharesAmount = asset.convertToShares(usdxAmount);
if (sharesBalance < sharesAmount) sharesAmount = sharesBalance;

uint256 usdceAmount = asset.convertToAssets(sharesAmount);
asset.withdraw(usdceAmount, address(this), address(this));
usdceAmount = usdc_e.balanceOf(address(this));

uint256 amountOutMin = OvnMath.subBasisPoints(usdceAmount, slippage);
uint256 usdcAmount = swap(address(usdc_e), address(usdx), usdceAmount, 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);
}

}
55 changes: 55 additions & 0 deletions scripts/deploy/assets/yearn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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 V3 Asset";
const sweep = tokens.sweep;
const usdc = tokens.usdc;
const usdc_e = tokens.usdc_e;
const oracleUsdc = chainlink.usdc_usd;
const vault = protocols.yearn.vault;
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("USDC.e:", usdc_e);
console.log("USDC/USD Chainlink Oracle:", oracleUsdc);
console.log("Yearn Vault:", vault);
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
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} ${oracleUsdc} ${borrower} ${router}`);
}

main();


63 changes: 63 additions & 0 deletions test/assets/yearn_v3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const { expect } = require("chai");
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 () {
if (Number(network.id) !== 137) return;

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

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

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

Asset = await ethers.getContractFactory("YearnV3Asset");
asset = await Asset.deploy(
'Yearn V3 Asset',
tokens.sweep,
tokens.usdc,
tokens.usdc_e,
protocols.yearn.vault,
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("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, 20000);

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);
});
});
});
14 changes: 12 additions & 2 deletions utils/networks/polygon.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,26 @@ module.exports = {
multisig: '0x47671b43B6E05FC6f423595F625716A06d76D9Ec',
owner: '0x7Adc86401f246B87177CEbBEC189dE075b75Af3A',
borrower: '0xF2D3Ba4Ad843Ac0842Baf487660FCb3B208c988c',
usdc_holder: '0x4b6f17856215eab57c29ebfa18b0a0f74a3627bb',
},

tokens: {
sweep: '0xB88a5Ac00917a02d82c7cd6CEBd73E2852d43574',
sweepr: '0x89B1e7068bF8E3232dD8f16c35cAc45bDA584f4E',
usdc: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
usdc_e: '',
usdc_e: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
},

protocols: {},
protocols: {
yearn: {
vault: "0xA013Fbd4b711f9ded6fB09C1c0d358E2FbC2EAA0",
},
curve: {
crvUSD: '0xc4ce1d6f5d98d65ee25cf85e9f2e9dcfee6cb5d6',
crvUSD_USDCe_pool: '0x864490cf55dc2dee3f0ca4d06f5f80b2bb154a03',
crvUSD_USDC_pool: '0x5225010a0ae133b357861782b0b865a48471b2c5',
},
},

chainlink: {
usdc_usd: '0xfE4A8cc5b5B2366C1B58Bea3858e81843581b2F7',
Expand Down

0 comments on commit 7fa68e2

Please sign in to comment.