Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5d7e30f
feat: normalized txId with 0x prefix
0xNilesh Mar 4, 2026
bf7c268
feat: normalized utxId and outboundId in msgVoteOutbound
0xNilesh Mar 4, 2026
4785209
fix: fixed VoteOutbound nil errors
0xNilesh Mar 4, 2026
e6ebd54
feat: normalized utxId with 0x prefix
0xNilesh Mar 4, 2026
d14f515
tests: added tests for 0x prefix normalization in outbound
0xNilesh Mar 4, 2026
c1b418c
feat: added supply-burn upgrade handler
0xNilesh Mar 4, 2026
0a653dd
test: added integration test for supply-burn upgrade
0xNilesh Mar 4, 2026
80957d0
feat: added burn_targets.json for supply-burn upgrade
0xNilesh Mar 5, 2026
5c1e20f
feat: added vault methods in uregistry chain config proto
0xNilesh Mar 5, 2026
0bfdae0
refactor: added generated protobuf
0xNilesh Mar 5, 2026
ef6a2ff
feat: added validations for new vault methods in chain config
0xNilesh Mar 5, 2026
6baaf5a
feat: added validations for vault methods
0xNilesh Mar 5, 2026
15244fb
tests: added tests for vault methods
0xNilesh Mar 5, 2026
fe9e368
tests: modified chain config tests for vault methods
0xNilesh Mar 5, 2026
ebb4e02
feat: added chain config migration method
0xNilesh Mar 5, 2026
8c2f58f
feat: bumped up the consensus version of uregistry module
0xNilesh Mar 5, 2026
71a9895
tests: added unit tests for chain config migration
0xNilesh Mar 5, 2026
63000e4
refactor: added generated protobuf
0xNilesh Mar 5, 2026
e4767ab
feat: removed vault address from the chain config proto
0xNilesh Mar 5, 2026
fc5ecb1
refactor: added generated protobuf
0xNilesh Mar 5, 2026
f03ceaf
refactor: added generated protobuf
0xNilesh Mar 5, 2026
f5a45c8
feat: updated chain config and migration with removed vault address c…
0xNilesh Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,143 changes: 989 additions & 154 deletions api/uregistry/v1/types.pulsar.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions app/upgrades.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
pcmintcap "github.com/pushchain/push-chain-node/app/upgrades/pc-mint-cap"
proxybytecodefix "github.com/pushchain/push-chain-node/app/upgrades/proxy-bytecode-fix"
supplyslash "github.com/pushchain/push-chain-node/app/upgrades/supply-slash"
supplyburn "github.com/pushchain/push-chain-node/app/upgrades/supply-burn"
removefeeabsv1 "github.com/pushchain/push-chain-node/app/upgrades/remove-fee-abs-v1"
solanafix "github.com/pushchain/push-chain-node/app/upgrades/solana-fix"
tsscore "github.com/pushchain/push-chain-node/app/upgrades/tss-core"
Expand All @@ -41,6 +42,7 @@ var Upgrades = []upgrades.Upgrade{
universaltxv1.NewUpgrade(),
proxybytecodefix.NewUpgrade(),
supplyslash.NewUpgrade(),
supplyburn.NewUpgrade(),
}

// RegisterUpgradeHandlers registers the chain upgrade handlers
Expand Down
27 changes: 27 additions & 0 deletions app/upgrades/supply-burn/burn_targets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[
{
"address": "push1p9wxp8uczwdmt0d5f4nzayqezhha5lrxv0heqg",
"keep_pc": 0,
"note": "Hegemon — burn all spendable"
},
{
"address": "push1fv2fm76q7cjnr58wdwyntzrjgtc7qya6ctk0zm",
"keep_pc": 0,
"note": "Dev Donut operator — burn all spendable"
},
{
"address": "push12jzrpp4pkucxxvj6hw4dfxsnhcpy6ddt0ljtrn",
"keep_pc": 0,
"note": "Trader's Espresso operator — burn all spendable"
},
{
"address": "push1vzuw2x3k2ccme70zcgswv8d88kyc07grx5h6v7",
"keep_pc": 0,
"note": "Validator's Valhalla operator — burn all spendable (incl. 296B PC liquid)"
},
{
"address": "push1wvy6j98pkuj4pua96ctm0y59w6nv64x2dwuzqx",
"keep_pc": 0,
"note": "User's Utopia operator — burn all spendable"
}
]
138 changes: 138 additions & 0 deletions app/upgrades/supply-burn/upgrades.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package supplyburn

import (
"context"
_ "embed"
"encoding/json"

sdkmath "cosmossdk.io/math"
storetypes "cosmossdk.io/store/types"
upgradetypes "cosmossdk.io/x/upgrade/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"

"github.com/pushchain/push-chain-node/app/upgrades"
pchaintypes "github.com/pushchain/push-chain-node/types"
uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types"
)

const UpgradeName = "supply-burn"

// BurnEntry specifies one address and how much PC to keep (in whole PC tokens).
// The handler burns: spendable_balance - keep_pc * 10^18 upc.
// Set keep_pc = 0 to burn the entire spendable balance.
type BurnEntry struct {
Address string `json:"address"`
KeepPC int64 `json:"keep_pc"` // whole PC to keep liquid (0 = burn all spendable)
}

//go:embed burn_targets.json
var burnTargetsJSON []byte

// NewUpgrade constructs the upgrade definition.
func NewUpgrade() upgrades.Upgrade {
return upgrades.Upgrade{
UpgradeName: UpgradeName,
CreateUpgradeHandler: CreateUpgradeHandler,
StoreUpgrades: storetypes.StoreUpgrades{},
}
}

// CreateUpgradeHandler burns spendable balances of all addresses in burn_targets.json,
// keeping keep_pc * 10^18 upc per address.
// Burn amount is computed from the live spendable balance at upgrade time —
// not from a snapshot — so it works correctly regardless of accrued rewards.
func CreateUpgradeHandler(
mm upgrades.ModuleManager,
configurator module.Configurator,
ak *upgrades.AppKeepers,
) upgradetypes.UpgradeHandler {
return func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)
sdkCtx.Logger().Info("Running upgrade", "name", UpgradeName)

var entries []BurnEntry
if err := json.Unmarshal(burnTargetsJSON, &entries); err != nil {
panic("supply-burn: failed to parse burn_targets.json: " + err.Error())
}

totalBurned, success, skipped, errCount := ExecuteBurnEntries(sdkCtx, ak.BankKeeper, entries)
sdkCtx.Logger().Info("supply-burn: upgrade complete",
"total_burned_upc", totalBurned.String(),
"success", success,
"skipped", skipped,
"errors", errCount,
)

return mm.RunMigrations(ctx, configurator, fromVM)
}
}

// ExecuteBurnEntries burns (spendable_balance - keep_pc * 10^18) upc from each address.
// Returns total burned and per-outcome counts.
//
// Entries are skipped (not errors) when:
// - spendable balance <= keep amount (nothing to burn)
//
// Entries are counted as errors when:
// - address fails bech32 decoding
//
// Panics if BurnCoins fails after SendCoinsFromAccountToModule succeeds.
func ExecuteBurnEntries(
ctx sdk.Context,
bk bankkeeper.BaseKeeper,
entries []BurnEntry,
) (totalBurned sdkmath.Int, success, skipped, errCount int) {
totalBurned = sdkmath.ZeroInt()
upcPerPC := sdkmath.NewInt(1_000_000_000_000_000_000)

for _, entry := range entries {
addr, err := sdk.AccAddressFromBech32(entry.Address)
if err != nil {
ctx.Logger().Error("supply-burn: invalid address, skipping",
"address", entry.Address, "error", err.Error())
errCount++
continue
}

keepAmt := sdkmath.NewInt(entry.KeepPC).Mul(upcPerPC)

spendable := bk.SpendableCoins(ctx, addr)
spendableAmt := spendable.AmountOf(pchaintypes.BaseDenom)

if spendableAmt.LTE(keepAmt) {
ctx.Logger().Info("supply-burn: spendable <= keep amount, skipping",
"address", entry.Address,
"spendable_upc", spendableAmt.String(),
"keep_upc", keepAmt.String())
skipped++
continue
}

burnAmt := spendableAmt.Sub(keepAmt)
burnCoins := sdk.NewCoins(sdk.NewCoin(pchaintypes.BaseDenom, burnAmt))

// Two-step burn: send to uexecutor module (has Burner permission), then burn.
if err := bk.SendCoinsFromAccountToModule(ctx, addr, uexecutortypes.ModuleName, burnCoins); err != nil {
ctx.Logger().Error("supply-burn: SendCoinsFromAccountToModule failed, skipping",
"address", entry.Address, "error", err.Error())
errCount++
continue
}

if err := bk.BurnCoins(ctx, uexecutortypes.ModuleName, burnCoins); err != nil {
// Coins left the account but BurnCoins failed: supply accounting is broken.
panic("supply-burn: BurnCoins failed after SendCoinsFromAccountToModule: " + err.Error())
}

totalBurned = totalBurned.Add(burnAmt)
success++
ctx.Logger().Info("supply-burn: burned",
"address", entry.Address,
"burned_upc", burnAmt.String(),
"kept_upc", keepAmt.String())
}

return totalBurned, success, skipped, errCount
}
14 changes: 14 additions & 0 deletions proto/uregistry/v1/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ message GatewayMethods {
ConfirmationType confirmation_type = 4; // Which block confirmation to use for this method
}

// VaultMethods defines the configuration for a method exposed by the vault contract
message VaultMethods {
option (amino.name) = "uregistry/vault_methods";
option (gogoproto.equal) = true;
option (gogoproto.goproto_stringer) = false;

string name = 1; // Human-readable method name (e.g. "deposit")
string identifier = 2; // Hex-encoded selector or discriminator for the method
string event_identifier = 3; // Hex-encoded topic or identifier for emitted event
ConfirmationType confirmation_type = 4; // Which block confirmation to use for this method
}

// BlockConfirmation defines the number of blocks to wait for confirmation on the external chain
message BlockConfirmation {
option (amino.name) = "uregistry/block_confirmation";
Expand Down Expand Up @@ -100,6 +112,8 @@ message ChainConfig {

ChainEnabled enabled = 7; // Whether this chain is currently enabled or not
google.protobuf.Duration gas_oracle_fetch_interval = 8 [(gogoproto.nullable) = false, (gogoproto.stdduration) = true]; // how often relayers should fetch gas prices

repeated VaultMethods vault_methods = 9; // List of methods exposed by the vault contract (optional)
}

message NativeRepresentation {
Expand Down
84 changes: 84 additions & 0 deletions test/integration/uexecutor/vote_outbound_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,88 @@ func TestOutboundVoting(t *testing.T) {
require.Equal(t, outbound.Sender, ob.PcRevertExecution.Sender)
})

t.Run("vote with 0x-prefixed utxId and txId still finalizes correctly", func(t *testing.T) {
app, ctx, vals, utxId, outbound, coreVals :=
setupOutboundVotingTest(t, 4)

// Simulate UVs submitting IDs with 0x prefix (as observed on testnet).
// The handler strips the prefix exactly once before the keeper lookup.
prefixedUtxId := "0x" + utxId
prefixedOutbound := *outbound
prefixedOutbound.Id = "0x" + outbound.Id

for i := 0; i < 3; i++ {
valAddr, err := sdk.ValAddressFromBech32(coreVals[i].OperatorAddress)
require.NoError(t, err)

coreAcc := sdk.AccAddress(valAddr).String()
err = utils.ExecVoteOutbound(
t,
ctx,
app,
vals[i],
coreAcc,
prefixedUtxId,
&prefixedOutbound,
true,
"",
)
require.NoError(t, err)
}

utx, _, err := app.UexecutorKeeper.GetUniversalTx(ctx, utxId)
require.NoError(t, err)

ob := utx.OutboundTx[0]
require.Equal(t, uexecutortypes.Status_OBSERVED, ob.OutboundStatus)
require.NotNil(t, ob.ObservedTx)
require.True(t, ob.ObservedTx.Success)
})

t.Run("vote with unknown utxId returns error", func(t *testing.T) {
app, ctx, vals, _, outbound, coreVals :=
setupOutboundVotingTest(t, 4)

valAddr, _ := sdk.ValAddressFromBech32(coreVals[0].OperatorAddress)
coreAcc := sdk.AccAddress(valAddr).String()

err := utils.ExecVoteOutbound(
t,
ctx,
app,
vals[0],
coreAcc,
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
outbound,
true,
"",
)
require.Error(t, err)
require.Contains(t, err.Error(), "not found")
})

t.Run("vote with unknown outboundId returns error", func(t *testing.T) {
app, ctx, vals, utxId, outbound, coreVals :=
setupOutboundVotingTest(t, 4)

valAddr, _ := sdk.ValAddressFromBech32(coreVals[0].OperatorAddress)
coreAcc := sdk.AccAddress(valAddr).String()

badOutbound := *outbound
badOutbound.Id = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"

err := utils.ExecVoteOutbound(
t,
ctx,
app,
vals[0],
coreAcc,
utxId,
&badOutbound,
true,
"",
)
require.Error(t, err)
require.Contains(t, err.Error(), "not found")
})
}
Loading
Loading