Flow

Sablier
FoundryDeFi
20,000 USDC
View results
Submission Details
Severity: medium
Invalid

Invariant check is not implemented in the code

Summary

The protocol has an invariant that requires token.balanceOf(SablierFlow) >= aggregateBalance[token], but lacks explicit validation in the code. While this may work with standard ERC20s, it could break with non-standard tokens.

Please do note that I am aware of the tokens like FoT, and rebase tokens are OOS but the invariant can be breaked if these tokens are used because it is not handled in the code.

Vulnerability Details

The invariant check is missing from withdraw, deposit, and `refund`

Consider this real-world analogy:

Bank System:
- Ledger Balance (aggregateBalance): What bank thinks it has
- Actual Cash (token.balanceOf): What's actually in vault
- Requirement: Actual Cash >= Ledger Balance

Current Implementation Problem:

function _deposit(uint256 streamId, uint128 amount) internal {
// PROBLEM: Updates ledger before checking vault
aggregateBalance[token] += amount; // Ledger: +100
token.safeTransferFrom(..., amount); // Vault: might get less than 100
}

Real World Example:

// Initial State
Ledger (aggregateBalance): 1000 USDT
Vault (token.balanceOf): 1000 USDT
// Deposit 100 USDT with 1% fee token
1. Updates Ledger: 1000 + 100 = 1100 USDT
2. Actual Transfer: 99 USDT received (1% fee)
3. Final State:
Ledger: 1100 USDT
Vault: 1099 USDT
// Bank thinks it has more than it does!

Impact

HIGH - Like a protocol promising more money than it has:

  1. Token balance inconsistencies can occur

  2. Deflationary/fee-on-transfer tokens could break accounting

  3. Protocol could promise more tokens than it has

  4. Multiple streams could become insolvent

Proof of concept

Let's do some pseudo code PoC

function testInvariantBreak() public {
// Setup fee token (1% fee)
FeeToken token = new FeeToken();
// Create stream
uint256 streamId = flow.create(sender, recipient, rate, token, true);
// Initial deposit 1000
flow.deposit(streamId, 1000);
// Contract gets 990, but aggregateBalance = 1000
// Check invariant
assertLt(
token.balanceOf(address(flow)), // 990
flow.aggregateBalance(token) // 1000
);
}

Recommendation

Add a generic invariant validation function like below and call it in all functions where it is needed e.g in withdraw, refund and deposit.

function _validateBalanceInvariant(IERC20 token) internal view {
uint256 actualBalance = token.balanceOf(address(this));
uint256 expectedMinBalance = aggregateBalance[token];
require(
actualBalance >= expectedMinBalance,
"Balance invariant violation"
);
}

This ensures:

  1. Actual token balance ≥ tracked balance

  2. Accounting remains correct with non-standard tokens

  3. Early detection of balance inconsistencies

  4. Protocol solvency guarantee

Updates

Lead Judging Commences

inallhonesty Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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