The SDLPoolCCIPControllerPrimary.sol::distributeRewards() distributes the rewards in all supported tokens across all supported chains. If a wrapped token is registered for any of the reward tokens, then the wrapped version will be used to distribute the reward instead. However, if both the base token and wrapped token are supported in the pool, the method will revert due to trying to spend the entire wrapped token's balance twice. In this case the reward distribution will always fail.
The SDLPoolCCIPControllerPrimary.sol::distributeRewards() is called periodically by RewardsInitiator.sol contract, based on a Chainlink Automation trigger. This method is responsible for distributing the rewards in supported tokens among all supported chains.
First the method claims the rewards from SDLPrimaryPool. Next, it proceeds to determine the amounts of rewards in each token for each supported chain. To achieve it, the following helper variable is used:
The distributionAmounts is a two-dimensional array, in which the value distributionAmounts[x][y] holds the amount of token x to be distributed on chain y. In order to populate this array, the method iterates through the list of all whitelsited chains and all supported tokens and calculates the reward value. In a nutshell, the total balance of token x gets distributed across the chains proportionally to reSDLSupplyByChain. The sum of the array distributionAmounts[x] is the total balance of token x held by the SDLPoolCCIPControllerPrimary.sol contract.
If any of the supported tokens has a registered wrapped version, then the wrapped token will be used in the place of base token.
The problem is if the tokens array already contained the wrappedToken, then the wrappedToken balance will be saved to distributionAmounts twice. Let's say that the SDLPoolPrimary.sol supported tokens is TKN and its wrapped version wTKN. In a situation like that, when attempting reward distribution and processing TKN, SDLPoolCCIPControllerPrimary.sol::distributeRewards() will replace TKN its balance with wTKN token and save it inside the distributionAmounts array. However, during processing wTKN, it will save its balance again. As such, the method will eventually try to distribute the balance of wTKN twice, which will always revert.
Adding both base token and its wrapped token to the SDLPrimaryPool.sol contract is indeed possible, as the method addToken() inherited from parent contract only checks against simple address duplicates. It can hardly be considered an owner error as well - one can easily imagine a scenario when it will be desireble to support the rewards in both base and wrapped token.
For the POC, please add the following line to the distributeRewards should work correctly with wrapped tokens test in the sdl-pool-ccip-controller-primary.test.ts file. The test will be reverting with ERC20: transfer amount exceeds balance error until the issue is addressed.
Impact is high, as the core functionality of the Protocol will be broken and the rewards will never be successfuly distributed.
Likelihood is low, as the issue occurs in a very specific scenario, which, although possible, is not very likely.
The severity is therefore estimated as medium.
Manual review
Inside the SDLPoolCCIPControllerPrimary.sol::distributeRewards() method, if both base token TKN and wrapped token wTKN are supported by the pool, the logic should look wrap the TKN to wTKN and continue to the next loop iteration.
Expand the interface ISDLPool.sol with the following method:
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.