Sample report (synthetic)

Sample, fictional contract. A fictional report showing how our findings are written. Nothing on this page is from a real engagement.

Sample, fictional contract. This report is invented to show our format. The contract, commit, finding and code below are fictional. It is not from a real engagement and describes no real client, protocol or vulnerability.

1. Engagement summary

  • ClientExample Labs (fictional)
  • Repositoryexample-labs/example-vault (fictional)
  • Commit0000000000000000000000000000000000000000 (placeholder)
  • In scopesrc/ExampleVault.sol (fictional)
  • MethodManual review, invariant testing with Foundry, triaged static analysis
  • Findings1 shown here, for format only

2. Scope and limits

The review covers only the files listed above at the listed commit. Code outside scope, later changes, deployment, key management and off-chain components are not covered. A review reduces risk; it does not certify that code is free of vulnerabilities.

3. Threat model (abridged)

Assets: deposited assets and vault shares. Actors: depositors, a privileged owner, any external caller. Assumption: the owner key is not compromised.

4. Findings

NM-SAMPLE-01 · Share calculation rounds in the caller’s favour on withdraw

  • SeverityMedium (illustrative)
  • ImpactMedium: small, repeatable loss to remaining depositors
  • LikelihoodMedium: anyone can trigger it; profitable only where gas is cheap
  • LocationExampleVault.sol, withdraw() (fictional)
  • StatusFixed in 1111111 (placeholder)

Description

In withdraw(assets), the vault computes the shares to burn with rounding down (it should round up, per ERC-4626 previewWithdraw). The caller burns slightly fewer shares than the assets they receive are worth, so each call moves a small amount of value from other depositors to the caller.

Severity reasoning

Impact: each call leaks at most one unit of rounding, so total loss is bounded by call count and gas cost, which keeps impact at Medium. Likelihood: anyone can trigger it with no preconditions, but it pays only where gas is cheap relative to the unit, so Medium.

Proof of concept (Foundry, fictional)

function test_withdrawRoundsInCallerFavour() public {
    vault.deposit(1_000e18, bob);
    vault.deposit(1_000e18, alice);
    asset.transfer(address(vault), 1e18); // simulated yield: share price above 1, so withdraw(1) burns 0 shares
    uint256 before = vault.convertToAssets(vault.balanceOf(bob));
    for (uint256 i; i < 100; ++i) vault.withdraw(1, alice, alice);
    assertLt(vault.convertToAssets(vault.balanceOf(bob)), before);
}

Recommendation

Round up when converting assets to shares on withdraw (round against the caller), and add an invariant that total assets cover total shares.

Fix review

Verified at the fix commit: rounding now favours the vault and the new invariant holds under stateful fuzzing.

Real reports state the actual repository, commit hash and files in scope, and are delivered only to the client. Publication is the client’s decision.