Skip to content

Commit

Permalink
feat: PTUSDe oracle
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeNervoXS committed Apr 26, 2024
1 parent 7340a3b commit 351f548
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 15 deletions.
40 changes: 40 additions & 0 deletions contracts/oracle/morpho/mainnet/MorphoFeedPTUSDe.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.12;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

import "../../BaseFeedPTPendle.sol";

/// @title MorphoFeedPTUSDe
/// @author Angle Labs, Inc.
/// @notice Gives the price of PT-USDe in ETH in base 18
contract MorphoFeedPTUSDe is BaseFeedPTPendle {
string public constant description = "PT-USDe/USDe Oracle";

Check warning on line 13 in contracts/oracle/morpho/mainnet/MorphoFeedPTUSDe.sol

View workflow job for this annotation

GitHub Actions / lint

Constant name must be in capitalized SNAKE_CASE

constructor(
IAccessControlManager accessControlManager,

Check notice

Code scanning / Slither

Local variable shadowing Low

uint256 _maxImpliedRate,
uint32 _twapDuration
) BaseFeedPTPendle(accessControlManager, _maxImpliedRate, _twapDuration) {}

/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
OVERRIDES
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/

function asset() public pure override returns (address) {
return 0x4c9EDD5852cd905f086C759E8383e09bff1E68B3;
}

function sy() public pure override returns (address) {
return 0x42862F48eAdE25661558AFE0A630b132038553D0;
}

function maturity() public pure override returns (uint256) {
return 1721865600;
}

function market() public pure override returns (address) {
return 0x19588F29f9402Bb508007FeADd415c875Ee3f19F;
}
}
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[profile.default]
evm_version = "shanghai"
src = 'contracts'
out = 'out'
test = 'test'
Expand Down
34 changes: 34 additions & 0 deletions scripts/foundry/mainnet/PTUSDeOracle.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.17;

import "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
import { MorphoFeedPTUSDe } from "borrow-contracts/oracle/morpho/mainnet/MorphoFeedPTUSDe.sol";
import "utils/src/CommonUtils.sol";
import { IAccessControlManager } from "borrow-contracts/interfaces/IAccessControlManager.sol";

contract PTweETHOracleDeploy is Script, CommonUtils {
function run() external {
uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
address deployer = vm.addr(deployerPrivateKey);
vm.startBroadcast(deployerPrivateKey);

// TODO
uint256 chainId = CHAIN_ETHEREUM;
address coreBorrow = 0x5bc6BEf80DA563EBf6Df6D6913513fa9A7ec89BE;
uint32 _TWAP_DURATION = 30 minutes;
uint256 _MAX_IMPLIED_RATE = 0.5 ether;
// end TODO

MorphoFeedPTUSDe oracle = new MorphoFeedPTUSDe(
IAccessControlManager(address(coreBorrow)),
_MAX_IMPLIED_RATE,
_TWAP_DURATION
);
(, int256 answer, , , ) = oracle.latestRoundData();
console.log("oracle value ", uint256(answer));
console.log("Successfully deployed PT-weETH: ", address(oracle));

vm.stopBroadcast();
}
}
63 changes: 52 additions & 11 deletions test/foundry/oracles/morpho/MorphoChainlinkOracleTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.0;
import { console } from "forge-std/console.sol";
import { stdStorage, StdStorage, Test } from "forge-std/Test.sol";
import { MorphoFeedPTweETH, BaseFeedPTPendle } from "borrow-contracts/oracle/morpho/mainnet/MorphoFeedPTweETH.sol";
import { MorphoFeedPTUSDe } from "borrow-contracts/oracle/morpho/mainnet/MorphoFeedPTUSDe.sol";
import { MockTreasury } from "borrow-contracts/mock/MockTreasury.sol";
import { IAgToken } from "borrow-contracts/interfaces/IAgToken.sol";
import { IMorphoChainlinkOracleV2Factory } from "borrow-contracts/interfaces/external/morpho/IMorphoChainlinkOracleV2Factory.sol";
Expand Down Expand Up @@ -31,13 +32,18 @@ contract MorphoChainlinkOracleTest is Test, CommonUtils {
address internal _alice = address(uint160(uint256(keccak256(abi.encodePacked("alice")))));
address internal _governor = address(uint160(uint256(keccak256(abi.encodePacked("governor")))));
address internal _guardian = address(uint160(uint256(keccak256(abi.encodePacked("guardian")))));
address constant PTWeETH = 0xc69Ad9baB1dEE23F4605a82b3354F8E40d1E5966;
address constant PTUSDe = 0xa0021EF8970104c2d008F38D92f115ad56a9B8e1;
address constant oracleWeETH = 0xdDb6F90fFb4d3257dd666b69178e5B3c5Bf41136;
address constant oracleUSDe = 0xbC5FBcf58CeAEa19D523aBc76515b9AEFb5cfd58;

uint256 public constant YEAR = 365 days;
uint32 internal _TWAP_DURATION;
uint32 internal _STALE_PERIOD;
uint256 internal _MAX_IMPLIED_RATE;

MockCoreBorrow public coreBorrow;
MorphoFeedPTweETH internal _oracle;
BaseFeedPTPendle internal _oracle;
IMorphoChainlinkOracleV2 public morphoOracle;
IERC20Metadata public agToken;
IERC20Metadata public collateral;
Expand All @@ -46,10 +52,10 @@ contract MorphoChainlinkOracleTest is Test, CommonUtils {

function setUp() public {
uint256 chainId = CHAIN_ETHEREUM;
ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_ETHEREUM"), 19739082);
ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_ETHEREUM"));
forkIdentifier[CHAIN_ETHEREUM] = ethereumFork;

_TWAP_DURATION = 1 hours;
_TWAP_DURATION = 15 minutes;
_STALE_PERIOD = 24 hours;
_MAX_IMPLIED_RATE = 0.5 ether;

Expand All @@ -58,27 +64,62 @@ contract MorphoChainlinkOracleTest is Test, CommonUtils {
coreBorrow.toggleGuardian(_guardian);
coreBorrow.toggleGovernor(_governor);
agToken = IERC20Metadata(0x0000206329b97DB379d5E1Bf586BbDB969C63274);
_oracle = new MorphoFeedPTweETH(IAccessControlManager(address(coreBorrow)), _MAX_IMPLIED_RATE, _TWAP_DURATION);
}

function test_PTweETH_Success() public {
_oracle = BaseFeedPTPendle(
address(
new MorphoFeedPTweETH(IAccessControlManager(address(coreBorrow)), _MAX_IMPLIED_RATE, _TWAP_DURATION)
)
);
// Missing a vault like cntract to go from weETH to eeETH
morphoOracle = MORPHO_FACTORY.createMorphoChainlinkOracleV2(
IERC4626(address(0)),
1,
AggregatorV3Interface(address(_oracle)),
AggregatorV3Interface(address(0xdDb6F90fFb4d3257dd666b69178e5B3c5Bf41136)),
IERC20Metadata(address(0xc69Ad9baB1dEE23F4605a82b3354F8E40d1E5966)).decimals(),
AggregatorV3Interface(address(oracleWeETH)),
IERC20Metadata(address(PTWeETH)).decimals(),
IERC4626(address(0)),
1,
AggregatorV3Interface(address(0)),
AggregatorV3Interface(address(0)),
agToken.decimals(),
hex""
);
(, int256 answer, , , ) = AggregatorV3Interface(address(oracleWeETH)).latestRoundData();
uint8 decimalCl = AggregatorV3Interface(address(oracleWeETH)).decimals();
(, int256 pricePT, , , ) = _oracle.latestRoundData();

uint256 morphoPrice = morphoOracle.price();
assertEq(10 ** 10, morphoOracle.SCALE_FACTOR());
assertApproxEqRel(
((uint256(answer) * uint256(pricePT)) / 10 ** decimalCl) * 1 ether,
morphoPrice,
0.00001 ether
);
assertApproxEqRel(3040 ether, morphoPrice / 10 ** 18, 0.01 ether);
}

function test_PTweETH_Success() public {
(, int256 answer, , , ) = AggregatorV3Interface(address(0xdDb6F90fFb4d3257dd666b69178e5B3c5Bf41136))
.latestRoundData();
uint8 decimalCl = AggregatorV3Interface(address(0xdDb6F90fFb4d3257dd666b69178e5B3c5Bf41136)).decimals();
function test_PTUSDe_Success() public {
_oracle = BaseFeedPTPendle(
address(new MorphoFeedPTUSDe(IAccessControlManager(address(coreBorrow)), _MAX_IMPLIED_RATE, _TWAP_DURATION))
);
// Missing a vault like cntract to go from weETH to eeETH
morphoOracle = MORPHO_FACTORY.createMorphoChainlinkOracleV2(
IERC4626(address(0)),
1,
AggregatorV3Interface(address(_oracle)),
AggregatorV3Interface(oracleUSDe),
IERC20Metadata(PTUSDe).decimals(),
IERC4626(address(0)),
1,
AggregatorV3Interface(address(0)),
AggregatorV3Interface(address(0)),
agToken.decimals(),
hex""
);
(, int256 answer, , , ) = AggregatorV3Interface(oracleUSDe).latestRoundData();
uint8 decimalCl = AggregatorV3Interface(oracleUSDe).decimals();
(, int256 pricePT, , , ) = _oracle.latestRoundData();

uint256 morphoPrice = morphoOracle.price();
Expand All @@ -88,6 +129,6 @@ contract MorphoChainlinkOracleTest is Test, CommonUtils {
morphoPrice,
0.00001 ether
);
assertApproxEqRel(3040 ether, morphoPrice / 10 ** 18, 0.01 ether);
assertApproxEqRel(0.9 ether, morphoPrice / 10 ** 18, 0.01 ether);
}
}
2 changes: 1 addition & 1 deletion test/foundry/oracles/pendle/BaseOraclePTPendle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ contract BaseOraclePendlePT is Test {
function setUp() public virtual {
arbitrumFork = vm.createFork(vm.envString("ETH_NODE_URI_ARBITRUM"));
avalancheFork = vm.createFork(vm.envString("ETH_NODE_URI_AVALANCHE"));
ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_ETHEREUM"), 19739082);
ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_ETHEREUM"), 19740549);
optimismFork = vm.createFork(vm.envString("ETH_NODE_URI_OPTIMISM"));
polygonFork = vm.createFork(vm.envString("ETH_NODE_URI_POLYGON"));
gnosisFork = vm.createFork(vm.envString("ETH_NODE_URI_GNOSIS"));
Expand Down
10 changes: 7 additions & 3 deletions test/foundry/oracles/pendle/MorphoFeedPTPendle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ contract MorphoFeedPTPendleTest is Test {
uint256 internal _MAX_IMPLIED_RATE;

MockCoreBorrow public coreBorrow;
MorphoFeedPTweETH internal _oracle;
BaseFeedPTPendle internal _oracle;

function setUp() public virtual {
ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_ETHEREUM"), 19739082);
ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_ETHEREUM"), 19740549);
forkIdentifier[CHAIN_ETHEREUM] = ethereumFork;

_TWAP_DURATION = 1 hours;
Expand All @@ -55,7 +55,11 @@ contract MorphoFeedPTPendleTest is Test {
coreBorrow = new MockCoreBorrow();
coreBorrow.toggleGuardian(_guardian);
coreBorrow.toggleGovernor(_governor);
_oracle = new MorphoFeedPTweETH(IAccessControlManager(address(coreBorrow)), _MAX_IMPLIED_RATE, _TWAP_DURATION);
_oracle = BaseFeedPTPendle(
address(
new MorphoFeedPTweETH(IAccessControlManager(address(coreBorrow)), _MAX_IMPLIED_RATE, _TWAP_DURATION)
)
);
}

function _economicLowerBound(uint256 maxImpliedRate, uint256 maturity) internal view returns (uint256) {
Expand Down
91 changes: 91 additions & 0 deletions test/foundry/oracles/pendle/MorphoFeedPTUSDe.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;

import "./MorphoFeedPTPendle.t.sol";
import { MorphoFeedPTUSDe } from "borrow-contracts/oracle/morpho/mainnet/MorphoFeedPTUSDe.sol";
import { IAccessControlManager } from "borrow-contracts/interfaces/IAccessControlManager.sol";

contract MorphoFeedPTUSDeTest is MorphoFeedPTPendleTest {
using stdStorage for StdStorage;

function setUp() public override {
super.setUp();

_TWAP_DURATION = 30 minutes;
_STALE_PERIOD = 24 hours;
_MAX_IMPLIED_RATE = 0.5 ether;

_oracle = BaseFeedPTPendle(
address(new MorphoFeedPTUSDe(IAccessControlManager(address(coreBorrow)), _MAX_IMPLIED_RATE, _TWAP_DURATION))
);
}

/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
CORE LOGIC
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/

function test_Description_Success() public {
assertEq(_oracle.description(), "PT-USDe/USDe Oracle");
}

function test_Simple_Success() public {
(, int256 answer, , , ) = _oracle.latestRoundData();
uint256 value = uint256(answer);

assertApproxEqAbs(value, 0.90 ether, 0.01 ether);
}

function test_EconomicalLowerBound_tooSmall() public {
vm.prank(_governor);
_oracle.setMaxImpliedRate(uint256(1e1));
uint256 pendleAMMPrice = PendlePtOracleLib.getPtToAssetRate(IPMarket(_oracle.market()), _TWAP_DURATION);

(, int256 answer, , , ) = _oracle.latestRoundData();
uint256 value = uint256(answer);

assertEq(value, pendleAMMPrice);
}

function test_AfterMaturity_Success() public {
// Adavnce to the PT maturity
vm.warp(_oracle.maturity());

uint256 pendleAMMPrice = PendlePtOracleLib.getPtToAssetRate(IPMarket(_oracle.market()), _TWAP_DURATION);
(, int256 answer, , , ) = _oracle.latestRoundData();
uint256 value = uint256(answer);

assertEq(value, pendleAMMPrice);
assertEq(value, 1 ether);
}

function test_HackRemove_Success(uint256 slash) public {
slash = bound(slash, 1, BASE_18);
// Remove part of the SY backing collateral to simulate a hack
IERC20 weETH = IERC20(address(_oracle.asset()));
uint256 prevBalance = weETH.balanceOf(_oracle.sy());
uint256 postBalance = (prevBalance * slash) / BASE_18;
deal(address(weETH), _oracle.sy(), postBalance);

uint256 lowerBound = _economicLowerBound(_MAX_IMPLIED_RATE, _oracle.maturity());
(, int256 answer, , , ) = _oracle.latestRoundData();
uint256 value = uint256(answer);

assertLe(value, (lowerBound * slash) / BASE_18);
if (slash > 0) assertGe(value, (lowerBound * (slash - 1)) / BASE_18);
}

function test_HackExpand_Success(uint256 expand) public {
expand = bound(expand, BASE_18, BASE_18 * 1e7);
// Remove part of the SY backing collateral to simulate a hack
IERC20 weETH = IERC20(address(_oracle.asset()));
uint256 prevBalance = weETH.balanceOf(_oracle.sy());
uint256 postBalance = (prevBalance * expand) / BASE_18;
deal(address(weETH), _oracle.sy(), postBalance);

uint256 lowerBound = _economicLowerBound(_MAX_IMPLIED_RATE, _oracle.maturity());
(, int256 answer, , , ) = _oracle.latestRoundData();
uint256 value = uint256(answer);

assertEq(value, lowerBound);
}
}

0 comments on commit 351f548

Please sign in to comment.