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)