GHSA-P7GW-2PCP-5PF8

Vulnerability from github – Published: 2026-08-28 16:22 – Updated: 2026-08-28 16:22
VLAI
Summary
Klever: Marketplace settlement mints KLV when referral % + royalty % exceed the bid (negative seller share silently skipped)
Details

Summary

When a marketplace order is settled (MarketBuy / BuyItNow, and auction Claim), the buyer's payment is split three ways — referral, royalties, and the seller (market-order owner) remainder:

marketOwnerAmount = CurrentBid − referralAmount − royaltiesAmount

Referral and royalties are paid out unconditionally, but the seller remainder is only paid when positive (computeMarketOwnerAmount returns Ok and pays nothing when the amount is <= 0). When referral% + royalty% exceeds 100% of the bid, marketOwnerAmount goes negative and is silently skipped — so the marketplace pays out more KLV / sale currency than the buyer paid in, minting the difference out of thin air.

The combined ceiling royalty% + referral% <= 100% is checked once, at listing time (Sell). But the two percentages are sourced asymmetrically at settlement:

  • referral % is snapshotted into the order at Sell (MarketOrderData.ReferralPercentage);
  • royalty % is never snapshotted — it is read live from the asset at buy time (asset.Royalties.MarketPercentage).

So the listing-time invariant is a time-of-check/time-of-use guarantee only. After a valid listing, the asset owner raises the royalty MarketPercentage via AssetTrigger → UpdateRoyalties; at the next buy the live royalty plus the snapshotted referral exceed 100%, and the settlement mints the overflow. The minted funds land in attacker-controlled referral / royalty addresses.

This was actively exploited on mainnet (see Evidence), minting tens of millions of KLV before the emergency guard was deployed.

Affected component

  • Repository: klever-io/klever-go (node).
  • Settlement / mint site: core/kapp/market/market.goexecuteBuyMarket (L575+), computeReferralAmount (L361+), computeRoyaltiesAmount (L490+), computeRoyaltiesFixedDeposit (L443+), computeMarketOwnerAmount (L540+).
  • TOCTOU sources: Sell combined check (market.go:908), order snapshot of referral but not royalty (market.go:997), live royalty mutation via core/kapp/kda/trigger.gohandleUpdateRoyaltiesNFTandSFT (L613+, sets asset.Royalties.MarketPercentage at L670).
  • Reachable from both Buy (BuyItNow, market.go:204+) and auction Claim (market.go:705, market.go:731).
  • Pre-fix: not gated by any fork flag — exploitable on mainnet. The fix is gated behind the new FixMarketBuyOverflow activation-epoch flag.

Root cause

1. Settlement pays referral + royalty unconditionally, seller remainder only if positive

core/kapp/market/market.goexecuteBuyMarket (L575+):

referralAmount, _  := tools.ComputePercentageI64(marketOrder.CurrentBid,
                          int64(marketOrder.ReferralPercentage), ...)        // L583: SNAPSHOT referral %
royaltiesAmount, _ := tools.ComputePercentageI64(marketOrder.CurrentBid,
                          int64(asset.Royalties.MarketPercentage), ...)      // L587: LIVE royalty %
marketOwnerAmount := marketOrder.CurrentBid - referralAmount - royaltiesAmount  // L591: can go negative

// ---- FIX (FixMarketBuyOverflow), added by the patch ----
if m.forkController.FixMarketBuyOverflow() && marketOwnerAmount < 0 {         // L593-596
    ctx.Receipts().AddError(ctx.ContractID(), common.ErrFieldInvalidRoyalties, common.ErrInvalidValue.Error())
    return transaction.Transaction_AmountInvalid, common.ErrInvalidValue
}

m.computeReferralAmount(ctx, marketOrder, referralAmount, currencyID)   // pays referral in full
m.computeRoyaltiesFixedDeposit(ctx, marketOrder, asset)                 // pays fixed royalty (KLV)
m.computeRoyaltiesAmount(ctx, marketOrder, asset, currencyID, royaltiesAmount) // pays % royalty in full
m.computeMarketOwnerAmount(ctx, marketOrder, currencyID, marketOwnerAmount)    // <-- skips when <= 0

computeMarketOwnerAmount (L540-542) — the silent skip:

func (m *marketKapp) computeMarketOwnerAmount(... marketOwnerAmount int64) (... , error) {
    if marketOwnerAmount <= 0 {
        return transaction.Transaction_Ok, nil   // negative seller share dropped, NO error
    }
    // ... AddToBalance(marketOwnerAmount) ...
}

Meanwhile computeReferralAmount (L376) and computeRoyaltiesAmount (L515) each AddToBalance(...) the full computed amount with no matching debit from the buyer beyond the single bidderAcc.SubFromBalance(amount) taken in Buy (market.go:301).

Conservation breaks: buyer is debited bid once; recipients are credited referralAmount + royaltiesAmount. When that sum > bid, the surplus (referralAmount + royaltiesAmount − bid) is minted.

2. The combined ≤100% invariant is enforced only at listing time

Sell (market.go:908) correctly rejects a listing whose combined cut exceeds 100%:

if asset.Royalties.MarketPercentage + marketplace.ReferralPercentage > core.HundredPercent {
    return transaction.Transaction_ParameterInvalid, common.ErrInvalidValue
}

…and snapshots referral into the order, but not royalty (market.go:997-998):

marketOrder := &kapps.MarketOrderData{
    // ...
    ReferralPercentage:    marketplace.ReferralPercentage, // snapshotted
    RoyaltiesFixedDeposit: asset.Royalties.MarketFixed,    // snapshotted
    // NOTE: asset.Royalties.MarketPercentage is NOT snapshotted -> read live at buy
}

MarketOrderData has no field for the royalty percentage (kapps/market.pb.go), so settlement always re-reads it live from the (mutable) asset.

3. Royalty % is mutable after listing

core/kapp/kda/trigger.gohandleUpdateRoyaltiesNFTandSFT (L613+) lets the asset owner overwrite asset.Royalties.MarketPercentage (L670) with only a per-field <= 100% check (CheckValid100Params, L651) — it has no knowledge of any outstanding marketplace listing's snapshotted referral. So the owner can list at, e.g., referral 100% / royalty 0% (sum 100%, passes Sell), then raise royalty to 100%, making the buy-time sum 200%.

The shipped emergency-guard source documents this exact vector: "The royalty percentage is read live at buy time, so a listing made now can be weaponised later via UpdateRoyalties." (common/emergencyGuard.go)

Net effect: referralAmount + royaltiesAmount = bid + bid = 2·bid; marketOwnerAmount = −bid (skipped); bid KLV minted per settlement, paid to attacker-controlled addresses.


Proof of Concept

A. Committed regression test (deterministic, runnable today)

core/kapp/market/market_test.goTestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation. It builds an order with ReferralPercentage = 100% and an asset with MarketPercentage = 100% (the attacker is both the referral and the royalty address), then settles a bid of 25,600,000 KLV (25600000000000 base units):

go test ./core/kapp/market/ -run TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation -v
  • FixDisabled_MintsKLVFromThinAir: settlement returns Ok; the attacker address ends with 2·bid credited while only bid was paid in — i.e. bid KLV minted.
  • FixEnabled_RejectsInflation: with FixMarketBuyOverflow on, settlement returns Transaction_AmountInvalid and the attacker balance stays 0no payout runs.

B. End-to-end on a local node (the real attack path)

A single-node local network is sufficient. The exploit is four transactions from one ordinary funded account; nothing privileged is required.

  1. Create an NFT collection you own, with royalties.marketPercentage = 0 and a royalties address you control.
  2. Create a marketplace with referralPercentage = 10000 (100%) and a referral address you control (CreateMarketplace).
  3. List one NFT for sale (Sell) on that marketplace. The Sell check passes because 0 (royalty) + 10000 (referral) = 10000 = HundredPercent. The order snapshots ReferralPercentage = 10000.
  4. Raise the royalty on the asset to 100% (AssetTrigger / UpdateRoyalties, marketPercentage = 10000). Allowed: the per-field check passes and the live combined invariant is never re-evaluated against the open listing.
  5. Buy the listing (MarketBuy) from a second account (or settle the auction via Claim). referralAmount = bid, royaltiesAmount = bid, marketOwnerAmount = −bid (skipped). Your referral + royalty addresses receive 2·bid; the buyer paid bid; bid KLV is minted.

Because the attacker controls buyer, seller, referral and royalty addresses, the only real cost is transaction fees; the cycle is repeatable until supply targets are met.


Evidence

Regression test (local, verbatim)

=== RUN   TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation
=== RUN   TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation/FixDisabled_MintsKLVFromThinAir
=== RUN   TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation/FixEnabled_RejectsInflation
--- PASS: TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation (0.00s)
    --- PASS: TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation/FixDisabled_MintsKLVFromThinAir (0.00s)
    --- PASS: TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation/FixEnabled_RejectsInflation (0.00s)
PASS
ok      github.com/klever-io/klever-go/core/kapp/market 0.279s

FixDisabled asserts the attacker balance equals 2·bid = 51,200,000 KLV for a single settlement (bid = 25,600,000 KLV), with bid of that minted. FixEnabled asserts rejection and a 0 balance.

Mainnet exploitation (observed)

The bug was exploited in production, and was detected and characterised externally by the community monitoring project KleverPuls / kpulse.tech before the root cause was known internally. Over a ~24h window kpulse isolated a single wallet (opened 2026-06-04, ~204 transactions in ~24h, funded only by a ~242K KLV KuCoin withdrawal, no treasury/foundation funding) that:

  • self-issued 3 NFT collections named "InflationPOC" and wash-traded one (NFLATION-ESGO/1) 29 times through self-created marketplaces — ~$450K of artificial, economically empty NFT volume;
  • swapped the proceeds KLV → USDC / USDT / WBTC / WETH on KleverSwap and bridged ~$72K of value to Ethereum via wrapped-asset burns over 24h (USDC −12,487 ≈ $12.5K; USDT −26,362 ≈ $26.4K; WBTC −0.31 ≈ $19.6K; WETH −7.55 ≈ $14K);
  • surfaced a spurious "1.86B KLV outflow" headline that kpulse correctly identified as a wash-trade receipt-doubling artifact with small real net KLV flow.

That "doubling of marketplace receipts" is precisely the on-chain signature of this bug: each abusive settlement pays out a referral cut (bid) plus a royalty cut (bid) while the buyer paid only bid once — the market contract emits ~2× the value it took in, which is the mint. The attacker's self-issued collection and self-created marketplaces are exactly the self-dealing setup the regression test reproduces (the test reuses the real on-chain identifiers: collectionID = "NFLATION-ESGO", asset 1, market name "Inflation Market").

kpulse could not determine the cause from on-chain data alone and flagged the activity for confirmation; the Klever core team then traced it to the referral+royalty settlement defect described above and shipped the emergency guard + protocol fix.

Each abusive settlement minted one bid of KLV; the observed bid was 25,600,000 KLV, repeated and funnelled through a short hop chain before being swapped and bridged. The emergency guard (common/emergencyGuard.go) blocks the following observed sender public keys (hex):

Public key (hex) Address Role (observed)
54ea28e527d4136508be955374afa54a8c25c19a48c674f412f7ce02db0f4e1b klv12n4z3ef86sfk2z97j4fhfta9f2xztsv6frr8faqj7l8q9kc0fcdsfjfqez root / minter
bb687dbba23e1844fec674a32cb8809f0d3207506c53fc3d637e40dc56708d63 klv1hd58mwaz8cvyflkxwj3jewyqnuxnyp6sd3flc0tr0eqdc4ns343skngdjq collector hop (~125M KLV)
77388d3dfe6cd88e8da723254c11abf3d9cccb6fb77b000e5038fc3ff92b964d klv1wuug6007dnvgard8yvj5cydt70vuejm0kaasqrjs8r7rl7ftjexsglalf6 direct recipient (25.6M, idle)
a196789b026f996867f08317cc6c5a4eb9ad3a59b1be3716420bc8692d4c3048 klv15xt83xczd7vkselssvtucmz6f6u66wjekxlrw9jzp0yxjt2vxpyq2nawrw hop-2 recipient (25M)

The single-bid per-settlement size (25.6M KLV) matches the "direct recipient, 25.6M" entry, and the ~125M at the collector hop is consistent with roughly five abusive settlements.


Impact

  • Unbounded inflation of KLV (and of any sale currency used for the listing), repeatable for only transaction fees, by any account that creates its own collection + marketplace.
  • The minted KLV is created by direct AddToBalance to attacker addresses (no tracked Mint), so the asset's booked supply does not change — the inflation is off the books and only detectable by summing balances / auditing receipts (it surfaces on-chain as doubled marketplace receipts).
  • Realized impact (observed): the attacker minted KLV via ~29 self-dealt settlements, swapped to stable/wrapped assets on KleverSwap, and off-ramped ~$72K to Ethereum via the bridge (USDC, USDT, WBTC, WETH) before the emergency guard halted the activity, alongside ~$450K of artificial NFT wash-trade volume.
  • Total loss of economic integrity for all KLV / token holders.

Who can exploit it / prerequisites

  • Any account that can pay the one-time collection-create + marketplace-create fees and tx fees. No roles, admin, or allowlist.
  • Any client. The settlement is triggered by standard MarketBuy / Claim contracts POSTed to the public, unauthenticated /transaction/send RPC (network/api/transaction/routes.go, SendTX / BroadcastTX). The operator CLI, the SDKs, or a hand-signed curl all work.
  • Deterministic; the abusive state is reached with one extra UpdateRoyalties after a normal listing.

Remediation

Shipped as a layered response (embargoed):

Layer 0 — emergency guard (deployed first, fork-proof) — GHSA-p7gw rc1

common/emergencyGuard.go + data/transaction/emergencyGuard.go: matching transactions are kept out of blocks this node proposes (core/process/block/preprocess/transactions.go) and refused at the node API (node.go SendTransaction / SendBulkTransactions). It never changes block validity, so a partial-fleet rollout cannot fork the chain. It blocks the known attacker senders (all contract types) plus all MarketBuy, Sell, and CreateMarketplace / ConfigMarketplace operations while the protocol fix rolls out. Enforcement is by proposer cooperation, not protocol — coverage equals the share of block producers running the guard.

Layer 1 — protocol fix (consensus, epoch-gated) — GHSA-p7gw rc2

core/kapp/market/market.go:593 rejects the settlement when marketOwnerAmount < 0, before any payout runs, returning Transaction_AmountInvalid. Gated behind the new FixMarketBuyOverflow activation-epoch flag (config/enableEpochs.*, core/fork/forks.go, core/interface.go) so historical blocks reprocess identically. Covered by TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation.

Recommended hardening (defense in depth)

  1. Snapshot the royalty % into MarketOrderData at Sell (as referral already is) and pay from the snapshot, eliminating the TOCTOU entirely; or re-evaluate the combined referral% + royalty% <= 100% invariant at settlement.
  2. Treat a negative seller remainder as a hard error everywhere, and only treat exactly 0 as a no-op skip (computeMarketOwnerAmount), so a future regression aborts the tx instead of minting.
  3. After splitting a payment pool, assert conservation (referral + royalties + ownerShare == bid) so any drift aborts the transaction.

Notes

  • Triggered by both BuyItNow (Buy) and auction settlement (Claim).
  • The same family of "pay full cut, silently drop the negative remainder" minting also exists in the royalty-split paths and is tracked separately under GHSA-cgc5-v3f2-8m2v (split-royalty uint32 overflow). This advisory covers the top-level referral+royalty > bid case; the FixMarketBuyOverflow guard here only checks marketOwnerAmount, not intra-split over-payments.

Acknowledgments

  • KleverPuls / kpulse.tech — community monitoring project that first detected and characterised the exploitation in the wild. kpulse isolated the attacker wallet and its "InflationPOC" collections, identified the marketplace wash-trading of NFLATION-ESGO/1 and the KleverSwap → bridge off-ramp (~$72K to Ethereum), and flagged the anomalous doubling of marketplace receipts — the exact on-chain signature of this bug — prompting the incident response that led to this fix. The root cause was then identified and remediated by the Klever core team.

Source

  • Vulnerable / fixed code: core/kapp/market/market.go:540-545,575-596,908,997-998, core/kapp/kda/trigger.go:613-676, core/process/kda/assetHelper.go:101, tools/converters.go:102, core/constants.go:18.
  • Emergency guard: common/emergencyGuard.go, data/transaction/emergencyGuard.go, core/process/block/preprocess/transactions.go, node/node.go.
  • Fork flag: config/enableEpochs.go, config/node/enableEpochs.yaml, core/fork/forks.go, core/interface.go.
  • Regression test: core/kapp/market/market_test.go (TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation).
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/klever-io/klever-go"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.7.19"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-54754"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-191",
      "CWE-367",
      "CWE-682"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-28T16:22:15Z",
    "nvd_published_at": null,
    "severity": "CRITICAL"
  },
  "details": "## Summary\n\nWhen a marketplace order is settled (`MarketBuy` / `BuyItNow`, and auction `Claim`), the buyer\u0027s\npayment is split three ways \u2014 **referral**, **royalties**, and the **seller (market-order owner)\nremainder**:\n\n```\nmarketOwnerAmount = CurrentBid \u2212 referralAmount \u2212 royaltiesAmount\n```\n\nReferral and royalties are paid out **unconditionally**, but the seller remainder is only paid\n**when positive** (`computeMarketOwnerAmount` returns `Ok` and pays nothing when the amount is\n`\u003c= 0`). When `referral% + royalty%` exceeds 100% of the bid, `marketOwnerAmount` goes **negative**\nand is silently skipped \u2014 so the marketplace pays out **more KLV / sale currency than the buyer\npaid in**, minting the difference out of thin air.\n\nThe combined ceiling `royalty% + referral% \u003c= 100%` **is** checked once, at listing time (`Sell`).\nBut the two percentages are sourced asymmetrically at settlement:\n\n- **referral %** is **snapshotted** into the order at `Sell` (`MarketOrderData.ReferralPercentage`);\n- **royalty %** is **never snapshotted** \u2014 it is read **live** from the asset at buy time\n  (`asset.Royalties.MarketPercentage`).\n\nSo the listing-time invariant is a **time-of-check/time-of-use** guarantee only. After a valid\nlisting, the asset owner raises the royalty `MarketPercentage` via `AssetTrigger \u2192 UpdateRoyalties`;\nat the next buy the live royalty plus the snapshotted referral exceed 100%, and the settlement mints\nthe overflow. The minted funds land in attacker-controlled referral / royalty addresses.\n\nThis was **actively exploited on mainnet** (see *Evidence*), minting tens of millions of KLV before\nthe emergency guard was deployed.\n\n## Affected component\n\n- Repository: `klever-io/klever-go` (node).\n- Settlement / mint site: `core/kapp/market/market.go` \u2014 `executeBuyMarket` (L575+),\n  `computeReferralAmount` (L361+), `computeRoyaltiesAmount` (L490+),\n  `computeRoyaltiesFixedDeposit` (L443+), `computeMarketOwnerAmount` (L540+).\n- TOCTOU sources: `Sell` combined check (`market.go:908`), order snapshot of referral but **not**\n  royalty (`market.go:997`), live royalty mutation via\n  `core/kapp/kda/trigger.go` \u2014 `handleUpdateRoyaltiesNFTandSFT` (L613+, sets\n  `asset.Royalties.MarketPercentage` at L670).\n- Reachable from both `Buy` (BuyItNow, `market.go:204+`) and auction `Claim`\n  (`market.go:705`, `market.go:731`).\n- Pre-fix: **not** gated by any fork flag \u2014 exploitable on mainnet. The fix is gated behind the new\n  `FixMarketBuyOverflow` activation-epoch flag.\n\n---\n\n## Root cause\n\n### 1. Settlement pays referral + royalty unconditionally, seller remainder only if positive\n`core/kapp/market/market.go` \u2014 `executeBuyMarket` (L575+):\n```go\nreferralAmount, _  := tools.ComputePercentageI64(marketOrder.CurrentBid,\n                          int64(marketOrder.ReferralPercentage), ...)        // L583: SNAPSHOT referral %\nroyaltiesAmount, _ := tools.ComputePercentageI64(marketOrder.CurrentBid,\n                          int64(asset.Royalties.MarketPercentage), ...)      // L587: LIVE royalty %\nmarketOwnerAmount := marketOrder.CurrentBid - referralAmount - royaltiesAmount  // L591: can go negative\n\n// ---- FIX (FixMarketBuyOverflow), added by the patch ----\nif m.forkController.FixMarketBuyOverflow() \u0026\u0026 marketOwnerAmount \u003c 0 {         // L593-596\n    ctx.Receipts().AddError(ctx.ContractID(), common.ErrFieldInvalidRoyalties, common.ErrInvalidValue.Error())\n    return transaction.Transaction_AmountInvalid, common.ErrInvalidValue\n}\n\nm.computeReferralAmount(ctx, marketOrder, referralAmount, currencyID)   // pays referral in full\nm.computeRoyaltiesFixedDeposit(ctx, marketOrder, asset)                 // pays fixed royalty (KLV)\nm.computeRoyaltiesAmount(ctx, marketOrder, asset, currencyID, royaltiesAmount) // pays % royalty in full\nm.computeMarketOwnerAmount(ctx, marketOrder, currencyID, marketOwnerAmount)    // \u003c-- skips when \u003c= 0\n```\n`computeMarketOwnerAmount` (L540-542) \u2014 the silent skip:\n```go\nfunc (m *marketKapp) computeMarketOwnerAmount(... marketOwnerAmount int64) (... , error) {\n\tif marketOwnerAmount \u003c= 0 {\n\t\treturn transaction.Transaction_Ok, nil   // negative seller share dropped, NO error\n\t}\n\t// ... AddToBalance(marketOwnerAmount) ...\n}\n```\nMeanwhile `computeReferralAmount` (L376) and `computeRoyaltiesAmount` (L515) each `AddToBalance(...)`\nthe full computed amount with **no matching debit** from the buyer beyond the single\n`bidderAcc.SubFromBalance(amount)` taken in `Buy` (`market.go:301`).\n\n**Conservation breaks:** buyer is debited `bid` once; recipients are credited\n`referralAmount + royaltiesAmount`. When that sum `\u003e bid`, the surplus\n`(referralAmount + royaltiesAmount \u2212 bid)` is **minted**.\n\n### 2. The combined \u2264100% invariant is enforced only at listing time\n`Sell` (`market.go:908`) correctly rejects a listing whose combined cut exceeds 100%:\n```go\nif asset.Royalties.MarketPercentage + marketplace.ReferralPercentage \u003e core.HundredPercent {\n\treturn transaction.Transaction_ParameterInvalid, common.ErrInvalidValue\n}\n```\n\u2026and snapshots **referral** into the order, but **not** royalty (`market.go:997-998`):\n```go\nmarketOrder := \u0026kapps.MarketOrderData{\n\t// ...\n\tReferralPercentage:    marketplace.ReferralPercentage, // snapshotted\n\tRoyaltiesFixedDeposit: asset.Royalties.MarketFixed,    // snapshotted\n\t// NOTE: asset.Royalties.MarketPercentage is NOT snapshotted -\u003e read live at buy\n}\n```\n`MarketOrderData` has no field for the royalty percentage (`kapps/market.pb.go`), so settlement\nalways re-reads it live from the (mutable) asset.\n\n### 3. Royalty % is mutable after listing\n`core/kapp/kda/trigger.go` \u2014 `handleUpdateRoyaltiesNFTandSFT` (L613+) lets the asset owner overwrite\n`asset.Royalties.MarketPercentage` (L670) with only a **per-field** `\u003c= 100%` check (`CheckValid100Params`,\nL651) \u2014 it has no knowledge of any outstanding marketplace listing\u0027s snapshotted referral. So the\nowner can list at, e.g., referral 100% / royalty 0% (sum 100%, passes `Sell`), then raise royalty to\n100%, making the buy-time sum 200%.\n\n\u003e The shipped emergency-guard source documents this exact vector:\n\u003e *\"The royalty percentage is read live at buy time, so a listing made now can be weaponised later\n\u003e via UpdateRoyalties.\"* (`common/emergencyGuard.go`)\n\n**Net effect:** `referralAmount + royaltiesAmount = bid + bid = 2\u00b7bid`; `marketOwnerAmount = \u2212bid`\n(skipped); **`bid` KLV minted per settlement**, paid to attacker-controlled addresses.\n\n---\n\n## Proof of Concept\n\n### A. Committed regression test (deterministic, runnable today)\n`core/kapp/market/market_test.go` \u2014 `TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation`.\nIt builds an order with `ReferralPercentage = 100%` and an asset with `MarketPercentage = 100%`\n(the attacker is both the referral and the royalty address), then settles a `bid` of\n`25,600,000 KLV` (`25600000000000` base units):\n\n```bash\ngo test ./core/kapp/market/ -run TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation -v\n```\n\n- `FixDisabled_MintsKLVFromThinAir`: settlement returns `Ok`; the attacker address ends with\n  `2\u00b7bid` credited while only `bid` was paid in \u2014 i.e. **`bid` KLV minted**.\n- `FixEnabled_RejectsInflation`: with `FixMarketBuyOverflow` on, settlement returns\n  `Transaction_AmountInvalid` and the attacker balance stays `0` \u2014 **no payout runs**.\n\n### B. End-to-end on a local node (the real attack path)\nA single-node local network is sufficient. The exploit is four transactions from one ordinary\nfunded account; nothing privileged is required.\n\n1. **Create an NFT collection** you own, with `royalties.marketPercentage = 0` and a royalties\n   address you control.\n2. **Create a marketplace** with `referralPercentage = 10000` (100%) and a referral address you\n   control (`CreateMarketplace`).\n3. **List** one NFT for sale (`Sell`) on that marketplace. The `Sell` check passes because\n   `0 (royalty) + 10000 (referral) = 10000 = HundredPercent`. The order snapshots\n   `ReferralPercentage = 10000`.\n4. **Raise the royalty** on the asset to 100% (`AssetTrigger / UpdateRoyalties`,\n   `marketPercentage = 10000`). Allowed: the per-field check passes and the live combined invariant\n   is never re-evaluated against the open listing.\n5. **Buy** the listing (`MarketBuy`) from a second account (or settle the auction via `Claim`).\n   `referralAmount = bid`, `royaltiesAmount = bid`, `marketOwnerAmount = \u2212bid` (skipped). Your\n   referral + royalty addresses receive `2\u00b7bid`; the buyer paid `bid`; **`bid` KLV is minted**.\n\nBecause the attacker controls buyer, seller, referral and royalty addresses, the only real cost is\ntransaction fees; the cycle is repeatable until supply targets are met.\n\n---\n\n## Evidence\n\n### Regression test (local, verbatim)\n```\n=== RUN   TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation\n=== RUN   TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation/FixDisabled_MintsKLVFromThinAir\n=== RUN   TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation/FixEnabled_RejectsInflation\n--- PASS: TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation (0.00s)\n    --- PASS: TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation/FixDisabled_MintsKLVFromThinAir (0.00s)\n    --- PASS: TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation/FixEnabled_RejectsInflation (0.00s)\nPASS\nok  \tgithub.com/klever-io/klever-go/core/kapp/market\t0.279s\n```\n`FixDisabled` asserts the attacker balance equals `2\u00b7bid = 51,200,000 KLV` for a single settlement\n(`bid = 25,600,000 KLV`), with `bid` of that minted. `FixEnabled` asserts rejection and a `0`\nbalance.\n\n### Mainnet exploitation (observed)\nThe bug was exploited in production, and was **detected and characterised externally** by the\ncommunity monitoring project **[KleverPuls / kpulse.tech](https://kpulse.tech)** before the root\ncause was known internally. Over a ~24h window kpulse isolated a single wallet (opened\n**2026-06-04**, ~**204 transactions** in ~24h, funded only by a ~**242K KLV KuCoin withdrawal**, no\ntreasury/foundation funding) that:\n\n- **self-issued 3 NFT collections named \"InflationPOC\"** and **wash-traded one (`NFLATION-ESGO/1`)\n  29 times** through self-created marketplaces \u2014 ~**$450K of artificial, economically empty NFT\n  volume**;\n- **swapped the proceeds KLV \u2192 USDC / USDT / WBTC / WETH on KleverSwap** and **bridged ~$72K of value\n  to Ethereum** via wrapped-asset burns over 24h (USDC \u221212,487 \u2248 $12.5K; USDT \u221226,362 \u2248 $26.4K; WBTC\n  \u22120.31 \u2248 $19.6K; WETH \u22127.55 \u2248 $14K);\n- surfaced a spurious **\"1.86B KLV outflow\"** headline that kpulse correctly identified as a\n  wash-trade **receipt-doubling** artifact with small real net KLV flow.\n\nThat \"doubling of marketplace receipts\" is **precisely the on-chain signature of this bug**: each\nabusive settlement pays out a referral cut (`bid`) **plus** a royalty cut (`bid`) while the buyer paid\nonly `bid` once \u2014 the market contract emits ~2\u00d7 the value it took in, which *is* the mint. The\nattacker\u0027s self-issued collection and self-created marketplaces are exactly the self-dealing setup the\nregression test reproduces (the test reuses the real on-chain identifiers: `collectionID =\n\"NFLATION-ESGO\"`, asset `1`, market name \"Inflation Market\").\n\nkpulse could not determine the cause from on-chain data alone and flagged the activity for\nconfirmation; the Klever core team then traced it to the referral+royalty settlement defect described\nabove and shipped the emergency guard + protocol fix.\n\nEach abusive settlement minted one `bid` of KLV; the observed `bid` was `25,600,000 KLV`, repeated and\nfunnelled through a short hop chain before being swapped and bridged. The emergency guard\n(`common/emergencyGuard.go`) blocks the following observed sender public keys (hex):\n\n| Public key (hex) | Address | Role (observed) |\n|---|---|---|\n| `54ea28e527d4136508be955374afa54a8c25c19a48c674f412f7ce02db0f4e1b` | `klv12n4z3ef86sfk2z97j4fhfta9f2xztsv6frr8faqj7l8q9kc0fcdsfjfqez` | root / minter |\n| `bb687dbba23e1844fec674a32cb8809f0d3207506c53fc3d637e40dc56708d63` | `klv1hd58mwaz8cvyflkxwj3jewyqnuxnyp6sd3flc0tr0eqdc4ns343skngdjq` | collector hop (~125M KLV) |\n| `77388d3dfe6cd88e8da723254c11abf3d9cccb6fb77b000e5038fc3ff92b964d` | `klv1wuug6007dnvgard8yvj5cydt70vuejm0kaasqrjs8r7rl7ftjexsglalf6` | direct recipient (25.6M, idle) |\n| `a196789b026f996867f08317cc6c5a4eb9ad3a59b1be3716420bc8692d4c3048` | `klv15xt83xczd7vkselssvtucmz6f6u66wjekxlrw9jzp0yxjt2vxpyq2nawrw` | hop-2 recipient (25M) |\n\nThe single-`bid` per-settlement size (25.6M KLV) matches the \"direct recipient, 25.6M\" entry, and the\n~125M at the collector hop is consistent with roughly five abusive settlements.\n\n---\n\n## Impact\n\n- **Unbounded inflation of KLV** (and of any sale currency used for the listing), repeatable for only\n  transaction fees, by any account that creates its own collection + marketplace.\n- The minted KLV is created by direct `AddToBalance` to attacker addresses (no tracked `Mint`), so\n  the asset\u0027s booked supply does not change \u2014 the inflation is **off the books** and only detectable\n  by summing balances / auditing receipts (it surfaces on-chain as *doubled* marketplace receipts).\n- **Realized impact (observed):** the attacker minted KLV via ~29 self-dealt settlements, swapped to\n  stable/wrapped assets on KleverSwap, and **off-ramped ~$72K to Ethereum via the bridge** (USDC,\n  USDT, WBTC, WETH) before the emergency guard halted the activity, alongside ~$450K of artificial\n  NFT wash-trade volume.\n- Total loss of economic integrity for all KLV / token holders.\n\n## Who can exploit it / prerequisites\n\n- **Any account** that can pay the one-time collection-create + marketplace-create fees and tx\n  fees. No roles, admin, or allowlist.\n- **Any client.** The settlement is triggered by standard `MarketBuy` / `Claim` contracts POSTed to\n  the public, unauthenticated `/transaction/send` RPC (`network/api/transaction/routes.go`,\n  `SendTX` / `BroadcastTX`). The `operator` CLI, the SDKs, or a hand-signed `curl` all work.\n- Deterministic; the abusive state is reached with one extra `UpdateRoyalties` after a normal listing.\n\n---\n\n## Remediation\n\nShipped as a layered response (embargoed):\n\n### Layer 0 \u2014 emergency guard (deployed first, fork-proof) \u2014 `GHSA-p7gw` rc1\n`common/emergencyGuard.go` + `data/transaction/emergencyGuard.go`: matching transactions are kept\nout of blocks this node proposes (`core/process/block/preprocess/transactions.go`) and refused at\nthe node API (`node.go` `SendTransaction` / `SendBulkTransactions`). It **never changes block\nvalidity**, so a partial-fleet rollout cannot fork the chain. It blocks the known attacker senders\n(all contract types) plus all `MarketBuy`, `Sell`, and `CreateMarketplace` / `ConfigMarketplace`\noperations while the protocol fix rolls out. Enforcement is by proposer cooperation, not protocol \u2014\ncoverage equals the share of block producers running the guard.\n\n### Layer 1 \u2014 protocol fix (consensus, epoch-gated) \u2014 `GHSA-p7gw` rc2\n`core/kapp/market/market.go:593` rejects the settlement when `marketOwnerAmount \u003c 0`, **before any\npayout runs**, returning `Transaction_AmountInvalid`. Gated behind the new `FixMarketBuyOverflow`\nactivation-epoch flag (`config/enableEpochs.*`, `core/fork/forks.go`, `core/interface.go`) so\nhistorical blocks reprocess identically. Covered by\n`TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation`.\n\n### Recommended hardening (defense in depth)\n1. **Snapshot the royalty %** into `MarketOrderData` at `Sell` (as referral already is) and pay from\n   the snapshot, eliminating the TOCTOU entirely; or re-evaluate the combined\n   `referral% + royalty% \u003c= 100%` invariant at settlement.\n2. Treat a **negative** seller remainder as a hard error everywhere, and only treat exactly `0` as a\n   no-op skip (`computeMarketOwnerAmount`), so a future regression aborts the tx instead of minting.\n3. After splitting a payment pool, **assert conservation** (`referral + royalties + ownerShare == bid`)\n   so any drift aborts the transaction.\n\n---\n\n## Notes\n\n- Triggered by both BuyItNow (`Buy`) and auction settlement (`Claim`).\n- The same family of \"pay full cut, silently drop the negative remainder\" minting also exists in the\n  royalty-split paths and is tracked separately under **GHSA-cgc5-v3f2-8m2v** (split-royalty `uint32`\n  overflow). This advisory covers the top-level referral+royalty \u003e bid case; the\n  `FixMarketBuyOverflow` guard here only checks `marketOwnerAmount`, not intra-split over-payments.\n\n## Acknowledgments\n\n- **[KleverPuls / kpulse.tech](https://kpulse.tech)** \u2014 community monitoring project that **first\n  detected and characterised the exploitation in the wild**. kpulse isolated the attacker wallet and\n  its \"InflationPOC\" collections, identified the marketplace wash-trading of `NFLATION-ESGO/1` and the\n  KleverSwap \u2192 bridge off-ramp (~$72K to Ethereum), and flagged the anomalous *doubling* of\n  marketplace receipts \u2014 the exact on-chain signature of this bug \u2014 prompting the incident response\n  that led to this fix. The root cause was then identified and remediated by the Klever core team.\n\n## Source\n\n- Vulnerable / fixed code: `core/kapp/market/market.go:540-545,575-596,908,997-998`,\n  `core/kapp/kda/trigger.go:613-676`, `core/process/kda/assetHelper.go:101`,\n  `tools/converters.go:102`, `core/constants.go:18`.\n- Emergency guard: `common/emergencyGuard.go`, `data/transaction/emergencyGuard.go`,\n  `core/process/block/preprocess/transactions.go`, `node/node.go`.\n- Fork flag: `config/enableEpochs.go`, `config/node/enableEpochs.yaml`, `core/fork/forks.go`,\n  `core/interface.go`.\n- Regression test: `core/kapp/market/market_test.go`\n  (`TestMarketKApp_ExecuteBuyMarket_RoyaltyReferralInflation`).",
  "id": "GHSA-p7gw-2pcp-5pf8",
  "modified": "2026-08-28T16:22:15Z",
  "published": "2026-08-28T16:22:15Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/klever-io/klever-go/security/advisories/GHSA-p7gw-2pcp-5pf8"
    },
    {
      "type": "WEB",
      "url": "https://github.com/klever-io/klever-go/commit/8bcc600b0ac88070740c63c7ce1c8a968dd85251"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/klever-io/klever-go"
    },
    {
      "type": "WEB",
      "url": "https://github.com/klever-io/klever-go/releases/tag/v1.7.19"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Klever: Marketplace settlement mints KLV when referral % + royalty % exceed the bid (negative seller share silently skipped)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…