sherlock-audit / 2024-03-flat-money-fix-review-contest-judging

3 stars 2 forks source link

xiaoming90 - Malicious users could grief innocent users into receiving fewer points #14

Closed sherlock-admin4 closed 4 months ago

sherlock-admin4 commented 4 months ago

xiaoming90

medium

Malicious users could grief innocent users into receiving fewer points

Summary

Malicious users could grief innocent users into receiving fewer points.

Vulnerability Detail

Malicious users could grief innocent users by repeatedly depositing or opening leverage of the smallest possible amount to trigger an update of the mintRate.lastMintTimestamp state variable to the current timestamp. The minimum deposit is 1e6 wei and costs 0.000000003523 USD, which is almost nothing for malicious users to carry out the attack. There is no minimum size for opening a leverage position. They could use multiple accounts to trigger an update every block, Given that the gas fee in the Base chain is extremely cheap, it is practical to carry out this attack.

Let block.timestamp - mintRate.lastMintTimestamp in the formula be $\Delta T$. Since mintRate.lastMintTimestamp is updated frequently or updated every block, it is very close to the current block's timestamp (block.timestamp). Thus, $\Delta T$ will be an extremely small value.

Assume the following states:

Following is the formula for the getAccumulatedMint() function:

$$ \begin{align} decay = \frac{D - min(D, \Delta T)}{D} \ accMint = lastAccMint \times decay \end{align} $$

If $\Delta T$ is always an extremely small value, the decay will always be close to one ($\approx 0.99999..$). This means that there is almost no decay, or the decay has been significantly slowed down, and the value of $accMint$ returned will hover close to the $maxAccMint$.

https://github.com/sherlock-audit/2024-03-flat-money-fix-review-contest/blob/main/flatcoin-v1/src/PointsModule.sol#L138

File: PointsModule.sol
138:     function getAccumulatedMint() public view returns (uint256 accumulatedMintAmount) {
139:         return ((mintRate.lastAccumulatedMint *
140:             ((mintRate.decayTime - mintRate.decayTime.min(block.timestamp - mintRate.lastMintTimestamp)))) /
141:             mintRate.decayTime);
142:     }

Since the amount of points available to mint to the users is computed by $maxAccMint - accMint$​, only a very small amount (or dust amount of points) will be left to be minted to the users. Due to the griefing attack by malicious users, the innocent users will not be able to receive the points that they would have received if the griefing attack did not occur.

When the points contract is deployed, and the mintRate.maxAccumulatedMint is set to 10000e18, a malicious user could quickly mint all the 10000e18 points and perform the abovementioned attacks to prevent the decay mechanism from reducing the accumulated mint amount. As a result, the malicious user always owns the majority of the points in the protocol, while the rest of the users could only own a very small/dust amount of points, creating unfairness within the protocol.

Impact

Loss of points for the innocent users. In the main audit contest, points are deemed items that contain value as issues related to points are classified as Medium.

Code Snippet

https://github.com/sherlock-audit/2024-03-flat-money-fix-review-contest/blob/main/flatcoin-v1/src/PointsModule.sol#L138

Tool used

Manual Review

Recommendation

When the amount of points to be minted to the user is below the minMintAmount, the _mintTo function will short-circuit and exit immediately.

https://github.com/sherlock-audit/2024-03-flat-money-fix-review-contest/blob/main/flatcoin-v1/src/PointsModule.sol#L175

File: PointsModule.sol
175:     function _mintTo(address to, uint256 amount) internal {
176:         // Ignore dust amounts. It avoids potential precision errors on unlock time calculations
177:         if (amount < minMintAmount) return;

A similar approach could be taken within the _updateAccumulatedMint function to make such a griefing attack uneconomically for the malicious users. Currently, even if the returned _amountToMint is zero or 1 wei, the code at Line 219 will still proceed to update the mintRate.lastMintTimestamp to the current block's timestamp even when no points are minted to the users, which is incorrect and inconsistent.

https://github.com/sherlock-audit/2024-03-flat-money-fix-review-contest/blob/main/flatcoin-v1/src/PointsModule.sol#L219

File: PointsModule.sol
207:     /// @notice Update the accumulated points mint
208:     /// @dev It will only update the accumulated mint if it is within the rate limit
209:     function _updateAccumulatedMint(uint256 _mintAmount) internal returns (uint256 _amountToMint) {
210:         uint256 newAccumulatedMint = _mintAmount + getAccumulatedMint();
211: 
212:         if (newAccumulatedMint > mintRate.maxAccumulatedMint) {
213:             _amountToMint = getAvailableMint();
214:             mintRate.lastAccumulatedMint = mintRate.maxAccumulatedMint;
215:         } else {
216:             _amountToMint = _mintAmount;
217:             mintRate.lastAccumulatedMint = newAccumulatedMint;
218:         }
219:         mintRate.lastMintTimestamp = block.timestamp.toUint64();
220:     }

Consider the following changes to make the griefing attack more expensive to be carried out for deterrence.

/// @notice Update the accumulated points mint
/// @dev It will only update the accumulated mint if it is within the rate limit
function _updateAccumulatedMint(uint256 _mintAmount) internal returns (uint256 _amountToMint) {
    uint256 newAccumulatedMint = _mintAmount + getAccumulatedMint();

    if (newAccumulatedMint > mintRate.maxAccumulatedMint) {
        _amountToMint = getAvailableMint();
        mintRate.lastAccumulatedMint = mintRate.maxAccumulatedMint;
    } else {
        _amountToMint = _mintAmount;
        mintRate.lastAccumulatedMint = newAccumulatedMint;
    }
+   if (amount < minMintAmount) {
        mintRate.lastMintTimestamp = block.timestamp.toUint64();
+       }
}

In addition, the current minMintAmount is set to 1e6 during initialization, and there is no way to update this value after initialization. Consider allowing the minMintAmount variable to be updatable by the owner and the value to be set to a sufficiently high value (1e6 is too small) so that it will be expensive for the attacker to mint the minimum amount of points repeatedly. If the users do not meet the minMintAmount, the minting of points should be skipped, and no update to mintRate.lastAccumulatedMint should be made.

Duplicate of #10