Open c4-bot-9 opened 8 months ago
GalloDaSballo marked the issue as insufficient quality report
GalloDaSballo marked the issue as remove high or low quality report
GalloDaSballo marked the issue as sufficient quality report
Worth flagging as it seems valid but may not be necessary if the sponsor is opting into using the previous deployment (which may not be deprecated)
GalloDaSballo marked the issue as primary issue
By definition a codebase can never be guaranteed to be compatible with the latest version. Requesting warden to provide evidence lack of integration with v3 achieves M+ severity.
trust1995 marked the issue as unsatisfactory: Out of scope
@trust1995 The reason why I reported this was because the sponsor told me they expect compatibility with both. The problem is that an older version of pendle router is used here and the calls for addLiquiditySingleSy()
and removeLiquiditySingleSy()
are basically incompatible with the new one and will revert because of a missing parameter. To understand the rationale behind this submission, let's examine the deployed contracts:
Current router version (taken from test files): https://arbiscan.io/address/0x0000000001E4ef00d069e71d6bA041b0A16F7eA0 PendleRouterV3 (latest): https://arbiscan.io/address/0x00000000005BBB0EF59571E58418F9a4357b68A0
Here's the code for both on Deth.net: Old router: https://arbiscan.deth.net/address/0xFc0617465474a6b1CA0E37ec4E67B3EEFf93bc63 New one: https://arbiscan.deth.net/address/0x00000000005BBB0EF59571E58418F9a4357b68A0
The functions can be found inActionAddRemoveLiq
and ActionAddRemoveLiqV3
Old one:
/// @dev swaps SY to PT, then adds liquidity
function _addLiquiditySingleSy(
address receiver,
address market,
IPYieldToken YT,
uint256 netSyIn,
uint256 minLpOut,
ApproxParams calldata guessPtReceivedFromSy
) internal returns (uint256 netLpOut, uint256 netSyFee) {
MarketState memory state = IPMarket(market).readState(address(this));
// ...
}
New one:
function addLiquiditySingleToken(
address receiver,
address market,
uint256 minLpOut,
ApproxParams calldata guessPtReceivedFromSy,
TokenInput calldata input,
LimitOrderData calldata limit
) external payable returns (uint256 netLpOut, uint256 netSyFee, uint256 netSyInterm) {
(IStandardizedYield SY, , IPYieldToken YT) = IPMarket(market).readTokens();
netSyInterm = _mintSyFromToken(_entry_addLiquiditySingleSy(market, limit), address(SY), 1, input);
(netLpOut, netSyFee) = _addLiquiditySingleSy(
receiver,
market,
SY,
YT,
netSyInterm,
minLpOut,
guessPtReceivedFromSy,
limit
);
// ...
}
function _addLiquiditySingleSy(
address receiver,
address market,
IStandardizedYield SY,
IPYieldToken YT,
uint256 netSyIn,
uint256 minLpOut,
ApproxParams calldata guessPtReceivedFromSy,
LimitOrderData calldata limit
) internal returns (uint256 netLpOut, uint256 netSyFee) {
uint256 netSyLeft = netSyIn;
uint256 netPtReceived;
if (!_isEmptyLimit(limit)) {
(netSyLeft, netPtReceived, netSyFee, ) = _fillLimit(market, SY, netSyLeft, limit);
_transferOut(address(SY), market, netSyLeft);
}
(uint256 netPtOutMarket, , ) = _readMarket(market).approxSwapSyToAddLiquidity(
YT.newIndex(),
netSyLeft,
netPtReceived,
block.timestamp,
guessPtReceivedFromSy
);
// ...
}
_readMarket()
comes from ActionBase
, here's it's definition:
function _readMarket(address market) internal view returns (MarketState memory) {
return IPMarket(market).readState(address(this));
}
You can see that both call readState()
from the underlying Pendle market. Here you can find all market deployments: https://docs.pendle.finance/Developers/Deployments/Arbitrum#markets
I'll use the first one for the example. Arbiscan: https://arbiscan.io/address/0x7D49E5Adc0EAAD9C027857767638613253eF125f Deth.net: https://arbiscan.deth.net/address/0x7D49E5Adc0EAAD9C027857767638613253eF125f
If you look at the definion of readState()
, you'll see the following:
function readState(address router) public view returns (MarketState memory market) {
market.totalPt = _storage.totalPt;
market.totalSy = _storage.totalSy;
market.totalLp = totalSupply().Int();
(market.treasury, market.lnFeeRateRoot, market.reserveFeePercent) = IPMarketFactory(
factory
).getMarketConfig(router);
market.scalarRoot = scalarRoot;
market.expiry = expiry;
market.lastLnImpliedRate = _storage.lastLnImpliedRate;
}
readState()
on all markets reaches out to the factory contract it was deployed with to grab the market config. And there are two versions: MarketFactory
and MarketFactoryV3
. Again, both can be found in the docs but here are the links:
MarketFactory: https://arbiscan.io/address/0xf5a7De2D276dbda3EEf1b62A9E718EFf4d29dDC8 MarketFactory V3: https://arbiscan.io/address/0x2FCb47B58350cD377f94d3821e7373Df60bD9Ced
You can take a market from the docs and query Market::isValidMarket()
. Using the first one, the old factory will return true whereas the new one, false.
So basically what this all means is that the protocol won't be able to use new markets. Pendle can start phasing out the old ones and migrating them over. The fix is rather easy on the protocol's end, they just need to account for the newly added parameter and can support both, if they wish, using a conditional check.
@vonMangoldt can you confirm the warden's claims around your intentions?
@trust1995 Still no response from the sponsor here
Without sponsor's take the warden's claim that V3 should be compatible with the design is accepted.
trust1995 marked the issue as satisfactory
trust1995 marked the issue as selected for report
For transparency and per conversation with the sponsors, see here for the Wise Lending team's mitigation.
Lines of code
https://github.com/pendle-finance/pendle-core-v2-public/blob/main/contracts/router/ActionAddRemoveLiqV3.sol#L166-L172 https://github.com/pendle-finance/pendle-core-v2-public/blob/main/contracts/router/ActionAddRemoveLiqV3.sol#L444-L449 https://github.com/code-423n4/2024-02-wise-lending/blob/main/contracts/PowerFarms/PendlePowerFarm/PendlePowerFarmLeverageLogic.sol#L467-L482 https://github.com/code-423n4/2024-02-wise-lending/blob/main/contracts/PowerFarms/PendlePowerFarm/PendlePowerFarmLeverageLogic.sol#L165-L171
Vulnerability details
Impact
PendlePowerManager
is incompatible with the latest deployment ofPendleRouter
and will cause reverts when attempting to open a farm position.Proof of Concept
The latest deployment of the Pendle router,
PendleRouterV3
, is incompatible with thePendlePowerManager
contract. The sponsor is expecting full compatibility with both but this is not the case here. The problem stems from the calls made toPENDLE_ROUTER.removeLiquiditySingleSy()
andPENDLE_ROUTER.addLiquiditySingleSy()
:The issue is that the signatures of those have changed in V3:
There's a new parameter called
limit
that's not accounted for in the calls in thePendlePowerFarmLeverageLogic
helper contract. This will lead to calls always reverting withRouterInvalidAction
due to Pendle's proxy not being able to locate the selector used.Coded POC (
PendlePowerFarmControllerBase.t.sol
):Tools Used
Manual Review
Recommended Mitigation Steps
Support only the latest router (V3) or add conditional checks to use the respective selector for each router version.
Assessed type
Other