Company Simulator

First Flight #51
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: medium
Valid

L02. CutomerEngine - Send can silently fails

Root + Impact

Description

  • Normal behavior: When a user calls trigger_demand() with more ETH than required for their requested items, the contract should refund the excess ETH to msg.sender safely and reliably.

  • Problem: The refund logic uses Vyper’s send() function, which only forwards 2300 gas and does not revert on failure. If the receiver is a contract that requires more than 2300 gas in its fallback function, the refund silently fails and the excess ETH becomes permanently trapped inside the contract.

# ETH payment enforcement
total_cost: uint256 = requested * ITEM_PRICE
assert msg.value >= total_cost, "Insufficient payment!!!"
# Refund excess ETH
excess: uint256 = msg.value - total_cost
if excess > 0:
@> send(msg.sender, excess) # silently may fail if receiver is a contract

Risk

Likelihood:

  • The failure occurs when msg.sender is a contract wallet or proxy that executes code in its fallback/receive function, consuming more than 2300 gas.

  • Many modern smart contract wallets (e.g., Gnosis Safe, Argent, or multi-sig wallets) have non-trivial receive hooks, which frequently exceed 2300 gas.

Impact:

  • The excess ETH is never returned to the user and becomes irrecoverable without an owner-implemented rescue function.

  • Users lose funds whenever a refund attempt silently fails, leading to permanent loss of ETH for certain wallet types.

Proof of Concept

# Malicious or complex receiver contract that always uses >2300 gas
@external
@payable
def __init__():
pass
@external
@payable
def __default__():
x: uint256 = 0
for i in range(1000): # burn gas intentionally
x += i
# From EOA:
# Call trigger_demand() with msg.value > required total_cost.
# The refund path executes send(msg.sender, excess).
# send() forwards only 2300 gas; the fallback uses much morerefund fails silently.
# Excess ETH remains trapped in the Customer Engine contract.

Explanation:
In this scenario, the receiving contract’s fallback consumes more than 2300 gas. Because send() does not revert on failure, the refund fails but execution continues successfully, leaving the excess ETH stuck.

Recommended Mitigation

Use raw_call with explicit error handling instead of send(). This allows safe gas forwarding and reverts on failed transfers.

- if excess > 0:
- send(msg.sender, excess)
+ if excess > 0:
+ _success: bool = raw_call(msg.sender, b"", value=excess)
+ assert _success, "Refund failed"

This ensures that the refund either succeeds or the transaction reverts, preventing silent ETH loss and making the transfer behavior deterministic.

Updates

Lead Judging Commences

0xshaedyw Lead Judge
5 days ago
0xshaedyw Lead Judge 4 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Medium – Refund DOS via send()

Vyper’s send() reverts on failure, so refund attempts to contract wallets with complex fallback logic can halt trigger_demand() entirely. This causes a denial-of-service for affected users but does not result in fund loss.

Appeal created

s3mvl4d Auditor
3 days ago
0xshaedyw Lead Judge
1 day ago
0xshaedyw Lead Judge 1 day ago
Submission Judgement Published
Validated
Assigned finding tags:

Medium – Refund DOS via send()

Vyper’s send() reverts on failure, so refund attempts to contract wallets with complex fallback logic can halt trigger_demand() entirely. This causes a denial-of-service for affected users but does not result in fund loss.

Support

FAQs

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