External calls
All external calls the RouteProcessor3 contract makes are on behalf of users. The external calls are public functions anyone can call without going through RouteProcessor3. This means the only functional difference is the msg.sender
, which is set to RouteProcessor3 when calling an external function through the contract.
In order to steal user funds exploiting RouteProcessor3, the attacker must find an external call where calling it through RouteProcessor3 and calling it directly gives a different result. That is, msg.sender == RouteProcessor3
gives special privileges.
We make the following assumptions:
External contracts behave following a common convention. We assume that, for example,
IERC20.balanceOf
will not send funds to the caller.The RouteProcessor3 is not meant to hold funds between transactions. This is part of the design.
We will now analyze all external calls made by this contract. The following are all the non--state-mutating external calls made by RouteProcessor3.
IERC20(tokenOut).balanceOf(to)
bentoBox.strategyData(tokenIn).balance
bentoBox.totals(tokenIn).elastic
bentoBox.balanceOf(tokenIn, address(this));
IUniswapV2Pair(pool).getReserves();
These functions cannot mutate state and therefore cannot be directly used in an attack.
The following function calls transfer funds from the RouteProcessor3 itself.
IERC20(wrapToken).safeTransfer(to, amountIn);
IERC20(tokenIn).safeTransfer(address(bentoBox), amountIn);
IERC20(tokenIn).safeTransfer(pool, amountIn);
IERC20(tokenIn).safeTransfer(msg.sender, uint256(amount));
These function calls cannot be used in an attack since it only transfers from RouteProcessor3 itself, which is not supposed to hold funds.
The following function calls perform some action as the RouteProcessor3.
IERC20Permit(tokenIn).safePermit(msg.sender, address(this), value, deadline, v, r, s);
The permit must be constructed by the msg.sender
, since the signature is checked.
IWETH(wrapToken).deposit{value: amountIn}();
IUniswapV2Pair(pool).swap(amount0Out, amount1Out, to, new bytes(0));
IPool(pool).swap(swapData);
IUniswapV3Pool(pool).swap(
recipient,
zeroForOne,
int256(amountIn),
zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1,
abi.encode(tokenIn)
);
The following function calls transfer funds on behalf of a known address.
bentoBox.deposit(tokenIn, address(bentoBox), to, amountIn, 0);
bentoBox.withdraw(tokenIn, address(this), to, 0, amountIn);
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), uint256(amountIn));
These calls cannot be used to attack users since the payer is not controlled.
The following function calls transfer funds on behalf of the from
address.
IERC20(tokenIn).safeTransferFrom(from, address(this), amountIn);
IERC20(tokenIn).safeTransferFrom(from, address(bentoBox), amountIn);
bentoBox.transfer(tokenIn, from, address(this), amountIn);
IERC20(tokenIn).safeTransferFrom(from, pool, amountIn);
bentoBox.transfer(tokenIn, from, pool, amountIn);
The following are all sources of the from
variable.
distributeAndSwap(stream, address(this), NATIVE_ADDRESS, amountTotal);
distributeAndSwap(stream, address(this), token, amountTotal);
distributeAndSwap(stream, msg.sender, token, amountTotal);
swap(stream, address(this), token, 0);
swap(stream, address(this), token, amount);
The second parameter is the from
variable. The from
is either address(this)
or msg.sender
, which means the contract will not abuse another user's authorization.
Since all external calls do not grant any special privileges, the attacker cannot abuse this contract to attack other users.