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)