F-2025-0005·incorrect-arithmetic

Incorrect Reward Rate Selection in Cross-Halving Distributions Causes Token Underemission

Fixedliquid-stakinglststaking-poolsgithub.com/matchain/contracts
TL;DR

After the IMM-HIGH-01 fix, rewardDistribution uses getBlockReward(nextStep) (the rate at the END of each segment) instead of the rate at the start. Distributions crossing a halving boundary still emit ~33% fewer tokens than expected.

Severity
HIGH
Impact
HIGH
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
LOW
Exploitability
MEDIUM
02Section · Description

Description

In the rewardDistribution() function of the MATToken contract, the reward rate is incorrectly used when distribution crosses a halving boundary. This issue was introduced by the following pull request: https://github.com/matchain/contracts/pull/10, which was initially intended to address the IMM-HIGH-1 finding.

Specifically, the error is in these lines:

solidity
uint256 rewardAmount = getBlockReward(nextStep) * (nextStep - _lastRewardDistributionBlock);

And inside the while loop:

solidity
rewardAmount += getBlockReward(nextStep) * (nextStep - oldNextStep);

The problem is as follows:

  • In getBlockReward(nextStep), the reward rate of the next halving period is applied to all blocks in the current segment.
  • nextStep is the end of the segment, which in many cases already belongs to the next halving period with a reduced rate.
  • It should use the rate from the beginning of the segment (_lastRewardDistributionBlock or oldNextStep).
  • Since the reward rate decreases in each halving period, using the rate from the next period results in a lower emission than it should be.

The reward rate is constant within a single halving period but decreases by half between different halving periods. The problem occurs because the code uses the reward rate from the end of each segment (nextStep) for the entire segment calculation. When nextStep is the halving block or later, getBlockReward(nextStep) returns the reduced post-halving rate. This reduced rate is then incorrectly applied to all blocks in the segment, including those that occurred before the halving and should have received the higher rate.

03Section · Impact

Impact

A test simulating cross-halving distribution measured a 33% underemission compared to the expected average rate.

code
CASE 2: Distribution crossing halving
Blocks processed: 100
Tokens emitted: 8
Emission rate per block (scaled): 83 * 10^-3
ANALYSIS OF THE BUG:
Expected average rate (scaled): 124 * 10^-3
Expected emission crossing halving: 12
Actual emission crossing halving: 8
Difference of emission: 4
Percentage of underemission: 33 %

For a cross-halving period processing 100 blocks (50 before halving + 50 after), the emission rate matches Case 3 (post-halving) instead of being the average between Case 1 (pre-halving) and Case 3, resulting in 33% fewer tokens than the economic model intends.

04Section · Recommendation

Recommendation

Modify the rewardDistribution() function to use the reward rate from the beginning of each segment:

solidity
// For the first segment
uint256 rewardAmount = getBlockReward(_lastRewardDistributionBlock) * (nextStep - _lastRewardDistributionBlock);
// For subsequent segments in the while loop
rewardAmount += getBlockReward(oldNextStep) * (nextStep - oldNextStep);
Status
Fixed
Fix commit
7a68ac78c947
Tracking
F-2025-0005

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx