Skip to content
Merged
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
131 changes: 131 additions & 0 deletions docs/dos-analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Denial-of-Service, Gas-Griefing & Unbounded-Work Analysis

ChainProof provides a deterministic, production-grade static analysis engine for detecting Denial-of-Service (DoS), gas-griefing vectors, and unbounded-work vulnerabilities in Solidity smart contracts.

---

## 1. Overview & Threat Model

Denial-of-Service vulnerabilities in Ethereum and EVM-compatible blockchains rarely involve brute-force traffic volume; instead, they exploit economic and execution constraints of the EVM:

1. **Block Gas Limit deadlocks (30M gas ceiling):** When work complexity scales linearly or quadratically with dynamic storage arrays, the gas required to execute a transaction eventually exceeds the block gas limit, permanently freezing contract state transitions.
2. **Push-Payment griefing:** Sending Ether or tokens to untrusted recipient addresses inside loops or single execution paths allows a single malicious contract recipient to revert the entire transaction.
3. **Return Bombs & Quadratic Memory Expansion:** When contracts make high-level calls or low-level calls without capping returndata copying, a malicious recipient can return an arbitrarily large payload (e.g. megabytes of data), forcing exponential memory expansion gas costs that exhaust caller gas.
4. **Mass Storage Deletion:** Deleting storage elements (`delete`) inside unbounded loops costs full gas up front, while EIP-3529 limits refunds to at most 20% of the transaction gas limit.
5. **Insufficient Gas Forwarding (63/64th Rule):** EIP-150 forwards at most 63/64 of remaining gas to sub-calls. Without explicit gas stipends, relayers can grief transactions by providing barely enough gas for outer execution.

---

## 2. Rule Catalog

| Rule ID | Title | Default Severity | Category | SWC Reference |
|---|---|---|---|---|
| `CP-DOS-001` | Unbounded Loop Iteration Over Dynamic Storage Array | `High` | `denial_of_service` | SWC-128 |
| `CP-DOS-002` | Push-Payment Pattern with Unexpected Revert Risk | `High` | `denial_of_service` | SWC-113 |
| `CP-DOS-003` | External Call Fan-Out in Loop Iteration | `Medium` | `gas_griefing` | - |
| `CP-DOS-004` | Return Bomb / Unbounded Returndata Memory Expansion | `Medium` | `gas_griefing` | - |
| `CP-DOS-005` | Unbounded Storage Clearing / Mass Deletion | `Medium` | `unbounded_work` | - |
| `CP-DOS-006` | Insufficient Gas Forwarding / 63/64th Rule Griefing | `Medium` | `gas_griefing` | - |
| `CP-DOS-007` | Single-Transaction Block Gas Limit Deadlock | `High` | `denial_of_service` | - |
| `CP-DOS-008` | Unbounded Recursion Without Depth Guard | `High` | `denial_of_service` | SWC-128 |
| `CP-DOS-009` | Attacker-Controlled Array Growth / Storage Poisoning | `Medium` | `denial_of_service` | - |
| `CP-DOS-010` | Revert Propagation in Critical Batch Operation | `Low` | `gas_griefing` | - |

---

## 3. Recognized Mitigation Patterns

ChainProof's AST analyzer recognizes secure architecture patterns to eliminate false positives:

### 1. Pagination Pattern (`CP-DOS-001` suppressed)
Contracts that pass `offset` and `limit` / `count` with explicit upper bounds:
```solidity
function distributePaginated(uint256 offset, uint256 limit) external {
require(limit <= MAX_BATCH_SIZE, "Exceeds max batch");
uint256 end = offset + limit;
if (end > shareholders.length) end = shareholders.length;
for (uint256 i = offset; i < end; i++) {
// Safe bounded loop
}
}
```

### 2. Pull-Payment Pattern (`CP-DOS-002` suppressed)
Contracts that track pending balances internally and offer a dedicated `withdraw()` endpoint:
```solidity
mapping(address => uint256) public pendingWithdrawals;

function creditReward(address user, uint256 amount) internal {
pendingWithdrawals[user] += amount;
}

function withdraw() external {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0);
pendingWithdrawals[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
}
```

### 3. Failure Isolation with `try/catch` (`CP-DOS-003`, `CP-DOS-010` suppressed)
Batch executors that isolate individual transaction failures:
```solidity
for (uint256 i = 0; i < targets.length; i++) {
try IReceiver(targets[i]).processTask(taskIds[i]) {
emit TaskSucceeded(targets[i], taskIds[i]);
} catch (bytes memory reason) {
emit TaskFailed(targets[i], taskIds[i], reason);
}
}
```

### 4. Checkpointed State Machines (`CP-DOS-007` suppressed)
State machines that persist progress across multiple transactions:
```solidity
uint256 public nextIndex;

function processBatch(uint256 count) external {
require(count <= CHUNK_SIZE);
uint256 total = queue.length;
uint256 processed = 0;
while (nextIndex < total && processed < count) {
processItem(queue[nextIndex]);
nextIndex++;
processed++;
}
}
```

---

## 4. CLI Reference

### `chainproof dos inspect-loops <targets...>`
Inspects all loops in Solidity files, classifying bounds (`storage_array_bounded`, `parameter_bounded`, `constant_bounded`, `paginated`, `unbounded`) and operations.

```bash
chainproof dos inspect-loops contracts/ --format table
```

### `chainproof dos fanout <targets...>`
Inspects all external calls and push payment vectors.

```bash
chainproof dos fanout contracts/ --format json
```

### `chainproof dos audit <targets...>`
Performs a complete DoS and unbounded-work audit.

```bash
chainproof dos audit contracts/ --fail-on high --format markdown --output dos-report.md
```

---

## 5. REST API Endpoints

- `POST /dos/inspect-loops`: Inspects loops and bound classifications across posted Solidity sources.
- `POST /dos/fanout`: Inspects external call fanout and payment vectors.
- `POST /dos/audit`: Generates a structured `DosAuditReport`.
31 changes: 31 additions & 0 deletions examples/contracts/dos/FailureIsolatedBatch.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

interface IReceiver {
function processTask(uint256 taskId) external;
}

/**
* @title FailureIsolatedBatch
* @notice Secure batch executor isolating individual task failures with try/catch.
*/
contract FailureIsolatedBatch {
uint256 public constant MAX_BATCH = 50;

event TaskSucceeded(address indexed target, uint256 taskId);
event TaskFailed(address indexed target, uint256 taskId, bytes reason);

function executeBatch(address[] calldata targets, uint256[] calldata taskIds) external {
require(targets.length == taskIds.length, "Mismatched lengths");
require(targets.length <= MAX_BATCH, "Exceeds max batch");

for (uint256 i = 0; i < targets.length; i++) {
try IReceiver(targets[i]).processTask(taskIds[i]) {
emit TaskSucceeded(targets[i], taskIds[i]);
} catch (bytes memory reason) {
// Failure is isolated: does not revert the entire batch
emit TaskFailed(targets[i], taskIds[i], reason);
}
}
}
}
28 changes: 28 additions & 0 deletions examples/contracts/dos/MassStorageDeletion.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

/**
* @title MassStorageDeletion
* @notice Vulnerable contract attempting mass deletion in unbounded loop.
*/
contract MassStorageDeletion {
address public admin;
uint256[] public entries;

constructor() {
admin = msg.sender;
}

function addEntry(uint256 value) external {
entries.push(value);
}

function clearAllEntries() external {
require(msg.sender == admin, "Not admin");

// Vulnerability: Deleting storage elements in unbounded loop (CP-DOS-005)
for (uint256 i = 0; i < entries.length; i++) {
delete entries[i];
}
}
}
38 changes: 38 additions & 0 deletions examples/contracts/dos/PaginatedDividendVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

/**
* @title PaginatedDividendVault
* @notice Secure dividend vault implementing bounded pagination.
*/
contract PaginatedDividendVault {
uint256 public constant MAX_BATCH_SIZE = 50;
address public owner;
address[] public shareholders;
mapping(address => uint256) public shares;
uint256 public totalShares;

constructor() {
owner = msg.sender;
}

function distributePaginated(uint256 offset, uint256 limit) external payable {
require(limit <= MAX_BATCH_SIZE, "Exceeds max batch");
require(totalShares > 0, "No shares");

uint256 end = offset + limit;
if (end > shareholders.length) {
end = shareholders.length;
}

for (uint256 i = offset; i < end; i++) {
address payable recipient = payable(shareholders[i]);
uint256 payout = (msg.value * shares[recipient]) / totalShares;
(bool ok, ) = recipient.call{value: payout}("");
// Failure isolation
if (!ok) {
// Log or track failure instead of blocking
}
}
}
}
39 changes: 39 additions & 0 deletions examples/contracts/dos/PullPaymentAuction.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

/**
* @title PullPaymentAuction
* @notice Secure auction implementing pull-over-push payment pattern.
*/
contract PullPaymentAuction {
address public highestBidder;
uint256 public highestBid;
mapping(address => uint256) public pendingReturns;

function bid() external payable {
require(msg.value > highestBid, "Bid too low");

if (highestBidder != address(0)) {
// Secure: Credit balance in internal ledger
pendingReturns[highestBidder] += highestBid;
}

highestBidder = msg.sender;
highestBid = msg.value;
}

function withdraw() external returns (bool) {
uint256 amount = pendingReturns[msg.sender];
require(amount > 0, "No funds to withdraw");

pendingReturns[msg.sender] = 0;

(bool success, ) = msg.sender.call{value: amount}("");
if (!success) {
pendingReturns[msg.sender] = amount;
return false;
}

return true;
}
}
24 changes: 24 additions & 0 deletions examples/contracts/dos/PushPaymentAuction.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

/**
* @title PushPaymentAuction
* @notice Vulnerable auction performing direct push refund on outbid.
*/
contract PushPaymentAuction {
address payable public highestBidder;
uint256 public highestBid;

function bid() external payable {
require(msg.value > highestBid, "Bid too low");

if (highestBidder != address(0)) {
// Vulnerability: Direct push payment refund (CP-DOS-002)
// If previous highest bidder is a malicious contract rejecting transfers, no one can outbid them!
highestBidder.transfer(highestBid);
}

highestBidder = payable(msg.sender);
highestBid = msg.value;
}
}
15 changes: 15 additions & 0 deletions examples/contracts/dos/ReturnBombGriefing.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

/**
* @title ReturnBombGriefing
* @notice Vulnerable relayer calling arbitrary targets without limiting returndata copying.
*/
contract ReturnBombGriefing {
function executeRelay(address target, bytes calldata data) external returns (bytes memory) {
// Vulnerability: Low-level call copying unbounded returndata (CP-DOS-004)
(bool success, bytes memory returnData) = target.call(data);
require(success, "Call failed");
return returnData;
}
}
35 changes: 35 additions & 0 deletions examples/contracts/dos/SafeChunkedQueue.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

/**
* @title SafeChunkedQueue
* @notice Secure checkpointed queue allowing partial progress across transactions.
*/
contract SafeChunkedQueue {
uint256 public constant CHUNK_SIZE = 20;
address[] public queue;
uint256 public nextIndex;

function enqueue(address user) external {
queue.push(user);
}

function processQueue(uint256 count) external {
require(count <= CHUNK_SIZE, "Count exceeds chunk size");

uint256 total = queue.length;
uint256 processed = 0;

while (nextIndex < total && processed < count) {
address user = queue[nextIndex];
nextIndex++;
processed++;

// Process individual item safely
(bool ok, ) = user.call("");
if (!ok) {
// Log and continue
}
}
}
}
39 changes: 39 additions & 0 deletions examples/contracts/dos/UnboundedDividendVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

/**
* @title UnboundedDividendVault
* @notice Vulnerable contract containing unbounded loop iteration, push payments, and array growth.
*/
contract UnboundedDividendVault {
address public owner;
address[] public shareholders;
mapping(address => uint256) public shares;
uint256 public totalShares;

constructor() {
owner = msg.sender;
}

function registerShareholder(address user, uint256 shareAmount) external {
// Vulnerability: Unrestricted array growth without limits or access control (CP-DOS-009)
shareholders.push(user);
shares[user] += shareAmount;
totalShares += shareAmount;
}

function distributeDividends() external payable {
require(msg.value > 0, "No dividends");
require(totalShares > 0, "No shares");

// Vulnerability: Unbounded loop over dynamic storage array (CP-DOS-001)
for (uint256 i = 0; i < shareholders.length; i++) {
address payable recipient = payable(shareholders[i]);
uint256 payout = (msg.value * shares[recipient]) / totalShares;

// Vulnerability: Push-Payment pattern inside loop (CP-DOS-002)
// If one recipient reverts, entire distribution bricks!
recipient.transfer(payout);
}
}
}
Loading
Loading