Future Watermelon Marmot
Medium
Resetting penalty percentages in setPenaltyPercentages
function will allow attackers to reduce their effective penalties or reset them entirely
In the AirdropDistribution
smart contract, the setPenaltyPercentages
function allows the airdrop operator to set monthly penalty percentages for each user. However, a vulnerability exists where the penalty percentages for past months can be reset, even after they have already been used in calculating penalties. This oversight could lead to inconsistencies in penalty calculations, allowing manipulation of penalty amounts. By exploiting this, attackers can reduce their effective penalties or reset them entirely.
The vulnerability lies within the setPenaltyPercentages
function:
https://github.com/sherlock-audit/2024-10-usual-labs-v1/blob/4fb4a64a479e0b9b8f93934220e891c29d54df33/pegasus/packages/solidity/src/airdrop/AirdropDistribution.sol#L363-L394
function setPenaltyPercentages(
uint256[] memory penaltyPercentages,
address[] memory accounts,
uint256 month
) external {
uint256 monthsPassed = _calculateMonthsPassed();
// Validate the month is within the 6-month vesting period
if (month < monthsPassed || month > AIRDROP_VESTING_DURATION_IN_MONTHS) {
revert OutOfBounds();
}
// Validate the length of the arrays
if (penaltyPercentages.length != accounts.length) {
revert InvalidInputArraysLength();
}
AirdropDistributionStorageV0 storage $ = _airdropDistributionStorageV0();
$.registryAccess.onlyMatchingRole(AIRDROP_PENALTY_OPERATOR_ROLE);
for (uint256 i = 0; i < accounts.length; i++) {
if (penaltyPercentages[i] > BASIS_POINT_BASE) {
revert AmountTooBig();
}
if (penaltyPercentages[i] == $.penaltyPercentageByMonth[accounts[i]][month]) {
revert SameValue();
}
$.penaltyPercentageByMonth[accounts[i]][month] = penaltyPercentages[i];
}
emit PenaltyPercentagesSet(accounts, penaltyPercentages, month);
}
The function setPenaltyPercentages
is designed to set penalty percentages for each user on a monthly basis. However, it lacks a check to prevent modifying penalty percentages for months that have already passed and potentially been used in penalty calculations. This creates a vulnerability as an attacker (with the necessary role) can reset or reduce penalty percentages for prior months, altering their penalty history and reducing their penalty amount.
No response
No response
No response
This vulnerability allows an attacker with the AIRDROP_PENALTY_OPERATOR_ROLE
to reset their penalty percentages for past months, potentially eliminating their penalties altogether. The financial impact could be substantial if penalties represent a large percentage of the vesting distribution.
Even to exploit this an attacker needs a specific role, it can be an employer that wanna gain money and stay unknown in the system or something.
- Deploy the contract, setting up the necessary contracts, roles, and vesting parameters.
- Set penalty percentages for a user for a specific month.
- Simulate the passage of time to a later month.
- Attempt to modify the penalty percentage for a past month.
- Observe that the penalty amount is lower due to the modified percentage.
const { ethers } = require("hardhat");
const { expect } = require("chai");
describe("AirdropDistribution Penalty Reset Vulnerability Test", function () {
let airdropDistribution, owner, penaltyOperator, user;
beforeEach(async function () {
[owner, penaltyOperator, user] = await ethers.getSigners();
// Deploy contract and initialize
const AirdropDistribution = await ethers.getContractFactory("AirdropDistribution");
airdropDistribution = await AirdropDistribution.deploy();
await airdropDistribution.initialize(owner.address);
// Assign AIRDROP_PENALTY_OPERATOR_ROLE to penaltyOperator
const AIRDROP_PENALTY_OPERATOR_ROLE = ethers.utils.id("AIRDROP_PENALTY_OPERATOR_ROLE");
await airdropDistribution.connect(owner).grantRole(AIRDROP_PENALTY_OPERATOR_ROLE, penaltyOperator.address);
// Simulate initial penalty percentage for month 1
await airdropDistribution.connect(penaltyOperator).setPenaltyPercentages(
[500], // 5% penalty
[user.address],
1
);
});
it("Should allow resetting of penalty percentages for past months", async function () {
// Fast-forward to month 3 (simulating time passage)
await ethers.provider.send("evm_increaseTime", [60 * 60 * 24 * 30 * 2]);
await ethers.provider.send("evm_mine");
// Reset penalty for month 1 to zero
await airdropDistribution.connect(penaltyOperator).setPenaltyPercentages(
[0], // 0% penalty
[user.address],
1
);
// Check penalty amount after resetting penalty percentage
const totalAmount = ethers.utils.parseEther("100");
const [, penaltyAmount] = await airdropDistribution.connect(user)._available(
user.address,
totalAmount,
true
);
console.log("Penalty amount after reset:", ethers.utils.formatEther(penaltyAmount));
expect(penaltyAmount).to.equal(0); // Expect penalty to be zero after reset
});
});
Running the test show that the penalty amount after resetting is 0
, demonstrating that the penalty percentages for past months can indeed be reset, leading to a reduction in penalties:
Penalty amount after reset: 0.0
This output confirms that an attacker can bypass the intended penalty by altering historical penalty data.
Add a check in setPenaltyPercentages
to ensure that month
is equal to or greater than the current month as calculated by _calculateMonthsPassed
.
if (month != monthsPassed) {
revert OutOfBounds(); // Prevent modification of past months' penalties
}