TLDR
This proposal schedules a coordinated upgrade of the Terra Classic mainnet from
v4.0.2 to v4.1.1.
The main reason for the upgrade is a fix for stuck (“ghost”) transactions:
under certain conditions a transaction could be accepted by a node, never be
included in a block, and never be dropped again. The account that signed it was
then unable to send any further transaction. This happened on mainnet to a
validator, who could not unjail for an extended period
(#648).
v4.1.1 removes the cause of that problem and adds a safeguard that prevents
this class of problem in general. It also contains two small fixes that harden
the tax module against panics, and a fix that makes accounts with a vesting
schedule (LazyGradedVestingAccount) readable and usable again.
The upgrade contains no state migrations, no new modules and no parameter
changes. One of the fixes changes which block proposals a validator accepts,
and another changes whether transactions from vesting accounts succeed, so all
validators have to switch at the same height. That is why a governance upgrade
is needed instead of a rolling update.
Mempool parity: what happened, in plain words
No technical background needed for this section. The technical details follow
in the next one.
How a transaction normally gets into a block. When you send a transaction,
it does not go into a block right away. It first waits in a queue on the
validators’ computers until a validator picks it up and puts it into the next
block. This queue is called the mempool.
Each validator actually keeps two lists of waiting transactions:
- one list for passing transactions on to the other validators, and
- one list for building the next block.
Both lists are supposed to always contain the same transactions.
What went wrong. In a rare case the two lists stopped matching. A
transaction was taken off the “build the next block” list, but stayed on the
“pass it on” list. From then on:
- no validator ever put it into a block, because it was missing from the list
blocks are built from, and - it never disappeared either, because validators kept passing it around to
each other.
The transaction was stuck: never confirmed, never rejected.
Why that blocks a whole account. Every account’s transactions are numbered
and must be processed strictly in order: number 5 can only go through after
number 4. If number 4 is stuck forever, everything the account sends afterwards
waits behind it. The account is effectively frozen, although nothing is wrong
with the account itself and no funds are lost.
What this meant in practice. This happened to a validator on mainnet. Their
validator had been jailed, and the transaction to unjail it was queued behind a
stuck transaction. They were unable to unjail for an extended period, and
neither the validator nor their delegators could do anything about it with
normal means. It took a manual rescue with the help of another validator
running a patched node to resolve it.
Why the transaction was dropped from one list in the first place. Every
transaction comes with a budget for the computing work it may use (“gas”).
Because of a bug, the chain charged a small extra amount of work at a moment
when it should not have. A transaction with a budget that was just enough at
the entrance check was then found to be just short one step later, and was
dropped from one list but not the other.
What the upgrade changes.
- The extra charge at the wrong moment is removed, so the entrance check and
the block-building step now agree with each other. This removes the cause. - Validators now compare the two lists after every block. A transaction that
is gone from one list is removed from the other one too. So even if a
similar mismatch ever happens for a different reason, the transaction is
simply dropped and the sender can send it again, instead of the account
being frozen.
What it means for you. Nothing changes in how you use the chain: no changes
to fees, taxes, balances or staking. Funds were not at risk from this bug, and
they are not affected by the upgrade. The chain will pause briefly at the
upgrade height while validators switch to the new version.
What is fixed (technical details)
1. Mempool parity — stuck transactions (#689)
The problem. A node keeps pending transactions in two places: the CometBFT
mempool (used for gossip between nodes) and the application mempool (used to
build blocks). The two could drift apart:
- The dynamic commission (
dyncomm) post handler also ran while a block
proposal was being prepared and verified, although no messages are
executed in those phases. It consumed about 7,000 gas there. - A
MsgEditValidatorwhose gas limit was enough for the admission check but
not for those extra 7,000 gas was accepted into both mempools, then failed
when the proposer tried to put it into a block. It was removed from the
application mempool, but CometBFT kept it and kept gossiping it. - The result was a transaction that could never be included and never expired.
It blocked the account’s sequence number, so every later transaction of that
account was stuck behind it. - As a side effect, a proposer holding such a transaction could build a
proposal that the other validators rejected, costing the network a consensus
round.
The fix has two parts:
- Root cause: the
dyncommpost handler now runs only where messages are
actually executed (block execution, and simulation so that--gas autostays
accurate). It no longer charges gas while proposals are prepared or verified.
Gas usage and results of executed transactions are unchanged. - Safeguard: a new mempool parity check keeps both mempools in line. On every
re-check after a block, a transaction that is no longer in the application
mempool is evicted from the CometBFT mempool as well, and a transaction that
fails admission is removed from the application mempool. A dropped
transaction is therefore reported as dropped and can simply be re-sent.
2. Tax: panic when the reverse-charge flag is absent (#667)
IsReverseCharge assumed that a flag was always present in the transaction
context and panicked when it was not. A missing flag is now treated as “no
reverse charge”.
3. Tax: division by zero in the community-tax split (#670)
With a community tax of 1.0 and an oracle split rate of 0 — both values can
be set by governance — the tax split divided by zero, which would have made
every taxable transaction panic. The divisor is now guarded. For all other
parameter values, including the current mainnet parameters, the calculation and
its rounding are unchanged.
4. Vesting: accounts with a vesting schedule could not be read (#692)
The problem. With Cosmos SDK v0.53 the account store decodes every
account as sdk.AccountI. Terra’s LazyGradedVestingAccount was registered
only for the deprecated x/auth interface of the same name, which is a
different interface to the decoder. Reading such an account therefore failed:
collections: encoding error: value decode: no concrete type registered for type URL /terra.vesting.v1beta1.LazyGradedVestingAccount against interface *types.AccountI
- Queries for these accounts (account, account info, spendable balances)
return this error on mainnet today. Plain balance queries still work. - A transaction signed by such an account fails, because the signer cannot be
loaded. The same applies to moving funds out of such an account on its
behalf. - The affected accounts are the vesting accounts from genesis. Their funds are
not lost; the accounts are just not usable.
The fix. LazyGradedVestingAccount and BaseVestingAccount are now
registered for sdk.AccountI. Nothing stored on chain changes and no migration
is needed: the accounts become readable as soon as the new binary runs.
Because transactions from these accounts fail on v4.0.2 and succeed on
v4.1.1, this fix also has to be activated by all validators at the same
height.
5. Documentation (#550)
Updated REST API demo endpoint in the README.
Impact
| Who | Impact |
|---|---|
| Users / dApps | No action needed. No change to fees, taxes, balances or contract behaviour. Transactions can no longer get permanently stuck in the way described above. Holders of vesting accounts (LazyGradedVestingAccount) can query their account and send transactions again. |
| Validators / node operators | Must switch to v4.1.1 at the upgrade height. See below. |
| Exchanges / infrastructure | Expect a short halt at the upgrade height. Nodes must be upgraded to continue syncing. No API or state format changes. |
- State migrations: none (the handler only runs the standard module
migrations, and no module version changes). - Store upgrades: none.
- Dependencies: unchanged from
v4.0.2(Cosmos SDKv0.53.6, CometBFT
v0.38.21, wasmvmv3.0.8, Go1.24).
Testing
In addition to unit tests for all four fixes, the upgrade was rehearsed on an
in-place testnet created from real mainnet state (height 30679563), using a
binary built from the v4.1.0 commit (v4.1.1 is v4.1.0 plus the vesting
fix #692 only):
- Before the upgrade (
v4.0.2): the stuck-transaction scenario was reproduced
— the transaction stayed in the CometBFT mempool, was never proposed, and the
account was blocked. - The
v14_4upgrade proposal passed and the upgrade was applied at the
scheduled height without errors. - After the upgrade (
v4.1.0): the same scenario no longer produces a stuck
transaction. The transaction is included (and consumes its sequence), dropped
transactions are evicted from the CometBFT mempool, and follow-up
transactions of the account go through. - Smoke tests after the upgrade passed: bank, staking (delegate, redelegate,
unbond), and CosmWasm (store, instantiate, execute, and queries/executions
against contracts deployed before the upgrade, including existing mainnet
contracts).
The root-cause fix was also verified earlier against the original mainnet
transactions from issue #648 on a copy of mainnet state.
The vesting fix (#692) was added after the rehearsal. The decoding error was
reproduced against mainnet for vesting accounts from genesis, and with the
application’s own codec: decoding a LazyGradedVestingAccount fails on
v4.1.0 and succeeds on v4.1.1.
Instructions for validators and node operators
Keep running v4.0.2 until the upgrade height. Do not switch to v4.1.1
early: it accepts block proposals slightly differently from v4.0.2.
At height 31010600 the node halts with
UPGRADE "v14_4" NEEDED at height: 31010600.
Build the new binary:
git clone https://github.com/classic-terra/core.git && cd core
git fetch --tags
git checkout v4.1.1
make install
terrad version # 4.1.1
With Cosmovisor — place the binary before the upgrade height:
mkdir -p $DAEMON_HOME/cosmovisor/upgrades/v14_4/bin
cp $(which terrad) $DAEMON_HOME/cosmovisor/upgrades/v14_4/bin/terrad
Without Cosmovisor — after the node has halted at the upgrade height, stop
the service, replace the terrad binary with v4.1.1, and start it again.
The chain resumes as soon as validators holding more than 2/3 of the voting
power are running the new binary.
Timeline
| Step | Date |
|---|---|
| Proposal submitted | 2026-10-12 12:29 UTC |
| Voting period ends (7 days after submission) | 2026-10-19 12:29 UTC |
Upgrade height 31010600* |
≈ 2026-10-26, 15:00 UTC |
*The upgrade height is an estimate and subject to a correction when the proposal is put up for on-chain vote. Varying blocktimes and network load can shift the estimated block height significantly.
Voting
- Yes — you agree to upgrade the mainnet to
v4.1.1at the height above. - No — you do not agree to the upgrade.
- No with veto — you consider the proposal harmful or spam.
- Abstain — you take part in the quorum without voting for or against.