Security research
How to spot hidden mint functions
A token can create unauthorized balances without exposing a function named mint. Follow every path that writes balances or changes the implementation, then test the accounting rules those paths must preserve.
Start with the interface and the balance writes
The ERC-20 standard specifies methods such as totalSupply, balanceOf and transfer. It does not require a public function named mint. A familiar ABI therefore cannot establish that supply is fixed. Search the complete implementation, inherited contracts and imported libraries for writes to the balance mapping and supply counter.
Compare static analysis tools for authority checks before selecting a source-analysis workflow.
In OpenZeppelin's supply example, _mint is an internal function. A contract can expose an external function that calls it. The relevant question is who can reach that call and under what conditions. An internal visibility label alone says nothing about whether an external caller can trigger the operation.
For this review, distinguish an intended issuance mechanism from an unexpected balance increase. A documented, restricted mint may be part of the token design. A balance write that violates that design is the finding that needs a reproducible test.
A one-character error in the earlier example
The earlier version of this article displayed uint120(0) in a modified subtraction function but explained the result of ~uint120(0). These expressions produce different values. Casting zero gives zero. Applying bitwise complement at a width of 120 bits gives 2^120 − 1.
We reproduced both branches locally, alongside ordinary checked subtraction. Each branch runs when amount == 1, before the balance check. The following results use an initial balance of 10 and an amount of 1:
| Operation | Returned balance |
|---|---|
| Checked subtraction | 9 |
return uint120(0) | 0 |
return ~uint120(0) | 1329227995784915872903807060280344575 |
Assigning the zero result would erase the sender's remaining balance in this example. Assigning the complement result would create a very large balance. Neither conclusion requires a function called mint. These are deliberately broken arithmetic cases, not evidence of a deployed token's behavior.
The deterministic matrix covers 16 pairs: every combination of balances and amounts in [0, 1, 2, 10]. The zero branch differs from checked subtraction in 3 pairs; the complement branch differs in 4. Differences include an unexpected success where ordinary subtraction rejects an insufficient balance. The zero branch happens to match the correct result for balance 1 and amount 1.
Check accounting independently of the supply counter
A separate test keeps a recorded supply of 10, assigns the complement result to the sender and assigns 1 to the recipient. The sum of those two modeled balances exceeds recorded supply even though the supply variable stays at 10.
Reading totalSupply() alone would miss that inconsistency.
For a conventional token transfer without fees or rebasing, a useful test checks that the sender's decrease equals the recipient's increase and that their combined balance is conserved. Write the property against the documented design: fee recipients, burns and rebasing require different accounting. Track every affected account in the test harness so that a missing recipient does not create a false finding.
Also inspect emitted events, but use state changes to validate the result. The presence of a familiar Transfer event does not prove that the implementation obeyed its intended accounting rules.
Follow permissions and implementation changes
- Identify the exact code. Record the chain, contract address, implementation address if proxied, source revision and compiler settings. A similarly named repository is insufficient.
- Trace balance writes. Follow entry points through internal calls, inherited overrides and custom arithmetic. Compare the actual imported library code with the claimed dependency version.
- Resolve privileged callers. Inspect who can mint, grant minting roles or change their administrators. OpenZeppelin's access-control reference distinguishes role membership from the power to grant and revoke roles.
- Inspect upgrade authority. An implementation review covers the code examined. A permitted upgrade can replace it; record the controller and conditions governing that change. See the OpenZeppelin upgrade guide.
- Reproduce the suspected violation. Preserve the initial state, caller, transaction sequence and expected accounting property. Check whether the behavior is reachable by the actor described in the finding.
Use Slither to help inspect Solidity code, Echidna to exercise accounting properties and Foundry for a regression case. Tool output still needs a reviewer to establish reachability and impact. The DSA review methodology explains the evidence required for a technical assessment.
Reproduce the correction
Download the instructions, Foundry configuration and Solidity test, retaining its test/ directory. The recorded run used Forge 1.5.1, solc 0.8.36 and the Paris EVM target. All three test functions passed; the result summary records the input set and counts.
forge test --root . --use 0.8.36 -vv
This finite test does not implement a complete ERC-20 token, establish vulnerability prevalence or demonstrate that an inflated balance can be sold. It isolates the arithmetic correction and a balance-accounting failure for readers reviewing similar code.
Sources
Checked . Local examples include their inputs and limits.
About this revision
Corrected the zero-versus-bitwise-complement example, clarified ERC-20 mint visibility and added local regression results. The original contribution was attributed to SolidProof; this revision was prepared by DeFiSec editorial and has not been represented as a new SolidProof review.
Suggest a correction with evidence