One Shot: Reloaded

First Flight #47
Beginner FriendlyNFT
100 EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

[M-01] Integer Underflow in Staking Rewards Calculation

Root + Impact

Description

The staking rewards calculation in streets::unstake uses integer division that truncates fractional days, causing users to lose rewards for staking periods under 24 hours. This creates unfair reward distribution where users staking for 23.9 hours receive zero rewards.

The calculation staked_duration / 86400 uses integer division that discards any remainder, effectively rounding down to the nearest whole day and penalizing users for partial day staking.

// In streets.move
@> let staked_duration = timestamp::now_seconds() - stake_info.start_time_seconds;
@> let days_staked = staked_duration / 86400; // Integer division truncates

Risk

Likelihood:

  • Users staking for less than 24 hours will receive no rewards due to integer truncation

  • The truncation occurs on every unstake operation, affecting all users

  • The issue affects every staking period that isn't an exact multiple of 24 hours

Impact:

  • Unfair reward distribution penalizing users for partial day staking

  • Economic inefficiency where protocol doesn't compensate shorter staking periods

  • User frustration from lost rewards for near-full day staking

Proof of Concept

This PoC demonstrates the exact reward loss due to integer truncation:

// Detailed demonstration of reward truncation
let stake_start = 1757755000; // Example start timestamp
let test_cases = [
(86399, "23.99 hours"), // Should get ~1 day reward, gets 0
(86400, "24.00 hours"), // Gets 1 day reward
(172799, "47.99 hours"), // Should get ~2 days, gets 1
(172800, "48.00 hours") // Gets 2 days reward
];
// Test each case
for (duration, description) in test_cases {
let days_staked = duration / 86400;
let actual_reward = days_staked * 4; // 4 CRED per day max
// Demonstrates the truncation issue
print!("Duration: {}, Expected: ~{}, Actual: {}",
description,
(duration as f64 / 86400.0 * 4.0) as u64,
actual_reward);
// 23.99 hours gets 0 instead of ~3.99 CRED
// 47.99 hours gets 4 instead of ~7.99 CRED
}

Recommended Mitigation

The mitigation provides multiple options to address the truncation issue:

- let days_staked = staked_duration / 86400;
+ // Option 1: Proportional rewards with proper rounding
+ let reward_amount = (staked_duration * 4 * 1000) / 86400; // Scale for precision
+ let actual_reward = reward_amount / 1000; // Scale back down
+
+ // Option 2: Fair rounding with minimum threshold
+ let days_staked = (staked_duration + 43200) / 86400; // Round to nearest day
+ assert!(staked_duration >= 3600, E_MINIMUM_STAKE_DURATION); // 1 hour minimum
+
+ // Option 3: Proportional rewards without truncation
+ let reward_per_second = 4000000 / 86400; // 4 CRED per day in micro-units
+ let total_reward = staked_duration * reward_per_second / 1000000;
+
// Apply rewards based on actual calculation
if (days_staked >= 1) { cred_token::mint(module_owner, staker_addr, actual_reward); };

This mitigation provides fair compensation for all staking durations by eliminating the harsh truncation at 24-hour boundaries.

Updates

Lead Judging Commences

bube Lead Judge 16 days ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.