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

Frax Convex Strategy with locked Frax Staked Convex LP tokens #1917

Closed
Closed
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b92b96e
Fix Natspec of old Convex and ThreePool strategies
naddison36 Nov 7, 2023
31aa809
WIP ConvexFraxLockingStrategy
naddison36 Nov 7, 2023
339d415
Locked Frax Staked Convex deploy script
naddison36 Nov 7, 2023
c7b71dc
Merge remote-tracking branch 'origin/nicka/convex-frxeth-weth-libs' i…
naddison36 Nov 8, 2023
185bd20
Fix findBalancesSlot for FXS
naddison36 Nov 9, 2023
6eb4817
Added FraxConvexStrategy to hot deploy
naddison36 Nov 9, 2023
cd60c74
removed Balancer vault, wstETH and Curve frxETH/WETH from WhaleAddresses
naddison36 Nov 9, 2023
2f29886
Merge remote-tracking branch 'origin/nicka/convex-frxeth-weth-libs' i…
naddison36 Nov 9, 2023
48625da
Refactor of FraxConvexStrategy
naddison36 Nov 10, 2023
e8e0806
Fix lint and slither
naddison36 Nov 10, 2023
02e986a
Generated new contract diagrams
naddison36 Nov 10, 2023
9f3482e
Update comments
naddison36 Nov 13, 2023
e78d5ff
Merge remote-tracking branch 'origin/nicka/convex-frxeth-weth-libs' i…
naddison36 Nov 27, 2023
7b6dc6f
Updated FraxConvexStrategy code comments
naddison36 Nov 28, 2023
8ae4702
Merge remote-tracking branch 'origin/nicka/convex-frxeth-weth-libs' i…
naddison36 Nov 30, 2023
fa86a9f
Merge remote-tracking branch 'origin/nicka/convex-frxeth-weth-libs' i…
naddison36 Nov 30, 2023
86643d4
Updated Frax Convex deploy file
naddison36 Nov 30, 2023
a5ad85e
fix storage variables visibility
naddison36 Nov 30, 2023
c2ba94b
Frax Convex Strategy simplification (#1951)
naddison36 Nov 30, 2023
04db36e
Added check that all assets have the same number of decimals
naddison36 Dec 1, 2023
e77f6e8
Fix slither
naddison36 Dec 1, 2023
f0551f4
Generated latest FraxConvexStrategy diagrams
naddison36 Dec 1, 2023
71a8bf8
Polish Frax Convex Strategy comments
naddison36 Dec 1, 2023
d8d635e
Merge remote-tracking branch 'origin/nicka/convex-frxeth-weth-libs' i…
naddison36 Dec 1, 2023
4b90c23
Added Frax Convex deposit
naddison36 Dec 4, 2023
caa92ce
Skip Convex frxETH/WETH strategy as its no longer being used
naddison36 Dec 5, 2023
6353dd8
Enable FXS harvesting (#1954)
sparrowDom Dec 6, 2023
b397370
Merge remote-tracking branch 'origin/nicka/convex-frxeth-weth-libs' i…
naddison36 Dec 6, 2023
421320c
Merge remote-tracking branch 'origin/nicka/convex-frax-locking' into …
naddison36 Dec 6, 2023
230d15e
Added harvest of FXS to value flow
naddison36 Dec 6, 2023
40f4853
add comment regarding stacking of errors
sparrowDom Dec 6, 2023
5870d1d
Merge remote-tracking branch 'refs/remotes/origin/nicka/convex-frax-l…
sparrowDom Dec 6, 2023
3c5fd03
Removed MIN_LOCK_AMOUNT
naddison36 Dec 7, 2023
ff38071
Merge remote-tracking branch 'origin/nicka/convex-frax-locking' into …
naddison36 Dec 7, 2023
883b06b
Generated new contract diagrams
naddison36 Dec 7, 2023
cc4c40a
Small gas optimization
naddison36 Dec 8, 2023
dcb83d1
Update value flow diagrams (#1953)
naddison36 Jan 15, 2024
34fde41
Merge remote-tracking branch 'origin/master' into nicka/convex-frax-l…
naddison36 Jan 15, 2024
1d6e3de
Fix AMO tests (#1977)
shahthepro Jan 16, 2024
7fb738f
Merge remote-tracking branch 'origin/master' into nicka/convex-frax-l…
naddison36 Jan 16, 2024
717f72d
Changes from yAudit review (#1976)
naddison36 Jan 17, 2024
f0e747b
N-01 Lack of Indexed Event Parameters (#1973)
naddison36 Jan 17, 2024
a1bda8e
N-05 Added CURVE_VIRTUAL_PRICE_SCALE constant (#1974)
naddison36 Jan 17, 2024
826735c
Merge branch 'nicka/convex-frxeth-weth-libs' into nicka/convex-frax-l…
sparrowDom Jan 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
107 changes: 107 additions & 0 deletions contracts/contracts/interfaces/IFraxConvexLocking.sol
@@ -0,0 +1,107 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IFraxConvexLocking {
/* ========== EVENTS ========== */
event LockedAdditional(
address indexed user,
bytes32 kek_id,
uint256 amount
);
event LockedLonger(
address indexed user,
bytes32 kek_id,
uint256 new_secs,
uint256 new_start_ts,
uint256 new_end_ts
);
event StakeLocked(
address indexed user,
uint256 amount,
uint256 secs,
bytes32 kek_id,
address source_address
);
event WithdrawLocked(
address indexed user,
uint256 liquidity,
bytes32 kek_id,
address destination_address
);

// Struct for the stake
struct LockedStake {
bytes32 kek_id;
uint256 start_timestamp;
uint256 liquidity;
uint256 ending_timestamp;
uint256 lock_multiplier; // 6 decimals of precision. 1x = 1000000
}

function curvePool() external view returns (address);

function curveToken() external view returns (address);

function earned(address account)
external
view
returns (uint256[] memory new_earned);

function getAllRewardTokens() external view returns (address[] memory);

function getReward(address destination_address)
external
returns (uint256[] memory);

function getReward2(address destination_address, bool claim_extra_too)
external
returns (uint256[] memory);

function getRewardForDuration()
external
view
returns (uint256[] memory rewards_per_duration_arr);

function lastRewardClaimTime(address) external view returns (uint256);

function lastUpdateTime() external view returns (uint256);

function lockAdditional(bytes32 kek_id, uint256 addl_liq) external;

function lockLonger(bytes32 kek_id, uint256 new_ending_ts) external;

function lockMultiplier(uint256 secs) external view returns (uint256);

function lockedLiquidityOf(address account) external view returns (uint256);

function lockedStakes(address, uint256)
external
view
returns (
bytes32 kek_id,
uint256 start_timestamp,
uint256 liquidity,
uint256 ending_timestamp,
uint256 lock_multiplier
);

function lockedStakesOf(address account)
external
view
returns (LockedStake[] memory);

function lockedStakesOfLength(address account)
external
view
returns (uint256);

function stakeLocked(uint256 liquidity, uint256 secs)
external
returns (bytes32);

function withdrawLocked(
bytes32 kek_id,
address destination_address,
bool claim_rewards
) external returns (uint256);
}
69 changes: 69 additions & 0 deletions contracts/contracts/interfaces/IFraxConvexStaking.sol
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IFraxConvexStaking is IERC20 {
event Deposited(
address indexed _user,
address indexed _account,
uint256 _amount,
bool _wrapped
);
event Withdrawn(address indexed _user, uint256 _amount, bool _unwrapped);
event RewardInvalidated(address _rewardToken);
event RewardRedirected(address indexed _account, address _forward);
event RewardAdded(address _token);
event Shutdown();

struct EarnedData {
address token;
uint256 amount;
}

function collateralVault() external view returns (address);

function convexBooster() external view returns (address);

function convexPool() external view returns (address);

function convexPoolId() external view returns (uint256);

function convexToken() external view returns (address);

function curveToken() external view returns (address);

function deposit(uint256 _amount, address _to) external;

function earned(address _account)
external
returns (EarnedData[] memory claimable);

function getReward(address _account, address _forwardTo) external;

function getReward(address _account) external;

function isShutdown() external view returns (bool);

function rewardLength() external view returns (uint256);

function rewardRedirect(address) external view returns (address);

function rewards(uint256)
external
view
returns (
address reward_token,
address reward_pool,
uint256 reward_integral,
uint256 reward_remaining
);

function stake(uint256 _amount, address _to) external;

function totalBalanceOf(address _account) external view returns (uint256);

function withdraw(uint256 _amount) external;

function withdrawAndUnwrap(uint256 _amount) external;
}
4 changes: 3 additions & 1 deletion contracts/contracts/mocks/MockOracleRouterNoStale.sol
Expand Up @@ -25,7 +25,9 @@ contract MockOracleRouterNoStale is OracleRouter {

// @notice Oracle Router used to bypass staleness
contract MockOETHOracleRouterNoStale is OETHOracleRouter {
constructor(address auraPriceFeed) OETHOracleRouter(auraPriceFeed) {}
constructor(address auraPriceFeed, address fxsPriceFeed)
OETHOracleRouter(auraPriceFeed, fxsPriceFeed)
{}

function feedMetadata(address asset)
internal
Expand Down
14 changes: 13 additions & 1 deletion contracts/contracts/oracle/OETHOracleRouter.sol
Expand Up @@ -10,9 +10,11 @@ contract OETHOracleRouter is OracleRouterBase {
using StableMath for uint256;

address public immutable auraPriceFeed;
address public immutable fxsPriceFeed;

constructor(address _auraPriceFeed) {
constructor(address _auraPriceFeed, address _fxsPriceFeed) {
auraPriceFeed = _auraPriceFeed;
fxsPriceFeed = _fxsPriceFeed;
}

/**
Expand Down Expand Up @@ -105,6 +107,16 @@ contract OETHOracleRouter is OracleRouterBase {
// AURA/ETH
feedAddress = auraPriceFeed;
maxStaleness = 0;
} else if (asset == 0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0) {
/* FXS/ETH PriceFeedPar feed contract
*
* Is combining the following to feeds to achieve target feed:
* - https://data.chain.link/ethereum/mainnet/crypto-usd/fxs-usd
* - https://data.chain.link/ethereum/mainnet/crypto-usd/eth-usd (inverted)
*/
feedAddress = fxsPriceFeed;
// eth-usd heartbeat is 1 hour and fxs-usd feed is 1 day
maxStaleness = 1 days + STALENESS_BUFFER;
} else {
revert("Asset not available");
}
Expand Down
139 changes: 139 additions & 0 deletions contracts/contracts/oracle/PriceFeedPair.sol
@@ -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);

Check warning on line 53 in contracts/contracts/oracle/PriceFeedPair.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/oracle/PriceFeedPair.sol#L53

Added line #L53 was not covered by tests
}
if (_addressFeed1 == address(0)) {
revert PriceFeedAddressError(_addressFeed1);

Check warning on line 56 in contracts/contracts/oracle/PriceFeedPair.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/oracle/PriceFeedPair.sol#L56

Added line #L56 was not covered by tests
}
if (_addressFeed0 == _addressFeed1) {
revert PriceFeedsMatchError();

Check warning on line 59 in contracts/contracts/oracle/PriceFeedPair.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/oracle/PriceFeedPair.sol#L59

Added line #L59 was not covered by tests
}

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);

Check warning on line 78 in contracts/contracts/oracle/PriceFeedPair.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/oracle/PriceFeedPair.sol#L78

Added line #L78 was not covered by tests
}

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);
Comment on lines +108 to +116
Copy link
Collaborator

@shahthepro shahthepro Dec 6, 2023

Choose a reason for hiding this comment

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

@sparrowDom @naddison36 Thinking about this, one potential concern I have is that latestRound of one feed can be way off from the other feed. Would it make sense to compare the updatedAt values and make sure both the updates aren't too far off?

Like if FXS<>USD was last updated 24h ago and ETH<>USD 1h ago, I don't think it'd be very reliable to use this price. Especially if there was too much of market movement/price action in the past 24h. More so, since this is a generic contract

Copy link
Collaborator

Choose a reason for hiding this comment

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

Another way to do this for FXS<>ETH pair would be just use Uniswap V3's TWAP (by doing something similar to what we do on AuraPriceFeed): https://blog.uniswap.org/uniswap-v3-oracles

Copy link
Member

Choose a reason for hiding this comment

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

We've discussed this on discord and figured out that the errors of multiple oracle price feeds stack. E.g. 2% error and 0.5% error price feeds result in 2.5% error price feed. We are OK keeping it like that for now, but still important to consider.

}

/**
* @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");
Copy link
Member

Choose a reason for hiding this comment

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

nit: can be a custom error

}
}
22 changes: 18 additions & 4 deletions contracts/contracts/oracle/README.md
Expand Up @@ -14,16 +14,30 @@

![OETH Oracle Router Storage](../../docs/OETHOracleRouterStorage.svg)

## Mix Oracle
## AURA/WETH Price Feed

### Hierarchy

![Mix Oracle Hierarchy](../../docs/MixOracleHierarchy.svg)
![AURA/WETH Price Feed Hierarchy](../../docs/AuraWETHPriceFeedHierarchy.svg)

### Squashed

![Mix Oracle Squashed](../../docs/MixOracleSquashed.svg)
![AURA/WETH Price Feed Squashed](../../docs/AuraWETHPriceFeedSquashed.svg)

### Storage

![Mix Oracle Storage](../../docs/MixOracleStorage.svg)
![AURA/WETH Price Feed Storage](../../docs/AuraWETHPriceFeedStorage.svg)

## Paired Price Feed

### Hierarchy

![Paired Price Feed Hierarchy](../../docs/PriceFeedPairHierarchy.svg)

### Squashed

![Paired Price Feed Squashed](../../docs/PriceFeedPairSquashed.svg)

### Storage

![Paired Price Feed Storage](../../docs/PriceFeedPairStorage.svg)