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

Aerodrome Collateral Plugin #1210

Open
wants to merge 13 commits into
base: 4.0.0
Choose a base branch
from
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ jobs:
restore-keys: |
hardhat-network-fork-${{ runner.os }}-
hardhat-network-fork-
- run: yarn hardhat test ./test/plugins/individual-collateral/{cbeth,aave-v3,compoundv3,stargate,lido}/*.test.ts
- run: yarn hardhat test ./test/plugins/individual-collateral/{cbeth,aave-v3,aerodrome,compoundv3,stargate,lido}/*.test.ts
env:
NODE_OPTIONS: '--max-old-space-size=32768'
TS_NODE_SKIP_IGNORE: true
Expand Down
4 changes: 4 additions & 0 deletions common/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export interface IPools {
crvTriCrypto?: string
crvMIM3Pool?: string
sdUSDCUSDCPlus?: string
aeroUSDCeUSD?: string
}

interface INetworkConfig {
Expand Down Expand Up @@ -516,6 +517,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
sUSDbC: '0x4c80e24119cfb836cdf0a6b53dc23f04f7e652ca',
wstETH: '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452',
STG: '0xE3B53AF74a4BF62Ae5511055290838050bf764Df',
AERO: '0x940181a94A35A4569E4529A3CDfB74e38FD98631',
eUSD: '0xCfA3Ef56d303AE4fAabA0592388F19d7C3399FB4',
},
chainlinkFeeds: {
DAI: '0x591e79239a7d679378ec8c847e5038150364c78f', // 0.3%, 24hr
Expand All @@ -532,6 +535,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
stETHETH: '0xf586d0728a47229e747d824a939000Cf21dEF5A0', // 0.5%, 24h
ETHUSD: '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70', // 0.15%, 20min
wstETHstETH: '0xB88BAc61a4Ca37C43a3725912B1f472c9A5bc061', // 0.5%, 24h
eUSD: '0x9b2C948dbA5952A1f5Ab6fA16101c1392b8da1ab',
},
GNOSIS_EASY_AUCTION: '0xb1875Feaeea32Bbb02DE83D81772e07E37A40f02', // mock
COMET_REWARDS: '0x123964802e6ABabBE1Bc9547D72Ef1B69B00A6b1',
Expand Down
49 changes: 49 additions & 0 deletions contracts/plugins/assets/aerodrome/AerodromeGaugeWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../erc20/RewardableERC20Wrapper.sol";
import "./vendor/IAeroGauge.sol";

// Note: Only supports AERO rewards.
contract AerodromeGaugeWrapper is RewardableERC20Wrapper {
using SafeERC20 for IERC20;

IAeroGauge public immutable gauge;

/// @param _lpToken The Aerodrome LP token, transferrable
constructor(
ERC20 _lpToken,
string memory _name,
string memory _symbol,
ERC20 _aero,
IAeroGauge _gauge
) RewardableERC20Wrapper(_lpToken, _name, _symbol, _aero) {
require(
address(_aero) != address(0) &&
address(_gauge) != address(0) &&
address(_lpToken) != address(0),
"invalid address"
);

require(address(_aero) == address(_gauge.rewardToken()), "wrong Aero");

gauge = _gauge;
}

// deposit an Aerodrome LP token
function _afterDeposit(uint256 _amount, address) internal override {
underlying.approve(address(gauge), _amount);
gauge.deposit(_amount);
}

// withdraw to Aerodrome LP token
function _beforeWithdraw(uint256 _amount, address) internal override {
gauge.withdraw(_amount);
}

// claim rewards - only supports AERO rewards
function _claimAssetRewards() internal virtual override {
gauge.getReward(address(this));
}
}
252 changes: 252 additions & 0 deletions contracts/plugins/assets/aerodrome/AerodromePoolTokens.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// SPDX-License-Identifier: ISC
pragma solidity 0.8.19;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "contracts/plugins/assets/OracleLib.sol";
import "contracts/libraries/Fixed.sol";
import "./vendor/IAeroPool.sol";

/// Supports Aerodrome stable pools (2 tokens)
contract AerodromePoolTokens {
using OracleLib for AggregatorV3Interface;
using FixLib for uint192;

error WrongIndex(uint8 maxLength);
error NoToken(uint8 tokenNumber);

uint8 internal constant nTokens = 2;

enum AeroPoolType {
Stable,
Volatile // not supported in this version
}

// === State (Immutable) ===

IAeroPool public immutable pool;
AeroPoolType public immutable poolType;

IERC20Metadata internal immutable token0;
IERC20Metadata internal immutable token1;

// For each token, we maintain up to two feeds/timeouts/errors
// The data below would normally be a struct, but we want bytecode substitution

AggregatorV3Interface internal immutable _t0feed0;
AggregatorV3Interface internal immutable _t0feed1;
uint48 internal immutable _t0timeout0; // {s}
uint48 internal immutable _t0timeout1; // {s}
uint192 internal immutable _t0error0; // {1}
uint192 internal immutable _t0error1; // {1}

AggregatorV3Interface internal immutable _t1feed0;
AggregatorV3Interface internal immutable _t1feed1;
uint48 internal immutable _t1timeout0; // {s}
uint48 internal immutable _t1timeout1; // {s}
uint192 internal immutable _t1error0; // {1}
uint192 internal immutable _t1error1; // {1}

// === Config ===

struct APTConfiguration {
IAeroPool pool;
AeroPoolType poolType;
AggregatorV3Interface[][] feeds; // row should multiply to give {UoA/ref}; max columns is 2
uint48[][] oracleTimeouts; // {s} same order as feeds
uint192[][] oracleErrors; // {1} same order as feeds
}

constructor(APTConfiguration memory config) {
require(maxFeedsLength(config.feeds) <= 2, "price feeds limited to 2");
require(
config.feeds.length == nTokens && minFeedsLength(config.feeds) != 0,
"each token needs at least 1 price feed"
);
require(address(config.pool) != address(0), "pool address is zero");

pool = config.pool;
poolType = config.poolType;

// Solidity does not support immutable arrays. This is a hack to get the equivalent of
// an immutable array so we do not have store the token feeds in the blockchain. This is
// a gas optimization since it is significantly more expensive to read and write on the
// blockchain than it is to use embedded values in the bytecode.

// === Tokens ===

if (config.poolType != AeroPoolType.Stable) {
revert("invalid poolType");
}

token0 = IERC20Metadata(pool.token0());
token1 = IERC20Metadata(pool.token1());

// === Feeds + timeouts ===
// I know this section at-first looks verbose and silly, but it's actually well-justified:
// - immutable variables cannot be conditionally written to
// - a struct or an array would not be able to be immutable
// - immutable variables means values get in-lined in the bytecode

// token0
bool more = config.feeds[0].length != 0;
// untestable:
// more will always be true based on previous feeds validations
_t0feed0 = more ? config.feeds[0][0] : AggregatorV3Interface(address(0));
_t0timeout0 = more && config.oracleTimeouts[0].length != 0
? config.oracleTimeouts[0][0]
: 0;
_t0error0 = more && config.oracleErrors[0].length != 0 ? config.oracleErrors[0][0] : 0;
if (more) {
require(address(_t0feed0) != address(0), "t0feed0 empty");
require(_t0timeout0 != 0, "t0timeout0 zero");
require(_t0error0 < FIX_ONE, "t0error0 too large");
}

more = config.feeds[0].length > 1;
_t0feed1 = more ? config.feeds[0][1] : AggregatorV3Interface(address(0));
_t0timeout1 = more && config.oracleTimeouts[0].length > 1 ? config.oracleTimeouts[0][1] : 0;
_t0error1 = more && config.oracleErrors[0].length > 1 ? config.oracleErrors[0][1] : 0;
if (more) {
require(address(_t0feed1) != address(0), "t0feed1 empty");
require(_t0timeout1 != 0, "t0timeout1 zero");
require(_t0error1 < FIX_ONE, "t0error1 too large");
}

// token1
// untestable:
// more will always be true based on previous feeds validations
more = config.feeds[1].length != 0;
_t1feed0 = more ? config.feeds[1][0] : AggregatorV3Interface(address(0));
_t1timeout0 = more && config.oracleTimeouts[1].length != 0
? config.oracleTimeouts[1][0]
: 0;
_t1error0 = more && config.oracleErrors[1].length != 0 ? config.oracleErrors[1][0] : 0;
if (more) {
require(address(_t1feed0) != address(0), "t1feed0 empty");
require(_t1timeout0 != 0, "t1timeout0 zero");
require(_t1error0 < FIX_ONE, "t1error0 too large");
}

more = config.feeds[1].length > 1;
_t1feed1 = more ? config.feeds[1][1] : AggregatorV3Interface(address(0));
_t1timeout1 = more && config.oracleTimeouts[1].length > 1 ? config.oracleTimeouts[1][1] : 0;
_t1error1 = more && config.oracleErrors[1].length > 1 ? config.oracleErrors[1][1] : 0;
if (more) {
require(address(_t1feed1) != address(0), "t1feed1 empty");
require(_t1timeout1 != 0, "t1timeout1 zero");
require(_t1error1 < FIX_ONE, "t1error1 too large");
}
}

/// @dev Warning: Can revert
/// @param index The index of the token: 0 or 1
/// @return low {UoA/ref_index}
/// @return high {UoA/ref_index}
function tokenPrice(uint8 index) public view virtual returns (uint192 low, uint192 high) {
if (index >= nTokens) revert WrongIndex(nTokens - 1);

// Use only 1 feed if 2nd feed not defined
// otherwise: multiply feeds together, e.g; {UoA/ref} = {UoA/target} * {target/ref}
uint192 x;
uint192 y = FIX_ONE;
uint192 xErr; // {1}
uint192 yErr; // {1}
// if only 1 feed: `y` is FIX_ONE and `yErr` is 0

if (index == 0) {
x = _t0feed0.price(_t0timeout0);
xErr = _t0error0;
if (address(_t0feed1) != address(0)) {
y = _t0feed1.price(_t0timeout1);
yErr = _t0error1;
}
} else {
x = _t1feed0.price(_t1timeout0);
xErr = _t1error0;
if (address(_t1feed1) != address(0)) {
y = _t1feed1.price(_t1timeout1);
yErr = _t1error1;
}
}

return toRange(x, y, xErr, yErr);
}

/// @param index The index of the token: 0 or 1
/// @return [{ref_index}]
function tokenReserve(uint8 index) public view virtual returns (uint256) {
if (index >= nTokens) revert WrongIndex(nTokens - 1);
// Maybe also cache token decimals as immutable?
IERC20Metadata tokenInterface = getToken(index);
if (index == 0) {
return shiftl_toFix(pool.reserve0(), -int8(tokenInterface.decimals()), FLOOR);
}
return shiftl_toFix(pool.reserve1(), -int8(tokenInterface.decimals()), FLOOR);
}

/// @param index The index of the token: 0 or 1
/// @return [address of chainlink feeds]
function tokenFeeds(uint8 index) public view virtual returns (AggregatorV3Interface[] memory) {
if (index >= nTokens) revert WrongIndex(nTokens - 1);
AggregatorV3Interface[] memory feeds = new AggregatorV3Interface[](2);
if (index == 0) {
feeds[0] = _t0feed0;
feeds[1] = _t0feed1;
} else {
feeds[0] = _t1feed0;
feeds[1] = _t1feed1;
}
return feeds;
}

// === Internal ===

function maxPoolOracleTimeout() internal view virtual returns (uint48) {
return
uint48(
Math.max(Math.max(_t0timeout0, _t1timeout0), Math.max(_t0timeout1, _t1timeout1))
);
}

// === Private ===

function getToken(uint8 index) private view returns (IERC20Metadata) {
// untestable:
// getToken is always called with a valid index
if (index >= nTokens) revert WrongIndex(nTokens - 1);
if (index == 0) return token0;
return token1;
}

function minFeedsLength(AggregatorV3Interface[][] memory feeds) private pure returns (uint8) {
uint8 minLength = type(uint8).max;
for (uint8 i = 0; i < feeds.length; ++i) {
minLength = uint8(Math.min(minLength, feeds[i].length));
}
return minLength;
}

function maxFeedsLength(AggregatorV3Interface[][] memory feeds) private pure returns (uint8) {
uint8 maxLength;
for (uint8 i = 0; i < feeds.length; ++i) {
maxLength = uint8(Math.max(maxLength, feeds[i].length));
}
return maxLength;
}

/// x and y can be any two fixes that can be multiplied
/// @param xErr {1} error associated with x
/// @param yErr {1} error associated with y
/// returns low and high extremes of x * y, given errors
function toRange(
uint192 x,
uint192 y,
uint192 xErr,
uint192 yErr
) private pure returns (uint192 low, uint192 high) {
low = x.mul(FIX_ONE - xErr).mul(y.mul(FIX_ONE - yErr), FLOOR);
high = x.mul(FIX_ONE + xErr).mul(y.mul(FIX_ONE + yErr), CEIL);
}
}
Loading
Loading