Skip to content

Commit

Permalink
Merge pull request #166 from SweeprFi/ethena-asset
Browse files Browse the repository at this point in the history
adds Ethena asset
  • Loading branch information
maxcoto authored Mar 7, 2024
2 parents 2f05b8f + 7d105d1 commit 0313460
Show file tree
Hide file tree
Showing 6 changed files with 380 additions and 1 deletion.
181 changes: 181 additions & 0 deletions contracts/Assets/EthenaAsset.sol
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;
}
}
19 changes: 19 additions & 0 deletions contracts/Assets/Interfaces/Ethena/IEthena.sol
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;
}
52 changes: 52 additions & 0 deletions scripts/deploy/assets/ethena.js
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();
124 changes: 124 additions & 0 deletions test/assets/mainnet/ethena.js
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);
})
});
});
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
Loading

0 comments on commit 0313460

Please sign in to comment.