Vyper Vested Claims

First Flight #34
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: medium
Invalid

Excessive Proof Length Can Be Used as a DoS Vector

Summary

The VestedAirdrop contract does not validate that proof lengths are reasonable for a Merkle tree, allowing attackers to submit excessively long proofs that can be used as a denial-of-service (DoS) vector against the contract.

Vulnerability Details

Neither the verify_proof nor the _verify_proof function validates that the proof length is reasonable for a Merkle tree:

def _verify_proof(proof: DynArray[bytes32, 20], leaf: bytes32) -> bool:
# No validation on proof length being reasonable
computed_hash: bytes32 = leaf
for proof_element: bytes32 in proof:
computed_hash = self._hash_pair(computed_hash, proof_element)
return computed_hash == self.merkle_root

While the DynArray has a maximum size of 20, this is still significantly larger than what would be needed for most practical Merkle trees. An attacker can exploit this by submitting proofs with the maximum allowed length, causing excessive gas consumption and potentially blocking the contract from processing legitimate claims during high-traffic periods.

Proof of Concept

The following Python code demonstrates how an attacker could create a DoS attack using excessively long proofs:

import time
# Simplified representation of the vulnerable contract
class VulnerableContract:
def __init__(self, merkle_root):
self.merkle_root = merkle_root
self.gas_limit = 30000000 # Typical block gas limit
def verify_proof(self, user, amount, proof):
"""Vulnerable verification function"""
# Construct leaf node
leaf = self.keccak256(self.concat(user, amount))
# Call internal verification without validation
return self._verify_proof(proof, leaf)
def _verify_proof(self, proof, leaf):
"""Internal verification without length validation"""
computed_hash = leaf
gas_used = 0
# Each iteration consumes gas
for proof_element in proof:
# Simulate gas consumption for each hash operation
gas_used += 2500 # Approximate gas cost per hash operation
computed_hash = self._hash_pair(computed_hash, proof_element)
return computed_hash == self.merkle_root, gas_used
# Helper functions
def keccak256(self, data):
return f"hashed_{data}"
def concat(self, a, b):
return f"{a}_{b}"
def _hash_pair(self, a, b):
if a < b:
return self.keccak256(self.concat(a, b))
return self.keccak256(self.concat(b, a))
# DoS Attack Simulation
def simulate_dos_attack():
# Setup
merkle_root = "0x1234567890123456789012345678901234567890123456789012345678901234"
contract = VulnerableContract(merkle_root)
# Normal user with reasonable proof length
normal_user = "0x1111111111111111111111111111111111111111"
normal_amount = 100
normal_proof = ["0x" + "1" * 64, "0x" + "2" * 64, "0x" + "3" * 64] # 3 elements
# Attacker with maximum length proof
attacker = "0x2222222222222222222222222222222222222222"
attack_amount = 200
attack_proof = []
for i in range(20): # Maximum allowed by DynArray[bytes32, 20]
attack_proof.append(f"0x{i:064x}")
# Simulate normal transaction
print("Normal user transaction:")
start_time = time.time()
result, gas_used = contract.verify_proof(normal_user, normal_amount, normal_proof)
normal_time = time.time() - start_time
print(f" Result: {result}")
print(f" Gas used: {gas_used}")
print(f" Time taken: {normal_time:.6f} seconds")
# Simulate attack transaction
print("\nAttacker transaction:")
start_time = time.time()
result, gas_used = contract.verify_proof(attacker, attack_amount, attack_proof)
attack_time = time.time() - start_time
print(f" Result: {result}")
print(f" Gas used: {gas_used}")
print(f" Time taken: {attack_time:.6f} seconds")
# Calculate impact
gas_increase = gas_used / (len(normal_proof) * 2500)
time_increase = attack_time / normal_time
print("\nDoS Impact Analysis:")
print(f" Gas usage increase: {gas_increase:.2f}x")
print(f" Processing time increase: {time_increase:.2f}x")
# Calculate block filling potential
txs_per_block = contract.gas_limit // gas_used
print(f" Maximum attack transactions per block: {txs_per_block}")
print(f" Percentage of block gas limit consumed: {(gas_used / contract.gas_limit) * 100:.2f}%")
# Demonstrate block congestion
if gas_used > contract.gas_limit / 10: # If a single tx uses more than 10% of block
print(" WARNING: A single attack transaction consumes a significant portion of a block's gas limit")
print(" This can cause severe network congestion during high-traffic periods")
# Run the simulation
simulate_dos_attack()

When executed, this PoC demonstrates:

  1. The significant difference in gas consumption between normal and attack transactions

  2. How an attacker can consume a disproportionate amount of block gas limit

  3. The potential for network congestion during high-traffic periods

Test Script

Create a file named test_dos_attack.py with the following content:

def test_dos_attack():
"""Test to demonstrate the DoS vulnerability in the VestedAirdrop contract"""
import time
# Simplified representation of the vulnerable contract
class VulnerableContract:
def __init__(self, merkle_root):
self.merkle_root = merkle_root
self.gas_limit = 30000000 # Typical block gas limit
def verify_proof(self, user, amount, proof):
"""Vulnerable verification function"""
# Construct leaf node
leaf = self.keccak256(self.concat(user, amount))
# Call internal verification without validation
return self._verify_proof(proof, leaf)
def _verify_proof(self, proof, leaf):
"""Internal verification without length validation"""
computed_hash = leaf
gas_used = 0
# Each iteration consumes gas
for proof_element in proof:
# Simulate gas consumption for each hash operation
gas_used += 2500 # Approximate gas cost per hash operation
computed_hash = self._hash_pair(computed_hash, proof_element)
return computed_hash == self.merkle_root, gas_used
# Helper functions
def keccak256(self, data):
return f"hashed_{data}"
def concat(self, a, b):
return f"{a}_{b}"
def _hash_pair(self, a, b):
if a < b:
return self.keccak256(self.concat(a, b))
return self.keccak256(self.concat(b, a))
# Setup
merkle_root = "0x1234567890123456789012345678901234567890123456789012345678901234"
contract = VulnerableContract(merkle_root)
# Normal user with reasonable proof length
normal_user = "0x1111111111111111111111111111111111111111"
normal_amount = 100
normal_proof = ["0x" + "1" * 64, "0x" + "2" * 64, "0x" + "3" * 64] # 3 elements
# Attacker with maximum length proof
attacker = "0x2222222222222222222222222222222222222222"
attack_amount = 200
attack_proof = []
for i in range(20): # Maximum allowed by DynArray[bytes32, 20]
attack_proof.append(f"0x{i:064x}")
# Simulate normal transaction
print("Normal user transaction:")
start_time = time.time()
result, gas_used = contract.verify_proof(normal_user, normal_amount, normal_proof)
normal_time = time.time() - start_time
print(f" Result: {result}")
print(f" Gas used: {gas_used}")
print(f" Time taken: {normal_time:.6f} seconds")
# Simulate attack transaction
print("\nAttacker transaction:")
start_time = time.time()
result, gas_used = contract.verify_proof(attacker, attack_amount, attack_proof)
attack_time = time.time() - start_time
print(f" Result: {result}")
print(f" Gas used: {gas_used}")
print(f" Time taken: {attack_time:.6f} seconds")
# Calculate impact
gas_increase = gas_used / (len(normal_proof) * 2500)
time_increase = attack_time / normal_time
print("\nDoS Impact Analysis:")
print(f" Gas usage increase: {gas_increase:.2f}x")
print(f" Processing time increase: {time_increase:.2f}x")
# Calculate block filling potential
txs_per_block = contract.gas_limit // gas_used
print(f" Maximum attack transactions per block: {txs_per_block}")
print(f" Percentage of block gas limit consumed: {(gas_used / contract.gas_limit) * 100:.2f}%")
# Demonstrate block congestion
if gas_used > contract.gas_limit / 10: # If a single tx uses more than 10% of block
print(" WARNING: A single attack transaction consumes a significant portion of a block's gas limit")
print(" This can cause severe network congestion during high-traffic periods")
# Assert statements to verify the vulnerability
assert gas_used > len(normal_proof) * 2500, "Attack should use more gas than normal transaction"
assert attack_time > normal_time, "Attack should take longer to process"
print("\nTest passed: DoS vulnerability confirmed")
if __name__ == "__main__":
test_dos_attack()

Impact

  • Denial of Service: Attackers can consume excessive gas, potentially blocking legitimate transactions

  • Block Congestion: During high-traffic periods, attack transactions can cause network congestion

  • Increased Costs: Users may need to pay higher gas fees to compete with attack transactions

  • Resource Exhaustion: Contract operations may become prohibitively expensive during an attack

Recommendation

Add explicit validation for reasonable proof lengths:

def verify_proof(user: address, amount: uint256, proof: DynArray[bytes32, 20]) -> bool:
# Input validation - limit to a reasonable depth for most applications
assert len(proof) <= 10, "Proof exceeds maximum reasonable depth"
# Then proceed with verification
return self._verify_proof(
proof,
keccak256(concat(convert(user, bytes20), convert(amount, bytes32)))
)

A comprehensive fix would implement all validations together:

def verify_proof(user: address, amount: uint256, proof: DynArray[bytes32, 20]) -> bool:
# Input validation
assert user != empty(address), "Invalid user address"
assert amount > 0, "Amount must be greater than zero"
assert len(proof) > 0, "Proof cannot be empty"
assert len(proof) <= 10, "Proof exceeds maximum reasonable depth"
# Then proceed with verification
return self._verify_proof(
proof,
keccak256(concat(convert(user, bytes20), convert(amount, bytes32)))
)
Updates

Appeal created

bube Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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