Closed sherlock-admin2 closed 7 months ago
2 comment(s) were left on this issue during the judging contest.
tsvetanovv commented:
That would be Admin mistake
takarez commented:
valid: This seem valid to me as the watson explained; high(5)
I think the issue is misunderstood. The NextNftId is a state varaible in FraxEtherRedemptionQueue
contract which get incremented after when ever new request is submitted, which can be used by multiple parties. So, there is a high chance of the requestId getting mismatched because Napier uses cached requestId. Additionally, FraxEtherRedemptionQueue returns a requestId whenever a new request is created.
you can have a look here:
// Napier calls this function
function enterRedemptionQueue(address _recipient, uint120 _amountToRedeem) public returns (uint256 _nftId) {
// Do all of the NFT-generating and accounting logic
_nftId = _enterRedemptionQueueCore(_recipient, _amountToRedeem);
// Interactions: Transfer frxEth in from the sender
IERC20(address(FRX_ETH)).safeTransferFrom({ from: msg.sender, to: address(this), value: _amountToRedeem });
}
//
function _enterRedemptionQueueCore(address _recipient, uint120 _amountToRedeem) internal nonReentrant returns (uint256 _nftId) {
// Get queue information
RedemptionQueueState memory _redemptionQueueState = redemptionQueueState;
RedemptionQueueAccounting memory _redemptionQueueAccounting = redemptionQueueAccounting;
// Calculations: redemption fee
uint120 _redemptionFeeAmount = ((uint256(_amountToRedeem) * _redemptionQueueState.redemptionFee) /
FEE_PRECISION).toUint120();
// Calculations: amount of ETH owed to the user
uint120 _amountEtherOwedToUser = _amountToRedeem - _redemptionFeeAmount;
// Calculations: increment ether liabilities by the amount of ether owed to the user
_redemptionQueueAccounting.etherLiabilities += uint128(_amountEtherOwedToUser);
// Calculations: increment unclaimed fees by the redemption fee taken
_redemptionQueueAccounting.unclaimedFees += _redemptionFeeAmount;
// Calculations: maturity timestamp
uint64 _maturityTimestamp = uint64(block.timestamp) + _redemptionQueueState.queueLengthSecs;
// Effects: Initialize the redemption ticket NFT information
nftInformation[_redemptionQueueState.nextNftId] = RedemptionQueueItem({
amount: _amountEtherOwedToUser,
maturity: _maturityTimestamp,
hasBeenRedeemed: false,
earlyExitFee: _redemptionQueueState.earlyExitFee
});
// Effects: Mint the redemption ticket NFT. Make sure the recipient supports ERC721.
_safeMint({ to: _recipient, tokenId: _redemptionQueueState.nextNftId });
// Emit here, before the state change
_nftId = _redemptionQueueState.nextNftId;
emit EnterRedemptionQueue({
nftId: _nftId,
sender: msg.sender,
recipient: _recipient,
amountFrxEthRedeemed: _amountToRedeem,
redemptionFeeAmount: _redemptionFeeAmount,
maturityTimestamp: _maturityTimestamp,
earlyExitFee: _redemptionQueueState.earlyExitFee
});
// Calculations: Increment the autoincrement
++_redemptionQueueState.nextNftId;
// Effects: Write all of the state changes to storage
redemptionQueueState = _redemptionQueueState;
// Effects: Write all of the accounting changes to storage
redemptionQueueAccounting = _redemptionQueueAccounting;
}
https://etherscan.io/address/0x82bA8da44Cd5261762e629dd5c605b17715727bd#code There are basically two issues in this implementation: 1). The protocol does not integrate with FraxEthAdapter as expected. 2). All the requests highly depend on the requestId. If the requestId stored in this contract is not the one owned by this contract, it will always revert and create a permanent denial-of-service (DOS) issue.
aman
high
SFrxETHAdapter:requestWithdrawal
andSFrxETHAdapter:requestWithdrawalAll
return wrong requestId, would result in DOSSummary
The
Rebalancer
wants to withdraw the staked ETH from the Frax protocol. They callBaseLSTAdapter:requestWithdrawal
for a bufferAmount or to withdraw all staking. For the latter, they callSFrxETHAdapter::requestWithdrawalAll
. Due to using the cached requestId, it is very likely that the requestId against which the NFT is minted and stored on the Frax protocol will be different from the one stored on the Napier side. The following POC would help to understand the use case.Vulnerability Detail
The
Rebalancer
wants to withdraw the staked ETH from the Frax protocol. They callBaseLSTAdapter:requestWithdrawal
for a bufferAmount or to withdraw all staking. For the latter, they callSFrxETHAdapter::requestWithdrawalAll
. Due to using the cached requestId, it is very likely that the requestId against which the NFT is minted and stored on the Frax protocol will be different from the one stored on the Napier side. The following POC would help to understand the use case.1). Let the
Rebalancer
call theBaseLSTAdapter:requestWithdrawal
function to withdraw staked tokens fromFrax
. The contract first retrieves the nextNft fromFraxEtherRedemptionQueue
by callingFraxEtherRedemptionQueue::redemptionQueueState().nextNftId
. Let's suppose the requestId returned is 1.2).Meanwhile, Bob calls
FraxEtherRedemptionQueue:enterRedemptionQueue
. TheFraxEtherRedemptionQueue
completes Bob's transaction and returns requestId 1 to Bob.3). The Rebalancer transaction completes the
STAKED_FRXETH.withdraw(withdrawAmount, address(this), address(this));
and submits the requestWithdrawalREDEMPTION_QUEUE.enterRedemptionQueue({amountToRedeem: frxEthWithdrawn.toUint120(), recipient: address(this)});
Since requestId 1 was already used and now the nextNftId is 2, theFraxEtherRedemptionQueue:enterRedemptionQueue
records the Rebalancer request against requestId 2.4). The
BaseLSTAdapter
stores requestId 1, but the requestId ofBaseLSTAdapter
is 2.https://github.com/sherlock-audit/2024-01-napier/blob/6313f34110b0d12677b389f0ecb3197038211e12/napier-v1/src/adapters/frax/SFrxETHAdapter.sol#L99
https://github.com/sherlock-audit/2024-01-napier/blob/6313f34110b0d12677b389f0ecb3197038211e12/napier-v1/src/adapters/frax/SFrxETHAdapter.sol#L108
https://github.com/sherlock-audit/2024-01-napier/blob/6313f34110b0d12677b389f0ecb3197038211e12/napier-v1/src/adapters/frax/SFrxETHAdapter.sol#L83
https://github.com/sherlock-audit/2024-01-napier/blob/6313f34110b0d12677b389f0ecb3197038211e12/napier-v1/src/adapters/frax/SFrxETHAdapter.sol#L89
This will cause all the calls
claimWithdrawal
,_requestWithdrawal
, andrequestWithdrawalAll
functions of theSFrxETHAdapter
contract to fail.Impact
Due to wrong
requestId
All the Assets stacked will stuck atFraxEtherRedemptionQueue
,, no new RequestWithdrawal will be requested, and there is no way to recover contract from this state.Code Snippet
Tool used
Manual Review
Recommendation
REDEMPTION_QUEUE.enterRedemptionQueue
return the nftId (aka requestId). instead of caching the requestId before ahead use the return requestId.