Beginner FriendlyFoundryDeFi
100 EXP
View results
Submission Details
Severity: high
Valid

Point rewards system can be gamed causing inflation

Summary

The point rewards system is based on an off-chain stateful subscriber which allocates points to wallets based on the Staked(amount, onBehalfOf) events emitted from the users' interaction with the Steaking contract on-chain, however this mechanism has a flawed and a malicious user can exploit it to mint unlimited points to themselves.

Vulnerability Details

The Steaking contract provides stake function for staking ether and another unstake function for unstaking ether. The off-chain subscriber is only aware of the former and ommits the Unstaked(by, amount) event.

Here is a scenario in which a malicious attacker can game the system.

  1. User stakes 1 ether in Steaking.

  2. Off-chain subscriber awards 1000 points to the user.

  3. User unstakes that 1 ether from Steaking.

  4. User stakes the same 1 ether in Steaking, again.

  5. Off-chain subscriber awards another 1000 points to the user, amounting to the total of 2000 points.

The same sequence can be repeated ad infinitum.

Impact

A malicious user can award themselves infinite points and break the invariant that the total points awarded must be proportional to the total ether staked in Steaking contract.
High impact as it is a fundamental disruption of functionality, however given that the mitigation is possible by fixing the bug on the server and replaying the blockchain history to recalculate the points I am downgrading to medium impact.

Tools Used

Manual Review

Recommendations

Fix the bug by handling the antagonist Unstaked(by, amount) event on the off-chain stateful subscriber in a similar way to the Staked(amount, onBehalfOf) event and reduce the points awarded when ether is unstaked from the Steaking contract on-chain.

Update the main loop of the server in main.js:

async function main() {
await connectToMongodb();
const { rpcUrl, steakingAddress } = getConfig();
const provider = new ethers.JsonRpcProvider(rpcUrl);
const steaking = new ethers.Contract(steakingAddress, steakingAbi, provider);
steaking.on(STAKED, async (_, amount, onBehalfOf) => {
let steakPoints;
steakPoints = await steakPointsModel.findOne({ walletAddress: onBehalfOf });
if (!steakPoints) {
steakPoints = new steakPointsModel({
walletAddress: onBehalfOf,
points: +ethers.formatEther(amount) * PRECISION,
});
} else {
steakPoints.points += +ethers.formatEther(amount) * PRECISION;
}
await steakPoints.save();
});
+ steaking.on(UNSTAKED, async (by, amount, _) => {
+ let steakPoints;
+ steakPoints = await steakPointsModel.findOne({ walletAddress: onBehalfOf });
+ steakPoints.points -= +ethers.formatEther(amount) * PRECISION;
+ await steakPoints.save();
+ });
}

However, depending on the implementation and the method used for querying (e.g., across different nodes or using filters), there might be inconsistencies or reordering. This is more likely when querying events across multiple blocks or when dealing with a large number of events.
If a block is part of a chain reorganization (reorg), the events from that block could be invalidated or re-emitted in a different order in the finalized block that replaces it.

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Steaking server is not taking unstakes into account

Support

FAQs

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