The Vibe Coder’s Quality Field Guide: Stop Shipping Broken AI-Generated Code

The Vibe Coder’s Quality Field Guide

Stop Shipping Broken AI-Generated Code

A practical reference for developers who use AI to build — and want what they ship to actually work.

Why This Guide Exists

You’ve felt it. You describe what you want, the AI produces something that looks right, you run it, and it breaks. Or worse — it works in demo, breaks in production. Or it works today and breaks when you add the next feature.

This isn’t a problem with AI. It’s a problem with how most people use AI to build software.

This guide is the playbook for doing it right.

Part 1: Before You Write a Single Prompt

1.1 Define the Contract First

The #1 mistake: letting the AI design the architecture while you’re describing a feature.

Before any code, write down:

  • What goes in (inputs, formats, sources)
  • What comes out (outputs, formats, destinations)
  • What can fail (error cases, edge cases, limits)
  • What success looks like (specific, measurable)

If you can’t write this in plain English in 5 minutes, you’re not ready to build it yet.

1.2 Scope Before You Build

Explicitly scope what you’re NOT building:

  • “No authentication in this version”
  • “No database persistence — just in-memory”
  • “No error handling beyond console.log for now”

This isn’t laziness. It’s discipline.

1.3 Choose the Right Model for the Job

Task Use
Architecture planning Larger models (Opus, GPT-4)
Code generation Sonnet, GPT-4o, Gemini Flash
Simple boilerplate Haiku, smaller fast models
Debugging / explanation Any strong model
Test writing Strong + specialized

Part 2: Prompting for Quality Code

2.1 The Quality Prompt Template

Bad prompt: “Write a function that processes payments”

Better prompt:

The difference: explicit types, explicit error cases, explicit boundaries.

2.2 One Function, One Responsibility

Never ask for more than one meaningful thing per prompt. Write 4 prompts for 4 parts. Review each output before moving to the next.

2.3 Ask for the Test Before the Code

“Write 5 test cases for a function that validates email addresses. Include normal cases, edge cases, and failure cases. Don’t write the function yet.”

Once the test cases are right, ask it to write the function that passes them.

2.4 The “What Could Go Wrong” Prompt

After any non-trivial code generation, ask:

“What are 5 ways this code could fail in production that aren’t covered by the current error handling?”

The AI is often better at finding its own holes than you are.

Part 3: Architecture Patterns That Don’t Collapse

3.1 Start With the Data Model

Every new conversation: paste this in as context. The AI will use it.

3.2 Boundaries Over Layers

3.3 Configuration at the Edges

Any hardcoded URL, port, timeout, threshold, or credential is a bug waiting to happen.

3.4 The “What If It’s Down” Pattern

AI almost never handles unavailable dependencies. Add it yourself.

Part 4: Testing Without Losing Your Mind

4.1 The 3 Tests Every Function Needs

  1. Happy path: correct inputs → correct output
  2. Bad input: wrong types, missing fields → doesn’t crash, returns useful error
  3. Edge case: empty list, zero, None, very long string, Unicode

If a function can’t pass these three, it’s not ready.

4.2 The “Break It” Prompt

After working code and tests:

“Without changing the tests, modify the function in a way that would make it fail in a real-world scenario but still pass all current tests.”

If the AI can do this easily, your tests are bad.

4.3 Integration Test Before Polish

When this passes end-to-end, the core works. Now clean up.

Part 5: The Review Process That Actually Catches Problems

5.1 The 5-Minute Pre-Ship Checklist

Security

  • [ ] No credentials or tokens in code
  • [ ] User inputs validated before use
  • [ ] SQL queries use parameterized values

Reliability

  • [ ] Every external call has error handling
  • [ ] Every async operation has a timeout
  • [ ] No infinite loops without exit conditions

Correctness

  • [ ] Integer math for money (not float)
  • [ ] Timezones are explicit
  • [ ] Null/None cases handled

5.2 Red Flags in AI-Generated Code

Pattern Problem
Silently swallowing errors
Hardcoded waits
without KeyError waiting to happen
, Security vulnerability

Part 6: When AI Code Breaks in Production

6.1 The Debugging Protocol

Step 1: Reproduce in isolation — strip to 20 lines

Step 2: State actual vs. expected. Write it down.

Step 3: Ask the AI to explain, not fix:

“Walk me through exactly what this function does with this input. Don’t suggest fixes yet.”

Step 4: Ask for the fix with constraints.

6.2 When to Rewrite vs. Patch

Patch when: logic is right, fix is contained, you understand the break

Rewrite when: you don’t understand the code, patching touches 5+ places, data model is wrong

Quick Reference Card

Before You Prompt

  • Define inputs, outputs, error cases in plain English
  • Scope what you’re NOT building
  • Establish data models and naming conventions

While Prompting

  • One task per prompt
  • Specify types explicitly
  • Ask for tests before implementation

After Generating

  • Ask “what could go wrong?”
  • Run the checklist before shipping
  • Integration test before polishing

When It Breaks

  • Reproduce in isolation
  • State actual vs. expected precisely
  • Ask for explanation before fix
  • Rewrite if you’ve patched 3+ times

If something in your project is broken and you want hands-on help, check out the Vibe Code Rescue service — code audit and fix starting at 9.

Meet Our AI Team: The 13 Agents Running Traffic Empire

We built a portfolio of 21 utility websites. None of them were built by humans.

Traffic Empire is a experiment in autonomous web development. A team of 13 AI agents — all powered by Claude — researches niches, designs brands, writes code, creates content, optimizes for SEO, and audits each other’s work. A human sets the vision. The agents do everything else.

Here’s the team:

The CEO Agent

Orchestrates the weekly cycle. Reads the registry (our source of truth for all sites), checks the auditor’s report, and decides what to build, improve, or deprecate. Every decision is data-driven — if the portfolio score drops, new builds are paused until the issue is fixed.

The Analytics Agent

Runs first every cycle. Checks if sites are alive, pulls traffic data, computes health scores. Without current data, nothing else can make good decisions.

The Research Agent

Finds high-value niches — topics with high search volume and weak competition. It tracks its own prediction accuracy and calibrates its scoring model over time. It’s gotten better at picking winners.

The Designer Agent

Creates brand identity for each site: colors, typography, component patterns, visual hierarchy. Every site gets a unique, professional look. No cookie-cutter templates.

The Builder Agent

Takes the designer’s spec and turns it into a live Next.js site. Scaffolds from a template, implements all calculators/tools, deploys to Netlify, and verifies with curl checks.

The Editorial Team (Editor + 5 Writers)

This is our newsroom. The Editor agent commissions article pitches from 5 AI writer personas — each with a distinct voice and expertise area. The Editor evaluates quality, approves or rejects drafts, and ensures everything meets editorial standards before publication.

The Content Agent

Handles the mechanical side of publishing: clones repos, places files, verifies builds, deploys, and runs post-deploy checks.

The SEO/GEO Agent

Optimizes for both traditional search engines AND LLM discovery. Every site gets /llms.txt, structured JSON-LD, and endpoints designed for the growing AI search layer.

The Auditor Agent

Runs last. Reviews every agent’s work, grades performance (A through D), and proposes instruction improvements. If an agent scores C or D for 3+ consecutive weeks, the Auditor directly edits that agent’s instructions. It’s the self-improvement mechanism.

The Ratchet

Inspired by Karpathy’s autoresearch pattern: every change is measured. If the portfolio score improves, the change stays. If it drops, it gets reverted. Git is the memory layer — agents read commit history to understand what was tried before.

The Portfolio So Far

21 live sites across fitness, finance, PDF tools, QR codes, text utilities, crypto, age calculators, baby names, password generators, and more. All at *.thicket.sh.

Some highlights:

  • fit.thicket.sh — 12 fitness calculators including a TDEE calculator validated against 1,090 lab measurements
  • money.thicket.sh — 31 finance calculators (mortgage, compound interest, DCA, retirement)
  • pdf.thicket.sh — Client-side PDF tools (merge, split, compress) — no file uploads to servers
  • qr.thicket.sh — QR code generator with 6 styles and bulk generation

What’s Next

We’re at 0 organic traffic and $0 revenue. The sites are built. Now comes the hard part: distribution. We’re submitting to search consoles, posting to social platforms, publishing an MCP server for AI assistants, and writing content that earns links.

The bet: can a team of AI agents, running autonomously with minimal human oversight, build a profitable web portfolio? We’re about to find out.

Traffic Empire is open about being AI-built. Every site, every article, every design decision — made by Claude agents coordinated through a single orchestration repo. Follow along as we figure out if this works.

Check out the sites: thicket.sh
MCP Server: @thicket-team/mcp-calculators on npm

Bakan Uraloğlu, bizzat test ettiği yeni sistemin detaylarını anlattı: Yollar artık daha güvenli

Haberler.com’a özel açıklamalarda bulunan Ulaştırma ve Altyapı Bakanı Abdulkadir Uraloğlu, İstanbul’da devreye alınan Türkiye’nin ilk akıllı ulaşım koridoruna ilişkin detayları paylaştı. Sistemin trafik güvenliğini artırmayı hedeflediğini belirten Uraloğlu, sürücülere anlık veri akışı sağlayarak kazaların önüne geçilmesinin amaçlandığını ifade etti. Bakan Uraloğlu, Türkiye genelinde 18 bölgede akıllı ulaşım sistemleri merkezlerinin bulunduğunu aktardı.

Meet Your New AI-Powered Senior DBA: Bridging the Gap Between Metadata and Architectural Excellence

Database schemas often become “black boxes” over time. They accumulate technical debt, hidden bottlenecks, and security vulnerabilities that even seasoned developers might miss during a hectic sprint.

I decided to change that.

I’m excited to introduce AI Database Analyzer & Architect, an enterprise-grade tool built with .NET MAUI and Clean Architecture that acts as an autonomous Senior Database Consultant in your pocket.

Why This Isn’t Just “Another AI Wrapper”

Unlike simple chat interfaces, this tool is designed with Architectural Intelligence. It doesn’t just read your schema; it audits it across 40+ specialized pillars including:

Performance Tuning: Detecting non-SARGable predicates, missing indexes, and trigger-induced deadlocks.

Security & Privacy: Using NER logic to find unmasked PII/PHI data and identifying SQL injection risks in stored procedures.

Scalability: Identifying horizontal scalability bottlenecks and ORM anti-patterns (like the dreaded N+1).

Data Modeling: Detecting normalization violations (3NF) and circular dependencies.

The Tech Stack
Frontend: .NET MAUI (Multi-platform) with a strict Senior-level MVVM pattern.

Hybrid AI: Toggle between Google Gemini for high-scale cloud intelligence and Ollama for local, privacy-first analysis.

Visualization: Interactive, zoomable ER diagrams powered by Mermaid.js.

Reporting: Executive-level PDF exports using QuestPDF.

Deep-Dive Audits

The analyzer goes deep. It checks for Identity Column Capacity (warning you before your INT runs out), Collation Mismatches that kill index performance, and even AI Model Collapse (detecting if your data is being polluted by synthetic LLM noise).

Open Source & Future

This project is built to bridge the gap between raw database metadata and actionable expert insights. Whether you are using SQL Server, PostgreSQL, or MySQL, this tool helps you ensure your data layer is production-ready.

Check out the repo here: https://github.com/furkiak/AIDatabaseAnalyzer

I’d love to hear your thoughts! What’s the biggest “hidden” database issue you’ve ever encountered in production? Let’s discuss in the comments!

I Built a Genetic Algorithm That Discovers Trading Strategies – Here’s What 89 Generations Found

I wanted a system that could discover trading strategies without me hand-tuning every parameter. So I built one.

finclaw is an open-source quantitative finance engine in Python. One of its core features is an evolution engine that uses genetic algorithm principles to mutate, evaluate, and improve trading strategies automatically. After running it for 89 generations on NVDA data, here’s what I learned.

The Problem With Manual Strategy Tuning

Every quant hits the same wall: you write a strategy, backtest it, tweak a parameter, backtest again. Repeat 200 times. You end up overfitting to historical data without realizing it.

I wanted something that could explore the strategy space systematically — try combinations I wouldn’t think of, and discard what doesn’t work through a principled selection process rather than gut feeling.

How The Evolution Engine Works

The core loop is deceptively simple:

  1. Seed — Start with a YAML strategy definition (entry rules, exit rules, filters)
  2. Evaluate — Backtest the strategy and compute a fitness score (return, Sharpe, drawdown, win rate)
  3. Analyze — Look at where the strategy fails (losing trade clusters, poor exits, bad market timing)
  4. Propose — Generate targeted mutations: tighten stop losses, adjust RSI thresholds, add volume filters
  5. Mutate — Apply the best proposal to create a child strategy
  6. Select — Keep a Pareto frontier of the top N strategies
  7. Repeat — Until convergence or max generations

Each strategy is a YAML file that the DSL engine compiles into executable trading rules:

name: momentum_rsi_evolved
entry:
  - rsi_14 < 35
  - ma_cross(5, 20) == "golden"
  - volume > volume_ma_20 * 1.3
exit:
  - rsi_14 > 72
  - trailing_stop: 8%
filters:
  - trend_adx_strength > 20

The mutator modifies these rules — widening RSI bands, swapping moving average periods, adding or removing filters — while the evaluator runs a full backtest on each variant.

Running It

# Install finclaw
pip install finclaw

# Evolve a strategy for 50 generations
finclaw evolve my_strategy.yaml --symbol NVDA --generations 50 --frontier-size 5

Or via the Python API:

from src.evolution.engine import EvolutionEngine, EvolutionConfig
from src.strategy.expression import OHLCVData

config = EvolutionConfig(
    max_generations=50,
    frontier_size=5,
    no_improvement_limit=10,
)
engine = EvolutionEngine(config=config)
result = engine.run(seed_strategy, my_data)

print(f"Best score: {result['best_score'].composite():.4f}")
print(f"Generations: {result['generations_run']}")

What 89 Generations Found

I seeded the engine with a basic golden-cross momentum strategy on NVDA (2022-2025 daily data) and let it run.

Generation 1 (seed): Sharpe 0.42, max drawdown -28%, win rate 38%

The seed was mediocre. Lots of whipsaw trades during the 2022 drawdown.

Generation 23: Sharpe 0.91, max drawdown -19%, win rate 44%

The engine discovered that adding a volume confirmation filter (volume > 1.5x 20-day average) eliminated most false breakouts. It also tightened the trailing stop from 12% to 8%.

Generation 56: Sharpe 1.24, max drawdown -15%, win rate 48%

A mutation added an ADX trend strength filter (ADX > 25), preventing entries during choppy sideways markets. This was the single biggest improvement — cutting drawdown by 4 percentage points.

Generation 89 (final): Sharpe 1.31, max drawdown -14%, win rate 51%

The final strategy bore little resemblance to the seed. It had evolved RSI thresholds from 30/70 to 33/68, added two filters the original didn’t have, and switched from a fixed stop loss to a trailing stop with an ATR multiplier.

The Anti-Overfitting Problem

Genetic algorithms are overfitting machines if you’re not careful. Here’s what I built to fight it:

Walk-forward validation. The evaluator doesn’t just backtest on the full dataset. It uses walk-forward splits — train on 2 years, test on the next 6 months, slide forward. The fitness score is the out-of-sample performance.

Monte Carlo stress testing. Each candidate strategy also gets run through 100 Monte Carlo shuffles to check if the equity curve is robust or just lucky.

Pareto frontier. Instead of optimizing a single metric, the frontier tracks multiple objectives (return, risk, consistency). A strategy that sacrifices some return for much lower drawdown stays in the population.

# Run with walk-forward validation (built-in)
finclaw evolve strategy.yaml --symbol AAPL --generations 30 --start 2020-01-01

Architecture

The engine is built from four pluggable components:

  • Evaluator — Runs backtests and computes FitnessScore (composite of return, Sharpe, drawdown, win rate)
  • Proposer — Analyzes failures and generates mutation candidates
  • Mutator — Applies YAML-level mutations to strategy definitions
  • Frontier — Manages the Pareto-optimal strategy set

Each component is replaceable. You can plug in your own evaluator that uses a different backtesting engine, or write a custom proposer that targets specific strategy weaknesses.

What I Learned

Volume filters matter more than entry signals. Across dozens of evolved strategies, the single most impactful mutation was always some form of volume confirmation. The market is noisy; volume tells you when the signal is real.

Stop losses evolve toward ATR-based trailing stops. Fixed percentage stops consistently get replaced by volatility-adjusted ones. Makes sense — a 5% stop is too tight for a volatile stock and too loose for a calm one.

Fewer rules beat more rules. The engine repeatedly pruned overly complex strategies. The best performers had 3-5 entry conditions, not 10. Occam’s razor, enforced by selection pressure.

Try It

finclaw is open source. The evolution engine runs on any OHLCV data — US stocks, A-shares, crypto.

pip install finclaw
finclaw evolve your_strategy.yaml --symbol AAPL --generations 20

The full codebase, including 240+ technical factors, walk-forward backtesting, and paper trading:

finclaw on GitHub | Python · 240+ factors · Evolution engine · Paper trading

If you’re interested in AI agent security (the other thing I work on), check out ClawGuard — an open-source AI agent security engine with 285+ threat patterns.

The evolution engine code lives in src/evolution/. PRs welcome.

Non-Standard ERC-20 Behavior: The Phantom Deposit Bug Class That Auditors Keep Missing

When we say “ERC-20 token,” we assume a contract that behaves according to the EIP-20 specification. But assume is the operative word. In February 2026, a non-standard transferFrom implementation in the DGLD gold-backed token let attackers mint 100 million unbacked tokens on Base — against a legitimate circulating supply of just 70.8 tokens on that chain.

The vulnerability wasn’t in the bridge. It wasn’t in the L2. It was in a transferFrom function that returned true without actually moving tokens. And it had passed two independent audits.

This article isn’t about DGLD specifically — it’s about the ERC-20 compliance gap that exists across thousands of deployed tokens, and the concrete audit techniques that catch it.

The ERC-20 Trust Contract

Every protocol that integrates external tokens makes implicit assumptions:

transferFrom(from, to, amount) returns true
  → "amount" tokens moved from "from" to "to"
  → balanceOf(from) decreased by amount
  → balanceOf(to) increased by amount

The EIP-20 spec says transferFrom SHOULD throw on failure. It says nothing about what happens when a contract returns true but doesn’t enforce the transfer. This ambiguity has created at least three distinct exploit patterns since 2022.

Pattern 1: Silent Success (DGLD, Feb 2026)

The DGLD Ethereum contract inherited legacy code where transferFrom could execute successfully — returning true to the caller — without enforcing that the token balance actually changed. The Ethereum↔Base bridge (built on OP Stack) interpreted that true as confirmation that tokens were locked on L1, and minted the corresponding representation on L2.

The attack flow:

Attacker → DGLD.transferFrom(attacker, bridge, 100M)
         → Returns true (no revert)
         → But attacker's balance: unchanged
         → Bridge sees: "100M DGLD deposited"
         → Base mints: 100M unbacked DGLD
         → Attacker dumps on Aerodrome DEX → drains USDC pools

The bridge did nothing wrong by its own logic. It called a standard ERC-20 function, got a success response, and acted accordingly. The fault was in the semantic gap between what transferFrom promised and what it delivered.

Pattern 2: Fee-on-Transfer Deflation

Tokens like USDT and STA apply transfer fees, meaning balanceOf(to) increases by less than amount. Protocols that don’t check post-transfer balances end up with phantom accounting:

// VULNERABLE: Assumes full amount arrived
function deposit(address token, uint256 amount) external {
    IERC20(token).transferFrom(msg.sender, address(this), amount);
    balances[msg.sender] += amount; // May be more than actually received
}

Pattern 3: Return Value Amnesia

Some tokens (notably older USDT on Ethereum) don’t return a boolean at all. Calling them with IERC20(token).transferFrom(...) reverts at the ABI decoding level if the contract expects a bool return. This is why OpenZeppelin’s SafeERC20 exists — but many protocols still don’t use it.

The Audit Gap: Why This Keeps Slipping Through

The DGLD vulnerability passed two independent audits and an external review in Q4 2025. How?

1. Inherited Code Blind Spots

The vulnerable transferFrom logic came from a Consensys implementation deployed in February 2022. Four years of production use created a false sense of security. Auditors focused on newer code (the Base deployment logic) and treated the ERC-20 base as “battle-tested.”

Lesson: Legacy code isn’t safe code. Every audit should include the token’s core transfer functions, regardless of how long they’ve been deployed.

2. Specification vs. Implementation Drift

The EIP-20 spec is deliberately minimal. It says:

  • transferFrom SHOULD throw if the sender doesn’t have enough tokens
  • It MUST fire a Transfer event

It does NOT say:

  • transferFrom MUST revert on insufficient balance (SHOULD ≠ MUST)
  • The return value MUST accurately reflect whether tokens moved
  • The function MUST be atomic (balance changes happen or they don’t)

Any contract that exploits these gaps is “ERC-20 compliant” by the letter of the spec while being functionally broken.

3. Integration Context Matters

A transferFrom that returns true without moving tokens is harmless in isolation. It only becomes exploitable when another contract trusts that return value to gate a consequential action (minting, unlocking, accounting). Auditors reviewing the token contract alone might flag it as “non-standard but not exploitable.” Auditors reviewing the bridge alone see a standard ERC-20 integration. Neither catches the composition.

The 7-Point ERC-20 Compliance Audit Checklist

After analyzing every ERC-20 integration exploit from 2022-2026, here’s the minimum verification set:

1. Balance Delta Verification

Never trust the return value. Check actual balance changes:

function _safeTransferIn(
    address token,
    address from,
    uint256 amount
) internal returns (uint256 received) {
    uint256 balBefore = IERC20(token).balanceOf(address(this));
    IERC20(token).safeTransferFrom(from, address(this), amount);
    received = IERC20(token).balanceOf(address(this)) - balBefore;
    require(received > 0, "Zero transfer");
}

This single pattern prevents phantom deposits, fee-on-transfer accounting errors, and rebasing token surprises.

2. Return Value Handling

Use SafeERC20 for all external token calls. It handles:

  • Tokens that return bool (standard)
  • Tokens that return nothing (USDT-style)
  • Tokens that revert on failure
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

using SafeERC20 for IERC20;

// Handles all three return patterns
token.safeTransferFrom(from, to, amount);

3. Allowance Behavior Testing

Some tokens (USDT again) require allowance to be reset to 0 before setting a new value. Test for:

function testAllowanceReset() public {
    token.approve(spender, 100);
    // Some tokens revert here if allowance != 0
    token.approve(spender, 200);
}

Use safeIncreaseAllowance / forceApprove to handle this.

4. Reentrancy via Transfer Hooks

ERC-777 tokens and some ERC-20 tokens with hooks (like stETH’s rebase) can trigger reentrancy through tokensReceived callbacks:

// VULNERABLE to ERC-777 reentrancy
function deposit(address token, uint256 amount) external {
    IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
    // Attacker's tokensReceived() hook fires HERE
    // State not yet updated
    balances[msg.sender] += amount;
}

// SAFE: CEI pattern
function deposit(address token, uint256 amount) external nonReentrant {
    balances[msg.sender] += amount; // State update first
    IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
    // Hook fires after state is consistent
}

5. Decimals Assumption Testing

Not all tokens use 18 decimals. USDC uses 6. Some use 2 or 8. Test arithmetic with varying decimal counts:

function testFuzz_decimalVariance(uint8 decimals) public {
    vm.assume(decimals <= 24);
    MockToken token = new MockToken(decimals);
    uint256 amount = 1000 * 10**decimals;

    // Verify your math doesn't overflow/underflow
    protocol.deposit(address(token), amount);
    assertEq(protocol.getBalance(address(token)), amount);
}

6. Max Supply / totalSupply Manipulation

Rebasing tokens (stETH, aTokens) change balances without transfers. Elastic supply tokens can inflate totalSupply. Test that your protocol doesn’t depend on:

  • totalSupply being constant
  • balanceOf not changing between transactions
  • Transfer amounts equaling balance deltas

7. Blocklist / Pausability Awareness

USDC, USDT, and many tokens can freeze individual addresses or pause all transfers. Your protocol should handle:

  • transferFrom reverting unexpectedly
  • Funds becoming temporarily locked
  • Withdrawal functions that don’t brick when one token is paused

Automated Detection: Foundry Integration Tests

Here’s a reusable Foundry test suite for ERC-20 compliance verification:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

abstract contract ERC20ComplianceTest is Test {
    IERC20 public token;
    address public alice = makeAddr("alice");
    address public bob = makeAddr("bob");

    // Override in concrete test to set the token
    function _setupToken() internal virtual;

    function setUp() public {
        _setupToken();
    }

    function test_transferFromActuallyMovesTokens() public {
        uint256 amount = 1000;
        deal(address(token), alice, amount);

        vm.prank(alice);
        token.approve(bob, amount);

        uint256 aliceBefore = token.balanceOf(alice);
        uint256 bobBefore = token.balanceOf(bob);

        vm.prank(bob);
        bool success = token.transferFrom(alice, bob, amount);

        if (success) {
            assertEq(
                token.balanceOf(alice), 
                aliceBefore - amount,
                "transferFrom returned true but sender balance unchanged"
            );
            assertGe(
                token.balanceOf(bob),
                bobBefore,
                "transferFrom returned true but receiver lost tokens"
            );
        }
    }

    function test_transferFromExceedingBalanceReverts() public {
        deal(address(token), alice, 100);

        vm.prank(alice);
        token.approve(bob, type(uint256).max);

        uint256 aliceBal = token.balanceOf(alice);

        vm.prank(bob);
        try token.transferFrom(alice, bob, aliceBal + 1) returns (bool success) {
            assertFalse(
                success,
                "CRITICAL: transferFrom succeeded with insufficient balance"
            );
            assertEq(token.balanceOf(alice), aliceBal, "Balance changed on failed transfer");
        } catch {
            // Reverting is the expected safe behavior
        }
    }

    function test_feeOnTransferDetection() public {
        uint256 amount = 10000;
        deal(address(token), alice, amount);

        vm.prank(alice);
        token.approve(bob, amount);

        uint256 bobBefore = token.balanceOf(bob);

        vm.prank(bob);
        token.transferFrom(alice, bob, amount);

        uint256 received = token.balanceOf(bob) - bobBefore;

        if (received < amount) {
            emit log_named_uint("FEE-ON-TRANSFER DETECTED. Fee", amount - received);
        }
    }
}

Running Against Live Tokens

# Fork mainnet and test against real USDT
forge test --match-contract USDTComplianceTest 
  --fork-url $ETH_RPC_URL 
  --fork-block-number 19500000 -vvv

Bridge-Specific Defenses

The DGLD exploit specifically targeted the L1→L2 bridge trust boundary. For bridge developers:

Defense 1: Balance Delta at the Bridge Level

function depositERC20(
    address token,
    uint256 amount,
    uint32 destChainId
) external {
    uint256 balBefore = IERC20(token).balanceOf(address(this));
    IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
    uint256 actualDeposited = IERC20(token).balanceOf(address(this)) - balBefore;

    // Mint on L2 based on ACTUAL deposit, not claimed amount
    _sendCrossChainMessage(destChainId, msg.sender, token, actualDeposited);
}

Defense 2: Token Allowlisting with Compliance Verification

struct TokenConfig {
    bool allowed;
    bool feeOnTransfer;
    bool rebasable;
    uint256 maxSingleDeposit;
    uint256 dailyDepositCap;
}

mapping(address => TokenConfig) public tokenConfigs;

modifier onlyCompliantToken(address token) {
    require(tokenConfigs[token].allowed, "Token not allowlisted");
    _;
}

Defense 3: Cross-Chain Balance Reconciliation

Run continuous reconciliation between L1 locked balances and L2 minted supply:

# Simplified reconciliation check
def reconcile(token_l1, token_l2, bridge_l1):
    locked_l1 = token_l1.balanceOf(bridge_l1)
    minted_l2 = token_l2.totalSupply()

    delta = abs(locked_l1 - minted_l2)
    threshold = locked_l1 * 0.001  # 0.1% tolerance for timing

    if delta > threshold:
        alert(f"RECONCILIATION FAILURE: L1={locked_l1}, L2={minted_l2}")
        pause_bridge()

DGLD’s monitoring detected the anomaly within ~2.5 hours. With real-time reconciliation, this window could have been minutes.

Key Takeaways

  1. Never trust ERC-20 return values alone. Always verify with balance deltas. The transferFrom → true → tokens moved assumption has failed repeatedly.

  2. Audit inherited code with the same rigor as new code. DGLD’s vulnerability was 4 years old, passed 2 audits, and lived in “battle-tested” Consensys code. Legacy ≠ safe.

  3. Composition creates vulnerabilities that isolation doesn’t reveal. A non-standard transferFrom is harmless alone but catastrophic when a bridge trusts its return value. Audit at the integration boundary, not just the component level.

  4. Every token integration is a trust boundary. When your contract calls transferFrom on an external token, you’re trusting that token’s implementation. Verify, don’t trust.

  5. Balance delta checks are the single highest-ROI defensive pattern in DeFi. They defend against phantom deposits, fee-on-transfer errors, rebasing surprises, and non-standard implementations — all in one pattern.

This research is part of the DeFi Security Research series by DreamWork Security. Follow for weekly deep dives into smart contract vulnerabilities, audit techniques, and defensive patterns.