Attacker can front run user transactions and steal user funds
Report ID
#26070
Report type
Smart Contract
Has PoC?
Yes
Target
Impacts
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Bug Description
Although the shift () is used when doing a multi-hop swap in 1 single transaction to reduce costs by shifting tokens from one Well to another rather than returning them to the multicall router.
The same shift() function can be exploited by malicious actors to steal user funds because of its weak access controls.
function shift(
IERC20 tokenOut,
uint256 minAmountOut,
address recipient
) external nonReentrant returns (uint256 amountOut) {
IERC20[] memory _tokens = tokens();
uint256 tokensLength = _tokens.length;
_updatePumps(tokensLength);
The malicious attacker can front run transactions being carried out by users who are using the router + shift() route and steal user tokens.
The same function can be exploited by attacker because the inputs are not validated and they are processed without confirming the source or validity of the inputed parameters
Impact
Critical as user funds are lost
Risk Breakdown
Difficulty to Exploit: Easy
Weakness: High
CVSS2 Score: 10
Recommendation
Access modifier or require statement whereby the shift() function can only be called by the router
function shift(
IERC20 tokenOut,
uint256 minAmountOut,
address recipient
) external onlyRole(Router) nonReentrant returns (uint256 amountOut) {
require (isAdmin(Router), "must have admin privileges" )
}
References
Proof of concept
Step a- A User named bobs utilizes the multi-hop swap of the pipeline multi-call contract.
Step b- Bob engages the router + Shift route to save gas
This router + shift route engages the balances of the pool whereby if there is a change in token balances relative to the currently stored reserves, the extra tokens are shifted into tokenOut
.
Step C - unknown to bob an attacker who is monitoring the well transactions gains advantage of the changes in token balances and invokes the shift() function also passing an IERC20 token similar to bobs own with their own address as recipient to steal user funds.
** Attacker can exploit this vulnerability perpetually under the nose of the protocol and continues to siphon user funds.**
This POC test that shows an attacker can advantage of the changes in token balances to steal user funds
function test_shift_tokenOut() public prank(user) {
amount = bound(amount, 1, 1000e18);
// Transfer `amount` of token0 to the Well
tokens[0].transfer(address(well), amount);
Balances memory wellBalanceBeforeShift = getBalances(address(well), well);
assertEq(wellBalanceBeforeShift.tokens[0], 1000e18 + amount, "Well should have received tokens");
// Get a user with a fresh address (no ERC20 tokens)
address _user = users.getNextUserAddress();
Balances memory userBalanceBeforeShift = getBalances(_user, well);
// Verify that the user has no tokens
assertEq(userBalanceBeforeShift.tokens[0], 0, "User should start with 0 of token0");
assertEq(userBalanceBeforeShift.tokens[1], 0, "User should start with 0 of token1");
uint256 minAmountOut = well.getShiftOut(tokens[0]);
uint256[] memory calcReservesAfter = new uint256[](2);
calcReservesAfter[0] = tokens[0].balanceOf(address(well)) - minAmountOut;
calcReservesAfter[1] = tokens[1].balanceOf(address(well));
vm.expectEmit(true, true, true, true);
emit Shift(calcReservesAfter, tokens[0], minAmountOut, _user);
// Shift the imbalanced token as the token out
well.shift(tokens[0], 0, _user);
uint256[] memory reserves = well.getReserves();
Balances memory userBalanceAfterShift = getBalances(_user, well);
Balances memory wellBalanceAfterShift = getBalances(address(well), well);
// User should have gained token0
assertEq(userBalanceAfterShift.tokens[0], amount, "User should have gained token0");
assertEq(
userBalanceAfterShift.tokens[1], userBalanceBeforeShift.tokens[1], "User should NOT have gained token1"
);
BIC Response
This is not a valid bug report because shift
should be used with a multicall contract, rather than two individual transactions. The report and PoC describe user error / intended behavior.
Due to these reasons, we are closing the submission and no reward will be issued.