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.