Severity: medium
Version concerned: CMTAT v3.2.0
Summary
setFrozenTokens(account, amount) can intentionally set frozen > balance (per ERC-7943 semantics), but some internal arithmetic paths historically assumed frozen <= balance.
This mismatch can lead to unexpected reverts/underflows in active-balance dependent flows if not explicitly handled.
Context
ERC-7943 allows absolute frozen amounts to exceed current balance (future balance withholding model).
So this behavior is valid from a spec standpoint, but implementation logic must be robust to it everywhere.
Impact
Without hardening, the following can break when frozen > balance:
- active balance reads
- transfer checks based on active balance
- forced transfer/unfreeze paths using arithmetic derived from active balance
Result: unexpected reverts and inconsistent behavior in enforcement flows.
Root cause
Internal code paths performed direct subtraction like:
active = balance - frozen
This is unsafe when frozen >= balance.
Reproduction (pre-fix behavior)
- Mint tokens to
account.
- Call
setFrozenTokens(account, balance + 1).
- Trigger active-balance dependent path (
transfer, getActiveBalanceOf, forced transfer internals).
- Observe underflow/revert or inconsistent behavior.
Remediation
Adopt permissive ERC-7943 model and harden arithmetic logic:
- Keep
setFrozenTokens permissive (frozen may exceed balance).
- In active-balance calculations, treat
frozen >= balance as active balance 0 (saturating logic).
- Ensure forced-transfer unfreeze path computes
activeBalance safely under the same condition.
- Add regression tests for
frozen > balance scenarios.
Implemented changes
Contract hardening
Updated:
contracts/modules/internal/ERC20EnforcementModuleInternal.sol
Changes:
_checkActiveBalance(...):
- if
frozen >= balance, returns (value == 0 ? true : false, 0).
_getActiveBalanceOf(...):
- returns
0 when frozen >= balance.
_unfreezeTokens(...):
- safe active-balance computation when
frozen > balance.
Added inline rationale comments:
Frozen amounts can be > balance through setFrozenTokens.
Test coverage
Updated:
test/common/ERC20EnforcementModuleCommon.js
Added tests:
testCanSetFrozenTokensGreaterThanBalance
testCanForcedTransferWhenFrozenTokensGreaterThanBalance
These verify:
- permissive
setFrozenTokens(balance + 1) is accepted,
- active balance is safely treated as
0,
- normal transfer fails with
ERC7943InsufficientUnfrozenBalance(..., unfrozen=0),
- forced transfer remains operational.
Notes
- This is a consistency hardening issue, not an ERC-7943 non-compliance issue.
- Optional alternative policy (not chosen here): enforce
setFrozenTokens <= balance and document as stricter-than-ERC-7943 profile.
Summary
setFrozenTokens(account, amount)can intentionally setfrozen > balance(per ERC-7943 semantics), but some internal arithmetic paths historically assumedfrozen <= balance.This mismatch can lead to unexpected reverts/underflows in active-balance dependent flows if not explicitly handled.
Context
ERC-7943 allows absolute frozen amounts to exceed current balance (future balance withholding model).
So this behavior is valid from a spec standpoint, but implementation logic must be robust to it everywhere.
Impact
Without hardening, the following can break when
frozen > balance:Result: unexpected reverts and inconsistent behavior in enforcement flows.
Root cause
Internal code paths performed direct subtraction like:
active = balance - frozenThis is unsafe when
frozen >= balance.Reproduction (pre-fix behavior)
account.setFrozenTokens(account, balance + 1).transfer,getActiveBalanceOf, forced transfer internals).Remediation
Adopt permissive ERC-7943 model and harden arithmetic logic:
setFrozenTokenspermissive (frozenmay exceedbalance).frozen >= balanceas active balance0(saturating logic).activeBalancesafely under the same condition.frozen > balancescenarios.Implemented changes
Contract hardening
Updated:
contracts/modules/internal/ERC20EnforcementModuleInternal.solChanges:
_checkActiveBalance(...):frozen >= balance, returns(value == 0 ? true : false, 0)._getActiveBalanceOf(...):0whenfrozen >= balance._unfreezeTokens(...):frozen > balance.Added inline rationale comments:
Frozen amounts can be > balance through setFrozenTokens.Test coverage
Updated:
test/common/ERC20EnforcementModuleCommon.jsAdded tests:
testCanSetFrozenTokensGreaterThanBalancetestCanForcedTransferWhenFrozenTokensGreaterThanBalanceThese verify:
setFrozenTokens(balance + 1)is accepted,0,ERC7943InsufficientUnfrozenBalance(..., unfrozen=0),Notes
setFrozenTokens <= balanceand document as stricter-than-ERC-7943 profile.