Insecure functionality In github.com/klever-io/klever-go
Description
Klever: Marketplace settlement mints KLV when referral % + royalty % exceed the bid (negative seller share silently skipped) ## 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: Sell combined check (market.go:908), order snapshot of referral but not royalty (market.go:997), live royalty mutation via core/kapp/kda/trigger.go — handleUpdateRoyaltiesNFTandSFT (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.go — executeBuyMarket (L575+): go 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: go 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%: go 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): go 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): bash 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 0 — 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. 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
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Component | Affected version | Patched versions |
|---|---|---|---|
go | 1.7.19 |
Aliases
References