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(); } diff --git a/src/abstract/OrderBookV6RaindexRouter.sol b/src/abstract/OrderBookV6RaindexRouter.sol new file mode 100644 index 0000000000..74a283cb0c --- /dev/null +++ b/src/abstract/OrderBookV6RaindexRouter.sol @@ -0,0 +1,199 @@ +// 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, + 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"; +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. + require(takeOrders.length == 2, "Unexpected take orders config length"); + if (takeOrders[0].orders.length == 0 || takeOrders[1].orders.length == 0) { + revert IOrderBookV6.NoOrders(); + } + + 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 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); + 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 + // 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); + + // set max io + if (LibDecimalFloat.gt(takeOrders[0].maximumIO, flashLoanAmountFloat)) { + takeOrders[0].maximumIO = flashLoanAmountFloat; + } + if (LibDecimalFloat.gt(takeOrders[0].maximumIO, maxOutput)) { + takeOrders[0].maximumIO = maxOutput; + } + 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..eb707fc425 --- /dev/null +++ b/src/concrete/arb/RaindexRouterOrderBookV6Arb.sol @@ -0,0 +1,118 @@ +// 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 (usually the router address) +// 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; + + //slither-disable-start calls-loop + 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"); + } + } + } + //slither-disable-end + + 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); + + // 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(); + + IERC20(fromToken).forceApprove(routeLeg.destination, 0); + IERC20(fromToken).forceApprove(routeLeg.destination, type(uint256).max); + uint256 amountOut = IRouteProcessor(routeLeg.destination).processRoute( + fromToken, fromTokenAmount, toToken, 0, address(this), route + ); + IERC20(fromToken).forceApprove(routeLeg.destination, 0); + + Float amountOutFloat = LibDecimalFloat.fromFixedDecimalLosslessPacked(amountOut, toTokenDecimals); + + return (amountOutFloat, toToken); + } + + /// Allow receiving gas. + fallback() 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..f3105dd399 --- /dev/null +++ b/test/concrete/arb/RaindexRouterOrderBookV6Arb.t.sol @@ -0,0 +1,330 @@ +// 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; + } +}