Bug Description
Found a math error in x/oracle/types/ballot.go line 112. When total validator power is odd, the weighted median picks a price with less than 50% support.
What is broken:
The code does if pivot >= (totalPower / 2) to check if we hit majority. But integer division rounds down, so with 101 total power, threshold becomes 50 instead of 51.
Which module is affected:
Oracle module (x/oracle/types)
Why is this a bug:
Say total power is 101. Integer division gives us 101 / 2 = 50. So a validator with exactly 50 power (which is 49.5%) gets picked as the median. That's not a majority - you need more than 50% to have majority, not exactly 50%.
The comment on line 97 even says "equal or major to 50% of total power" but 50 out of 101 is 49.5%, not 50%.
Steps to Reproduce
- Set up validators with odd total power (like 101)
- Two validators submit oracle prices:
- Validator A: 50 power, votes $1.00
- Validator B: 51 power, votes $2.00
- Oracle tally runs in EndBlocker
- Median picks $1.00 even though that validator only has 49.5% of power
Code location:
https://github.com/KiiChain/kiichain/blob/cc9754a/x/oracle/types/ballot.go#L112
func (ex ExchangeRateBallot) WeightedMedianWithAssertion() sdkMath.LegacyDec {
totalPower := ex.Power()
if ex.Len() > 0 {
pivot := int64(0)
for _, vote := range ex {
pivot += vote.Power
if pivot >= (totalPower / 2) { // ❌ Wrong for odd numbers
return vote.ExchangeRate
}
}
}
return sdkMath.LegacyZeroDec()
}
Expected Behavior
Should need more than 50% to pick a median. With 101 total power, need at least 51 to be selected.
Actual Behavior
Right now with 101 total power:
threshold = 101 / 2 = 50
Validator A has 50 power, votes $1.00
- pivot = 50
- Check: 50 >= 50 → passes
- Returns $1.00 ✗
But 50 out of 101 is only 49.5%, not a majority.
Should be:
- pivot = 50
- Check: 50 > 50 → fails
- Keep going
- pivot = 101 (after B)
- Check: 101 > 50 → passes
- Returns $2.00 ✓
This happens every vote period when oracle does price tally.
Environment
- Kiichain version / commit: v6.1.0 /
cc9754a (verified latest)
- Network: Affects all networks (local / testnet / mainnet)
- File:
x/oracle/types/ballot.go
- Lines: 112
- Function:
WeightedMedianWithAssertion
Impact Assessment
Consensus failure: Maybe - if validators disagree on which median to pick
Oracle manipulation: Yes - someone with exactly 50% power can force their price
Security risk: High - oracle consensus broken with 50% instead of needing >50%
This runs every vote period in EndBlocker. When total power is odd, a validator with exactly half can manipulate the oracle price even though they don't have real majority.
Severity: HIGH
Suggested Fix
Just change >= to >:
// Before
if pivot >= (totalPower / 2) {
return vote.ExchangeRate
}
// After
if pivot > (totalPower / 2) {
return vote.ExchangeRate
}
Or do ceiling division:
threshold := (totalPower + 1) / 2
if pivot >= threshold {
return vote.ExchangeRate
}
Validation
- ✅ Code still vulnerable in latest commit
cc9754a
- ✅ Not by design (mathematical error in majority calculation)
- ✅ Tested by code review and mathematical analysis
Reporter Declaration
By submitting this issue, I confirm that:
Bug Description
Found a math error in
x/oracle/types/ballot.goline 112. When total validator power is odd, the weighted median picks a price with less than 50% support.What is broken:
The code does
if pivot >= (totalPower / 2)to check if we hit majority. But integer division rounds down, so with 101 total power, threshold becomes 50 instead of 51.Which module is affected:
Oracle module (
x/oracle/types)Why is this a bug:
Say total power is 101. Integer division gives us
101 / 2 = 50. So a validator with exactly 50 power (which is 49.5%) gets picked as the median. That's not a majority - you need more than 50% to have majority, not exactly 50%.The comment on line 97 even says "equal or major to 50% of total power" but 50 out of 101 is 49.5%, not 50%.
Steps to Reproduce
Code location:
https://github.com/KiiChain/kiichain/blob/cc9754a/x/oracle/types/ballot.go#L112
Expected Behavior
Should need more than 50% to pick a median. With 101 total power, need at least 51 to be selected.
Actual Behavior
Right now with 101 total power:
This happens every vote period when oracle does price tally.
Environment
cc9754a(verified latest)x/oracle/types/ballot.goWeightedMedianWithAssertionImpact Assessment
Consensus failure: Maybe - if validators disagree on which median to pick
Oracle manipulation: Yes - someone with exactly 50% power can force their price
Security risk: High - oracle consensus broken with 50% instead of needing >50%
This runs every vote period in EndBlocker. When total power is odd, a validator with exactly half can manipulate the oracle price even though they don't have real majority.
Severity: HIGH
Suggested Fix
Just change
>=to>:Or do ceiling division:
Validation
cc9754aReporter Declaration
By submitting this issue, I confirm that: