[H-1] Unrestricted mint Function Allows Any Address to Create Unlimited Tokens
[M-1] _mint Function Does Not Check for Integer Overflow on totalSupply
[I-1] Token-0x Base Lacks Meta-Transaction Support (Context)
Token-0x is a secure and gas-efficient base ERC20 implementation that follows the ERC20 standard. The protocol achieves secure and cheap operations by using a combination of Solidity and Yul (inline assembly) in the base implementation.
Key Features:
Custom ERC20 base implementation with Yul optimizations in ERC20Internals.sol
Drop-in replacement for OpenZeppelin's ERC20 implementation
Designed for use as base tokens in DeFi protocols, reward systems, and protocol-native tokens
Contracts Under Review:
| Contract | LOC | Purpose |
|---|---|---|
Token.sol |
17 | Example token using Token-0x's custom ERC20 |
Token2.sol |
17 | Example token using OpenZeppelin's ERC20 |
ERC20.sol |
47 | Token-0x's custom ERC20 base contract |
ERC20Internals.sol |
175 | Yul-optimized internal functions |
IERC20Errors.sol |
45 | Custom error interface |
IERC20.sol |
21 | Standard ERC20 interface |
Key Architectural Difference:
The protocol provides two example implementations:
Token.sol - Imports from ../src/ERC20.sol (Token-0x's custom Yul-optimized implementation)
Token2.sol - Imports from @openzeppelin/contracts/token/ERC20/ERC20.sol (Battle-tested OpenZeppelin)
Both example contracts share identical critical vulnerabilities in their public interfaces, but the underlying base implementations differ significantly in security posture, gas efficiency, and battle-testing.
The NFTeria team makes all effort to find as many vulnerabilities in the code in the given time period, but holds no responsibilities for the findings provided in this document. A security audit by the team is not an endorsement of the underlying business or product. The audit was time-boxed and the review of the code was solely on the security aspects of the Solidity implementation of the contracts.
| Impact | ||||
|---|---|---|---|---|
| High | Medium | Low | ||
| High | H | H/M | M | |
| Likelihood | Medium | H/M | M | M/L |
| Low | M | M/L | L |
We use the CodeHawks severity matrix to determine severity. See the documentation for more details.
This audit employed a rigorous coverage-driven security methodology:
Initial Coverage Assessment - Ran forge coverage --report lcov to identify untested code paths
LCOV Analysis - Parsed coverage data to identify missing lines and branches
Targeted Test Development - Created tests specifically targeting uncovered branches
Test Harness Creation - Developed TokenHarness contract to expose internal functions for unreachable branches
Vulnerability Discovery - Coverage testing revealed execution path flaws
Final Verification - Achieved and verified 100% coverage across all metrics
Final Coverage Results (60 tests passed, 0 failed):
| File | Lines | Statements | Branches | Functions |
|---|---|---|---|---|
src/ERC20.sol |
100.00% (24/24) | 100.00% (17/17) | 100.00% (0/0) | 100.00% (10/10) |
src/helpers/ERC20Internals.sol |
100.00% (127/127) | 100.00% (119/119) | 100.00% (10/10) | 100.00% (8/8) |
test/Token.sol |
100.00% (4/4) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) |
test/Token2.sol |
100.00% (4/4) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) |
| Total | 100.00% (167/167) | 100.00% (144/144) | 100.00% (10/10) | 100.00% (26/26) |
Branch Coverage Breakdown (ERC20Internals.sol):
| Branch ID | Function | Condition | Test Method |
|---|---|---|---|
| BRDA:24 | _balanceOf |
iszero(owner) |
Direct call with address(0) |
| BRDA:42 | _approve |
iszero(owner) |
Test harness |
| BRDA:47 | _approve |
iszero(spender) |
Direct approve(address(0)) |
| BRDA:74 | _allowance |
iszero(owner) |
Direct call with address(0) |
| BRDA:94 | _transfer |
iszero(from) |
Test harness |
| BRDA:100 | _transfer |
iszero(to) |
transfer(address(0)) |
| BRDA:118 | _transfer |
lt(fromBalance, value) |
Insufficient balance test |
| BRDA:136 | _mint |
iszero(account) |
mint(address(0)) |
| BRDA:160 | _burn |
iszero(account) |
burn(address(0)) |
| BRDA:194 | _spendAllowance |
lt(currentAllowance, value) |
Exceed allowance test |
During the process of achieving 100% branch coverage, we discovered a critical execution path flaw that reveals a dead code vulnerability in the _transfer function.
Discovery Context:
When attempting to test the iszero(from) branch in _transfer via transferFrom(address(0), to, value), our test expected:
Actual Result:
Root Cause Analysis:
The transferFrom function executes _spendAllowance BEFORE _transfer:
When from = address(0):
_spendAllowance checks allowance[address(0)][msg.sender]
This allowance is always 0 (can never be set - no private key for address(0))
0 < value → reverts with ERC20InsufficientAllowance
_transfer is NEVER called
Implications:
This means the iszero(from) check in _transfer (lines 94-97) is unreachable dead code via the public interface. This has several security implications documented in finding [H-5].
mint Function Allows Any Address to Create Unlimited TokensDescription:
Both Token.sol and Token2.sol expose the mint function as public without any access control. This allows any external address to mint an arbitrary amount of tokens to any address, completely destroying tokenomics.
Impact:
Any attacker can mint unlimited tokens
Complete destruction of token value
Protocol becomes worthless immediately upon deployment
All DeFi integrations are compromised
Proof of Concept:
Recommended Mitigation:
burn Function Allows Any Address to Destroy Any User's TokensDescription:
The burn function accepts an arbitrary account parameter and burns tokens from that address without any authorization check. Any attacker can burn any other user's entire token balance.
This is a fundamental violation of the ERC20 security model. Compare with OpenZeppelin's ERC20Burnable:
Impact:
Any user can burn any other user's complete token balance
Complete theft of value through destruction
No recourse for victims
Griefing attacks are trivially easy
Proof of Concept:
Recommended Mitigation:
_mint Function in ERC20Internals.sol Does Not Emit Transfer EventDescription:
The _mint function in ERC20Internals.sol is missing the required Transfer event emission. According to EIP-20, all token transfers including mints (from address(0)) MUST emit a Transfer event.
Compare with _transfer which correctly emits the event:
Impact:
Non-compliant with EIP-20 standard
Block explorers (Etherscan) won't track minted tokens
Wallet applications won't detect new token balances
DeFi protocols relying on events will fail to track mints
Subgraphs and indexers will have incorrect data
Recommended Mitigation:
Add event emission to _mint:
_burn Function in ERC20Internals.sol Does Not Emit Transfer EventDescription:
Similar to _mint, the _burn function is missing the required Transfer event emission. Burns should emit a Transfer event to address(0).
Impact:
Non-compliant with EIP-20 standard
Burns are invisible to off-chain systems
Token supply tracking breaks
Recommended Mitigation:
Add event emission to _burn:
_transfer Creates False Sense of SecurityDiscovered During: 100% Branch Coverage Testing
Description:
The _transfer function contains a zero-address sender check (lines 94-97) that is unreachable via the public interface. This was discovered when our test for transferFrom(address(0), to, value) reverted with ERC20InsufficientAllowance instead of the expected ERC20InvalidSender.
Execution Path Analysis:
Why This Matters:
False Security Assumption - Developers may believe _transfer validates the from address, but this validation never executes for transferFrom
Inconsistent Error Messages - Users see ERC20InsufficientAllowance when the real issue is an invalid sender address
Defense-in-Depth Failure - The layered security check provides no actual protection
Inherited Contract Risk - Contracts inheriting ERC20 and calling _transfer directly could bypass allowance checks entirely
Proof of Concept:
Also Affects: The iszero(owner) check in _approve (lines 42-45) is similarly unreachable since approve() uses msg.sender which can never be address(0).
Recommended Mitigation:
Option 1 - Reorder checks in transferFrom:
Option 2 - Document as intentional defense-in-depth:
Option 3 - Remove dead code to reduce bytecode size and avoid confusion.
_mint Function Does Not Check for Integer Overflow on totalSupplyDescription:
The _mint function in ERC20Internals.sol uses raw Yul assembly which bypasses Solidity 0.8+'s built-in overflow protection. The addition to totalSupply does not check for overflow.
In Solidity 0.8+, this would be safe:
But in Yul, add(supply, value) silently wraps on overflow.
Impact:
If totalSupply + value > type(uint256).max, the value wraps to a small number
Could result in accounting inconsistencies
Breaks invariant: totalSupply == sum(all balances)
Recommended Mitigation:
Add overflow checks in Yul:
_burn Function Does Not Check for Integer UnderflowDescription:
The _burn function performs subtraction without checking for underflow. The totalSupply subtraction happens before balance validation.
Impact:
If burning more than totalSupply, the value wraps to a massive number
State corruption before potential revert
Recommended Mitigation:
Add underflow checks - validate balance FIRST:
_spendAllowance Does Not Handle type(uint256).max Infinite Approval PatternDescription:
The ERC20 standard convention allows setting allowance to type(uint256).max to indicate infinite approval (allowance that doesn't decrease on spending). The _spendAllowance function in ERC20Internals.sol does not implement this pattern.
Compare with OpenZeppelin's implementation:
Impact:
Gas inefficiency for infinite approvals (unnecessary storage writes)
Deviation from expected ERC20 behavior
May break DeFi protocol integrations expecting infinite approval pattern
Recommended Mitigation:
Discovered During: 100% Branch Coverage Testing
Description:
The error thrown when calling transferFrom(address(0), to, value) is semantically incorrect. The user receives ERC20InsufficientAllowance when the actual problem is an invalid sender address.
Expected Behavior:
Actual Behavior:
Impact:
Confusing error messages for developers and users
Debugging difficulty when integrating with the token
Potential for incorrect error handling in calling contracts
Recommended Mitigation:
Add explicit address validation at the start of transferFrom:
ERC20.sol Does Not Explicitly Implement IERC20 InterfaceDescription:
The ERC20.sol contract implements all ERC20 functions but does not explicitly inherit from IERC20. This means there's no compile-time guarantee of interface compliance.
Recommended Mitigation: Explicitly inherit from IERC20.
Description:
Both Token.sol and Token2.sol hardcode "Token" and "TKN" as name and symbol, limiting reusability.
Recommended Mitigation:
Description:
Neither token implementation enforces a maximum supply cap, allowing unlimited minting (after access control is fixed).
Recommended Mitigation:
_balanceOf and _allowanceDescription:
The _balanceOf and _allowance functions in ERC20Internals.sol revert with empty error data when given zero addresses:
Recommended Mitigation: Use custom error selectors consistently for better debugging.
Context)Token-0x's ERC20 uses msg.sender directly instead of _msgSender(), preventing ERC2771 meta-transaction compatibility.
All contracts lack comprehensive NatSpec documentation (@notice, @dev, @param, @return) which is important for code readability and automated documentation generation.
ERC20Permit allows approvals via signatures, enabling gasless approvals and better UX through meta-transactions.
Neither implementation includes emergency pause functionality which could be useful for emergency situations.
For upgradeable patterns and Yul code verification, the storage layout should be documented.
Discovered During: 100% Branch Coverage Testing
The unreachable branches identified in [H-5] represent dead code that:
Increases bytecode size unnecessarily
May confuse auditors and developers
Provides false security assurance
If kept as defense-in-depth, these branches should be clearly documented with NatSpec explaining their purpose and why they are unreachable via the public interface.
unchecked Block for Arithmetic Where Overflow Is ImpossibleAfter verifying sufficient balance, the subtraction cannot underflow. Using unchecked saves gas.
Gas Savings: ~20 gas per transferFrom
Consider caching storage reads when checking and updating allowances.
_transferEnsure no redundant storage reads exist in the transfer logic.
| File | Lines | Statements | Branches | Functions |
|---|---|---|---|---|
src/ERC20.sol |
100.00% (24/24) | 100.00% (17/17) | 100.00% (0/0) | 100.00% (10/10) |
src/helpers/ERC20Internals.sol |
93.70% (119/127) | 93.28% (111/119) | 60.00% (6/10) | 100.00% (8/8) |
Test Results: 24 tests passed, 0 failed
| File | Lines | Statements | Branches | Functions |
|---|---|---|---|---|
src/ERC20.sol |
100.00% (24/24) | 100.00% (17/17) | 100.00% (0/0) | 100.00% (10/10) |
src/helpers/ERC20Internals.sol |
100.00% (127/127) | 100.00% (119/119) | 100.00% (10/10) | 100.00% (8/8) |
| Total | 100.00% (167/167) | 100.00% (144/144) | 100.00% (10/10) | 100.00% (26/26) |
Test Results: 60 tests passed, 0 failed
| Operation | Token-0x | OpenZeppelin | Savings |
|---|---|---|---|
mint |
57,871 gas | 60,448 gas | 4.3% |
burn |
45,980 gas | 50,051 gas | 8.1% |
transfer |
92,254 gas | 92,529 gas | 0.3% |
transferFrom |
102,119 gas | 105,715 gas | 3.4% |
allowance |
90,942 gas | 93,603 gas | 2.8% |
| Contract | Issue | Severity | Affects Both? |
|---|---|---|---|
| Token.sol / Token2.sol | Unrestricted mint | HIGH | ✅ Yes |
| Token.sol / Token2.sol | Unrestricted burn (any address) | HIGH | ✅ Yes |
| ERC20Internals.sol | Missing mint Transfer event | HIGH | Token.sol only |
| ERC20Internals.sol | Missing burn Transfer event | HIGH | Token.sol only |
| ERC20Internals.sol | Unreachable dead code in _transfer | HIGH | Token.sol only |
| ERC20Internals.sol | Mint overflow risk | MEDIUM | Token.sol only |
| ERC20Internals.sol | Burn underflow risk | MEDIUM | Token.sol only |
| ERC20Internals.sol | No infinite approval pattern | MEDIUM | Token.sol only |
| ERC20.sol | Inconsistent error semantics | MEDIUM | Token.sol only |
| File | Status | Critical Issues |
|---|---|---|
| Token.sol | ✅ Reviewed | H-1, H-2 |
| Token2.sol | ✅ Reviewed | H-1, H-2 |
| ERC20.sol | ✅ Reviewed | L-1, M-4 |
| ERC20Internals.sol | ✅ Reviewed | H-3, H-4, H-5, M-1, M-2, M-3 |
| IERC20Errors.sol | ✅ Reviewed | None |
| IERC20.sol | ✅ Reviewed | None |
To achieve 100% branch coverage, a test harness was required to expose internal functions:
Tests Added for Previously Unreachable Branches:
Audit Completed By</p>
Rensley R.</p>
Solid Vyper-Pilled Security Researcher</p>
Organization</p>
NFTeria Inc</p>
December 10, 2025</p>
✓ Complete End-to-End Audit with 100% Test Coverage</p>
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.