-
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 #166 from SweeprFi/ethena-asset
adds Ethena asset
- Loading branch information
Showing
6 changed files
with
380 additions
and
1 deletion.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity 0.8.19; | ||
|
||
// ==================================================================== | ||
// ========================== EthenaAsset.sol ========================= | ||
// ==================================================================== | ||
|
||
import { ISUSDe } from "./Interfaces/Ethena/IEthena.sol"; | ||
import { ICurvePool } from "./Interfaces/Curve/ICurve.sol"; | ||
import { Stabilizer, OvnMath, TransferHelper, IERC20Metadata } from "../Stabilizer/Stabilizer.sol"; | ||
|
||
contract EthenaAsset is Stabilizer { | ||
|
||
// Variables | ||
ICurvePool public pool; | ||
ISUSDe public immutable asset; | ||
IERC20Metadata private immutable usde; | ||
|
||
mapping(address => uint8) public assetIndex; | ||
uint8 public constant USDe_IDX = 0; | ||
uint8 public constant USDC_IDX = 1; | ||
|
||
// Events | ||
event Invested(uint256 indexed tokenAmount); | ||
event Divested(uint256 indexed usdxAmount); | ||
|
||
error UnexpectedAmount(); | ||
error OperationNotAllowed(); | ||
|
||
constructor( | ||
string memory _name, | ||
address _sweep, | ||
address _usdx, | ||
address _usde, | ||
address _asset, | ||
address _oracleUsdx, | ||
address poolAddress, | ||
address _borrower | ||
) Stabilizer(_name, _sweep, _usdx, _oracleUsdx, _borrower) { | ||
asset = ISUSDe(_asset); | ||
usde = IERC20Metadata(_usde); | ||
pool = ICurvePool(poolAddress); | ||
|
||
assetIndex[_usdx] = USDC_IDX; | ||
assetIndex[_usde] = USDe_IDX; | ||
} | ||
|
||
/* ========== Views ========== */ | ||
|
||
/** | ||
* @notice Asset Value of investment. | ||
* @return the Returns the value of the investment in the USD coin | ||
* @dev the price is obtained from Chainlink | ||
*/ | ||
function assetValue() public view override returns (uint256) { | ||
uint256 sharesBalance = asset.balanceOf(address(this)); | ||
uint256 assetsBalance = asset.convertToAssets(sharesBalance); | ||
assetsBalance = assetsBalance / (10 ** (usde.decimals() - usdx.decimals())); | ||
|
||
return _oracleUsdxToUsd(assetsBalance); | ||
} | ||
|
||
/* ========== Actions ========== */ | ||
|
||
/** | ||
* @notice Invest. | ||
* @param usdxAmount Amount of usdx to be invested | ||
* @dev Swap from usdx to USDe and stake. | ||
*/ | ||
function invest(uint256 usdxAmount, uint256 slippage) | ||
external | ||
onlyBorrower | ||
whenNotPaused | ||
nonReentrant | ||
validAmount(usdxAmount) | ||
{ | ||
_invest(usdxAmount, 0, slippage); | ||
} | ||
|
||
/** | ||
* @notice Divest. | ||
* @param usdxAmount Amount to be divested. | ||
* @param slippage . | ||
* @dev Unsatke and swap from the token to usdx. | ||
*/ | ||
function divest( | ||
uint256 usdxAmount, | ||
uint256 slippage | ||
) | ||
external | ||
onlyBorrower | ||
nonReentrant | ||
validAmount(usdxAmount) | ||
{ | ||
_divest(usdxAmount, slippage); | ||
} | ||
|
||
function requestRedeem(uint256 usdxAmount) | ||
external | ||
onlyBorrower | ||
validAmount(usdxAmount) | ||
{ | ||
if(asset.cooldownDuration() == 0) revert OperationNotAllowed(); | ||
uint256 sharesAmount = _getShares(usdxAmount); | ||
|
||
asset.cooldownShares(sharesAmount); | ||
} | ||
|
||
/** | ||
* @notice Liquidate | ||
*/ | ||
function liquidate() external nonReentrant { | ||
if(auctionAllowed) revert ActionNotAllowed(); | ||
_liquidate(_getToken(), getDebt()); | ||
} | ||
|
||
/* ========== Internals ========== */ | ||
|
||
function _getToken() internal view override returns (address) { | ||
return address(asset); | ||
} | ||
|
||
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 minAmountOut = OvnMath.subBasisPoints(usdxAmount, slippage); | ||
uint256 usdeAmount = swap(address(usdx), address(usde), usdxAmount, minAmountOut); | ||
|
||
TransferHelper.safeApprove(address(usde), address(asset), usdeAmount); | ||
uint256 shares = asset.deposit(usdeAmount, address(this)); | ||
|
||
if(shares < asset.convertToShares(usdeAmount)) revert UnexpectedAmount(); | ||
emit Invested(usdeAmount); | ||
} | ||
|
||
function _divest(uint256 usdxAmount, uint256 slippage) internal override { | ||
address self = address(this); | ||
(uint104 cooldownEnd, uint152 underlyingAmount) = asset.cooldowns(self); | ||
|
||
if(asset.cooldownDuration() > 0) { | ||
if(cooldownEnd == 0 || cooldownEnd >= block.timestamp) revert OperationNotAllowed(); | ||
asset.unstake(self); | ||
} else { | ||
if(underlyingAmount > 0) asset.unstake(self); | ||
uint256 sharesAmount = _getShares(usdxAmount); | ||
asset.redeem(sharesAmount, self, self); | ||
} | ||
|
||
uint256 usdeBalance = usde.balanceOf(self); | ||
uint256 amountOutMin = OvnMath.subBasisPoints(usdeBalance, slippage); | ||
amountOutMin = (amountOutMin * (10 ** usdx.decimals())) / (10 ** usde.decimals()); | ||
uint256 usdcAmount = swap(address(usde), address(usdx), usdeBalance, amountOutMin); | ||
|
||
emit Divested(usdcAmount); | ||
} | ||
|
||
function swap(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin) | ||
internal returns (uint256 amountOut) | ||
{ | ||
TransferHelper.safeApprove(tokenIn, address(pool), amountIn); | ||
|
||
amountOut = pool.exchange( | ||
int8(assetIndex[tokenIn]), | ||
int8(assetIndex[tokenOut]), | ||
amountIn, | ||
amountOutMin, | ||
address(this) | ||
); | ||
} | ||
|
||
function _getShares(uint256 usdxAmount) internal view returns (uint256 sharesAmount) { | ||
uint256 sharesBalance = asset.balanceOf(address(this)); | ||
if (sharesBalance == 0) revert NotEnoughBalance(); | ||
|
||
usdxAmount = (usdxAmount * (10 ** usde.decimals())) / (10 ** usdx.decimals()); | ||
sharesAmount = asset.convertToShares(usdxAmount); | ||
if (sharesBalance < sharesAmount) sharesAmount = sharesBalance; | ||
} | ||
} |
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,19 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity 0.8.19; | ||
|
||
import "@openzeppelin/contracts/interfaces/IERC4626.sol"; | ||
|
||
interface ISUSDe is IERC4626 { | ||
function cooldowns(address assets) external view returns(uint104 cooldownEnd, uint152 underlyingAmount); | ||
|
||
function cooldownDuration() external view returns(uint24); | ||
|
||
// ****************************** // | ||
function cooldownAssets(uint256 assets) external returns (uint256 shares); | ||
|
||
function cooldownShares(uint256 shares) external returns (uint256 assets); | ||
|
||
function unstake(address receiver) external; | ||
|
||
function setCooldownDuration(uint24 duration) external; | ||
} |
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,52 @@ | ||
const { ethers } = require("hardhat"); | ||
const { ask } = require("../../../utils/helper_functions"); | ||
const { tokens, network, wallets, curve, chainlink } = require("../../../utils/constants"); | ||
|
||
async function main() { | ||
[deployer] = await ethers.getSigners(); | ||
|
||
const name = 'Ethena Asset'; | ||
const sweep = tokens.sweep; | ||
const usdc = tokens.usdc; | ||
const usde = tokens.usde; | ||
const susde = tokens.susde; | ||
const oracleUsdc = chainlink.usdc_usd; | ||
const poolAddress = curve.pool_usde; | ||
const borrower = wallets.borrower; | ||
|
||
console.log("==========================================="); | ||
console.log("ETHENA 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("USDe:", usde); | ||
console.log("sUSDe:", susde); | ||
console.log("USDC/USD Chainlink Oracle:", oracleUsdc); | ||
console.log("Pool Address (USDe/USDC):", poolAddress); | ||
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("EthenaAsset"); | ||
const asset = await Asset.deploy( | ||
name, | ||
sweep, | ||
usdc, | ||
usde, | ||
susde, | ||
oracleUsdc, | ||
poolAddress, | ||
borrower | ||
); | ||
|
||
console.log("Ethena Asset deployed to:", asset.address); | ||
console.log(`\nnpx hardhat verify --network ${network.name} ${asset.address} "${name}" ${sweep} ${usdc} ${usde} ${susde} ${oracleUsdc} ${poolAddress} ${borrower}`); | ||
} | ||
|
||
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,124 @@ | ||
const { ethers } = require('hardhat'); | ||
const { expect } = require("chai"); | ||
const { network, tokens, wallets } = require("../../../utils/constants"); | ||
const { impersonate, sendEth, increaseTime } = require("../../../utils/helper_functions"); | ||
|
||
contract('Ethena Asset', async () => { | ||
if (Number(network.id) !== 1) return; | ||
|
||
before(async () => { | ||
[owner, lzEndpoint] = await ethers.getSigners(); | ||
|
||
BORROWER = owner.address; | ||
HOLDER = wallets.usdc_holder; | ||
ADMIN = "0x3b0aaf6e6fcd4a7ceef8c92c32dfea9e64dc1862"; | ||
|
||
depositAmount = 10000e6; | ||
investAmount = 6000e6; | ||
divestAmount = 7000e6; | ||
redeemAmount = 2000e6; | ||
SLIPPAGE = 1000; | ||
|
||
sweep = await ethers.getContractAt("SweepCoin", tokens.sweep); | ||
usdx = await ethers.getContractAt("ERC20", tokens.usdc); | ||
susde = await ethers.getContractAt("ISUSDe", tokens.susde); | ||
|
||
Oracle = await ethers.getContractFactory("AggregatorMock"); | ||
usdcOracle = await Oracle.deploy(); | ||
|
||
Asset = await ethers.getContractFactory("EthenaAsset"); | ||
asset = await Asset.deploy( | ||
"Ethena Asset", | ||
tokens.sweep, | ||
tokens.usdc, | ||
tokens.usde, | ||
tokens.susde, | ||
usdcOracle.address, | ||
"0x02950460E2b9529D0E00284A5fA2d7bDF3fA4d72", // Curve ~ pool | ||
BORROWER | ||
); | ||
|
||
ASSET = asset.address; | ||
user = await impersonate(HOLDER); | ||
await sendEth(HOLDER); | ||
await usdx.connect(user).transfer(asset.address, depositAmount); | ||
}); | ||
|
||
describe("Main test", async function () { | ||
it("invests correctly ~ Swap USDC ->> USDe and stake", async function () { | ||
assetValue = await asset.assetValue(); | ||
|
||
await expect(asset.invest(0, SLIPPAGE)) | ||
.to.be.revertedWithCustomError(asset, 'OverZero'); | ||
|
||
expect(assetValue).to.equal(0); | ||
expect(await asset.currentValue()).to.equal(depositAmount); | ||
expect(await usdx.balanceOf(ASSET)).to.equal(depositAmount); | ||
|
||
await asset.invest(investAmount, SLIPPAGE); | ||
|
||
expect(await asset.assetValue()).to.greaterThan(0); | ||
expect(await usdx.balanceOf(ASSET)).to.equal(depositAmount - investAmount); | ||
|
||
await asset.invest(investAmount, SLIPPAGE); | ||
|
||
expect(await asset.assetValue()).to.greaterThan(0); | ||
expect(await usdx.balanceOf(ASSET)).to.equal(0); | ||
expect(await susde.balanceOf(ASSET)).to.greaterThan(0); | ||
}); | ||
|
||
it("divests correctly ~ Unstake and swap USDe ->> USDC", async function () { | ||
assetValue = await asset.assetValue(); | ||
await expect(asset.invest(investAmount, SLIPPAGE)) | ||
.to.be.revertedWithCustomError(asset, 'NotEnoughBalance'); | ||
|
||
await expect(asset.divest(0, SLIPPAGE)) | ||
.to.be.revertedWithCustomError(asset, 'OverZero'); | ||
|
||
await asset.requestRedeem(divestAmount); | ||
await increaseTime(60*60*24*7); | ||
await asset.divest(divestAmount, SLIPPAGE); | ||
|
||
expect(await asset.assetValue()).to.lessThan(assetValue); | ||
expect(await usdx.balanceOf(ASSET)).to.greaterThan(0); | ||
|
||
await expect(asset.divest(divestAmount, SLIPPAGE)) | ||
.to.be.revertedWithCustomError(asset, 'OperationNotAllowed'); | ||
|
||
await asset.requestRedeem(divestAmount); | ||
await expect(asset.divest(divestAmount, SLIPPAGE)) | ||
.to.be.revertedWithCustomError(asset, 'OperationNotAllowed'); | ||
|
||
await increaseTime(60*60*24*7); | ||
await asset.divest(divestAmount, SLIPPAGE); | ||
|
||
expect(await asset.assetValue()).to.equal(0); | ||
expect(await susde.balanceOf(ASSET)).to.equal(0); | ||
}); | ||
|
||
it("divests correctly when cooldown duration = 0", async function () { | ||
await asset.invest(investAmount, SLIPPAGE); | ||
expect(await asset.assetValue()).to.greaterThan(0); | ||
expect(await susde.balanceOf(ASSET)).to.greaterThan(0); | ||
balance = await usdx.balanceOf(ASSET); | ||
|
||
await asset.requestRedeem(redeemAmount); | ||
resp = await susde.cooldowns(ASSET); | ||
|
||
expect(resp.cooldownEnd).to.greaterThan(0); | ||
expect(resp.underlyingAmount).to.greaterThan(0); | ||
expect(await susde.cooldownDuration()).to.greaterThan(0); | ||
|
||
user = await impersonate(ADMIN); | ||
await sendEth(ADMIN); | ||
await susde.connect(user).setCooldownDuration(0); | ||
expect(await susde.cooldownDuration()).to.equal(0); | ||
|
||
await asset.divest(divestAmount, SLIPPAGE); | ||
|
||
expect(await usdx.balanceOf(ASSET)).to.greaterThan(balance); | ||
expect(await asset.assetValue()).to.equal(0); | ||
expect(await susde.balanceOf(ASSET)).to.equal(0); | ||
}) | ||
}); | ||
}); |
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
Oops, something went wrong.