stake.link

stake.link
DeFiHardhatBridge
27,500 USDC
View results
Submission Details
Severity: medium
Invalid

Cross-chain transfers risk reward loss in SDLPoolCCIPControllerPrimary.sol, lacking synchronization for accurate balances.

Summary

Loss of rewards during cross-chain transfers. When reward distribution is initiated in SDLPoolCCIPControllerPrimary.sol, it sends tokens to secondary chains via CCIP. If a user's reSDL NFT is in-transit between chains during this process, they will fail to receive their share of rewards.

Vulnerability Details

There is no synchronization or check to ensure a user's effective balance is reflected accurately across chains before distributing rewards.

Some scenarios where users could lose rewards:

  1. User transfers reSDL NFT from secondary chain to primary

  2. Rewards initiated on primary before NFT arrival

  3. User balance not accounted for when calculating rewards

Impact

The key segment from SDLPoolCCIPControllerPrimary.sol that distributes rewards: SDLPoolCCIPControllerPrimary.sol#distributeRewards](https://github.com/Cyfrin/2023-12-stake-link/blob/549b2b8c4a5b841686fceb9c311dca9ac58225df/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol#L56-L93)

function distributeRewards() external onlyRewardsInitiator {
uint256 totalRESDL = ISDLPoolPrimary(sdlPool).effectiveBalanceOf(address(this));
address[] memory tokens = ISDLPoolPrimary(sdlPool).supportedTokens();
uint256 numDestinations = whitelistedChains.length;
ISDLPoolPrimary(sdlPool).withdrawRewards(tokens);
uint256[][] memory distributionAmounts = new uint256[][](numDestinations);
for (uint256 i = 0; i < numDestinations; ++i) {
distributionAmounts[i] = new uint256[](tokens.length);
}
for (uint256 i = 0; i < tokens.length; ++i) {
address token = tokens[i];
uint256 tokenBalance = IERC20(token).balanceOf(address(this));
address wrappedToken = wrappedRewardTokens[token];
if (wrappedToken != address(0)) {
IERC677(token).transferAndCall(wrappedToken, tokenBalance, "");
tokens[i] = wrappedToken;
tokenBalance = IERC20(wrappedToken).balanceOf(address(this));
}
uint256 totalDistributed;
for (uint256 j = 0; j < numDestinations; ++j) {
uint64 chainSelector = whitelistedChains[j];
uint256 rewards = j == numDestinations - 1
? tokenBalance - totalDistributed
: (tokenBalance * reSDLSupplyByChain[chainSelector]) / totalRESDL;
distributionAmounts[j][i] = rewards;
totalDistributed += rewards;
}
}
for (uint256 i = 0; i < numDestinations; ++i) {
_distributeRewards(whitelistedChains[i], tokens, distributionAmounts[i]);
}
}

As you can see, it calculates the reward allocation based on the reSDLSupplyByChain - which tracks reSDL balances on each secondary chain.

This balance may be inaccurate if reSDL NFT transfers are in-flight.

So users who recently transferred reSDLs to/from a secondary would fail to receive their entitled rewards.

Tools Used

Manual Review

Recommendations

  1. Implement synchronization checks before distributing rewards

  2. Build a buffer period between cross-chain transfers and reward cycles

  3. Support retroactive user reward claims if missed

Adding additional synchronization around cross-chain transfers would help minimize issues with rewards.

Updates

Lead Judging Commences

0kage Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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