Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/lint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ It helps enforce best practices and improve code quality within Foundry projects
- `low-level-calls`: Direct use of low-level calls should be avoided.
- `event-fields`: `address` event parameters should be `indexed` for efficient log filtering.
- `todo-comment`: Detects unresolved `TODO` and `FIXME` markers in comments.
- `reentrancy-unlimited-gas`: Flags state changes or event emissions after `transfer`/`send`, whose fixed gas stipend may become reentrant after gas repricing.
- `unused-error`: Custom error declarations that are never referenced should be removed.
- `literal-instead-of-constant`: A literal value repeated inside a contract should be a named constant.
- `function-init-state`: State variable initializers run before the constructor; depending on a non-pure function or another state variable there observes partial state.
Expand Down
61 changes: 61 additions & 0 deletions crates/lint/docs/reentrancy-unlimited-gas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Reentrancy through fixed-gas ETH transfers

**Severity**: `Info`
**ID**: `reentrancy-unlimited-gas`

Flags state changes and event emissions that occur after Solidity's built-in `transfer` or `send`
on the same reachable path. These operations forward a fixed gas stipend, but opcode repricing can
make new callback behavior affordable within that stipend.

## What it does

The lint tracks Solidity's built-in `transfer` and `send` calls through public and external entry
points, modifiers, inherited helpers, and other internal calls. It follows branches and loop
backedges and warns when a later reachable operation writes contract state or emits an event.
Writes through storage references, storage-array `push`/`pop` calls, and Yul `sstore`/`tstore`
operations count as state changes; Yul log operations count as event emissions. Both calls are
tracked regardless of the transferred amount because even a zero-value call executes recipient
code.

For `send` used directly in a Boolean control-flow condition or through a stored result, only
continuations on which the call may have succeeded are treated as reentrant because a failed call
reverts its callee's effects. Sibling expression order is conservatively treated as unspecified,
matching Solidity's evaluation-order rules; effects on an always-reverting expression path are
discarded.

Contract methods that merely reuse the names `transfer` or `send` are excluded by checking the
compiler-resolved call target. Low-level calls, including calls with an explicit 2,300 gas cap, are
outside this detector's Slither-compatible scope. Constructors, local-variable writes, effects
that occur only before the call, and effects on mutually exclusive paths are not reported.

## Why is this bad?

The fixed stipend is not a stable reentrancy boundary. Changes to EVM opcode costs can alter which
fallback operations fit within it, so code that assumes `transfer` or `send` can never call back may
become unsafe after a network upgrade. A callback before a later state write can observe stale
state, while a callback before a later event can reorder logs consumed by off-chain systems.

Apply checks-effects-interactions: commit state and emit its corresponding event before interacting
with the recipient. Use a reentrancy guard when the interaction cannot safely be moved last.

## Example

### Bad

```solidity
function withdraw(address payable recipient, uint256 amount) external {
recipient.transfer(amount);
balances[recipient] -= amount;
emit Withdrawal(recipient, amount);
}
```

### Good

```solidity
function withdraw(address payable recipient, uint256 amount) external {
balances[recipient] -= amount;
emit Withdrawal(recipient, amount);
recipient.transfer(amount);
}
```
4 changes: 2 additions & 2 deletions crates/lint/src/sol/high/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use controlled_delegatecall::CONTROLLED_DELEGATECALL;
use encode_packed_collision::ENCODE_PACKED_COLLISION;
use incorrect_exp::INCORRECT_EXP;
use incorrect_shift::INCORRECT_SHIFT;
use reentrancy::{REENTRANCY_ETH, REENTRANCY_NO_ETH};
use reentrancy::{REENTRANCY_ETH, REENTRANCY_NO_ETH, REENTRANCY_UNLIMITED_GAS};
use rtlo::RTLO;
use unchecked_calls::{ERC20_UNCHECKED_TRANSFER, UNCHECKED_CALL};
use unprotected_initializer::UNPROTECTED_INITIALIZER;
Expand All @@ -29,7 +29,7 @@ register_lints!(
(EncodedPackedCollision, late, (ENCODE_PACKED_COLLISION)),
(IncorrectExp, late, (INCORRECT_EXP)),
(IncorrectShift, early, (INCORRECT_SHIFT)),
(ReentrancyEth, late, (REENTRANCY_ETH, REENTRANCY_NO_ETH)),
(ReentrancyEth, late, (REENTRANCY_ETH, REENTRANCY_NO_ETH, REENTRANCY_UNLIMITED_GAS)),
(UncheckedCall, early, (UNCHECKED_CALL)),
(UncheckedTransferERC20, late, (ERC20_UNCHECKED_TRANSFER)),
(UnprotectedInitializer, late, (UNPROTECTED_INITIALIZER)),
Expand Down
Loading
Loading