Since init_contract mints the ED and calls deactivate(ed) only when !account_exists (exec.rs) while destroy_contract unconditionally calls burn_from(ed) plus reactivate(ed) on termination (exec.rs, bodies at deposit_payment.rs), and the deactivate/reactivate pair only balances when both halves run, an attacker can transfer ED to a future CREATE2 address X (leaving AccountInfoOf[X] unset so instantiation is not blocked), deploy at X so that account_exists(X) is already true and init_contract's deactivate is skipped, then terminate so the unconditional reactivate(ed) fires with no matching deactivate, moving ED from the inactive pool into active issuance on every repeat and drifting active_issuance (which feeds inflation, staking rewards, and OpenGov conviction) upward, permissionlessly and repeatably.
Fix: gate the destroy side reactivate/burn on whether the pallet actually minted the ED for this account, so the two sides stay symmetric.
Since
init_contractmints the ED and callsdeactivate(ed)only when!account_exists(exec.rs) whiledestroy_contractunconditionally callsburn_from(ed)plusreactivate(ed)on termination (exec.rs, bodies at deposit_payment.rs), and thedeactivate/reactivatepair only balances when both halves run, an attacker can transfer ED to a futureCREATE2addressX(leavingAccountInfoOf[X]unset so instantiation is not blocked), deploy atXso thataccount_exists(X)is already true andinit_contract'sdeactivateis skipped, then terminate so the unconditionalreactivate(ed)fires with no matchingdeactivate, moving ED from the inactive pool into active issuance on every repeat and driftingactive_issuance(which feeds inflation, staking rewards, and OpenGov conviction) upward, permissionlessly and repeatably.Fix: gate the destroy side
reactivate/burnon whether the pallet actually minted the ED for this account, so the two sides stay symmetric.