stake.link

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

Updates from the `secondary pool` to the `primary pool` may not be sent because there are `no rewards` for the secondary pool

Summary

The SDLPoolCCIPControllerSecondary::performUpkeep() function is only available when there is a message of rewards from the SDLPoolCCIPControllerPrimary. That could be a problem if there are not rewards to distribute in a specific secondary chain causing that queue updates from the secondarly chain will not be informed to the SDLPoolPrimary.

Vulnerability Details

The secondary chain informs to the primary chain the new numNewRESDLTokens and totalRESDLSupplyChange using the SDLPoolCCIPControllerSecondary::performUpkeep function, then the primary chain receives the information and it calculates the new mintStartIndex. Note that the primary chain increments the reSDLSupplyByChain in the code line 300, this so that the primary chain has the information on how much supply of reSDL tokens there is in the secondary chain:

File: SDLPoolCCIPControllerPrimary.sol
294: function _ccipReceive(Client.Any2EVMMessage memory _message) internal override {
295: uint64 sourceChainSelector = _message.sourceChainSelector;
296:
297: (uint256 numNewRESDLTokens, int256 totalRESDLSupplyChange) = abi.decode(_message.data, (uint256, int256));
298:
299: if (totalRESDLSupplyChange > 0) {
300: reSDLSupplyByChain[sourceChainSelector] += uint256(totalRESDLSupplyChange);
301: } else if (totalRESDLSupplyChange < 0) {
302: reSDLSupplyByChain[sourceChainSelector] -= uint256(-1 * totalRESDLSupplyChange);
303: }
304:
305: uint256 mintStartIndex = ISDLPoolPrimary(sdlPool).handleIncomingUpdate(numNewRESDLTokens, totalRESDLSupplyChange);
306:
307: _ccipSendUpdate(sourceChainSelector, mintStartIndex);
308:
309: emit MessageReceived(_message.messageId, sourceChainSelector);
310: }

Now the mintStartIndex is send to the secondary chain code line 307 and the secondary chain receives the new mintStartIndex. This entire process helps to keep the information updated between the primary chain and the secondary chain.

On the other hand, when a secondary chain receive rewards, the secondary chain can call the function SDLPoolCCIPControllerSecondary::performUpkeep since shouldUpdate is true at code line 157:

File: SDLPoolCCIPControllerSecondary.sol
147: function _ccipReceive(Client.Any2EVMMessage memory _message) internal override {
148: if (_message.data.length == 0) {
149: uint256 numRewardTokens = _message.destTokenAmounts.length;
150: address[] memory rewardTokens = new address[](numRewardTokens);
151: if (numRewardTokens != 0) {
152: for (uint256 i = 0; i < numRewardTokens; ++i) {
153: rewardTokens[i] = _message.destTokenAmounts[i].token;
154: IERC20(rewardTokens[i]).safeTransfer(sdlPool, _message.destTokenAmounts[i].amount);
155: }
156: ISDLPoolSecondary(sdlPool).distributeTokens(rewardTokens);
157: if (ISDLPoolSecondary(sdlPool).shouldUpdate()) shouldUpdate = true;
158: }
159: } else {
160: uint256 mintStartIndex = abi.decode(_message.data, (uint256));
161: ISDLPoolSecondary(sdlPool).handleIncomingUpdate(mintStartIndex);
162: }
163:
164: emit MessageReceived(_message.messageId, _message.sourceChainSelector);
165: }

Once shouldUpdate is true, the function SDLPoolCCIPControllerSecondary::performUpkeep can be called in order to send the new information (numNewRESDLTokens and totalRESDLSupplyChange) to the primary chain:

function performUpkeep(bytes calldata) external {
if (!shouldUpdate) revert UpdateConditionsNotMet();
shouldUpdate = false;
_initiateUpdate(primaryChainSelector, primaryChainDestination, extraArgs);
}

The problem is that the primary chain needs to send rewards to the secondary chain so that shouldUpdate is true and the function SDLPoolCCIPControllerSecondary::performUpkeep can be called. However, in certain circumstances it is possible that the secondary chain may never be able to send information to the primary chain since there may not be any rewards for the secondary chain. Please consider the next scenario:

  1. UserA stakes directly in the secondary chain and the queuedRESDLSupplyChange increments

  2. The increase in supply CANNOT be reported to the primary chain since shouldUpdate = false and the function SDLPoolCCIPControllerSecondary::performUpkeep will be reverted.

  3. Rewards are calculated on the primary chain, however because the secondary chain has not been able to send the new supply information, zero rewards reSDLSupplyByChain will be calculated for the secondary chain since reSDLSupplyByChain[chainSelector] has not been increased with the new information from step 1.

  4. Since there are NO rewards assigned for the secondary chain, it is not possible to set shouldUpdate=True, therefore the function SDLPoolCCIPControllerSecondary::performUpkeep will be reverted.

The following test shows that a user can send sdl tokens to the secondary pool however SDLPoolCCIPControllerSecondary::performUpkeep cannot be called since there are no rewards assigned to the secondary pool:

// File: test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts
// $ yarn test --grep "codehawks performUpkeep reverts"
//
it('codehawks performUpkeep reverts', async () => {
await token1.transfer(tokenPool.address, toEther(1000))
let rewardsPool1 = await deploy('RewardsPool', [sdlPool.address, token1.address])
await sdlPool.addToken(token1.address, rewardsPool1.address)
assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 400)
assert.equal((await controller.checkUpkeep('0x'))[0], false)
assert.equal(await controller.shouldUpdate(), false)
//
// 1. Mint in the secondary pool
await sdlToken.transferAndCall(
sdlPool.address,
toEther(100),
ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0])
)
//
// 2. The secondary pool needs to update data to the primary chain but the `controller.shouldUpdate` is false so `performUpkeep` reverts the transaction
assert.equal(await sdlPool.shouldUpdate(), true)
assert.equal((await controller.checkUpkeep('0x'))[0], false)
assert.equal(await controller.shouldUpdate(), false)
await expect(controller.performUpkeep('0x')).to.be.revertedWith('UpdateConditionsNotMet()')
})

Impact

numNewRESDLTokens and totalRESDLSupplyChange updates from the secondary pool to the primary pool may not be executed, causing the rewards calculation to be incorrect for each chain.

Tools used

Manual review

Recommendations

The SDLPoolCCIPControllerSecondary::performUpkeep function may check if the secondary pool has new information and so do not wait for rewards to be available for the secondary pool:

function performUpkeep(bytes calldata) external {
-- if (!shouldUpdate) revert UpdateConditionsNotMet();
++ if (!shouldUpdate && !ISDLPoolSecondary(sdlPool).shouldUpdate()) revert UpdateConditionsNotMet();
shouldUpdate = false;
_initiateUpdate(primaryChainSelector, primaryChainDestination, extraArgs);
}
Updates

Lead Judging Commences

0kage Lead Judge
almost 2 years ago
0kage Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

first-reward-update

Support

FAQs

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