stake.link

stake.link
DeFiHardhatBridge
27,500 USDC
View results
Submission Details
Severity: high
Invalid

One Failed Cross-Chain Transaction , Break Yhe Multi-Chain Functionality

Summary

  • the cross-chain update mechanism of the SDLPoolSecondary can lead to a deadlock situation, where the updateInProgress flag remains indefinitely set to 1. This occurs when the update transaction on the primary chain or the callback transaction to the secondary chain fails. As a result, the system is unable to process new updates, effectively freezing the staking system in the secondary pool and leaving user actions in a state of suspension without a recovery mechanism in place.

Vulnerability Details

function performUpkeep(bytes calldata) external {
if (!shouldUpdate) revert UpdateConditionsNotMet();
shouldUpdate = false;
_initiateUpdate(primaryChainSelector, primaryChainDestination, extraArgs);
}
function _initiateUpdate(uint64 _destinationChainSelector, address _destination, bytes memory _extraArgs)
internal
{
>> (uint256 numNewRESDLTokens, int256 totalRESDLSupplyChange) = ISDLPoolSecondary(sdlPool).handleOutgoingUpdate();
Client.EVM2AnyMessage memory evm2AnyMessage =
_buildCCIPMessage(_destination, numNewRESDLTokens, totalRESDLSupplyChange, _extraArgs);
IRouterClient router = IRouterClient(this.getRouter());
uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);
if (fees > maxLINKFee) revert FeeExceedsLimit(fees);
>> bytes32 messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);
emit MessageSent(messageId, _destinationChainSelector, fees);
}
  • in the handleOutgoingUpdate function it checks if an update is already in progress by examining the updateInProgress flag. If set to 1, the function will revert, preventing any further updates.If no update is in progress, handleOutgoingUpdate sets updateInProgress to 1 and prepares to send the number of new queued locks (numNewQueuedLocks) and the change in the reSDL supply (reSDLSupplyChange) to the primary chain:

function handleOutgoingUpdate() external onlyCCIPController returns (uint256, int256) {
>> if (updateInProgress == 1) revert UpdateInProgress();
uint256 numNewQueuedLocks = queuedNewLocks[updateBatchIndex].length;
int256 reSDLSupplyChange = queuedRESDLSupplyChange;
queuedRESDLSupplyChange = 0;
updateBatchIndex++;
>> updateInProgress = 1;
updateNeeded = 0;
queuedNewLocks.push();
emit OutgoingUpdate(updateBatchIndex - 1, numNewQueuedLocks, reSDLSupplyChange);
return (numNewQueuedLocks, reSDLSupplyChange);
}
/// SDLPoolCCIPControllerPrimary
function ccipReceive(Client.Any2EVMMessage calldata _message) external override onlyRouter {
>> _verifyCCIPSender(_message);
if (_message.destTokenAmounts.length == 1 && _message.destTokenAmounts[0].token == address(sdlToken)) {
IRESDLTokenBridge(reSDLTokenBridge).ccipReceive(_message);
} else {
>> _ccipReceive(_message);//@auditor : this function will be excuted in our case
}
}
function _ccipReceive(Client.Any2EVMMessage memory _message) internal override {
uint64 sourceChainSelector = _message.sourceChainSelector;
(uint256 numNewRESDLTokens, int256 totalRESDLSupplyChange) = abi.decode(_message.data, (uint256, int256));
if (totalRESDLSupplyChange > 0) {
reSDLSupplyByChain[sourceChainSelector] += uint256(totalRESDLSupplyChange);
} else if (totalRESDLSupplyChange < 0) {
reSDLSupplyByChain[sourceChainSelector] -= uint256(-1 * totalRESDLSupplyChange);
}
>> uint256 mintStartIndex =ISDLPoolPrimary(sdlPool).handleIncomingUpdate(numNewRESDLTokens, totalRESDLSupplyChange);
>> _ccipSendUpdate(sourceChainSelector, mintStartIndex);// @auditor : callback to the secondary chain
emit MessageReceived(_message.messageId, sourceChainSelector);
}
  • the issue occurs if the transaction on the primary chain reverts or the callback transaction that sends mintStartIndex from primary to secondary chain reverts for any reason (which high likely, for example miss passing the _extraArgs , not enough LINK tokens in SDLPoolCCIPControllerPrimary to pay fee for the callback ...ect). This event would severely disrupt this secondary pool functionality. In the event of a transaction revert, there is no recovery mechanism, and sending updates to the primary chain for new actions on the secondary chain becomes impossible because the ccipControllerSecondary is the sole contract authorized to send such transaction, but it can only do so when the updateInProgress flag is zero,and the updateInProgress flag only get reset to 0, when the udpated sended to the primary has returned the mintStartIndex to the function handleIncomingUpdate in the secondaryPool, which won't happend cause the transaction failed :

function handleIncomingUpdate(uint256 _mintStartIndex) external onlyCCIPController {
if (updateInProgress == 0) revert NoUpdateInProgress();
if (_mintStartIndex != 0) {
// the _mintStartIndex will be minted the first then increament the by one for the next id ..
uint256 newLastLockId = _mintStartIndex + queuedNewLocks[updateBatchIndex - 1].length - 1;
if (newLastLockId > lastLockId) lastLockId = newLastLockId;
}
currentMintLockIdByBatch.push(_mintStartIndex);
>> updateInProgress = 0;
emit IncomingUpdate(updateBatchIndex - 1, _mintStartIndex);
}
  • This results in a deadlock situation where the system is indefinitely stalled, unable to process any new updates. The updateInProgress flag remains perpetually set to 1 signaling that an update is still being processed and thus blocking the initiation of any subsequent updates.

Impact

  • This vulnerability stops all updates between the secondary and primary pools, leaving users' staked funds inaccessible and disrupting the staking service.

Tools Used

manual review

Recommendations

Introduce a fallback mechanism that resets the updateInProgress flag in the event of a transaction failure. This could involve a timeout function or a manual override by an authorized administrator to ensure that updates can resume and the system can recover from stalled states.

Updates

Lead Judging Commences

0kage Lead Judge almost 2 years ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.