name: "zero-state-return" description: "Trigger Pattern Vault/pool/first-depositor pattern detected - Inject Into Depth-edge-case"
ZERO_STATE_RETURN Skill
Trigger Pattern: Vault/pool/first-depositor pattern detected Inject Into: Depth-edge-case Purpose: Analyze zero-state transitions in Aptos Move protocols -- initial zero state, return to zero after operations, residual assets, and re-entry vulnerabilities
Overview
This skill covers BOTH initial zero state AND return-to-zero-state analysis:
- Protocol initialization and first deposit conditions
- Protocol returning to zero after normal operations
- Residual assets when supply returns to zero
- Re-entry vulnerabilities after full exit
1. Identify Zero-State Transitions
Find all vault/pool/staking mechanisms and their zero-state boundaries:
| State | Resource / Variable | Zero Condition | Trigger | Code Location |
|---|---|---|---|---|
| Total shares | {resource.total_supply} | == 0 | All users withdrew/burned | {module:line} |
| Total assets | {resource.total_assets} | == 0 | No funds deposited | {module:line} |
| Pool liquidity | {resource.reserves} | Both reserves == 0 | All LP withdrawn | {module:line} |
| Staking pool | {resource.total_staked} | == 0 | All unstaked | {module:line} |
For each state: what is the protocol behavior when this condition is true?
2. First Depositor Analysis
Can the first depositor manipulate share price?
2a. Share Minting Formula at Zero State
| Protocol | Formula | When totalShares == 0 | First Deposit Behavior |
|---|---|---|---|
| {name} | shares = amount * totalShares / totalAssets | {special case?} | {describe} |
Classic first depositor attack on Aptos:
- First depositor deposits minimal amount (e.g., 1 unit)
- Attacker directly deposits tokens to the protocol's FungibleStore (unsolicited -- bypasses accounting)
- Exchange rate inflates:
totalAssetsincreases buttotalSharesstays at 1 - Next depositor receives 0 shares due to rounding (their deposit amount < inflated share price)
- First depositor withdraws, capturing the second depositor's funds
Checks:
- Is there a minimum first deposit requirement?
- Does the protocol use virtual shares/assets (e.g., add 1 to both numerator and denominator)?
- Is there a dead shares mechanism (burn initial shares to zero address)?
- Can unsolicited deposits to the protocol's store inflate
totalAssets? - Does the protocol use internal accounting (resistant) or direct balance queries (vulnerable)?
2b. First Deposit Protection Mechanisms
| Protection | Present? | Implementation | Bypass Possible? |
|---|---|---|---|
| Minimum first deposit | YES/NO | {code ref} | {analysis} |
| Virtual shares/assets offset | YES/NO | {code ref} | {analysis} |
| Dead shares (initial mint to zero) | YES/NO | {code ref} | {analysis} |
| Internal accounting (not balance-based) | YES/NO | {code ref} | {analysis} |
| Decimal offset in share calculation | YES/NO | {code ref} | {analysis} |
3. Return to Zero Analysis
After normal operations, can the protocol return to zero state?
3a. Return-to-Zero Scenarios
| Scenario | Trigger | Residual State After | Re-entry Safe? |
|---|---|---|---|
| All shares redeemed | Last user withdraws | {what remains?} | YES/NO |
| Emergency withdraw | Admin drains | {what remains?} | YES/NO |
| All stakers unstake | Last unstake | {what remains?} | YES/NO |
| Pool fully drained | All LP removed | {what remains?} | YES/NO |
3b. Can Total Shares Reach Exactly Zero?
Trace the withdrawal/burn path:
- Can the last user withdraw ALL their shares? (no minimum balance lock?)
- Does the protocol enforce a minimum share amount that prevents reaching zero?
- If dead shares exist,
totalSharesnever reaches 0 -- is this protection consistent?
4. Residual Asset Check
When supply returns to zero, check for stranded value:
4a. Accrued Rewards
| Reward Source | Persists When totalShares = 0? | Claimable By Next Depositor? | Amount Bounded? |
|---|---|---|---|
| {reward_source} | YES/NO | YES/NO | {max amount or UNBOUNDED} |
If rewards persist AND next depositor can claim -> FINDING (severity based on amount).
4b. Unclaimed Fees
| Fee Type | Persists When totalShares = 0? | Captured By Next Depositor? | Reconciliation Mechanism? |
|---|---|---|---|
| {fee_type} | YES/NO | YES/NO | {mechanism or NONE} |
4c. Dust Balances
- Can dust (sub-unit amounts) remain in FungibleStore after all withdrawals?
- Does dust affect exchange rate calculations on re-entry? (e.g.,
totalAssets = 1 wei, totalShares = 0) - Does the protocol handle
totalAssets > 0 AND totalShares == 0explicitly?
4d. Pending Operations
- Are there pending withdrawals/claims that persist after zero state?
- What happens to in-flight multi-step operations when supply hits zero?
- Are there resources or objects that reference the pool/vault state that become orphaned?
5. Re-Entry Vulnerability Analysis
Does re-entering zero state recreate first-depositor attack conditions?
| Scenario | Initial State | Return-to-Zero State | Same Vulnerability? |
|---|---|---|---|
| First depositor attack | totalSupply=0, totalAssets=0 | totalSupply=0, totalAssets=X (residual) | WORSE if residual > 0 |
| Exchange rate manipulation | No shares exist | No shares, but balance exists | YES + amplified |
| Donation attack | Clean state | Dirty state | YES + pre-seeded |
Key question: Is the first-deposit protection (from Section 2b) applied ONLY on initial deployment, or does it also trigger when totalShares returns to 0?
Trace the share minting code:
// Pattern: Protection covers initial AND return-to-zero
if (total_shares == 0) {
// First deposit logic with protection
}
// vs Pattern: Protection only on first-ever deposit
if (!initialized) {
// Protection here
} else if (total_shares == 0) {
// NO protection -- vulnerable on return-to-zero
}
5b. Default/Uninitialized State Values
For each state field used in arithmetic or control flow, check its initial value before any user interaction:
- Default zero: Move initializes struct fields to their declared defaults (typically 0 for integers,
@0x0for addresses). If a function useslast_timestamp,start_time, orlast_updatein subtraction or division BEFORE it has ever been set, the result may be unexpected (e.g.,timestamp::now_seconds() - 0= enormous elapsed time, or division by a value derived from 0). - First-call path: Trace the FIRST invocation of each state-modifying function. Does it assume a prior call already initialized dependent fields?
- Check: For each field read in a function, is there a code path where that field still holds its default value (0, @0x0, false)? If yes, does the function behave correctly with that default?
6. Empty Pool Edge Cases
6a. Division by Zero
| Expression | When totalShares = 0 | Behavior | Impact |
|---|---|---|---|
amount * totalShares / totalAssets | 0 / totalAssets | Returns 0 | {impact} |
amount * totalAssets / totalShares | amount * X / 0 | ABORT | {DoS, broken withdrawal} |
rewards / totalShares | rewards / 0 | ABORT | {reward distribution broken} |
For each division: is there a zero-check guard? If not, what transaction aborts?
6b. Zero-Amount Operations at Zero State
| Operation | At Zero State | Result | Expected? |
|---|---|---|---|
| deposit(0) at totalShares=0 | {behavior} | {shares issued?} | {analysis} |
| withdraw(0) at totalShares=0 | {behavior} | {aborts?} | {analysis} |
| claim_rewards() at totalShares=0 | {behavior} | {rewards distributed?} | {analysis} |
7. Protocol Reset Functions
Check for admin functions that can force zero state:
| Function | Access Control | Clears All State? | Residual After Reset |
|---|---|---|---|
| {emergency_withdraw_fn} | {who} | YES/NO | {what remains} |
| {rescue_tokens_fn} | {who} | YES/NO | {what remains} |
| {pause + drain_fn} | {who} | YES/NO | {what remains} |
| {migrate_fn} | {who} | YES/NO | {what remains in old module} |
For each: what state persists after the "reset"? Can it be exploited?
Instantiation Parameters
{CONTRACTS} -- Move modules containing vault/pool logic
{SHARE_VARIABLES} -- Variables tracking total shares/supply
{ASSET_VARIABLES} -- Variables tracking total assets/deposits
{SHARE_MINT_FORMULA} -- Share calculation formula at deposit
{FIRST_DEPOSIT_GUARDS} -- Existing first-deposit protections
Finding Template
**ID**: [ZS-N]
**Severity**: [typically HIGH if funds extractable, MEDIUM if DoS]
**Step Execution**: checkmark1,2,3,4,5,6,7 | x(reasons) | ?(uncertain)
**Rules Applied**: [R4:Y, R10:Y, R11:Y]
**Location**: module::function:LineN
**Title**: [Zero-state type] allows [attack] due to [residual state / missing protection]
**Description**:
- Protocol can reach totalShares=0 via [mechanism]
- When this happens, [state variable] retains value of [amount]
- A new depositor can [exploit path]
**Impact**: [Fund extraction / exchange rate manipulation / DoS]
Output Schema
| Field | Required | Description |
|---|---|---|
| zero_state_transitions | yes | All paths to zero state |
| first_depositor_analysis | yes | First deposit attack assessment |
| residual_assets | yes | What persists after zero state |
| re_entry_vulnerability | yes | Whether return-to-zero recreates first-depositor conditions |
| edge_cases | yes | Division by zero and zero-amount operations |
| finding | yes | CONFIRMED / REFUTED / CONTESTED |
| evidence | yes | Code locations with line numbers |
| step_execution | yes | Status for each step |
Step Execution Checklist (MANDATORY)
| Section | Required | Completed? | Notes |
|---|---|---|---|
| 1. Identify Zero-State Transitions | YES | Y/x/? | |
| 2. First Depositor Analysis | YES | Y/x/? | Including 2a formula + 2b protections |
| 3. Return to Zero Analysis | YES | Y/x/? | Including 3a scenarios + 3b exact zero trace |
| 4. Residual Asset Check | YES | Y/x/? | All sub-checks: 4a rewards, 4b fees, 4c dust, 4d pending |
| 5. Re-Entry Vulnerability Analysis | YES | Y/x/? | Compare initial vs return-to-zero protections |
| 6. Empty Pool Edge Cases | YES | Y/x/? | Division by zero + zero-amount ops |
| 7. Protocol Reset Functions | IF admin reset exists | Y/x(N/A)/? |
Cross-Reference Markers
After Section 2 (First Depositor): Cross-reference with TOKEN_FLOW_TRACING.md Section 5 for unsolicited deposit vectors that amplify first-depositor attacks.
After Section 4 (Residual Assets): If residual rewards/fees found, cross-reference with ECONOMIC_DESIGN_AUDIT.md for whether fee/reward accumulation is bounded.
After Section 5 (Re-Entry): If return-to-zero is possible AND first-deposit protection is initial-only -> FINDING (minimum Medium, upgrade to High if unsolicited deposits can amplify).