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

Add AERO/WETH Oracle #2043

Merged
merged 9 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
489 changes: 489 additions & 0 deletions contracts/contracts/harvest/AeroHavester.sol

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions contracts/contracts/mocks/MockVaultForBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import "hardhat/console.sol";
interface IERC20MintableBurnable {
function mintTo(address to, uint256 value) external;

function mint(address to, uint256 value) external;

function burnFrom(address account, uint256 value) external;

function transfer(address account, uint256 value) external;
Expand Down
78 changes: 78 additions & 0 deletions contracts/contracts/oracle/BaseOETHOracleRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../interfaces/chainlink/AggregatorV3Interface.sol";
import { OracleRouterBase } from "./OracleRouterBase.sol";
import { StableMath } from "../utils/StableMath.sol";

// @notice Oracle Router that denominates all prices in ETH for base network
contract BaseOETHOracleRouter is OracleRouterBase {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this particular case the word "Base" can be confusing since we are already using it in OracleRouterBase to communicate the abstract classes shared by many routers. Maybe a BaseChainOETHOracleRouter would be e better name

using StableMath for uint256;

address public immutable aeroPriceFeed;

constructor(address _aeroPriceFeed) {
aeroPriceFeed = _aeroPriceFeed;
}

/**
* @notice Returns the total price in 18 digit units for a given asset.
* This implementation does not (!) do range checks as the
* parent OracleRouter does.
* @param asset address of the asset
* @return uint256 unit price for 1 asset unit, in 18 decimal fixed
*/
function price(address asset)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is identical to the one in OETHOracleRouter.sol. Why not let your contract inherit from that one?

A good rule of thumb is to try to never have copy/pasted - duplicated code in your repository. Because when there is a bug discovered you might fix it in 1 location but not the other.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we inherit from OETHOracleRouter, we might wanna initialize the parent contract by passing the Aura price feed address. Since it is designed to work with Base, it made more sense to inherit from the OracleRouterBase rather than OETHOracleRouter.sol. let me know wdyt.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes you are right I haven't noticed that. What I'd do is create then another abstract contract called OracleRouterNoPriceRange that inherits from OracleRouterBase and only implements the price the way it is implemented in OETHOracleRouter.

Then alter the code so that OETHOracleRouter and BaseOETHOracleRouter both inherit from that contract and not re-implement the price function.

external
view
virtual
override
returns (uint256)
{
(address _feed, uint256 maxStaleness) = feedMetadata(asset);
if (_feed == FIXED_PRICE) {
return 1e18;
}
require(_feed != address(0), "Asset not available");

// slither-disable-next-line unused-return
(, int256 _iprice, , uint256 updatedAt, ) = AggregatorV3Interface(_feed)
.latestRoundData();

require(
updatedAt + maxStaleness >= block.timestamp,
"Oracle price too old"
);

uint8 decimals = getDecimals(_feed);
uint256 _price = uint256(_iprice).scaleBy(18, decimals);
return _price;
}

/**
* @dev The price feed contract to use for a particular asset along with
* maximum data staleness
* @param asset address of the asset
* @return feedAddress address of the price feed for the asset
* @return maxStaleness maximum acceptable data staleness duration
*/
function feedMetadata(address asset)
internal
view
virtual
override
returns (address feedAddress, uint256 maxStaleness)
{
if (asset == 0x4200000000000000000000000000000000000006) {
// FIXED_PRICE: WETH/ETH
feedAddress = FIXED_PRICE;
maxStaleness = 0;
} else if (asset == 0x940181a94A35A4569E4529A3CDfB74e38FD98631) {
// AERO/ETH
feedAddress = aeroPriceFeed;
maxStaleness = 1 days;
} else {
revert("Asset not available");
}
}
}
139 changes: 139 additions & 0 deletions contracts/contracts/oracle/PriceFeedPair.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { AggregatorV3Interface } from "../interfaces/chainlink/AggregatorV3Interface.sol";
import { StableMath } from "../utils/StableMath.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";

/**
* @notice Price feed when the feed of 2 oracle prices need combining to achieve desired result.
*
* @dev multiplying oracle pair prices has combining properties. E.g. price FXS/USD multiplied by
* USD/ETH results effectively in FXS/ETH price. Since oracle prices express asset on the left priced
* by the asset on the right, we sometimes need to reverse prices in order to achieve desired results.
* ETH/USD reversed is USD/ETH.
*
* In our first usage of this contract we required FXS/ETH price. It can be derived using FXS/USD and
* ETH/USD prices. Since we need the latter reversed to get the desired result we configure the contract
* by using FXS/USD as feed 0 and USD/ETH (reversed from ETH/USD) as feed 1.
*
* IMPORTANT: It is important to consider that combining 2 Oracle price feeds increases their
* error rate using addition. Meaning if feed 0 has error rate of X and feed 1 error rate of Y
* the resulting error rate is X + Y. E.g.
* - FXS/ETH combines FXS/USD (possible deviation 2%), ETH/USD (possible deviation 0.5%) resulting in
* FXS/USD having possible deviation of 2.5%
*/
contract PriceFeedPair is AggregatorV3Interface {
using SafeCast for uint256;
using SafeCast for int256;
using StableMath for uint256;

// Fields to make it compatible with `AggregatorV3Interface`
uint8 public constant override decimals = 18;
string public constant override description = "";
uint256 public constant override version = 1;
address public immutable addressFeed0;
address public immutable addressFeed1;
bool public immutable reverseFeed0;
bool public immutable reverseFeed1;
uint8 internal immutable decimalsFeed0;
uint8 internal immutable decimalsFeed1;

error PriceFeedAddressError(address _address);
error PriceFeedsMatchError();

constructor(
address _addressFeed0,
address _addressFeed1,
bool _reverseFeed0,
bool _reverseFeed1
) {
if (_addressFeed0 == address(0)) {
revert PriceFeedAddressError(_addressFeed0);
}
if (_addressFeed1 == address(0)) {
revert PriceFeedAddressError(_addressFeed1);
}
if (_addressFeed0 == _addressFeed1) {
revert PriceFeedsMatchError();
}

decimalsFeed0 = AggregatorV3Interface(_addressFeed0).decimals();
decimalsFeed1 = AggregatorV3Interface(_addressFeed1).decimals();
addressFeed0 = _addressFeed0;
addressFeed1 = _addressFeed1;
reverseFeed0 = _reverseFeed0;
reverseFeed1 = _reverseFeed1;
}

function _calculatePrice(int256 priceFeed0, int256 priceFeed1)
internal
view
returns (int256)
{
uint256 price0 = priceFeed0.toUint256().scaleBy(18, decimalsFeed0);

if (reverseFeed0) {
price0 = uint256(1e18).divPrecisely(price0);
}

uint256 price1 = priceFeed1.toUint256().scaleBy(18, decimalsFeed1);

if (reverseFeed1) {
price1 = uint256(1e18).divPrecisely(price1);
}

return price0.mulTruncate(price1).toInt256();
}

/**
* @notice This function exists to make the contract compatible
* with AggregatorV3Interface (which OETHOracleRouter uses to
* get the price).
**/
function latestRoundData()
external
view
override
returns (
uint80,
int256 price,
uint256,
uint256 updatedAt,
uint80
)
{
// slither-disable-next-line unused-return
(, int256 _price0, , uint256 updatedAt0, ) = AggregatorV3Interface(
addressFeed0
).latestRoundData();
// slither-disable-next-line unused-return
(, int256 _price1, , uint256 updatedAt1, ) = AggregatorV3Interface(
addressFeed1
).latestRoundData();
updatedAt = Math.min(updatedAt0, updatedAt1);
price = _calculatePrice(_price0, _price1);
}

/**
* @notice This function exists to make the contract compatible
* with AggregatorV3Interface. The two oracles don't have rounds
* in sync and for that reason we can not query arbitrary oracle
* round and combine it.
**/
function getRoundData(uint80)
external
pure
override
returns (
uint80,
int256 price,
uint256,
uint256 updatedAt,
uint80
)
{
revert("No data present");
}
}
13 changes: 8 additions & 5 deletions contracts/contracts/strategies/AerodromeEthStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,12 @@ contract AerodromeEthStrategy is InitializableAbstractStrategy {
IVault(vaultAddress).mintForStrategy(oethToAdd);

// adjust for slippage
oethToAdd = oethToAdd.mulTruncate(uint256(1e18) - MAX_SLIPPAGE);
wethToAdd = wethToAdd.mulTruncate(uint256(1e18) - MAX_SLIPPAGE);

uint256 minOethToAdd = oethToAdd.mulTruncate(
uint256(1e18) - MAX_SLIPPAGE
);
uint256 minWethToAdd = wethToAdd.mulTruncate(
uint256(1e18) - MAX_SLIPPAGE
);
// Do the deposit to the Aerodrome pool
// slither-disable-next-line arbitrary-send
(, , uint256 lpReceived) = aeroRouterAddress.addLiquidity(
Expand All @@ -223,8 +226,8 @@ contract AerodromeEthStrategy is InitializableAbstractStrategy {
true,
wethToAdd,
oethToAdd,
0,
0,
minWethToAdd,
minOethToAdd,
address(this),
block.timestamp
);
Expand Down
2 changes: 2 additions & 0 deletions contracts/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions contracts/tasks/aeroAmoStrategy.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
const { formatUnits, parseUnits } = require("ethers/lib/utils");
const { formatUnits } = require("ethers/lib/utils");
const { BigNumber } = require("ethers");

const addresses = require("../utils/addresses");
const { getSigner } = require("../utils/signers");
const { getDiffBlocks } = require("./block");
const { scaleAmount } = require("../utils/units");
const {
Expand Down
6 changes: 0 additions & 6 deletions contracts/tasks/aerodrome.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,9 @@
const addresses = require("../utils/addresses");
const { resolveAsset } = require("../utils/assets");
const { getDiffBlocks } = require("./block");
const { getSigner } = require("../utils/signers");

const log = require("../utils/logger")("task:aerodrome");

const advanceTime = async (seconds) => {
seconds = Math.floor(seconds);
await hre.ethers.provider.send("evm_increaseTime", [seconds]);
await hre.ethers.provider.send("evm_mine");
};
/**
* Hardhat task to dump the current state of a Aerodrome sAMM pool used for AMO
*/
Expand All @@ -31,7 +25,7 @@
blockTag,
fromBlockTag,
output,
fixture,

Check failure on line 28 in contracts/tasks/aerodrome.js

View workflow job for this annotation

GitHub Actions / Contracts Linter

Duplicate key 'fixture'
});
}

Expand Down Expand Up @@ -301,12 +295,12 @@
function sqrt(value) {
const ONE = ethers.BigNumber.from(1);
const TWO = ethers.BigNumber.from(2);
x = ethers.BigNumber.from(value);

Check failure on line 298 in contracts/tasks/aerodrome.js

View workflow job for this annotation

GitHub Actions / Contracts Linter

'x' is not defined
let z = x.add(ONE).div(TWO);

Check failure on line 299 in contracts/tasks/aerodrome.js

View workflow job for this annotation

GitHub Actions / Contracts Linter

'x' is not defined
let y = x;

Check failure on line 300 in contracts/tasks/aerodrome.js

View workflow job for this annotation

GitHub Actions / Contracts Linter

'x' is not defined
while (z.sub(y).isNegative()) {
y = z;
z = x.div(z).add(z).div(TWO);

Check failure on line 303 in contracts/tasks/aerodrome.js

View workflow job for this annotation

GitHub Actions / Contracts Linter

'x' is not defined
}
return y;
}
Expand Down
45 changes: 44 additions & 1 deletion contracts/test/_fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
ousdUnits,
units,
isFork,
getBlockTimestamp,

Check failure on line 30 in contracts/test/_fixture.js

View workflow job for this annotation

GitHub Actions / Contracts Linter

'getBlockTimestamp' is assigned a value but never used
fundAccount,

Check failure on line 31 in contracts/test/_fixture.js

View workflow job for this annotation

GitHub Actions / Contracts Linter

'fundAccount' is assigned a value but never used
} = require("./helpers");
const { hardhatSetBalance, setERC20TokenBalance } = require("./_fund");

Expand All @@ -50,7 +50,7 @@
const { defaultAbiCoder, parseUnits, parseEther } = require("ethers/lib/utils");
const balancerStrategyDeployment = require("../utils/balancerStrategyDeployment");
const { impersonateAndFund } = require("../utils/signers");
const { deployWithConfirmation } = require("../utils/deploy.js");

Check failure on line 53 in contracts/test/_fixture.js

View workflow job for this annotation

GitHub Actions / Contracts Linter

'deployWithConfirmation' is assigned a value but never used

const log = require("../utils/logger")("test:fixtures");

Expand Down Expand Up @@ -1714,9 +1714,11 @@

fixture.weth = wETH;
fixture.oeth = oETH;
const [deployer, josh, governorAddr] = await ethers.getSigners();
const [defaultSigner, josh, governorAddr, rewardHarvester] =

Check failure on line 1717 in contracts/test/_fixture.js

View workflow job for this annotation

GitHub Actions / Contracts Linter

'defaultSigner' is assigned a value but never used
await ethers.getSigners();
const { strategistAddr, timelockAddr } = await getNamedAccounts();

fixture.rewardHarvester = rewardHarvester;
fixture.strategist = ethers.provider.getSigner(strategistAddr);
fixture.timelock = ethers.provider.getSigner(timelockAddr);

Expand Down Expand Up @@ -1888,6 +1890,47 @@
}
}

// Deploy Oracle and Harvester contracts
const AeroWethOracle = await ethers.getContractFactory("AeroWEthPriceFeed");
let aeroWethOracle = await AeroWethOracle.deploy(
addresses.base.ethUsdPriceFeed,
addresses.base.aeroUsdPriceFeed
);
fixture.aeroWethOracle = aeroWethOracle;

const AeroHarvester = await ethers.getContractFactory("AeroHarvester");
let harvester = await AeroHarvester.deploy(
aeroWethOracle.address,
addresses.base.wethTokenAddress
);
await harvester.deployed();
await harvester.setRewardTokenConfig(
addresses.base.aeroTokenAddress,
{
allowedSlippageBps: 300,
harvestRewardBps: 100,
swapPlatform: 0, // Aerodrome
swapPlatformAddr: addresses.base.aeroRouterAddress,
liquidationLimit: 0,
doSwapRewardToken: true,
},
[
{
from: addresses.base.aeroTokenAddress,
to: addresses.base.wethTokenAddress,
stable: true,
factory: addresses.base.aeroFactoryAddress,
},
]
);

await harvester.setSupportedStrategy(aerodromeEthStrategy.address, true);
await harvester.setRewardProceedsAddress(rewardHarvester.address);

fixture.harvester = harvester;

await aerodromeEthStrategy.setHarvesterAddress(harvester.address);

return fixture;
}

Expand Down
Loading
Loading