Skip to content

Commit

Permalink
fix: pendle audit issues
Browse files Browse the repository at this point in the history
* fix: add pendle markets registry for market validations and twap

* fix: ignore any tracked PTs and LPTs as-reward tokens

* refactor: fix broken links to pendle docs

---------

Co-authored-by: Sean Casey <sean@seanjcasey.com>
  • Loading branch information
gabrocheleau and SeanJCasey authored Apr 22, 2024
1 parent 1f70103 commit 473d4b2
Show file tree
Hide file tree
Showing 14 changed files with 745 additions and 503 deletions.
18 changes: 0 additions & 18 deletions contracts/external-interfaces/IPendleV2MarketFactory.sol

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@

pragma solidity >=0.6.0 <0.9.0;

/// @title IPendleV2PtOracle Interface
/// @title IPendleV2PtAndLpOracle Interface
/// @author Enzyme Council <security@enzyme.finance>
interface IPendleV2PtOracle {
function getPtToAssetRate(address _market, uint32 _duration) external view returns (uint256 ptToAssetRate_);

interface IPendleV2PtAndLpOracle {
function getOracleState(address _market, uint32 _duration)
external
view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,6 @@ interface IPendleV2Position is IExternalPosition {
ClaimRewards
}

function getMarketForPrincipalToken(address _principalTokenAddress)
external
view
returns (address marketAddress_);

function getOraclePricingDurationForMarket(address _marketAddress)
external
view
returns (uint32 pricingDuration_);

function getLPTokens() external view returns (address[] memory lpTokenAddresses_);

function getPrincipalTokens() external view returns (address[] memory principalTokenAddresses_);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,28 @@ abstract contract PendleV2PositionDataDecoder {
internal
pure
returns (
address principalTokenAddress_,
IPendleV2Market market_,
uint32 pricingDuration_,
address depositTokenAddress_,
uint256 depositAmount_,
IPendleV2Router.ApproxParams memory guessPtOut_,
uint256 minPtOut_
)
{
return abi.decode(
_actionArgs, (address, IPendleV2Market, uint32, address, uint256, IPendleV2Router.ApproxParams, uint256)
);
return abi.decode(_actionArgs, (IPendleV2Market, address, uint256, IPendleV2Router.ApproxParams, uint256));
}

/// @dev Helper to decode args used during the SellPrincipalToken action
function __decodeSellPrincipalTokenActionArgs(bytes memory _actionArgs)
internal
pure
returns (
IPendleV2PrincipalToken principalTokenAddress_,
IPendleV2Market market_,
address withdrawalTokenAddress_,
uint256 withdrawalAmount_,
uint256 minIncomingAmount_
)
{
return abi.decode(_actionArgs, (IPendleV2PrincipalToken, IPendleV2Market, address, uint256, uint256));
return abi.decode(_actionArgs, (IPendleV2Market, address, uint256, uint256));
}

/// @dev Helper to decode args used during the AddLiquidity action
Expand All @@ -57,15 +52,13 @@ abstract contract PendleV2PositionDataDecoder {
pure
returns (
IPendleV2Market market_,
uint32 pricingDuration_,
address depositTokenAddress_,
uint256 depositAmount_,
IPendleV2Router.ApproxParams memory guessPtReceived_,
uint256 minLpOut_
)
{
return
abi.decode(_actionArgs, (IPendleV2Market, uint32, address, uint256, IPendleV2Router.ApproxParams, uint256));
return abi.decode(_actionArgs, (IPendleV2Market, address, uint256, IPendleV2Router.ApproxParams, uint256));
}

/// @dev Helper to decode args used during the AddLiquidity action
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,20 @@ contract PendleV2PositionParser is PendleV2PositionDataDecoder, IExternalPositio
)
{
if (_actionId == uint256(IPendleV2Position.Actions.BuyPrincipalToken)) {
(,,, address depositTokenAddress, uint256 depositAmount,,) =
(, address depositTokenAddress, uint256 depositAmount,,) =
__decodeBuyPrincipalTokenActionArgs(_encodedActionArgs);

assetsToTransfer_ = new address[](1);
assetsToTransfer_[0] = __parseTokenAddressInput(depositTokenAddress);
amountsToTransfer_ = new uint256[](1);
amountsToTransfer_[0] = depositAmount;
} else if (_actionId == uint256(IPendleV2Position.Actions.SellPrincipalToken)) {
(,, address withdrawalTokenAddress,,) = __decodeSellPrincipalTokenActionArgs(_encodedActionArgs);
(, address withdrawalTokenAddress,,) = __decodeSellPrincipalTokenActionArgs(_encodedActionArgs);

assetsToReceive_ = new address[](1);
assetsToReceive_[0] = __parseTokenAddressInput(withdrawalTokenAddress);
} else if (_actionId == uint256(IPendleV2Position.Actions.AddLiquidity)) {
(,, address depositTokenAddress, uint256 depositAmount,,) =
(, address depositTokenAddress, uint256 depositAmount,,) =
__decodeAddLiquidityActionArgs(_encodedActionArgs);

assetsToTransfer_ = new address[](1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,15 @@ pragma solidity 0.8.19;
/// a numbered PendleV2PositionLibBaseXXX that inherits the previous base.
/// e.g., `PendleV2PositionLibBase2 is PendleV2PositionLibBase1`
abstract contract PendleV2PositionLibBase1 {
event PrincipalTokenAdded(address indexed principalToken, address indexed market);
event PrincipalTokenAdded(address indexed principalToken);

event PrincipalTokenRemoved(address indexed principalToken);

event LpTokenAdded(address indexed lpToken);

event LpTokenRemoved(address indexed lpToken);

event OracleDurationForMarketAdded(address indexed market, uint32 indexed pricingDuration);

address[] internal principalTokens;

address[] internal lpTokens;

mapping(address principalToken => address market) internal principalTokenToMarket;

mapping(address market => uint32 pricingDuration) internal marketToOraclePricingDuration;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: GPL-3.0

/*
This file is part of the Enzyme Protocol.
(c) Enzyme Council <council@enzyme.finance>
For the full license information, please view the LICENSE
file that was distributed with this source code.
*/

pragma solidity >=0.6.0 <0.9.0;

/// @title IPendleV2MarketRegistry Interface
/// @author Enzyme Council <security@enzyme.finance>
interface IPendleV2MarketRegistry {
/// @param marketAddress The Pendle market address to register
/// @param duration The TWAP duration to use for marketAddress
struct UpdateMarketInput {
address marketAddress;
uint32 duration;
}

function getMarketOracleDurationForUser(address _user, address _marketAddress)
external
view
returns (uint32 duration_);

function getPtOracleMarketAndDurationForUser(address _user, address _ptAddress)
external
view
returns (address marketAddress_, uint32 duration_);

function getPtOracleMarketForUser(address _user, address _ptAddress)
external
view
returns (address marketAddress_);

function updateMarketsForCaller(UpdateMarketInput[] calldata _updateMarketInputs, bool _skipValidation) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: GPL-3.0

/*
This file is part of the Enzyme Protocol.
(c) Enzyme Council <council@enzyme.finance>
For the full license information, please view the LICENSE
file that was distributed with this source code.
*/

pragma solidity 0.8.19;

import {IDispatcher} from "../../../../../../persistent/dispatcher/IDispatcher.sol";
import {IPendleV2Market} from "../../../../../../external-interfaces/IPendleV2Market.sol";
import {IPendleV2PrincipalToken} from "../../../../../../external-interfaces/IPendleV2PrincipalToken.sol";
import {IPendleV2PtAndLpOracle} from "../../../../../../external-interfaces/IPendleV2PtAndLpOracle.sol";
import {IPendleV2MarketRegistry} from "./IPendleV2MarketRegistry.sol";

/// @title PendleV2MarketRegistry Contract
/// @author Enzyme Council <security@enzyme.finance>
/// @notice A contract for the per-user registration of Pendle v2 markets
contract PendleV2MarketRegistry is IPendleV2MarketRegistry {
event MarketForUserUpdated(address indexed user, address indexed marketAddress, uint32 duration);

event PtForUserUpdated(address indexed user, address indexed ptAddress, address indexed marketAddress);

error InsufficientOracleState(bool increaseCardinalityRequired, bool oldestObservationSatisfied);

IPendleV2PtAndLpOracle private immutable PENDLE_PT_AND_LP_ORACLE;

mapping(address => mapping(address => uint32)) private userToMarketToOracleDuration;
mapping(address => mapping(address => address)) private userToPtToLinkedMarket;

constructor(IPendleV2PtAndLpOracle _pendlePtAndLpOracle) {
PENDLE_PT_AND_LP_ORACLE = _pendlePtAndLpOracle;
}

/// @notice Updates the market registry specific to the caller
/// @param _updateMarketInputs An array of market config inputs to set
/// @param _skipValidation True to skip optional validation of _updateMarketInputs
/// @dev See UpdateMarketInput definition for struct param details
function updateMarketsForCaller(UpdateMarketInput[] calldata _updateMarketInputs, bool _skipValidation)
external
override
{
address user = msg.sender;

for (uint256 i; i < _updateMarketInputs.length; i++) {
UpdateMarketInput memory marketInput = _updateMarketInputs[i];

// Does not validate zero-duration, which is a valid oracle deactivation
if (marketInput.duration > 0 && !_skipValidation) {
__validateMarketConfig({_marketAddress: marketInput.marketAddress, _duration: marketInput.duration});
}

// Store the market duration
userToMarketToOracleDuration[user][marketInput.marketAddress] = marketInput.duration;
emit MarketForUserUpdated(user, marketInput.marketAddress, marketInput.duration);

// Handle PT-market link
(, IPendleV2PrincipalToken pt,) = IPendleV2Market(marketInput.marketAddress).readTokens();
bool ptIsLinkedToMarket =
getPtOracleMarketForUser({_user: user, _ptAddress: address(pt)}) == marketInput.marketAddress;

if (marketInput.duration > 0) {
// If new duration is non-zero, cache PT-market link (i.e., always follow the last active market)

if (!ptIsLinkedToMarket) {
userToPtToLinkedMarket[user][address(pt)] = marketInput.marketAddress;
emit PtForUserUpdated(user, address(pt), marketInput.marketAddress);
}
} else if (ptIsLinkedToMarket) {
// If the PT's linked market duration is being set to 0, remove link to the market

// Unlink the PT from the market
userToPtToLinkedMarket[user][address(pt)] = address(0);
emit PtForUserUpdated(user, address(pt), address(0));
}
}
}

/// @dev Helper to validate user-input market config.
/// Only validates the recommended oracle state,
/// not whether duration provides a sufficiently secure TWAP price.
/// src: https://docs.pendle.finance/Developers/Integration/HowToIntegratePtAndLpOracle.
function __validateMarketConfig(address _marketAddress, uint32 _duration) private view {
(bool increaseCardinalityRequired,, bool oldestObservationSatisfied) =
PENDLE_PT_AND_LP_ORACLE.getOracleState({_market: _marketAddress, _duration: _duration});

if (increaseCardinalityRequired || !oldestObservationSatisfied) {
revert InsufficientOracleState(increaseCardinalityRequired, oldestObservationSatisfied);
}
}

///////////////////
// STATE GETTERS //
///////////////////

// EXTERNAL

/// @notice Gets the oracle market and its duration for a principal token, as-registered by the given user
/// @param _user The user
/// @param _ptAddress The principal token
/// @return marketAddress_ The market
/// @return duration_ The duration
function getPtOracleMarketAndDurationForUser(address _user, address _ptAddress)
external
view
returns (address marketAddress_, uint32 duration_)
{
marketAddress_ = getPtOracleMarketForUser({_user: _user, _ptAddress: _ptAddress});
duration_ = getMarketOracleDurationForUser({_user: _user, _marketAddress: marketAddress_});

return (marketAddress_, duration_);
}

// PUBLIC

/// @notice Gets the oracle duration for a market, as-registered by the given user
/// @param _user The user
/// @param _marketAddress The market
/// @return duration_ The duration
function getMarketOracleDurationForUser(address _user, address _marketAddress)
public
view
returns (uint32 duration_)
{
return userToMarketToOracleDuration[_user][_marketAddress];
}

/// @notice Gets the linked market for a principal token, as-registered by the given user
/// @param _user The user
/// @param _ptAddress The principal token
/// @return marketAddress_ The market
function getPtOracleMarketForUser(address _user, address _ptAddress) public view returns (address marketAddress_) {
return userToPtToLinkedMarket[_user][_ptAddress];
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

/// @title IPendleV2PtOracle Interface
/// @title IPendleV2PtAndLpOracle Interface
/// @author Enzyme Council <security@enzyme.finance>
interface IPendleV2PtOracle {
interface IPendleV2PtAndLpOracle {
function getPtToAssetRate(address _market, uint32 _duration) external view returns (uint256 ptToAssetRate_);

function getOracleState(address _market, uint32 _duration)
Expand Down
1 change: 1 addition & 0 deletions tests/interfaces/interfaces.txt
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ IUniswapV3LiquidityPositionLib.sol: UniswapV3LiquidityPositionLib.abi.json
IUniswapV3LiquidityPositionParser.sol: UniswapV3LiquidityPositionParser.abi.json
ITermFinanceV1LendingPositionLib.sol: TermFinanceV1LendingPositionLib.abi.json
ITermFinanceV1LendingPositionParser.sol: TermFinanceV1LendingPositionParser.abi.json
IPendleV2MarketRegistry.sol: PendleV2MarketRegistry.abi.json
IPendleV2PositionLib.sol: PendleV2PositionLib.abi.json
IPendleV2PositionParser.sol: PendleV2PositionParser.abi.json
IMorphoBluePositionLib.sol: MorphoBluePositionLib.abi.json
Expand Down
Loading

0 comments on commit 473d4b2

Please sign in to comment.