Skip to content

Latest commit

 

History

History
141 lines (108 loc) · 5.12 KB

File metadata and controls

141 lines (108 loc) · 5.12 KB

Scruffy Berry Ape

High

The fixed slot limit 256 will cause a DOS for high value vouchers as a Malicious Voucher can fill all slots with minimum value vouches.

Summary

The lack of value based slot allocation in EthosVouch.sol will cause a complete denial of service for high-value vouchers as an attacker can fill all available slots with minimum value vouches (0.0001 ETH).

Root Cause

In EthosVouch.sol there is a fixed maximum of 256 slots per profile with no consideration of vouch value: https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol?plain=1#L29

https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/EthosVouch.sol?plain=1#L330

The current implementation allows a malicious voucher to block high value vouches by filling slots with minimum value vouches (0.0001 ETH), which directly contradicts the protocol's core philosophy that "Credibility is based on stake value, not popularity".

Internal pre-conditions

  • Profile must exist in EthosProfile contract
  • Malicious Voucher needs enough ETH to fill slots (at least 0.0256 ETH = 256 * 0.0001 ETH minimum) + Gas Fees
  • Target profile must have available vouch slots (not already at 256)
  • Contract must not be paused

External pre-conditions

None. attack can be executed independently

Attack Path

1- Malicious Voucher identifies valuable profile to target 2- Malicious Voucher calls vouchByProfileId() repeatedly with minimum amount (0.0001 ETH) to fill slots 3- When high value voucher attempts to vouch, their transaction reverts due to MaximumVouchesExceeded

4- Malicious Voucher can then:

  • Demand payment to release slots through unvouch()
  • Maintain control to collect pool fees
  • Selectively allow vouches by controlling slot availability

Impact

An attacker can: 1- Fill all 256 slots with minimum vouches (total cost 0.0256 ETH + Gas fees) 2- Block legitimate high value vouches (eg 1 ETH) 3- Extract value through slot release extortion 4- Capture vouching pool fees 5- Control who can vouch for a profile

PoC

No response

Mitigation

An "Accept Vouch" mechanism where profile owners must accept vouches before they count against slot limits is optimal but requires major changes in the current implementation. Here's a simpler mitigation that focuses on preventing rapid consecutive vouches:

contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, ReentrancyGuard {
    // ... existing code ...

+   // Cooldown period between vouches for same profile 
+   uint256 public constant VOUCH_COOLDOWN = 1 hours;
+   
+   // Track last vouch timestamp per profile
+   mapping(uint256 => uint256) public lastVouchTimestamp;
+   
+   // Track number of vouches in cooldown window
+   mapping(uint256 => uint256) public recentVouchCount;
+   
+   // Maximum vouches allowed in cooldown window
+   uint256 public constant MAX_VOUCHES_IN_WINDOW = 5;

    function vouchByProfileId(
        uint256 subjectProfileId,
        string calldata comment,
        string calldata metadata
    ) public payable whenNotPaused nonReentrant {
+       // Check cooldown and rate limiting
+       _enforceVouchRateLimit(subjectProfileId);

        // ... existing checks ...

        // store vouch details
        uint256 count = vouchCount;
        vouchIdsByAuthor[authorProfileId].push(count);
        vouchIdsByAuthorIndex[authorProfileId][count] = vouchIdsByAuthor[authorProfileId].length - 1;
        vouchIdsForSubjectProfileId[subjectProfileId].push(count);
        vouchIdsForSubjectProfileIdIndex[subjectProfileId][count] =
            vouchIdsForSubjectProfileId[subjectProfileId].length -
            1;

        vouchIdByAuthorForSubjectProfileId[authorProfileId][subjectProfileId] = count;
        vouches[count] = Vouch({
            archived: false,
            unhealthy: false,
            authorProfileId: authorProfileId,
            authorAddress: msg.sender,
            vouchId: count,
            balance: toDeposit,
            subjectProfileId: subjectProfileId,
            comment: comment,
            metadata: metadata,
            activityCheckpoints: ActivityCheckpoints({
                vouchedAt: block.timestamp,
                unvouchedAt: 0,
                unhealthyAt: 0
            })
        });

        emit Vouched(count, authorProfileId, subjectProfileId, msg.value);
        vouchCount++;

+       // Update tracking
+       lastVouchTimestamp[subjectProfileId] = block.timestamp;
+       recentVouchCount[subjectProfileId]++;
    }

+   function _enforceVouchRateLimit(uint256 profileId) internal {
+       // Reset counter if cooldown window has passed
+       if (block.timestamp > lastVouchTimestamp[profileId] + VOUCH_COOLDOWN) {
+           recentVouchCount[profileId] = 0;
+       }
+       
+       // Check rate limits
+       require(
+           recentVouchCount[profileId] < MAX_VOUCHES_IN_WINDOW,
+           "Too many vouches in time window"
+       );
+       
+       // For first vouch in new window
+       if (recentVouchCount[profileId] == 0) {
+           lastVouchTimestamp[profileId] = block.timestamp;
+       }
+   }
}