stake.link

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

Error in RewardPool accounting if rewards are distributed during an update

Summary

if there is outstanding rewardPerToken unclaimed by the SDLPoolCCIPControllerPrimary and rewards are distributed in between an update, there is a possibility of error in the rewardPool accounting incorrectly increasing the effectiveBalance of SDLPoolCCIPControllerPrimary and denying other users their rewards and incorrectly sending them to secondary chains

Vulnerability Details

the update process is started in the SDLPoolCCIPControllerSecondary by the function

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);
}

the numNewRESDLTokens and totalRESDLSupplyChange are then sent over to the primary chain.
if there is a net increase in the totalRESDLSupplyChange when the SDLPoolCCIPControllerPrimary recieves the message, it calls the function

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);
emit MessageReceived(_message.messageId, sourceChainSelector);
}

which then calls ISDLPoolPrimary(sdlPool).handleIncomingUpdate(numNewRESDLTokens, totalRESDLSupplyChange) and updates the effective balance of the SDLPoolCCIPControllerPrimary. this is where the issue is, if there is unclaimed rewardsPerToken in the rewardsPool and rewards are distributed, the SDLPoolCCIPControllerPrimary rewards will be incorrect, depriving other users their rewards

Impact

some users on the primary chain are denied their rewards while other users on the secondary chains are given extra rewards

Tools Used

manual audit

Recommendations

update the rewards after every operation that changes the effective balance of any user

change

function handleIncomingUpdate(uint256 _numNewRESDLTokens, int256 _totalRESDLSupplyChange)
external
onlyCCIPController
returns (uint256)

to

function handleIncomingUpdate(uint256 _numNewRESDLTokens, int256 _totalRESDLSupplyChange)
external
onlyCCIPController
updateRewards(ccipController)
returns (uint256)
Updates

Lead Judging Commences

0kage Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

incorrect rewards

Support

FAQs

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