DEV Community

rim dinov
rim dinov

Posted on

Finding a Critical Logic Flaw in Legion Protocol’s Epoch Vesting

By RimDinov (@rdin777)

While performing a deep-dive security audit of the Legion Protocol, I identified a critical vulnerability in their linear epoch-based vesting contract. This flaw isn't just a minor edge case — it’s a fundamental logic error that can lead to permanent loss of user funds and broken protocol invariants.

In this article, I’ll break down how the vulnerability works, why the math fails, and how I built a Proof-of-Concept (PoC) using Foundry to prove it.

  1. The Architecture: Epoch vs. Linear Vesting Most vesting contracts use a simple linear formula based on block.timestamp. However, Legion implemented an Epoch-based approach. Tokens are unlocked in "chunks" (epochs) rather than every second.

While this design can be useful for certain tokenomics, its implementation in LegionLinearEpochVesting.sol introduced a dangerous state dependency.

  1. The Vulnerability: State-Dependent Vesting Math The core issue lies in the _vestingSchedule function. Instead of being a pure function of time, the amount of vested tokens is calculated based on the global state variable s_lastClaimedEpoch.

The "Double-Claim" Trap
Take a look at the vulnerable code snippet:

Solidity
// @notice Vulnerable calculation in LegionLinearEpochVesting.sol
if (currentEpoch > s_lastClaimedEpoch) {
amountVested = ((currentEpoch - 1) * _totalAllocation) / s_numberOfEpochs;
}
The Logic Flaw: If a user (or bot) calls the release() function twice during the same epoch:
The first time currentEpoch > s_lastClaimedEpoch - the transaction goes through, tokens are transferred, s_lastClaimedEpoch is updated.

The second time, currentEpoch becomes EQUAL to s_lastClaimedEpoch. The if condition fails, and amountVested returns 0.

In certain scenarios, this may cause the contract to consider tokens "issued" even though the user received 0.

  1. The "Dust" Problem: Precision Loss Another critical issue is the handling of token decimals. The contract uses a fixed 1e18 denominator (likely from a Constants.sol file) without properly scaling for tokens like USDC or USDT (6 decimals).

This causes massive rounding errors. For low-decimal tokens, a significant portion of the funds (the "dust") will remain stuck in the contract forever because the math simply truncates the value to zero.

  1. Proof of Concept (PoC) To verify the impact, I wrote a dedicated exploit test in Foundry. The test simulates a user trying to claim multiple times and proves that funds become inaccessible due to the epoch-tracking logic.

You can find the full PoC and the audit repository here:
👉 https://github.com/rdin777/ltgion-newaudit

Pro-Tip: Always run your tests with forge test -vvvv to see the exact state changes during the exploit.

  1. Mitigation: How to Fix It To fix these issues, I recommended the following to the Legion team:

Remove State Dependency: Calculate the vested amount based strictly on block.timestamp and the totalAllocated, subtracting only the totalReleased so far.

Dynamic Scaling: Use ERC20(token).decimals() to handle precision instead of hardcoded constants.

Conclusion
Smart contract security is about more than just finding reentrancy or overflow bugs. It's about understanding the business logic. Even a simple vesting contract can hide critical flaws if the state management isn't handled with extreme care.

Follow me for more security deep-dives:

GitHub: https://github.com/rdin777

Top comments (0)