Summary
There are no checks implemented to make sure that the broker address is not the same as the recipient or the sender address when calling the depositViaBroker function on a stream.
Vulnerability Details
The depositViaBroker function allows the depositors to pay a certain fee to the broker while depositing into the stream.
function depositViaBroker(
uint256 streamId,
uint128 totalAmount,
address sender,
address recipient,
Broker calldata broker
)
external
override
noDelegateCall
notNull(streamId)
notVoided(streamId)
updateMetadata(streamId)
{
_verifyStreamSenderRecipient(streamId, sender, recipient);
_depositViaBroker(streamId, totalAmount, broker);
}
function _depositViaBroker(uint256 streamId, uint128 totalAmount, Broker memory broker) internal {
(uint128 brokerFeeAmount, uint128 depositAmount) =
Helpers.checkAndCalculateBrokerFee(totalAmount, broker, MAX_FEE);
_deposit(streamId, depositAmount);
_streams[streamId].token.safeTransferFrom({ from: msg.sender, to: broker.account, value: brokerFeeAmount });
}
Accoring to the comments in the ISablierFlow.sol interface contract, a broker is defined as:
function depositViaBroker(
uint256 streamId,
uint128 totalAmount,
address sender,
address recipient,
Broker calldata broker
)
external;
Therefor, it makes no sense for the recipient or the sender to be the broker of a given stream as they are participating actors in the stream instead of being an actor that assists in creating the stream. However, lack of checks allow these addresses to also be the broker of the stream. This makes it possible for depositors to set the broker address as either the sender or the recipient address, breaking the logic of having a broker in the first place. This vulnerability makes it easier to:
1) Have accidental inputs to set broker as the recipient or the sender as they are also inputting these addresses when depositing.
2) Maliciously entering the recipient or the sender as the broker address to bypass the fees that should be given to the broker and acting like this is a mistake. As the depositors can not refund their deposited amount, they can't refund and deposit again with the correct broker address.
Keep in mind that there are no trust relationship between the depositors and the broker according to the Known Issues section.
It is assumed that a trust relationship is formed between the sender, recipient, and depositors participating in a stream.
Proof of Concept
Add the following test contract in the tests file and run it to observe the vulnerability. In order to run this test, add a mint function in the MockERC20.sol
pragma solidity >=0.8.22;
import { Test } from "forge-std/src/Test.sol";
import { console } from "forge-std/src/console.sol";
import "../src/SablierFlow.sol";
import "../src/interfaces/IFlowNFTDescriptor.sol";
import "./mocks/ERC20Mock.sol";
contract MockNFTDescriptor is IFlowNFTDescriptor {
function tokenURI(IERC721Metadata, uint256) external pure override returns (string memory) {
return "test_uri";
}
}
contract CustomTest is Test {
SablierFlow private flow;
ERC20Mock private token;
ERC20Mock private wbtc;
MockNFTDescriptor private nftDescriptor;
address private sender = address(0x1);
address private recipient = address(0x2);
address private admin = address(0x3);
address private depositor = address(0x4);
address private brokerA = address(0x5);
uint128 private amount = 200e8;
uint256 private streamId;
function setUp() public {
token = new ERC20Mock("MockToken", "MTK", 0);
wbtc = new ERC20Mock("Wrapped Bitcoin", "WBTC", 8);
nftDescriptor = new MockNFTDescriptor();
token.mint(depositor, amount);
wbtc.mint(depositor, amount);
flow = new SablierFlow(address(admin), nftDescriptor);
vm.startPrank(depositor);
token.approve(address(flow), amount);
wbtc.approve(address(flow), amount);
vm.stopPrank();
}
function testDepositViaBroker() public {
Broker memory broker = Broker({ account: recipient, fee: UD60x18.wrap(0.1e18) });
uint128 depositAmount = 200;
uint256 brokerFee = (depositAmount * 10) / 100;
vm.prank(sender);
uint256 streamId = flow.create(sender, recipient, ud21x18(1e1), token, true);
vm.prank(depositor);
flow.depositViaBroker(streamId, depositAmount, sender, recipient, broker);
uint256 recipientBalance = token.balanceOf(recipient);
assertEq(recipientBalance, brokerFee);
}
Impact
Impact: Medium as it makes it easier to maliciously or accidentally bypass the broker
Likelihood: Low
Tools Used
Manual review, foundry
Recommendations
Implement checks to make sure that the sender or the recipient address can not be the broker address.
Alternatively, in the stream creation, make the sender set the broker address and only allow depositViaBroker to be used for deposits. Use the address set at the stream creation instead of taking user input for this address.
An example fix is shown below.
function _depositViaBroker(uint256 streamId, uint128 totalAmount, Broker memory broker) internal {
if (broker.account == _ownerOf(streamId) || broker.account == _streams[streamId].sender,){
revert Errors.SablierFlow_InvalidAddress(broker.account);
}