Description
function _deposit(uint256 streamId, uint128 amount) internal {
if (amount == 0) {
revert Errors.SablierFlow_DepositAmountZero(streamId);
}
IERC20 token = _streams[streamId].token;
_streams[streamId].balance += amount;
unchecked {
aggregateBalance[token] += amount;
}
token.safeTransferFrom({ from: msg.sender, to: address(this), value: amount });
emit ISablierFlow.DepositFlowStream({ streamId: streamId, funder: msg.sender, amount: amount });
}
The function _deposit
is defected due to the fact _streams[streamId].balance
and aggregateBalance[token]
are updated before token.safeTransferFrom
Impact
1) Failed Transfer Leading to Inconsistent State:
If the safeTransfer
fails (e.g., due to insufficient balance, incorrect approval, or a transfer rejection by the token contract), the function would typically revert. However, if the state has already been updated, it could lead to an inconsistent state, as the contract would have already recorded the deposit even though the transfer did not actually occur.
In this case, the streams' balance would be incremented without actually paying off the debt, resulting in a situation where the contract thinks the debt has been reduced when it hasn't.
2) Reentrancy Exploit Scenario:
If the token contract is malicious or has a fallback that calls back into the contract, an attacker could exploit this to re-enter the deposit function and manipulate the state in unintended ways
3) Debtor could use tokens with:
Blacklisting
Pausing
Rebasing mechanisms
Proof of Concepts
1)
contract DebtorAttack1 {
VulnerableStreamDeposit private stream;
IERC20 private token;
uint256 private streamId;
function attack(uint256 _streamId, uint128 amount) external {
stream.deposit(_streamId, amount);
}
}
2)
contract MaliciousToken is IERC20 {
mapping(address => uint256) private _balances;
VulnerableStreamDeposit private stream;
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool) {
revert("Transfer blocked");
}
}
3)
contract ReentrantToken is IERC20 {
VulnerableStreamDeposit private stream;
uint256 private streamId;
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool) {
if (to == address(stream)) {
_mint(from, amount);
stream.deposit(streamId, uint128(amount));
_burn(from, amount);
}
return true;
}
}
Recommended mitigation
-
Transfer before state updates
-
Verify actual received amounts
-
Update state based on verified amounts
function _deposit(uint256 streamId, uint128 amount) internal {
if (amount == 0) revert("Amount zero");
IERC20 token = _streams[streamId]token;
uint256 balanceBefore = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), amount);
uint256 received = token.balanceOf(address(this)) - balanceBefore;
if (received != amount) revert("Invalid transfer amount");
stream.balance += amount;
aggregateBalance[token] += amount;
emit DepositFlowStream(streamId, msg.sender, amount);
}