Skip to content

fix(statedb): snapshot locked balance on statedb account #1187

Merged
mattac21 merged 5 commits into
mainfrom
ma/locked-snapshot
May 20, 2026
Merged

fix(statedb): snapshot locked balance on statedb account #1187
mattac21 merged 5 commits into
mainfrom
ma/locked-snapshot

Conversation

@mattac21

@mattac21 mattac21 commented May 18, 2026

Copy link
Copy Markdown
Contributor

Description

Snapshots accounts locked balance when EVM accounts are fetched from the keeper. This is to ensure that the accounts total bank balance is properly reconstructed when its locked balance has been modified via a precompile.

Closes: STACK-2786


Author Checklist

All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.

I have...

  • tackled an existing issue or discussed with a team member
  • left instructions on how to review the changes
  • targeted the main branch

mattac21 added 2 commits May 18, 2026 16:15
…dable + locked bank balance after locked balance changes via precompile
@mattac21 mattac21 requested a review from a team as a code owner May 18, 2026 20:21
@greptile-apps

greptile-apps Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR snapshots the EVM-denom locked balance (vesting LockedCoins) onto statedb.Account at load time so that SetAccount at commit can reconstruct the correct full bank balance even when a precompile has mutated DelegatedVesting mid-execution. Without the snapshot, SetBalance re-read LockedCoins after the precompile ran, yielding a stale (reduced) locked value and an under-inflated bank balance.

  • x/vm/statedb/state_object.go — adds unexported lockedBalance *big.Int to Account, a NewAccount constructor, and a defensive LockedBalanceSnapshot() accessor that returns a copy.
  • x/vm/keeper/statedb.go — introduces SetBalanceWithLocked (accepts a pre-computed locked value), SetAccountBalance (dispatches on whether a snapshot is present), and routes SetBalance through the new function; GetAccount now populates the snapshot via lockedCoin; DeleteAccount passes explicit big.NewInt(0) after converting to BaseAccount.
  • x/erc20/keeper/dynamic_precompiles.goRegisterERC20CodeHash reuses the existing Account (including its snapshot) rather than building a new struct literal, preserving balance fidelity.

Confidence Score: 5/5

Safe to merge. The snapshot approach correctly reconstructs the bank balance when a staking precompile modifies DelegatedVesting mid-execution, and the fallback re-read path is preserved for accounts without a snapshot.

The logic is sound: load-time locked balance is snapshotted and carried through the full EVM execution lifecycle, eliminating the stale re-read that caused incorrect bank balance reconstruction after precompile-driven delegation from vesting tokens. Fallback to SetBalance (re-read path) for accounts created without a snapshot is correct and unchanged. DeleteAccount explicitly passes zero locked after the BaseAccount conversion, which produces the same result as the previous re-read. Integration and unit tests cover the primary vesting delegation scenario.

No files require special attention.

Important Files Changed

Filename Overview
x/vm/statedb/state_object.go Adds unexported lockedBalance field to Account, NewAccount constructor, and LockedBalanceSnapshot accessor returning a defensive copy. Design is correct — unexported field is preserved through value-copy across packages.
x/vm/keeper/statedb.go Refactors SetBalance into SetBalanceWithLocked (uses caller-supplied locked value) and SetBalance (re-reads locked then delegates). SetAccountBalance dispatches on snapshot presence. GetAccount now captures locked snapshot at load. DeleteAccount correctly zeroes locked after BaseAccount conversion.
x/vm/keeper/keeper.go Adds lockedCoin helper that reads LockedCoins for the EVM denom. Logic is symmetric with SpendableCoin. No issues.
x/erc20/keeper/dynamic_precompiles.go RegisterERC20CodeHash now reuses the full Account (with locked snapshot) instead of constructing a bare struct literal. Behavioral change is safe — snapshot is read immediately before SetAccount is called, so no precompile mutation can intervene.
tests/integration/x/vm/test_statedb.go Adds TestGetAccountLocked and TestSetBalanceWithLocked covering snapshot population and locked override vs re-read for vesting and non-vesting accounts.
tests/integration/precompiles/staking/test_integration.go Adds end-to-end integration test verifying bank balance and total supply are preserved when a vesting account delegates within its spendable funds via the staking precompile.
tests/integration/precompiles/staking/test_staking.go Gas constants bumped from 21559 to 25000 to account for the extra LockedCoins read added to GetAccount. Expected consequence of this fix.

Sequence Diagram

sequenceDiagram
    participant EVM
    participant StateDB
    participant Keeper
    participant BankWrapper
    participant VestingAccount

    Note over EVM,VestingAccount: Account Load (before EVM execution)
    EVM->>StateDB: getStateObject(addr)
    StateDB->>Keeper: GetAccount(addr)
    Keeper->>BankWrapper: SpendableCoin(addr)
    BankWrapper-->>Keeper: "spendable = 2e18"
    Keeper->>BankWrapper: LockedCoins(addr)
    BankWrapper->>VestingAccount: LockedCoins()
    VestingAccount-->>BankWrapper: "locked = 2e18 (pre-precompile)"
    BankWrapper-->>Keeper: "locked = 2e18"
    Keeper-->>StateDB: "Account{Balance:2e18, lockedBalance:2e18}"

    Note over EVM,VestingAccount: Staking precompile delegate (during EVM execution)
    EVM->>BankWrapper: DelegateCoins(1e18 from vesting)
    BankWrapper->>VestingAccount: "DelegatedVesting += 1e18"
    Note right of VestingAccount: LockedCoins drops to 1e18
    BankWrapper-->>EVM: "bank balance = 3e18"
    EVM->>StateDB: SubBalance(1e18)
    StateDB-->>EVM: "in-memory balance = 1e18 - gas"

    Note over EVM,VestingAccount: Commit (after EVM execution)
    StateDB->>Keeper: "SetAccount(addr, Account{Balance:1e18-gas, lockedBalance:2e18})"
    Keeper->>Keeper: "SetAccountBalance → SetBalanceWithLocked(1e18-gas, snapshot=2e18)"
    Note right of Keeper: newBankBalance = (1e18-gas) + 2e18 = 3e18-gas
    Keeper->>BankWrapper: SetBalance(3e18-gas)
Loading

Reviews (2): Last reviewed commit: "update comments" | Re-trigger Greptile

Comment thread x/vm/keeper/keeper.go Outdated
Comment thread x/vm/keeper/statedb.go Outdated
Comment thread x/vm/keeper/statedb.go
Comment thread x/vm/statedb/state_object.go
Comment thread x/vm/statedb/state_object.go
Comment thread x/vm/keeper/statedb.go
Comment thread x/vm/keeper/statedb.go
Comment thread x/vm/keeper/statedb.go
Comment thread x/vm/statedb/state_object.go
Comment thread x/erc20/keeper/dynamic_precompiles.go
@mattac21

Copy link
Copy Markdown
Contributor Author

@greptile re review

@codecov

codecov Bot commented May 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.72727% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 66.99%. Comparing base (7d11196) to head (889dd45).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
x/vm/keeper/statedb.go 95.45% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1187      +/-   ##
==========================================
+ Coverage   66.32%   66.99%   +0.66%     
==========================================
  Files         336      320      -16     
  Lines       23959    23433     -526     
==========================================
- Hits        15891    15698     -193     
+ Misses       6891     6578     -313     
+ Partials     1177     1157      -20     
Files with missing lines Coverage Δ
x/erc20/keeper/dynamic_precompiles.go 94.11% <100.00%> (-1.01%) ⬇️
x/vm/keeper/keeper.go 89.89% <100.00%> (+0.37%) ⬆️
x/vm/statedb/state_object.go 95.03% <100.00%> (+0.72%) ⬆️
x/vm/keeper/statedb.go 92.52% <95.45%> (+0.52%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@linear

linear Bot commented May 18, 2026

Copy link
Copy Markdown

STACK-2786

Comment thread x/vm/keeper/statedb.go Outdated
Comment thread tests/integration/precompiles/staking/test_staking.go
Co-authored-by: Vlad J <vladjdk@gmail.com>
@mattac21 mattac21 enabled auto-merge May 20, 2026 17:28
@mattac21 mattac21 added this pull request to the merge queue May 20, 2026
Merged via the queue into main with commit 008c171 May 20, 2026
17 checks passed
@mattac21 mattac21 deleted the ma/locked-snapshot branch May 20, 2026 17:40
@mattac21

Copy link
Copy Markdown
Contributor Author

@mergify backport to release/v0.7.x

@mattac21

Copy link
Copy Markdown
Contributor Author

@mergify backport to release/v0.6.x

@mergify

mergify Bot commented May 20, 2026

Copy link
Copy Markdown

backport to release/v0.7.x

❌ No backport have been created

Details
  • Backport to branch to failed

GitHub error: Branch not found

Cherry-pick of 008c171 has failed:

On branch mergify/bp/release/v0.7.x/pr-1187
Your branch is up to date with 'origin/release/v0.7.x'.

You are currently cherry-picking commit 008c171.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:
	modified:   tests/integration/precompiles/staking/test_integration.go
	modified:   tests/integration/precompiles/staking/test_staking.go
	modified:   tests/integration/x/vm/test_statedb.go
	modified:   x/erc20/keeper/dynamic_precompiles.go
	modified:   x/vm/keeper/keeper.go
	modified:   x/vm/keeper/statedb.go

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   x/vm/statedb/state_object.go

To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally

@mergify

mergify Bot commented May 20, 2026

Copy link
Copy Markdown

backport to release/v0.6.x

❌ No backport have been created

Details
  • Backport to branch to failed

GitHub error: Branch not found

Cherry-pick of 008c171 has failed:

On branch mergify/bp/release/v0.6.x/pr-1187
Your branch is up to date with 'origin/release/v0.6.x'.

You are currently cherry-picking commit 008c171.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:
	modified:   tests/integration/precompiles/staking/test_integration.go
	modified:   tests/integration/x/vm/test_statedb.go
	modified:   x/erc20/keeper/dynamic_precompiles.go
	modified:   x/vm/keeper/keeper.go

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   tests/integration/precompiles/staking/test_staking.go
	both modified:   x/vm/keeper/statedb.go
	both modified:   x/vm/statedb/state_object.go

To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally

mattac21 added a commit that referenced this pull request May 20, 2026
) (#1189)

* fix(statedb): snapshot locked balance on statedb account  (#1187)

* snapshot locked balance on statedb account creation to calculate spendable + locked bank balance after locked balance changes via precompile

* lint formatting

* modify x/erc20 registering code hash to not silently drop existing accounts locked balacne

* update comments

* Update x/vm/keeper/statedb.go

Co-authored-by: Vlad J <vladjdk@gmail.com>

---------

Co-authored-by: Vlad J <vladjdk@gmail.com>
(cherry picked from commit 008c171)

# Conflicts:
#	x/vm/statedb/state_object.go

* conflicts

---------

Co-authored-by: mattac21 <matt@cosmoslabs.io>
vladjdk added a commit that referenced this pull request May 21, 2026
) (#1190)

* fix(statedb): snapshot locked balance on statedb account  (#1187)

* snapshot locked balance on statedb account creation to calculate spendable + locked bank balance after locked balance changes via precompile

* lint formatting

* modify x/erc20 registering code hash to not silently drop existing accounts locked balacne

* update comments

* Update x/vm/keeper/statedb.go

Co-authored-by: Vlad J <vladjdk@gmail.com>

---------

Co-authored-by: Vlad J <vladjdk@gmail.com>
(cherry picked from commit 008c171)

# Conflicts:
#	tests/integration/precompiles/staking/test_staking.go
#	x/vm/keeper/statedb.go
#	x/vm/statedb/state_object.go

* fix conflicts

* bump sol

---------

Co-authored-by: mattac21 <matt@cosmoslabs.io>
Co-authored-by: Vlad <vladjdk@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants