Soaring Lipstick Kitten - Inability To Remove Reward Token Due To Rounding Issues In ConfigureRewardToken
Soaring Lipstick Kitten - Inability to Remove Reward Token Due to Rounding Issues in configureRewardToken
Introduction
The Soaring Lipstick Kitten is a vulnerability discovered in the SymmStaking contract, which is part of the Symm-io protocol. The issue lies in the configureRewardToken
function, where the condition pendingRewards[token] > 10
is ineffective due to rounding issues in reward distribution calculations. This can lead to reward tokens becoming permanently non-removable, causing governance and protocol inefficiencies.
Root Cause
The root cause of this issue lies in the way rewards are calculated and distributed. When rewards are notified, pendingRewards[token]
is incremented by the deposited amount. However, the reward rate is calculated using integer division (state.rate = amount / state.duration
), which causes rounding down. When users claim rewards, the claimable amount is also rounded down, leading to discrepancies between distributed and pending rewards.
function configureRewardToken(address token, bool status) external onlyRole(REWARD_MANAGER_ROLE) {
_updateRewardsStates(address(0));
if (token == address(0)) revert ZeroAddress();
if (isRewardToken[token] == status) revert TokenWhitelistStatusUnchanged(token, status);
isRewardToken[token] = status;
if (!status) {
if (pendingRewards[token] > 10) revert OngoingRewardPeriodForToken(token, pendingRewards[token]);
Internal Pre-conditions
The internal pre-conditions for this issue are:
notifyRewardAmount
increasespendingRewards[token]
state.rate = amount / state.duration
leads to rounding down, causing a mismatch between expected and actual reward distribution- Users claiming rewards further reduce
pendingRewards[token]
in a rounded-down manner, making it nearly impossible to reach exactly 10 or below.
External Pre-conditions
There are no external pre-conditions for this issue.
Attack Path
The attack path for this issue is as follows:
- An attacker deposits a large amount of tokens into the contract.
- The contract calculates the reward rate using integer division, causing rounding down.
- The attacker claims their rewards, which are also rounded down.
- The
pendingRewards[token]
value remains high, making it impossible to remove the reward token.
Impact
The impact of this issue is significant, as it prevents proper reward token management by administrators and leads to the accumulation of unnecessary reward tokens.
Proof of Concept
The proof of concept for this issue is as follows:
describe("Reward Calculation", function () {
it("should still pending rewards left when all claimed", async function () {
// Scenario: Single depositor — user1 deposits 604,800 SYMM, waits 200s, claims 200 tokens.
const depositAmount = "604900"
const rewardAmount = depositAmount
await stakingToken.connect(admin).approve(await symmStaking.getAddress(), rewardAmount)
await symmStaking.connect(admin).configureRewardToken(await stakingToken.getAddress(), true)
await symmStaking.connect(admin).notifyRewardAmount([await stakingToken.getAddress()], [rewardAmount])
await stakingToken.connect(user1).approve(await symmStaking.getAddress(), depositAmount)
await symmStaking.connect(user1).deposit(depositAmount, user1.address)
const pending = await symmStaking.pendingRewards(await stakingToken.getAddress());
expect(pending).to.equal(depositAmount);
await time.increase(100*24*60*60)
const user1BalanceBefore = await stakingToken.balanceOf(user1.address)
await symmStaking.connect(user1).claimRewards()
const user1BalanceAfter = await stakingToken.balanceOf(user1.address)
const claimed = user1BalanceAfter - user1BalanceBefore
expect(claimed).to.equal("604797")
})
})
Mitigation
The mitigation for this issue is to replace the pendingRewards[token] > 10
condition with a more flexible threshold, such as a percentage of total rewards or a higher absolute limit. This will prevent the issue from occurring and ensure that reward tokens can be properly removed.
Conclusion
In conclusion, the Soaring Lipstick Kitten is a vulnerability discovered in the SymmStaking contract, which can lead to reward tokens becoming permanently non-removable. The issue is caused by rounding issues in reward distribution calculations and can be mitigated by replacing the pendingRewards[token] > 10
condition with a more flexible threshold.
Soaring Lipstick Kitten - Inability to Remove Reward Token Due to Rounding Issues in configureRewardToken
Q&A
Q: What is the Soaring Lipstick Kitten vulnerability?
A: The Soaring Lipstick Kitten is a vulnerability discovered in the SymmStaking contract, which is part of the Symm-io protocol. The issue lies in the configureRewardToken
function, where the condition pendingRewards[token] > 10
is ineffective due to rounding issues in reward distribution calculations.
Q: What is the root cause of this issue?
A: The root cause of this issue lies in the way rewards are calculated and distributed. When rewards are notified, pendingRewards[token]
is incremented by the deposited amount. However, the reward rate is calculated using integer division (state.rate = amount / state.duration
), which causes rounding down. When users claim rewards, the claimable amount is also rounded down, leading to discrepancies between distributed and pending rewards.
Q: What are the internal pre-conditions for this issue?
A: The internal pre-conditions for this issue are:
notifyRewardAmount
increasespendingRewards[token]
state.rate = amount / state.duration
leads to rounding down, causing a mismatch between expected and actual reward distribution- Users claiming rewards further reduce
pendingRewards[token]
in a rounded-down manner, making it nearly impossible to reach exactly 10 or below.
Q: What are the external pre-conditions for this issue?
A: There are no external pre-conditions for this issue.
Q: What is the attack path for this issue?
A: The attack path for this issue is as follows:
- An attacker deposits a large amount of tokens into the contract.
- The contract calculates the reward rate using integer division, causing rounding down.
- The attacker claims their rewards, which are also rounded down.
- The
pendingRewards[token]
value remains high, making it impossible to remove the reward token.
Q: What is the impact of this issue?
A: The impact of this issue is significant, as it prevents proper reward token management by administrators and leads to the accumulation of unnecessary reward tokens.
Q: How can this issue be mitigated?
A: This issue can be mitigated by replacing the pendingRewards[token] > 10
condition with a more flexible threshold, such as a percentage of total rewards or a higher absolute limit.
Q: What is the proof of concept for this issue?
A: The proof of concept for this issue is as follows:
describe("Reward Calculation", function () {
it("should still pending rewards left when all claimed", async function () {
// Scenario: Single depositor — user1 deposits 604,800 SYMM, waits 200s, claims 200 tokens.
const depositAmount = "604900"
const rewardAmount = depositAmount
await stakingToken.connect(admin).approve(await symmStaking.getAddress(), rewardAmount)
await symmStaking.connect(admin).configureRewardToken(await stakingToken.getAddress(), true)
await symmStaking.connect(admin).notifyRewardAmount([await stakingToken.getAddress()], [rewardAmount])
await stakingToken.connect(user1).approve(await symmStaking.getAddress(), depositAmount)
await symmStaking.connect(user1).deposit(depositAmount, user1.address)
const pending = await symmStaking.pendingRewards(await stakingToken.getAddress());
expect(pending).to.equal(depositAmount);
await time.increase(100*24*60*60)
const user1BalanceBefore = await stakingToken.balanceOf(user1.address)
await symmStaking.connect(user1).claimRewards()
const user1BalanceAfter = await stakingToken.balanceOf(user1.address)
const claimed = user1BalanceAfter - user1BalanceBefore
expect(claimed).to.equal("604797")
})
})
Q: What is the conclusion of this issue?
A: In conclusion, the Soaring Lipstick Kitten is a vulnerability discovered in the SymmStaking contract, which can lead to reward tokens becoming permanently non-removable. The issue is caused by rounding issues in reward distribution calculations and can be mitigated by replacing the pendingRewards[token] > 10
condition with a more flexible threshold.