[M-01] Incorrect Balance Handling in LSTRewardsSplitterController::removeSplitter Causes Revert on Splitter Removal
Summary
The LSTRewardsSplitterController::removeSplitter function is designed to handle various conditions that a splitter might have. However, when the balance is not equal to 0 and not equal to principalDeposits, an issue arises. The balance variable is not updated after the LSTRewardsSplitter::splitRewards function is called, which can result in an arithmetic overflow and a revert. This prevents the owner from removing the splitter, even if desired.
Vulnerability Details
In the removeSplitter function, after the checks for balance != 0 and balance != principalDeposits, there is a call to the removing splitter to split any remaining rewards. This changes the splitter's balance, but the balance is not recalculated before the withdrawal operation, leading to a revert due to the mismatch between the old balance and the current balance.
function removeSplitter(address _account) external onlyOwner {
ILSTRewardsSplitter splitter = splitters[_account];
if (address(splitter) == address(0)) revert SplitterNotFound();
uint256 balance = IERC20(lst).balanceOf(address(splitter));
uint256 principalDeposits = splitter.principalDeposits();
if (balance != 0) {
@> if (balance != principalDeposits) {
@> splitter.splitRewards();
@> }
@> splitter.withdraw(balance, _account);
}
delete splitters[_account];
uint256 numAccounts = accounts.length;
for (uint256 i = 0; i < numAccounts; ++i) {
if (accounts[i] == _account) {
accounts[i] = accounts[numAccounts - 1];
accounts.pop();
break;
}
}
IERC677(lst).safeApprove(address(splitter), 0);
}
As shown above, the LSTRewardsSplitter::_splitReward function transfers some of the balance to fee receivers, which causes the balance of the splitter to change.
function _splitRewards(uint256 _rewardsAmount) private {
for (uint256 i = 0; i < fees.length; ++i) {
Fee memory fee = fees[i];
uint256 amount = (_rewardsAmount * fee.basisPoints) / 10000;
if (fee.receiver == address(lst)) {
IStakingPool(address(lst)).burn(amount);
} else {
@> lst.safeTransfer(fee.receiver, amount);
}
}
principalDeposits = lst.balanceOf(address(this));
emit RewardsSplit(_rewardsAmount);
}
Impact
This issue can lead to situations where the owner of the controller is unable to remove a splitter, either intentionally or unintentionally, due to the outdated balance calculation.
Proof of Concept (POC)
The following test can be added to lst-rewards-splitter.test.ts to demonstrate the problem. This test sets up conditions that trigger a call to splitRewards, and checks for a reversion when trying to remove the splitter.
it('should be able to remove splitter', async () => {
const { signers, accounts, controller, token, splitter0, splitter1 } = await loadFixture(deployFixture)
await token.transferAndCall(controller.target, toEther(100), '0x')
await token.connect(signers[1]).transferAndCall(controller.target, toEther(200), '0x')
await token.transfer(splitter1.target, toEther(80))
console.log(await token.balanceOf(splitter1.target))
console.log(await splitter1.principalDeposits())
await expect(controller.removeSplitter(accounts[1])).to.be.revertedWithPanic()
})
Tools Used
Manual Review
Recommendations
The fix is straightforward—update the balance after calling splitRewards to ensure the correct balance is used in the withdrawal.
function removeSplitter(address _account) external onlyOwner {
ILSTRewardsSplitter splitter = splitters[_account];
if (address(splitter) == address(0)) revert SplitterNotFound();
uint256 balance = IERC20(lst).balanceOf(address(splitter));
uint256 principalDeposits = splitter.principalDeposits();
if (balance != 0) {
if (balance != principalDeposits) {
splitter.splitRewards();
+ balance = IERC20(lst).balanceOf(address(splitter));
}
splitter.withdraw(balance, _account);
}
delete splitters[_account];
uint256 numAccounts = accounts.length;
for (uint256 i = 0; i < numAccounts; ++i) {
if (accounts[i] == _account) {
accounts[i] = accounts[numAccounts - 1];
accounts.pop();
break;
}
}
IERC677(lst).safeApprove(address(splitter), 0);
}