-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #163 from SweeprFi/yearn
Yearn V3 Asset for Polygon
- Loading branch information
Showing
5 changed files
with
288 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters