From 078dae83b14b474bbb5ab015493a8e3ecc62a296 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 28 Jan 2026 23:43:54 +0000 Subject: [PATCH 1/9] init --- src/abstract/OrderBookV6RaindexRouter.sol | 179 +++++++++ .../arb/RaindexRouterOrderBookV6Arb.sol | 119 ++++++ src/concrete/ob/OrderBookV6.sol | 7 +- .../arb/RaindexRouterOrderBookV6Arb.t.sol | 370 ++++++++++++++++++ 4 files changed, 674 insertions(+), 1 deletion(-) create mode 100644 src/abstract/OrderBookV6RaindexRouter.sol create mode 100644 src/concrete/arb/RaindexRouterOrderBookV6Arb.sol create mode 100644 test/concrete/arb/RaindexRouterOrderBookV6Arb.t.sol diff --git a/src/abstract/OrderBookV6RaindexRouter.sol b/src/abstract/OrderBookV6RaindexRouter.sol new file mode 100644 index 0000000000..c558698c96 --- /dev/null +++ b/src/abstract/OrderBookV6RaindexRouter.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.19; + +import {ERC165, IERC165} from "openzeppelin-contracts/contracts/utils/introspection/ERC165.sol"; +import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "openzeppelin-contracts/contracts/utils/Address.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; +import {ON_FLASH_LOAN_CALLBACK_SUCCESS} from "rain.orderbook.interface/interface/ierc3156/IERC3156FlashBorrower.sol"; +import { + IOrderBookV6, + TakeOrdersConfigV5, + TaskV2, + Float +} from "rain.orderbook.interface/interface/unstable/IOrderBookV6.sol"; +import {IERC3156FlashBorrower} from "rain.orderbook.interface/interface/ierc3156/IERC3156FlashBorrower.sol"; +import {OrderBookV6ArbConfig, OrderBookV6ArbCommon} from "./OrderBookV6ArbCommon.sol"; +import {LibOrderBookArb} from "../lib/LibOrderBookArb.sol"; +import {LibTOFUTokenDecimals} from "rain.tofu.erc20-decimals/lib/LibTOFUTokenDecimals.sol"; +import {LibDecimalFloat} from "rain.math.float/lib/LibDecimalFloat.sol"; + +/// Thrown when the initiator is not the order book. +/// @param badInitiator The untrusted initiator of the flash loan. +error BadInitiator(address badInitiator); + +/// Thrown when the flash loan fails somehow. +error FlashLoanFailed(); + +/// Thrown when the swap fails. +error SwapFailed(); + +/// @title OrderBookV6RaindexRouter +/// @notice Abstract contract that liq-source specifialized contracts can inherit +/// to provide flash loan based routed arbitrage against external liquidity sources to +/// fill orderbook orders. +/// +/// For example consider circuit: +/// +/// start input = DAI +/// start output = USDC +/// external source router = USDC -> USDT +/// end input = USDT +/// end output = DAI +/// +/// Assume external liq can exchange USDC to USDT, so 2 raindex orders can be traded +/// against eachother while their IO are NOT mirror: +/// +/// - Flash loan 100 DAI from `Orderbook` +/// - Take the first order of the circuit (flash loan amount (DAI) goes as input and the order's output (USDC) is taken) +/// - Sell the first order's output (USDC) for market price through the external source (sushi, balancer, etc) for USDT +/// - Take the last order of the circuit (given the USDT as input and take its DAI as output) +/// - The circuit is now closed so we can repay the flash loan amount (DAI) and keep the profit (it can be both USDT and DAI) +abstract contract OrderBookV6RaindexRouter is IERC3156FlashBorrower, ReentrancyGuard, ERC165, OrderBookV6ArbCommon { + using Address for address; + using SafeERC20 for IERC20; + + constructor(OrderBookV6ArbConfig memory config) OrderBookV6ArbCommon(config) {} + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC3156FlashBorrower).interfaceId || super.supportsInterface(interfaceId); + } + + /// Hook that inheriting contracts MUST implement in order to achieve + /// anything other than raising the ambient temperature of the room. + /// `_exchange` is responsible for converting the flash loaned assets into + /// the assets required to fill the orders. Generally this can only be + /// achieved by interacting with an external liquidity source that is + /// offering a better price than the orders require. + /// @param takeOrders As per `arb`. + /// @param exchangeData As per `arb`. + // slither-disable-next-line dead-code + function _exchange(TakeOrdersConfigV5[] memory takeOrders, bytes memory exchangeData) internal virtual {} + + /// @inheritdoc IERC3156FlashBorrower + function onFlashLoan(address initiator, address, uint256, uint256, bytes calldata data) + external + returns (bytes32) + { + // As per reference implementation. + if (initiator != address(this)) { + revert BadInitiator(initiator); + } + + (TakeOrdersConfigV5[] memory takeOrders, bytes memory exchangeData) = + abi.decode(data, (TakeOrdersConfigV5[], bytes)); + + // Dispatch the `_exchange` hook to ensure we have the correct asset + // type and amount to fill the orders. + _exchange(takeOrders, exchangeData); + + return ON_FLASH_LOAN_CALLBACK_SUCCESS; + } + + /// Primary function to process arbitrage opportunities. + /// Firstly the access gate is evaluated to ensure the sender is allowed to + /// submit arbitrage. If there is no access control the sender should expect + /// to be front run on the arb for any sufficiently profitable opportunity. + /// This may be desirable in some cases, as the sender may simply want to + /// be clearing the orderbook and they are expecting profit/utility from the + /// orderbook strategies themselves somehow. + /// + /// Secondly the flash loan is taken and the `_exchange` hook is called to + /// allow the inheriting contract to convert the flash loaned assets into + /// the assets required to fill the orders. + /// + /// Finally the orders are taken and the remaining assets are sent to the + /// sender. + /// + /// @param orderBook The orderbook address + /// @param takeOrders As per `IOrderBookV5.takeOrders3`. + /// @param exchangeData Arbitrary bytes that will be passed to `_exchange` + /// after the flash loan is taken. The inheriting contract is responsible + /// for decoding this data and defining how it controls interactions with + /// the external liquidity. For example, `GenericPoolOrderBookV5FlashBorrower` + /// uses this data as a literal encoded external call. + function arb4( + IOrderBookV6 orderBook, + TakeOrdersConfigV5[] memory takeOrders, + bytes calldata exchangeData, + TaskV2 calldata task + ) external payable nonReentrant onlyValidTask(task) { + // Mimic what OB would do anyway if called with zero orders. + if (takeOrders[0].orders.length == 0 || takeOrders[1].orders.length == 0) { + revert IOrderBookV6.NoOrders(); + } + + require(takeOrders.length == 2, "Unexpected take orders config length"); + + address startTakeOrdersInputToken = takeOrders[0].orders[0].order.validInputs[takeOrders[0].orders[0].inputIOIndex].token; + address endTakeOrdersInputToken = takeOrders[1].orders[0].order.validInputs[takeOrders[1].orders[0].inputIOIndex].token; + + require( + startTakeOrdersInputToken == takeOrders[1].orders[0].order.validOutputs[ + takeOrders[1].orders[0].outputIOIndex + ].token, + "start and end orders IO do NOT close the route circuit" + ); + + uint8 startInputDecimals = LibTOFUTokenDecimals.safeDecimalsForToken(startTakeOrdersInputToken); + uint8 endInputDecimals = LibTOFUTokenDecimals.safeDecimalsForToken(endTakeOrdersInputToken); + + // Take the flash loan, which will in turn call `onFlashLoan`, which is + // expected to process an exchange against external liq to pay back the + // flash loan, cover the orders and remain in profit. + // + // We take all the current balance of orderbook divided by 2 as loan, + // that's because its the max possible crealable amount, because the loan is + // taken before any takeOrders4() is processed, the loan goes for the input + // amount of first order of the circuit, and orderbook needs to have balance + // left to finish the last takeOrders4(), all this while the flash loan is + // still open (not repaid), after the last takeOrder4() is processed then + // the flash loan can be repaid, in order words half of the orderbook token + // balance is used for completing the first takeOrders4() as a flash loan + // and half for the last as flash loan repay + // Ofcourse if the half value exceeds max IO, the first order can be cleared + // in its full capacity + uint256 flashLoanAmount = IERC20(startTakeOrdersInputToken).balanceOf(address(orderBook)) / 2; + Float flashLoanAmountFloat = LibDecimalFloat.fromFixedDecimalLosslessPacked(flashLoanAmount, startInputDecimals); + + IERC20(startTakeOrdersInputToken).forceApprove(address(orderBook), 0); + IERC20(startTakeOrdersInputToken).forceApprove(address(orderBook), type(uint256).max); + + if (LibDecimalFloat.gt(takeOrders[0].maximumIO, flashLoanAmountFloat)) { + takeOrders[0].maximumIO = flashLoanAmountFloat; + } + takeOrders[0].IOIsInput = false; // must always be false + + bytes memory data = abi.encode(takeOrders, exchangeData); + + if (!orderBook.flashLoan(this, startTakeOrdersInputToken, flashLoanAmount, data)) { + revert FlashLoanFailed(); + } + IERC20(startTakeOrdersInputToken).forceApprove(address(orderBook), 0); + + LibOrderBookArb.finalizeArb(task, endTakeOrdersInputToken, endInputDecimals, startTakeOrdersInputToken, startInputDecimals); + } +} diff --git a/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol b/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol new file mode 100644 index 0000000000..fbd2aa1c2d --- /dev/null +++ b/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {IRouteProcessor} from "sushixswap-v2/src/interfaces/IRouteProcessor.sol"; +import {LibDecimalFloat} from "rain.math.float/lib/LibDecimalFloat.sol"; +import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IOrderBookV6, Float} from "rain.orderbook.interface/interface/unstable/IOrderBookV6.sol"; +import { + OrderBookV6RaindexRouter, + SafeERC20, + IERC20, + Address, + TakeOrdersConfigV5, + OrderBookV6ArbConfig +} from "../../abstract/OrderBookV6RaindexRouter.sol"; + +// Define possible route leg types +enum RouteLegType { + RAINDEX, + SUSHI, + BALANCER, + STABULL +} + +// data and destination address of a route leg +// the data field needs to be decoded based on the type +struct RouteLeg { + RouteLegType routeLegType; + address destination; + bytes data; +} + +contract RaindexRouterOrderBookV6Arb is OrderBookV6RaindexRouter { + using SafeERC20 for IERC20; + using Address for address; + + constructor(OrderBookV6ArbConfig memory config) OrderBookV6RaindexRouter(config) {} + + /// @inheritdoc OrderBookV6RaindexRouter + function _exchange(TakeOrdersConfigV5[] memory takeOrders, bytes memory exchangeData) internal virtual override { + + address prevLegTokenAddress = takeOrders[0].orders[0].order.validOutputs[takeOrders[0].orders[0].outputIOIndex].token; + (Float startLegTotalOutput, Float startLegTotalInput) = IOrderBookV6(msg.sender).takeOrders4(takeOrders[0]); + (startLegTotalInput); + + Float prevLegOutputAmount = startLegTotalOutput; + + if (exchangeData.length > 0) { + RouteLeg[] memory routeLegs = abi.decode(exchangeData, (RouteLeg[])); + + for (uint256 i = 0; i < routeLegs.length; i++) { + RouteLeg memory leg = routeLegs[i]; + + if (leg.routeLegType == RouteLegType.SUSHI) { + (prevLegOutputAmount, prevLegTokenAddress) = _processSushiLeg(leg, prevLegOutputAmount, prevLegTokenAddress); + } else if (leg.routeLegType == RouteLegType.RAINDEX) { + revert("raindex route leg type is not yet implemented"); + } else if (leg.routeLegType == RouteLegType.BALANCER) { + revert("balancer route leg type is not yet implemented"); + } else if (leg.routeLegType == RouteLegType.STABULL) { + revert("stabull route leg type is not yet implemented"); + } + } + } + + address endTakeOrdersInputToken = takeOrders[1].orders[0].order.validInputs[takeOrders[1].orders[0].inputIOIndex].token; + IERC20(endTakeOrdersInputToken).forceApprove(msg.sender, 0); + IERC20(endTakeOrdersInputToken).forceApprove(msg.sender, type(uint256).max); + + // set max io to previous leg output amount + if (LibDecimalFloat.gt(takeOrders[1].maximumIO, prevLegOutputAmount)) { + takeOrders[1].maximumIO = prevLegOutputAmount; + } + takeOrders[1].IOIsInput = false; // must always be false + + (Float finalLegTotalOutput, Float finalLegTotalInput) = IOrderBookV6(msg.sender).takeOrders4(takeOrders[1]); + (finalLegTotalOutput, finalLegTotalInput); + + IERC20(endTakeOrdersInputToken).forceApprove(msg.sender, 0); + } + + //slither-disable-next-line no-unused-vars + function _processSushiLeg( + RouteLeg memory routeLeg, + Float prevLegOutputAmount, + address prevLegTokenAddress + ) internal returns (Float, address) { + (address fromToken, address toToken, bytes memory route) = abi.decode(routeLeg.data, (address, address, bytes)); + + require(prevLegTokenAddress == fromToken, "token mismatch"); + + (uint256 fromTokenAmount, bool losslessInputAmount) = + LibDecimalFloat.toFixedDecimalLossy(prevLegOutputAmount, IERC20Metadata(fromToken).decimals()); + (losslessInputAmount); + + uint8 toTokenDecimals = IERC20Metadata(toToken).decimals(); + (uint256 toTokenAmount, bool lossless) = LibDecimalFloat.toFixedDecimalLossy(LibDecimalFloat.FLOAT_ZERO, toTokenDecimals); + if (!lossless) { + toTokenAmount++; + } + + IERC20(fromToken).forceApprove(routeLeg.destination, 0); + IERC20(fromToken).forceApprove(routeLeg.destination, type(uint256).max); + uint256 amountOut = IRouteProcessor(routeLeg.destination).processRoute( + fromToken, fromTokenAmount, toToken, toTokenAmount, address(this), route + ); + IERC20(fromToken).forceApprove(address(routeLeg.destination), 0); + + Float amountOutFloat = LibDecimalFloat.fromFixedDecimalLosslessPacked(amountOut, toTokenDecimals); + + return (amountOutFloat, toToken); + } + + /// Allow receiving gas. + fallback() external {} + + function a(RouteLeg calldata x) external {} +} diff --git a/src/concrete/ob/OrderBookV6.sol b/src/concrete/ob/OrderBookV6.sol index 7a39733598..6afb8f3938 100644 --- a/src/concrete/ob/OrderBookV6.sol +++ b/src/concrete/ob/OrderBookV6.sol @@ -504,7 +504,12 @@ contract OrderBookV6 is IOrderBookV6, IMetaV1_2, ReentrancyGuard, Multicall, Ord Float orderMaxInput = orderIOCalculation.IORatio.mul(orderIOCalculation.outputMax); takerOutput = orderMaxInput.min(remainingTakerIO); // This rounds down which favours the order/dex. - takerInput = takerOutput.div(orderIOCalculation.IORatio); + if (orderIOCalculation.IORatio.isZero()) { + // Zero ratio means order will empty its maxoutput + takerInput = orderIOCalculation.outputMax; + } else { + takerInput = takerOutput.div(orderIOCalculation.IORatio); + } remainingTakerIO = remainingTakerIO.sub(takerOutput); } diff --git a/test/concrete/arb/RaindexRouterOrderBookV6Arb.t.sol b/test/concrete/arb/RaindexRouterOrderBookV6Arb.t.sol new file mode 100644 index 0000000000..937ca4591a --- /dev/null +++ b/test/concrete/arb/RaindexRouterOrderBookV6Arb.t.sol @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; +import { + IOrderBookV6, + OrderV4, + OrderConfigV4, + TakeOrdersConfigV5, + TakeOrderConfigV4, + TaskV2, + Float, + IOV2 +} from "rain.orderbook.interface/interface/unstable/IOrderBookV6.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {OrderBookV6} from "src/concrete/ob/OrderBookV6.sol"; +import {RaindexRouterOrderBookV6Arb, RouteLeg, RouteLegType} from "src/concrete/arb/RaindexRouterOrderBookV6Arb.sol"; +import {OrderBookV6ArbConfig} from "src/abstract/OrderBookV6ArbCommon.sol"; +import {LibDecimalFloat} from "rain.math.float/lib/LibDecimalFloat.sol"; +import { + EvaluableV4, + IInterpreterStoreV3, + IInterpreterV4, + SignedContextV1 +} from "rain.interpreter.interface/interface/unstable/IInterpreterCallerV4.sol"; +import {TOFUTokenDecimals, LibTOFUTokenDecimals} from "rain.tofu.erc20-decimals/concrete/TOFUTokenDecimals.sol"; +import {RainterpreterParser} from "rain.interpreter/concrete/RainterpreterParser.sol"; +import {IParserV2} from "rain.interpreter.interface/interface/IParserV2.sol"; +import {Rainterpreter} from "rain.interpreter/concrete/Rainterpreter.sol"; +import {RainterpreterStore} from "rain.interpreter/concrete/RainterpreterStore.sol"; +import { + RainterpreterExpressionDeployer, + RainterpreterExpressionDeployerConstructionConfigV2 +} from "rain.interpreter/concrete/RainterpreterExpressionDeployer.sol"; +import {IRouteProcessor} from "sushixswap-v2/src/interfaces/IRouteProcessor.sol"; + +/// @title RaindexRouterOrderBookV6ArbTest +/// @notice Tests for the arb4() method in OrderBookV6RaindexRouter +contract RaindexRouterOrderBookV6ArbTest is Test { + using LibDecimalFloat for Float; + + OrderBookV6 internal orderBook; + RaindexRouterOrderBookV6Arb internal router; + + address internal alice; + address internal bob; + address internal arber; + + address internal tokenA; + address internal tokenB; + address internal tokenC; + + uint256 internal constant INITIAL_BALANCE = 1000e18; + bytes32 internal constant VAULT_ID = keccak256("vault"); + + IInterpreterV4 internal immutable iInterpreter; + IInterpreterStoreV3 internal immutable iStore; + RainterpreterParser internal immutable iParser; + IParserV2 internal immutable iParserV2; + IOrderBookV6 internal immutable iOrderbook; + MockSushiRP internal immutable sushi; + + constructor() { + // Put the TOFU decimals contract in place so that any calls to it + // succeed. This is because we don't have zoltu here. + vm.etch(address(LibTOFUTokenDecimals.TOFU_DECIMALS_DEPLOYMENT), type(TOFUTokenDecimals).runtimeCode); + + iInterpreter = IInterpreterV4(new Rainterpreter()); + iStore = IInterpreterStoreV3(new RainterpreterStore()); + iParser = new RainterpreterParser(); + iParserV2 = new RainterpreterExpressionDeployer( + RainterpreterExpressionDeployerConstructionConfigV2({ + interpreter: address(iInterpreter), + store: address(iStore), + parser: address(iParser) + }) + ); + + iOrderbook = IOrderBookV6(address(new OrderBookV6())); + sushi = new MockSushiRP(); + } + + function setUp() public { + // Deploy core contracts + orderBook = new OrderBookV6(); + + // Create test accounts + alice = makeAddr("alice"); + bob = makeAddr("bob"); + arber = makeAddr("arber"); + + // Deploy mock ERC20 tokens + tokenA = address(new Token("Token A", "TKNA")); + tokenB = address(new Token("Token B", "TKNB")); + tokenC = address(new Token("Token C", "TKNC")); + + // Setup router + OrderBookV6ArbConfig memory config = OrderBookV6ArbConfig({ + orderBook: address(orderBook), + task: TaskV2({ + evaluable: EvaluableV4({ + interpreter: iInterpreter, + store: iStore, + bytecode: new bytes(0) + }), + signedContext: new SignedContextV1[](0) + }), + implementationData: new bytes(0) + }); + + router = new RaindexRouterOrderBookV6Arb(config); + + // Fund accounts + deal(tokenA, alice, INITIAL_BALANCE); + deal(tokenB, bob, INITIAL_BALANCE); + + // Fund sushi RP for the swap + deal(tokenC, address(sushi), INITIAL_BALANCE); + + // Fund orderbook with some tokens + deal(tokenA, address(orderBook), INITIAL_BALANCE); + deal(tokenB, address(orderBook), INITIAL_BALANCE); + deal(tokenC, address(orderBook), INITIAL_BALANCE); + } + + /// @dev Test successful arb4() with simple two-order arbitrage + function testArb4Success() public { + // Alice creates order: sell tokenA for tokenB at 1:1.01 ratio + OrderV4 memory aliceOrder = createOrder( + alice, + tokenB, // input + tokenA, // output + "_ _: 100 0.2;:;" + ); + + // Bob creates order: sell tokenB for tokenA at 1:0.99 ratio + OrderV4 memory bobOrder = createOrder( + bob, + tokenC, // input + tokenB, // output + "_ _: 100 0.5;:;" + ); + + // Deposit tokens into vaults + vm.startPrank(alice); + IERC20(tokenA).approve(address(orderBook), type(uint256).max); + orderBook.deposit4( + tokenA, + VAULT_ID, + LibDecimalFloat.fromFixedDecimalLosslessPacked(100000000000000000000, 18), + new TaskV2[](0) + ); + vm.stopPrank(); + + vm.startPrank(bob); + IERC20(tokenB).approve(address(orderBook), type(uint256).max); + orderBook.deposit4( + tokenB, + VAULT_ID, + LibDecimalFloat.fromFixedDecimalLosslessPacked(100000000000000000000, 18), + new TaskV2[](0) + ); + vm.stopPrank(); + + // Add orders + vm.prank(alice); + orderBook.addOrder4( + createOrderConfig(aliceOrder), + new TaskV2[](0) + ); + + vm.prank(bob); + orderBook.addOrder4( + createOrderConfig(bobOrder), + new TaskV2[](0) + ); + + // Create take orders configs for arbitrage + TakeOrdersConfigV5 memory startTakeOrders = createTakeOrdersConfig( + aliceOrder, + 0, // inputIOIndex + 0, // outputIOIndex + LibDecimalFloat.FLOAT_MAX_POSITIVE_VALUE, // maximumIO + LibDecimalFloat.FLOAT_MAX_POSITIVE_VALUE, // maximumIORatio + false // IOIsInput + ); + + TakeOrdersConfigV5 memory endTakeOrders = createTakeOrdersConfig( + bobOrder, + 0, // inputIOIndex + 0, // outputIOIndex + LibDecimalFloat.FLOAT_MAX_POSITIVE_VALUE, // maximumIO + LibDecimalFloat.FLOAT_MAX_POSITIVE_VALUE, // maximumIORatio + false // IOIsInput + ); + TakeOrdersConfigV5[] memory takeOrders = new TakeOrdersConfigV5[](2); + takeOrders[0] = startTakeOrders; + takeOrders[1] = endTakeOrders; + + TaskV2 memory task = TaskV2({ + evaluable: EvaluableV4({ + interpreter: iInterpreter, + store: iStore, + bytecode: new bytes(0) + }), + signedContext: new SignedContextV1[](0) + }); + + RouteLeg[] memory routeLegs = new RouteLeg[](1); + routeLegs[0] = RouteLeg({ + routeLegType: RouteLegType.SUSHI, + destination: address(sushi), + data: abi.encode(tokenA, tokenC, new bytes(0)) + }); + bytes memory exchangeData = abi.encode(routeLegs); + + // Record balances before + uint256 orderBookTokenABefore = IERC20(tokenA).balanceOf(address(orderBook)); + uint256 orderBookTokenBBefore = IERC20(tokenB).balanceOf(address(orderBook)); + uint256 orderBookTokenCBefore = IERC20(tokenC).balanceOf(address(orderBook)); + uint256 arberTokenABefore = IERC20(tokenA).balanceOf(arber); + uint256 arberTokenBBefore = IERC20(tokenB).balanceOf(arber); + uint256 arberTokenCBefore = IERC20(tokenC).balanceOf(arber); + + // Execute arbitrage + vm.prank(arber); + router.arb4( + orderBook, + takeOrders, + exchangeData, + task + ); + + // Verify balances changed + uint256 orderBookTokenAAfter = IERC20(tokenA).balanceOf(address(orderBook)); + uint256 orderBookTokenBAfter = IERC20(tokenB).balanceOf(address(orderBook)); + uint256 orderBookTokenCAfter = IERC20(tokenC).balanceOf(address(orderBook)); + uint256 arberTokenAAfter = IERC20(tokenA).balanceOf(arber); + uint256 arberTokenBAfter = IERC20(tokenB).balanceOf(arber); + uint256 arberTokenCAfter = IERC20(tokenC).balanceOf(arber); + + assertNotEq(orderBookTokenABefore, orderBookTokenAAfter, "TokenA balance should change"); + assertNotEq(orderBookTokenBBefore, orderBookTokenBAfter, "TokenB balance should change"); + assertNotEq(orderBookTokenCBefore, orderBookTokenCAfter, "TokenC balance should change"); + + assertEq(arberTokenABefore, arberTokenAAfter, "arber TokenA balance should NOT change"); + + assertNotEq(arberTokenBBefore, arberTokenBAfter, "arber TokenB balance should change"); + assertNotEq(arberTokenCBefore, arberTokenCAfter, "arber TokenC balance should change"); + + assertEq(arberTokenBAfter, 8e19, "expected 80 token B for arber bounty"); + assertEq(arberTokenCAfter, 5e19, "expected 50 token C for arber bounty"); + + Float aliceOutputVaultBalanceFloat = orderBook.vaultBalance2(alice, tokenA, VAULT_ID); + Float aliceInputVaultBalanceFloat = orderBook.vaultBalance2(alice, tokenB, VAULT_ID); + uint256 aliceOutputVaultBalance = LibDecimalFloat.toFixedDecimalLossless(aliceOutputVaultBalanceFloat, 18); + uint256 aliceInputVaultBalance = LibDecimalFloat.toFixedDecimalLossless(aliceInputVaultBalanceFloat, 18); + assertEq(aliceOutputVaultBalance, 0, "expected zero vault balance for alice output vault"); + assertEq(aliceInputVaultBalance, 2e19, "expected 20 token B vault balance for alice output vault"); + + Float bobOutputVaultBalanceFloat = orderBook.vaultBalance2(bob, tokenB, VAULT_ID); + Float bobInputVaultBalanceFloat = orderBook.vaultBalance2(bob, tokenC, VAULT_ID); + uint256 bobOutputVaultBalance = LibDecimalFloat.toFixedDecimalLossless(bobOutputVaultBalanceFloat, 18); + uint256 bobInputVaultBalance = LibDecimalFloat.toFixedDecimalLossless(bobInputVaultBalanceFloat, 18); + assertEq(bobOutputVaultBalance, 0, "expected zero vault balance for bob output vault"); + assertEq(bobInputVaultBalance, 5e19, "expected 50 token C vault balance for alice output vault"); + } + + // Helper functions + function createOrder( + address owner, + address inputToken, + address outputToken, + string memory rl + ) internal view returns (OrderV4 memory) { + IOV2[] memory validInputs = new IOV2[](1); + validInputs[0] = IOV2({ + token: inputToken, + vaultId: VAULT_ID + }); + + IOV2[] memory validOutputs = new IOV2[](1); + validOutputs[0] = IOV2({ + token: outputToken, + vaultId: VAULT_ID + }); + + return OrderV4({ + owner: owner, + evaluable: EvaluableV4({ + interpreter: iInterpreter, + store: iStore, + bytecode: iParserV2.parse2(bytes(rl)) + }), + validInputs: validInputs, + validOutputs: validOutputs, + nonce: bytes32(0) + }); + } + + function createOrderConfig(OrderV4 memory order) + internal + pure + returns (OrderConfigV4 memory) + { + return OrderConfigV4({ + validInputs: order.validInputs, + validOutputs: order.validOutputs, + evaluable: order.evaluable, + meta: new bytes(0), + nonce: order.nonce, + secret: bytes32(0) + }); + } + + function createTakeOrdersConfig( + OrderV4 memory order, + uint256 inputIOIndex, + uint256 outputIOIndex, + Float maximumIO, + Float maximumIORatio, + bool ioIsInput + ) internal pure returns (TakeOrdersConfigV5 memory) { + TakeOrderConfigV4[] memory orders = new TakeOrderConfigV4[](1); + orders[0] = TakeOrderConfigV4({ + order: order, + inputIOIndex: inputIOIndex, + outputIOIndex: outputIOIndex, + signedContext: new SignedContextV1[](0) + }); + + return TakeOrdersConfigV5({ + minimumIO: Float.wrap(0), + maximumIO: maximumIO, + maximumIORatio: maximumIORatio, + IOIsInput: ioIsInput, + orders: orders, + data: new bytes(0) + }); + } +} + +contract Token is ERC20 { + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} + + function mint(address receiver, uint256 amount) external { + _mint(receiver, amount); + } +} + +contract MockSushiRP is IRouteProcessor { + function processRoute( + address tokenIn, + uint256 amountIn, + address tokenOut, + uint256 amountOutMin, + address to, + bytes calldata route + ) external payable returns (uint256 amountOut) { + IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn); + IERC20(tokenOut).approve(address(this), 0); + IERC20(tokenOut).approve(address(this), type(uint256).max); + IERC20(tokenOut).transferFrom(address(this), msg.sender, amountIn); + IERC20(tokenOut).approve(address(this), 0); + return amountIn; + } +} From 0cadc02a3938f3a962a42bb6a54f0ab5ec007b87 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Fri, 30 Jan 2026 20:28:26 +0000 Subject: [PATCH 2/9] fmt and slither --- src/abstract/OrderBookV6RaindexRouter.sol | 17 ++- .../arb/RaindexRouterOrderBookV6Arb.sol | 26 ++-- .../arb/RaindexRouterOrderBookV6Arb.t.sol | 126 ++++++------------ 3 files changed, 68 insertions(+), 101 deletions(-) diff --git a/src/abstract/OrderBookV6RaindexRouter.sol b/src/abstract/OrderBookV6RaindexRouter.sol index c558698c96..493615c69b 100644 --- a/src/abstract/OrderBookV6RaindexRouter.sol +++ b/src/abstract/OrderBookV6RaindexRouter.sol @@ -38,7 +38,7 @@ error SwapFailed(); /// For example consider circuit: /// /// start input = DAI -/// start output = USDC +/// start output = USDC /// external source router = USDC -> USDT /// end input = USDT /// end output = DAI @@ -128,13 +128,14 @@ abstract contract OrderBookV6RaindexRouter is IERC3156FlashBorrower, ReentrancyG require(takeOrders.length == 2, "Unexpected take orders config length"); - address startTakeOrdersInputToken = takeOrders[0].orders[0].order.validInputs[takeOrders[0].orders[0].inputIOIndex].token; - address endTakeOrdersInputToken = takeOrders[1].orders[0].order.validInputs[takeOrders[1].orders[0].inputIOIndex].token; + address startTakeOrdersInputToken = + takeOrders[0].orders[0].order.validInputs[takeOrders[0].orders[0].inputIOIndex].token; + address endTakeOrdersInputToken = + takeOrders[1].orders[0].order.validInputs[takeOrders[1].orders[0].inputIOIndex].token; require( - startTakeOrdersInputToken == takeOrders[1].orders[0].order.validOutputs[ - takeOrders[1].orders[0].outputIOIndex - ].token, + startTakeOrdersInputToken + == takeOrders[1].orders[0].order.validOutputs[takeOrders[1].orders[0].outputIOIndex].token, "start and end orders IO do NOT close the route circuit" ); @@ -174,6 +175,8 @@ abstract contract OrderBookV6RaindexRouter is IERC3156FlashBorrower, ReentrancyG } IERC20(startTakeOrdersInputToken).forceApprove(address(orderBook), 0); - LibOrderBookArb.finalizeArb(task, endTakeOrdersInputToken, endInputDecimals, startTakeOrdersInputToken, startInputDecimals); + LibOrderBookArb.finalizeArb( + task, endTakeOrdersInputToken, endInputDecimals, startTakeOrdersInputToken, startInputDecimals + ); } } diff --git a/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol b/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol index fbd2aa1c2d..5f61d6f6fe 100644 --- a/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol +++ b/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol @@ -39,13 +39,14 @@ contract RaindexRouterOrderBookV6Arb is OrderBookV6RaindexRouter { /// @inheritdoc OrderBookV6RaindexRouter function _exchange(TakeOrdersConfigV5[] memory takeOrders, bytes memory exchangeData) internal virtual override { - - address prevLegTokenAddress = takeOrders[0].orders[0].order.validOutputs[takeOrders[0].orders[0].outputIOIndex].token; + address prevLegTokenAddress = + takeOrders[0].orders[0].order.validOutputs[takeOrders[0].orders[0].outputIOIndex].token; (Float startLegTotalOutput, Float startLegTotalInput) = IOrderBookV6(msg.sender).takeOrders4(takeOrders[0]); (startLegTotalInput); Float prevLegOutputAmount = startLegTotalOutput; + //slither-disable-start calls-loop if (exchangeData.length > 0) { RouteLeg[] memory routeLegs = abi.decode(exchangeData, (RouteLeg[])); @@ -53,7 +54,8 @@ contract RaindexRouterOrderBookV6Arb is OrderBookV6RaindexRouter { RouteLeg memory leg = routeLegs[i]; if (leg.routeLegType == RouteLegType.SUSHI) { - (prevLegOutputAmount, prevLegTokenAddress) = _processSushiLeg(leg, prevLegOutputAmount, prevLegTokenAddress); + (prevLegOutputAmount, prevLegTokenAddress) = + _processSushiLeg(leg, prevLegOutputAmount, prevLegTokenAddress); } else if (leg.routeLegType == RouteLegType.RAINDEX) { revert("raindex route leg type is not yet implemented"); } else if (leg.routeLegType == RouteLegType.BALANCER) { @@ -63,8 +65,10 @@ contract RaindexRouterOrderBookV6Arb is OrderBookV6RaindexRouter { } } } + //slither-disable-end - address endTakeOrdersInputToken = takeOrders[1].orders[0].order.validInputs[takeOrders[1].orders[0].inputIOIndex].token; + address endTakeOrdersInputToken = + takeOrders[1].orders[0].order.validInputs[takeOrders[1].orders[0].inputIOIndex].token; IERC20(endTakeOrdersInputToken).forceApprove(msg.sender, 0); IERC20(endTakeOrdersInputToken).forceApprove(msg.sender, type(uint256).max); @@ -81,13 +85,12 @@ contract RaindexRouterOrderBookV6Arb is OrderBookV6RaindexRouter { } //slither-disable-next-line no-unused-vars - function _processSushiLeg( - RouteLeg memory routeLeg, - Float prevLegOutputAmount, - address prevLegTokenAddress - ) internal returns (Float, address) { + function _processSushiLeg(RouteLeg memory routeLeg, Float prevLegOutputAmount, address prevLegTokenAddress) + internal + returns (Float, address) + { (address fromToken, address toToken, bytes memory route) = abi.decode(routeLeg.data, (address, address, bytes)); - + require(prevLegTokenAddress == fromToken, "token mismatch"); (uint256 fromTokenAmount, bool losslessInputAmount) = @@ -95,7 +98,8 @@ contract RaindexRouterOrderBookV6Arb is OrderBookV6RaindexRouter { (losslessInputAmount); uint8 toTokenDecimals = IERC20Metadata(toToken).decimals(); - (uint256 toTokenAmount, bool lossless) = LibDecimalFloat.toFixedDecimalLossy(LibDecimalFloat.FLOAT_ZERO, toTokenDecimals); + (uint256 toTokenAmount, bool lossless) = + LibDecimalFloat.toFixedDecimalLossy(LibDecimalFloat.FLOAT_ZERO, toTokenDecimals); if (!lossless) { toTokenAmount++; } diff --git a/test/concrete/arb/RaindexRouterOrderBookV6Arb.t.sol b/test/concrete/arb/RaindexRouterOrderBookV6Arb.t.sol index 937ca4591a..f3105dd399 100644 --- a/test/concrete/arb/RaindexRouterOrderBookV6Arb.t.sol +++ b/test/concrete/arb/RaindexRouterOrderBookV6Arb.t.sol @@ -44,15 +44,15 @@ contract RaindexRouterOrderBookV6ArbTest is Test { OrderBookV6 internal orderBook; RaindexRouterOrderBookV6Arb internal router; - + address internal alice; address internal bob; address internal arber; - + address internal tokenA; address internal tokenB; address internal tokenC; - + uint256 internal constant INITIAL_BALANCE = 1000e18; bytes32 internal constant VAULT_ID = keccak256("vault"); @@ -86,33 +86,29 @@ contract RaindexRouterOrderBookV6ArbTest is Test { function setUp() public { // Deploy core contracts orderBook = new OrderBookV6(); - + // Create test accounts alice = makeAddr("alice"); bob = makeAddr("bob"); arber = makeAddr("arber"); - + // Deploy mock ERC20 tokens tokenA = address(new Token("Token A", "TKNA")); tokenB = address(new Token("Token B", "TKNB")); tokenC = address(new Token("Token C", "TKNC")); - + // Setup router OrderBookV6ArbConfig memory config = OrderBookV6ArbConfig({ orderBook: address(orderBook), task: TaskV2({ - evaluable: EvaluableV4({ - interpreter: iInterpreter, - store: iStore, - bytecode: new bytes(0) - }), + evaluable: EvaluableV4({interpreter: iInterpreter, store: iStore, bytecode: new bytes(0)}), signedContext: new SignedContextV1[](0) }), implementationData: new bytes(0) }); - + router = new RaindexRouterOrderBookV6Arb(config); - + // Fund accounts deal(tokenA, alice, INITIAL_BALANCE); deal(tokenB, bob, INITIAL_BALANCE); @@ -135,7 +131,7 @@ contract RaindexRouterOrderBookV6ArbTest is Test { tokenA, // output "_ _: 100 0.2;:;" ); - + // Bob creates order: sell tokenB for tokenA at 1:0.99 ratio OrderV4 memory bobOrder = createOrder( bob, @@ -143,41 +139,29 @@ contract RaindexRouterOrderBookV6ArbTest is Test { tokenB, // output "_ _: 100 0.5;:;" ); - + // Deposit tokens into vaults vm.startPrank(alice); IERC20(tokenA).approve(address(orderBook), type(uint256).max); orderBook.deposit4( - tokenA, - VAULT_ID, - LibDecimalFloat.fromFixedDecimalLosslessPacked(100000000000000000000, 18), - new TaskV2[](0) + tokenA, VAULT_ID, LibDecimalFloat.fromFixedDecimalLosslessPacked(100000000000000000000, 18), new TaskV2[](0) ); vm.stopPrank(); - + vm.startPrank(bob); IERC20(tokenB).approve(address(orderBook), type(uint256).max); orderBook.deposit4( - tokenB, - VAULT_ID, - LibDecimalFloat.fromFixedDecimalLosslessPacked(100000000000000000000, 18), - new TaskV2[](0) + tokenB, VAULT_ID, LibDecimalFloat.fromFixedDecimalLosslessPacked(100000000000000000000, 18), new TaskV2[](0) ); vm.stopPrank(); - + // Add orders vm.prank(alice); - orderBook.addOrder4( - createOrderConfig(aliceOrder), - new TaskV2[](0) - ); - + orderBook.addOrder4(createOrderConfig(aliceOrder), new TaskV2[](0)); + vm.prank(bob); - orderBook.addOrder4( - createOrderConfig(bobOrder), - new TaskV2[](0) - ); - + orderBook.addOrder4(createOrderConfig(bobOrder), new TaskV2[](0)); + // Create take orders configs for arbitrage TakeOrdersConfigV5 memory startTakeOrders = createTakeOrdersConfig( aliceOrder, @@ -187,7 +171,7 @@ contract RaindexRouterOrderBookV6ArbTest is Test { LibDecimalFloat.FLOAT_MAX_POSITIVE_VALUE, // maximumIORatio false // IOIsInput ); - + TakeOrdersConfigV5 memory endTakeOrders = createTakeOrdersConfig( bobOrder, 0, // inputIOIndex @@ -199,13 +183,9 @@ contract RaindexRouterOrderBookV6ArbTest is Test { TakeOrdersConfigV5[] memory takeOrders = new TakeOrdersConfigV5[](2); takeOrders[0] = startTakeOrders; takeOrders[1] = endTakeOrders; - + TaskV2 memory task = TaskV2({ - evaluable: EvaluableV4({ - interpreter: iInterpreter, - store: iStore, - bytecode: new bytes(0) - }), + evaluable: EvaluableV4({interpreter: iInterpreter, store: iStore, bytecode: new bytes(0)}), signedContext: new SignedContextV1[](0) }); @@ -216,7 +196,7 @@ contract RaindexRouterOrderBookV6ArbTest is Test { data: abi.encode(tokenA, tokenC, new bytes(0)) }); bytes memory exchangeData = abi.encode(routeLegs); - + // Record balances before uint256 orderBookTokenABefore = IERC20(tokenA).balanceOf(address(orderBook)); uint256 orderBookTokenBBefore = IERC20(tokenB).balanceOf(address(orderBook)); @@ -224,16 +204,11 @@ contract RaindexRouterOrderBookV6ArbTest is Test { uint256 arberTokenABefore = IERC20(tokenA).balanceOf(arber); uint256 arberTokenBBefore = IERC20(tokenB).balanceOf(arber); uint256 arberTokenCBefore = IERC20(tokenC).balanceOf(arber); - + // Execute arbitrage vm.prank(arber); - router.arb4( - orderBook, - takeOrders, - exchangeData, - task - ); - + router.arb4(orderBook, takeOrders, exchangeData, task); + // Verify balances changed uint256 orderBookTokenAAfter = IERC20(tokenA).balanceOf(address(orderBook)); uint256 orderBookTokenBAfter = IERC20(tokenB).balanceOf(address(orderBook)); @@ -241,19 +216,19 @@ contract RaindexRouterOrderBookV6ArbTest is Test { uint256 arberTokenAAfter = IERC20(tokenA).balanceOf(arber); uint256 arberTokenBAfter = IERC20(tokenB).balanceOf(arber); uint256 arberTokenCAfter = IERC20(tokenC).balanceOf(arber); - + assertNotEq(orderBookTokenABefore, orderBookTokenAAfter, "TokenA balance should change"); assertNotEq(orderBookTokenBBefore, orderBookTokenBAfter, "TokenB balance should change"); assertNotEq(orderBookTokenCBefore, orderBookTokenCAfter, "TokenC balance should change"); assertEq(arberTokenABefore, arberTokenAAfter, "arber TokenA balance should NOT change"); - + assertNotEq(arberTokenBBefore, arberTokenBAfter, "arber TokenB balance should change"); assertNotEq(arberTokenCBefore, arberTokenCAfter, "arber TokenC balance should change"); assertEq(arberTokenBAfter, 8e19, "expected 80 token B for arber bounty"); assertEq(arberTokenCAfter, 5e19, "expected 50 token C for arber bounty"); - + Float aliceOutputVaultBalanceFloat = orderBook.vaultBalance2(alice, tokenA, VAULT_ID); Float aliceInputVaultBalanceFloat = orderBook.vaultBalance2(alice, tokenB, VAULT_ID); uint256 aliceOutputVaultBalance = LibDecimalFloat.toFixedDecimalLossless(aliceOutputVaultBalanceFloat, 18); @@ -270,42 +245,27 @@ contract RaindexRouterOrderBookV6ArbTest is Test { } // Helper functions - function createOrder( - address owner, - address inputToken, - address outputToken, - string memory rl - ) internal view returns (OrderV4 memory) { + function createOrder(address owner, address inputToken, address outputToken, string memory rl) + internal + view + returns (OrderV4 memory) + { IOV2[] memory validInputs = new IOV2[](1); - validInputs[0] = IOV2({ - token: inputToken, - vaultId: VAULT_ID - }); - + validInputs[0] = IOV2({token: inputToken, vaultId: VAULT_ID}); + IOV2[] memory validOutputs = new IOV2[](1); - validOutputs[0] = IOV2({ - token: outputToken, - vaultId: VAULT_ID - }); - + validOutputs[0] = IOV2({token: outputToken, vaultId: VAULT_ID}); + return OrderV4({ owner: owner, - evaluable: EvaluableV4({ - interpreter: iInterpreter, - store: iStore, - bytecode: iParserV2.parse2(bytes(rl)) - }), + evaluable: EvaluableV4({interpreter: iInterpreter, store: iStore, bytecode: iParserV2.parse2(bytes(rl))}), validInputs: validInputs, validOutputs: validOutputs, nonce: bytes32(0) }); } - - function createOrderConfig(OrderV4 memory order) - internal - pure - returns (OrderConfigV4 memory) - { + + function createOrderConfig(OrderV4 memory order) internal pure returns (OrderConfigV4 memory) { return OrderConfigV4({ validInputs: order.validInputs, validOutputs: order.validOutputs, @@ -315,7 +275,7 @@ contract RaindexRouterOrderBookV6ArbTest is Test { secret: bytes32(0) }); } - + function createTakeOrdersConfig( OrderV4 memory order, uint256 inputIOIndex, @@ -331,7 +291,7 @@ contract RaindexRouterOrderBookV6ArbTest is Test { outputIOIndex: outputIOIndex, signedContext: new SignedContextV1[](0) }); - + return TakeOrdersConfigV5({ minimumIO: Float.wrap(0), maximumIO: maximumIO, From 45e48cb07bfb2b36a8c5383c2883a6807c48d011 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 2 Feb 2026 22:19:47 +0000 Subject: [PATCH 3/9] deploy --- script/Deploy.sol | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/script/Deploy.sol b/script/Deploy.sol index 9d0aa1eba2..92d4cba73f 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -8,6 +8,7 @@ import {OrderBookV6SubParser} from "src/concrete/parser/OrderBookV6SubParser.sol import {GenericPoolOrderBookV6ArbOrderTaker} from "src/concrete/arb/GenericPoolOrderBookV6ArbOrderTaker.sol"; import {RouteProcessorOrderBookV6ArbOrderTaker} from "src/concrete/arb/RouteProcessorOrderBookV6ArbOrderTaker.sol"; import {GenericPoolOrderBookV6FlashBorrower} from "src/concrete/arb/GenericPoolOrderBookV6FlashBorrower.sol"; +import {RaindexRouterOrderBookV6Arb} from "src/concrete/arb/RaindexRouterOrderBookV6Arb.sol"; import {OrderBookV6ArbConfig} from "src/abstract/OrderBookV6ArbCommon.sol"; import {IMetaBoardV1_2} from "rain.metadata/interface/unstable/IMetaBoardV1_2.sol"; import {LibDescribedByMeta} from "rain.metadata/lib/LibDescribedByMeta.sol"; @@ -130,6 +131,18 @@ contract Deploy is Script { "" ) ); + + // Raindex Router Arb. + new RaindexRouterOrderBookV6Arb( + OrderBookV6ArbConfig( + raindex, + TaskV2({ + evaluable: EvaluableV4(IInterpreterV4(address(0)), IInterpreterStoreV3(address(0)), hex""), + signedContext: new SignedContextV1[](0) + }), + "" + ) + ); } vm.stopBroadcast(); } From 2f0d03e61ea8437b41362c611012cfa001b5d6f9 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 18 Feb 2026 00:22:50 +0000 Subject: [PATCH 4/9] update --- src/abstract/OrderBookV6RaindexRouter.sol | 3 +-- src/concrete/arb/RaindexRouterOrderBookV6Arb.sol | 9 +-------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/abstract/OrderBookV6RaindexRouter.sol b/src/abstract/OrderBookV6RaindexRouter.sol index 493615c69b..5f1148b389 100644 --- a/src/abstract/OrderBookV6RaindexRouter.sol +++ b/src/abstract/OrderBookV6RaindexRouter.sol @@ -122,12 +122,11 @@ abstract contract OrderBookV6RaindexRouter is IERC3156FlashBorrower, ReentrancyG TaskV2 calldata task ) external payable nonReentrant onlyValidTask(task) { // Mimic what OB would do anyway if called with zero orders. + require(takeOrders.length == 2, "Unexpected take orders config length"); if (takeOrders[0].orders.length == 0 || takeOrders[1].orders.length == 0) { revert IOrderBookV6.NoOrders(); } - require(takeOrders.length == 2, "Unexpected take orders config length"); - address startTakeOrdersInputToken = takeOrders[0].orders[0].order.validInputs[takeOrders[0].orders[0].inputIOIndex].token; address endTakeOrdersInputToken = diff --git a/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol b/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol index 5f61d6f6fe..048c7052f7 100644 --- a/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol +++ b/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol @@ -98,16 +98,11 @@ contract RaindexRouterOrderBookV6Arb is OrderBookV6RaindexRouter { (losslessInputAmount); uint8 toTokenDecimals = IERC20Metadata(toToken).decimals(); - (uint256 toTokenAmount, bool lossless) = - LibDecimalFloat.toFixedDecimalLossy(LibDecimalFloat.FLOAT_ZERO, toTokenDecimals); - if (!lossless) { - toTokenAmount++; - } IERC20(fromToken).forceApprove(routeLeg.destination, 0); IERC20(fromToken).forceApprove(routeLeg.destination, type(uint256).max); uint256 amountOut = IRouteProcessor(routeLeg.destination).processRoute( - fromToken, fromTokenAmount, toToken, toTokenAmount, address(this), route + fromToken, fromTokenAmount, toToken, 0, address(this), route ); IERC20(fromToken).forceApprove(address(routeLeg.destination), 0); @@ -118,6 +113,4 @@ contract RaindexRouterOrderBookV6Arb is OrderBookV6RaindexRouter { /// Allow receiving gas. fallback() external {} - - function a(RouteLeg calldata x) external {} } From 543906adf2fb47f9116806ba8e9a4a4c13d466d8 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 18 Feb 2026 01:24:10 +0000 Subject: [PATCH 5/9] Update OrderBookV6RaindexRouter.sol --- src/abstract/OrderBookV6RaindexRouter.sol | 40 ++++++++++++++++------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/abstract/OrderBookV6RaindexRouter.sol b/src/abstract/OrderBookV6RaindexRouter.sol index 5f1148b389..77bccedfa4 100644 --- a/src/abstract/OrderBookV6RaindexRouter.sol +++ b/src/abstract/OrderBookV6RaindexRouter.sol @@ -12,7 +12,8 @@ import { IOrderBookV6, TakeOrdersConfigV5, TaskV2, - Float + Float, + QuoteV2 } from "rain.orderbook.interface/interface/unstable/IOrderBookV6.sol"; import {IERC3156FlashBorrower} from "rain.orderbook.interface/interface/ierc3156/IERC3156FlashBorrower.sol"; import {OrderBookV6ArbConfig, OrderBookV6ArbCommon} from "./OrderBookV6ArbCommon.sol"; @@ -146,24 +147,39 @@ abstract contract OrderBookV6RaindexRouter is IERC3156FlashBorrower, ReentrancyG // flash loan, cover the orders and remain in profit. // // We take all the current balance of orderbook divided by 2 as loan, - // that's because its the max possible crealable amount, because the loan is - // taken before any takeOrders4() is processed, the loan goes for the input - // amount of first order of the circuit, and orderbook needs to have balance - // left to finish the last takeOrders4(), all this while the flash loan is - // still open (not repaid), after the last takeOrder4() is processed then - // the flash loan can be repaid, in order words half of the orderbook token - // balance is used for completing the first takeOrders4() as a flash loan - // and half for the last as flash loan repay - // Ofcourse if the half value exceeds max IO, the first order can be cleared - // in its full capacity + // that's because its the max possible crealable amount by flash loan, + // because the loan is taken before any takeOrders4() is processed, the + // loan goes for the input amount of first order of the circuit, and + // orderbook needs to have balance left to finish the last takeOrders4(), + // all this while the flash loan is still open (not repaid), after the + // last takeOrder4() is processed then the flash loan can be repaid, in + // order words half of the orderbook token balance is used for completing + // the first takeOrders4() as a flash loan and half for the last as flash + // loan repay uint256 flashLoanAmount = IERC20(startTakeOrdersInputToken).balanceOf(address(orderBook)) / 2; Float flashLoanAmountFloat = LibDecimalFloat.fromFixedDecimalLosslessPacked(flashLoanAmount, startInputDecimals); + // getting the last order's maxOuput, as the first order cannot clear + // more than the maxOutput of the last order, the max possible clear + // amount is min of maxOutput and flashLoanAmount + //slither-disable-next-line unused-return + (, Float maxOutput,) = orderBook.quote2( + QuoteV2({ + order: takeOrders[1].orders[0].order, + inputIOIndex: takeOrders[1].orders[0].inputIOIndex, + outputIOIndex: takeOrders[1].orders[0].outputIOIndex, + signedContext: takeOrders[1].orders[0].signedContext + }) + ); + IERC20(startTakeOrdersInputToken).forceApprove(address(orderBook), 0); IERC20(startTakeOrdersInputToken).forceApprove(address(orderBook), type(uint256).max); - if (LibDecimalFloat.gt(takeOrders[0].maximumIO, flashLoanAmountFloat)) { + // set max io + if (LibDecimalFloat.gt(maxOutput, flashLoanAmountFloat)) { takeOrders[0].maximumIO = flashLoanAmountFloat; + } else { + takeOrders[0].maximumIO = maxOutput; } takeOrders[0].IOIsInput = false; // must always be false From 0a51bdb73b50f1c3e1ffdbc1143325a98400d8b3 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 18 Feb 2026 02:20:24 +0000 Subject: [PATCH 6/9] update --- src/concrete/arb/RaindexRouterOrderBookV6Arb.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol b/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol index 048c7052f7..0bd2ef6f30 100644 --- a/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol +++ b/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol @@ -23,7 +23,7 @@ enum RouteLegType { STABULL } -// data and destination address of a route leg +// data and destination address of a route leg (usually the router address) // the data field needs to be decoded based on the type struct RouteLeg { RouteLegType routeLegType; @@ -104,7 +104,7 @@ contract RaindexRouterOrderBookV6Arb is OrderBookV6RaindexRouter { uint256 amountOut = IRouteProcessor(routeLeg.destination).processRoute( fromToken, fromTokenAmount, toToken, 0, address(this), route ); - IERC20(fromToken).forceApprove(address(routeLeg.destination), 0); + IERC20(fromToken).forceApprove(routeLeg.destination, 0); Float amountOutFloat = LibDecimalFloat.fromFixedDecimalLosslessPacked(amountOut, toTokenDecimals); From e3e0a215fc2a4791d3915c373461971a24a79446 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 23 Feb 2026 14:35:49 +0000 Subject: [PATCH 7/9] Update OrderBookV6RaindexRouter.sol --- src/abstract/OrderBookV6RaindexRouter.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/abstract/OrderBookV6RaindexRouter.sol b/src/abstract/OrderBookV6RaindexRouter.sol index 77bccedfa4..c79fca7caa 100644 --- a/src/abstract/OrderBookV6RaindexRouter.sol +++ b/src/abstract/OrderBookV6RaindexRouter.sol @@ -176,9 +176,10 @@ abstract contract OrderBookV6RaindexRouter is IERC3156FlashBorrower, ReentrancyG IERC20(startTakeOrdersInputToken).forceApprove(address(orderBook), type(uint256).max); // set max io - if (LibDecimalFloat.gt(maxOutput, flashLoanAmountFloat)) { + if (LibDecimalFloat.gt(takeOrders[0].maximumIO, flashLoanAmountFloat)) { takeOrders[0].maximumIO = flashLoanAmountFloat; - } else { + } + if (LibDecimalFloat.gt(takeOrders[0].maximumIO, maxOutput)) { takeOrders[0].maximumIO = maxOutput; } takeOrders[0].IOIsInput = false; // must always be false From 453b687683e59c837a9d1243900d33a696e3e00e Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 23 Feb 2026 14:56:28 +0000 Subject: [PATCH 8/9] update --- src/abstract/OrderBookV6RaindexRouter.sol | 1 + src/concrete/arb/RaindexRouterOrderBookV6Arb.sol | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/abstract/OrderBookV6RaindexRouter.sol b/src/abstract/OrderBookV6RaindexRouter.sol index c79fca7caa..938b7283d9 100644 --- a/src/abstract/OrderBookV6RaindexRouter.sol +++ b/src/abstract/OrderBookV6RaindexRouter.sol @@ -158,6 +158,7 @@ abstract contract OrderBookV6RaindexRouter is IERC3156FlashBorrower, ReentrancyG // loan repay uint256 flashLoanAmount = IERC20(startTakeOrdersInputToken).balanceOf(address(orderBook)) / 2; Float flashLoanAmountFloat = LibDecimalFloat.fromFixedDecimalLosslessPacked(flashLoanAmount, startInputDecimals); + require (!LibDecimalFloat.isZero(flashLoanAmountFloat), "zero flash loan amount"); // getting the last order's maxOuput, as the first order cannot clear // more than the maxOutput of the last order, the max possible clear diff --git a/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol b/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol index 0bd2ef6f30..0c37ed4beb 100644 --- a/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol +++ b/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol @@ -69,6 +69,8 @@ contract RaindexRouterOrderBookV6Arb is OrderBookV6RaindexRouter { address endTakeOrdersInputToken = takeOrders[1].orders[0].order.validInputs[takeOrders[1].orders[0].inputIOIndex].token; + require (prevLegTokenAddress == endTakeOrdersInputToken, "token mismatch"); + IERC20(endTakeOrdersInputToken).forceApprove(msg.sender, 0); IERC20(endTakeOrdersInputToken).forceApprove(msg.sender, type(uint256).max); From 88029b7b59bc09c04caa0a83477c7d567ec2c078 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 23 Feb 2026 14:57:25 +0000 Subject: [PATCH 9/9] fmt --- src/abstract/OrderBookV6RaindexRouter.sol | 2 +- src/concrete/arb/RaindexRouterOrderBookV6Arb.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/abstract/OrderBookV6RaindexRouter.sol b/src/abstract/OrderBookV6RaindexRouter.sol index 938b7283d9..74a283cb0c 100644 --- a/src/abstract/OrderBookV6RaindexRouter.sol +++ b/src/abstract/OrderBookV6RaindexRouter.sol @@ -158,7 +158,7 @@ abstract contract OrderBookV6RaindexRouter is IERC3156FlashBorrower, ReentrancyG // loan repay uint256 flashLoanAmount = IERC20(startTakeOrdersInputToken).balanceOf(address(orderBook)) / 2; Float flashLoanAmountFloat = LibDecimalFloat.fromFixedDecimalLosslessPacked(flashLoanAmount, startInputDecimals); - require (!LibDecimalFloat.isZero(flashLoanAmountFloat), "zero flash loan amount"); + require(!LibDecimalFloat.isZero(flashLoanAmountFloat), "zero flash loan amount"); // getting the last order's maxOuput, as the first order cannot clear // more than the maxOutput of the last order, the max possible clear diff --git a/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol b/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol index 0c37ed4beb..eb707fc425 100644 --- a/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol +++ b/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol @@ -69,7 +69,7 @@ contract RaindexRouterOrderBookV6Arb is OrderBookV6RaindexRouter { address endTakeOrdersInputToken = takeOrders[1].orders[0].order.validInputs[takeOrders[1].orders[0].inputIOIndex].token; - require (prevLegTokenAddress == endTakeOrdersInputToken, "token mismatch"); + require(prevLegTokenAddress == endTakeOrdersInputToken, "token mismatch"); IERC20(endTakeOrdersInputToken).forceApprove(msg.sender, 0); IERC20(endTakeOrdersInputToken).forceApprove(msg.sender, type(uint256).max);