Summary
Klever: Marketplace settlement mints KLV when referral % + royalty % exceed the bid (negative seller share silently skipped)
Advisory 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.go—executeBuyMarket(L575+),computeReferralAmount(L361+),computeRoyaltiesAmount(L490+),computeRoyaltiesFixedDeposit(L443+),computeMarketOwnerAmount(L540+). - TOCTOU sources:
Sellcombined check (market.go:908), order snapshot of referral but not royalty (market.go:997), live royalty mutation viacore/kapp/kda/trigger.go—handleUpdateRoyaltiesNFTandSFT(L613+, setsasset.Royalties.MarketPercentageat L670). - Reachable from both
Buy(BuyItNow,market.go:204+) and auctionClaim(market.go:705,market.go:731). - Pre-fix: not gated by any fork flag — exploitable on mainnet. The fix is gated behind the new
FixMarketBuyOverflowactivation-epoch flag.
Root cause
1. Settlement pays referral + royalty unconditionally, seller remainder only if positive
core/kapp/market/market.go — executeBuyMarket (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.go — handleUpdateRoyaltiesNFTandSFT (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.go — TestMarketKApp_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 returnsOk; the attacker address ends with2·bidcredited while onlybidwas paid in — i.e.bidKLV minted.FixEnabled_RejectsInflation: withFixMarketBuyOverflowon, settlement returnsTransaction_AmountInvalidand the attacker balance stays0— no 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.
- Create an NFT collection you own, with
royalties.marketPercentage = 0and a royalties address you control. - Create a marketplace with
referralPercentage = 10000(100%) and a referral address you control (CreateMarketplace). - List one NFT for sale (
Sell) on that marketplace. TheSellcheck passes because0 (royalty) + 10000 (referral) = 10000 = HundredPercent. The order snapshotsReferralPercentage = 10000. - Raise the royalty on the asset to 100% (`AssetTrigger / UpdateRoyal
References
Related vulnerabilities
All Supply chain →- MEDIUMGHSA-2vh6-hw4j-32ww
gix-packetline: reachable panic on empty side-band packet (pre-auth network DoS)
- HIGHGHSA-mf7q-r4rv-jv94
Crossplane's TOCTOU between cosign verification and image fetch in xpkg.CachedClient allows tag-based package install to bypass signature check
- MEDIUMCVE-2026-55535
PraisonAI vulnerable to Server-Side Request Forgery via DNS rebinding bypass in webhook_url validation
- HIGHCVE-2026-55537
PraisonAI: Webhook SSRF via DNS fail-open in `JobSubmitRequest.validate_webhook_url()` — bypass of CVE-2026-40114
- HIGHCVE-2026-55524
praisonaiagents vulnerable to SSRF in web_crawl tool via redirect-following and DNS rebinding (validate-then-fetch gap)
- MEDIUMCVE-2026-70667
Lemur: SSRF protection in certificate revocation checking bypassable via HTTP redirects and DNS rebinding (incomplete fix for GHSA-54vg-pfh7-jq95)