diff --git a/.gas-snapshot b/.gas-snapshot index 310b99be7..8ed4c2ce5 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,43 +1,43 @@ -GenericPoolOrderBookFlashBorrowerTest:testMinimumOutput(uint256,uint256) (runs: 5096, μ: 2328298, ~: 2331602) -GenericPoolOrderBookFlashBorrowerTest:testTakeOrdersSender() (gas: 2243437) +GenericPoolOrderBookFlashBorrowerTest:testMinimumOutput(uint256,uint256) (runs: 5096, μ: 2326245, ~: 2329549) +GenericPoolOrderBookFlashBorrowerTest:testTakeOrdersSender() (gas: 2241372) LibOrderTest:testHashEqual((address,bool,(address,address,address),(address,uint8,uint256)[],(address,uint8,uint256)[])) (runs: 5096, μ: 194389, ~: 191140) LibOrderTest:testHashNotEqual((address,bool,(address,address,address),(address,uint8,uint256)[],(address,uint8,uint256)[]),(address,bool,(address,address,address),(address,uint8,uint256)[],(address,uint8,uint256)[])) (runs: 5096, μ: 298734, ~: 298554) -OrderBookAddOrderMockTest:testAddOrderSameAccountWithDifferentConfig(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address,address) (runs: 5096, μ: 2771896, ~: 2761563) -OrderBookAddOrderMockTest:testAddOrderTwoAccountsWithDifferentConfig(address,address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address,address) (runs: 5096, μ: 2626805, ~: 2600971) -OrderBookAddOrderMockTest:testAddOrderTwoAccountsWithSameConfig(address,address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 2493363, ~: 2492354) -OrderBookAddOrderMockTest:testAddOrderWithCalculationsInputsAndOutputsSucceeds(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 1295837, ~: 1282002) -OrderBookAddOrderMockTest:testAddOrderWithNonEmptyMetaEmitsMetaV1(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 1303948, ~: 1290925) -OrderBookAddOrderMockTest:testAddOrderWithNonEmptyMetaReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 703712, ~: 697944) -OrderBookAddOrderMockTest:testAddOrderWithoutCalculationsReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 226617, ~: 224824) -OrderBookAddOrderMockTest:testAddOrderWithoutInputsReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 182440, ~: 181924) -OrderBookAddOrderMockTest:testAddOrderWithoutOutputsReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 182862, ~: 182048) -OrderBookAddOrderTest:testAddOrderRealNoHandleIOReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 172809, ~: 171585) -OrderBookAddOrderTest:testAddOrderRealNoSourcesReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 176067, ~: 174435) -OrderBookAddOrderTest:testAddOrderRealOneStackCalculateReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 186152, ~: 184929) -OrderBookAddOrderTest:testAddOrderRealThreeStackCalculate(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 722175, ~: 718820) -OrderBookAddOrderTest:testAddOrderRealTwoStackCalculateReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 715348, ~: 711993) -OrderBookAddOrderTest:testAddOrderRealZeroStackCalculateReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 181324, ~: 180101) -OrderBookDepositTest:testDepositEvent(address,uint256,uint256) (runs: 5096, μ: 38710, ~: 38710) -OrderBookDepositTest:testDepositFail(address,uint256,uint256) (runs: 5096, μ: 8937393460516740786, ~: 8937393460516740786) +OrderBookAddOrderMockTest:testAddOrderSameAccountWithDifferentConfig(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address,address) (runs: 5096, μ: 2771808, ~: 2761475) +OrderBookAddOrderMockTest:testAddOrderTwoAccountsWithDifferentConfig(address,address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address,address) (runs: 5096, μ: 2626897, ~: 2601235) +OrderBookAddOrderMockTest:testAddOrderTwoAccountsWithSameConfig(address,address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 2493275, ~: 2492266) +OrderBookAddOrderMockTest:testAddOrderWithCalculationsInputsAndOutputsSucceeds(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 1295793, ~: 1281958) +OrderBookAddOrderMockTest:testAddOrderWithNonEmptyMetaEmitsMetaV1(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 1303882, ~: 1290859) +OrderBookAddOrderMockTest:testAddOrderWithNonEmptyMetaReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 703690, ~: 697922) +OrderBookAddOrderMockTest:testAddOrderWithoutCalculationsReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 226595, ~: 224802) +OrderBookAddOrderMockTest:testAddOrderWithoutInputsReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 182418, ~: 181902) +OrderBookAddOrderMockTest:testAddOrderWithoutOutputsReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 182840, ~: 182026) +OrderBookAddOrderTest:testAddOrderRealNoHandleIOReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 172765, ~: 171541) +OrderBookAddOrderTest:testAddOrderRealNoSourcesReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 176045, ~: 174413) +OrderBookAddOrderTest:testAddOrderRealOneStackCalculateReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 186108, ~: 184885) +OrderBookAddOrderTest:testAddOrderRealThreeStackCalculate(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 722153, ~: 718798) +OrderBookAddOrderTest:testAddOrderRealTwoStackCalculateReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 715304, ~: 711949) +OrderBookAddOrderTest:testAddOrderRealZeroStackCalculateReverts(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes)) (runs: 5096, μ: 181302, ~: 180079) +OrderBookDepositTest:testDepositEvent(address,uint256,uint256) (runs: 5096, μ: 38755, ~: 38755) +OrderBookDepositTest:testDepositFail(address,uint256,uint256) (runs: 5096, μ: 8937393460516740787, ~: 8937393460516740786) OrderBookDepositTest:testDepositGas00() (gas: 8176) OrderBookDepositTest:testDepositGas01() (gas: 34620) -OrderBookDepositTest:testDepositMany((address,address,uint256,uint248)[]) (runs: 5096, μ: 5183067, ~: 4940788) +OrderBookDepositTest:testDepositMany((address,address,uint256,uint248)[]) (runs: 5096, μ: 5173597, ~: 4894479) OrderBookDepositTest:testDepositOverflow(address,uint256,uint256,uint256) (runs: 5096, μ: 46645, ~: 46645) -OrderBookDepositTest:testDepositReentrancy(address,uint256,uint256,address,uint256,uint256) (runs: 5096, μ: 495280, ~: 496632) +OrderBookDepositTest:testDepositReentrancy(address,uint256,uint256,address,uint256,uint256) (runs: 5096, μ: 495128, ~: 496632) OrderBookDepositTest:testDepositSimple(address,uint256,uint256) (runs: 5096, μ: 37840, ~: 37840) OrderBookDepositTest:testDepositZero(address,uint256) (runs: 5096, μ: 12639, ~: 12639) OrderBookDepositTest:testVaultBalanceNoDeposits(address,uint256) (runs: 5096, μ: 8263, ~: 8263) -OrderBookDepositTest:testVaultBalanceReentrant(address,uint256,uint256,address,address,uint256) (runs: 5096, μ: 494116, ~: 495964) -OrderBookRemoveOrderMockTest:testRemoveOrderAddRemoveMulti(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 7256754, ~: 7139557) -OrderBookRemoveOrderMockTest:testRemoveOrderDifferent(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 5070453, ~: 5046460) -OrderBookRemoveOrderMockTest:testRemoveOrderDifferentOwners(address,address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 4854311, ~: 4841989) -OrderBookRemoveOrderMockTest:testRemoveOrderDifferentOwnersDifferent(address,address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 10467161, ~: 10453667) -OrderBookRemoveOrderMockTest:testRemoveOrderDoesNotExist(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 386746, ~: 381998) -OrderBookRemoveOrderMockTest:testRemoveOrderOnlyOwner(address,address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 2603146, ~: 2601124) -OrderBookWithdrawTest:testWithdrawEmptyVault(address,address,uint256,uint256) (runs: 5096, μ: 15251, ~: 15251) -OrderBookWithdrawTest:testWithdrawFailure(address,uint256,uint256,uint256) (runs: 5096, μ: 8937393460516719460, ~: 8937393460516700411) -OrderBookWithdrawTest:testWithdrawFullVault(address,uint256,uint256,uint256) (runs: 5096, μ: 41244, ~: 41242) -OrderBookWithdrawTest:testWithdrawMany((bool,address,address,uint256,uint248)[]) (runs: 5096, μ: 1389147, ~: 1313082) -OrderBookWithdrawTest:testWithdrawPartialVault(address,uint256,uint256,uint256) (runs: 5096, μ: 51914, ~: 51914) -OrderBookWithdrawTest:testWithdrawReentrant(address,uint256,uint256,address,address,uint256) (runs: 5096, μ: 506284, ~: 507991) -OrderBookWithdrawTest:testWithdrawZero(address,address,uint256) (runs: 5096, μ: 12809, ~: 12809) \ No newline at end of file +OrderBookDepositTest:testVaultBalanceReentrant(address,uint256,uint256,address,address,uint256) (runs: 5096, μ: 494155, ~: 495964) +OrderBookRemoveOrderMockTest:testRemoveOrderAddRemoveMulti(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 7256892, ~: 7139695) +OrderBookRemoveOrderMockTest:testRemoveOrderDifferent(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 5070545, ~: 5046552) +OrderBookRemoveOrderMockTest:testRemoveOrderDifferentOwners(address,address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 4855105, ~: 4842171) +OrderBookRemoveOrderMockTest:testRemoveOrderDifferentOwnersDifferent(address,address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 10467503, ~: 10454009) +OrderBookRemoveOrderMockTest:testRemoveOrderDoesNotExist(address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 386856, ~: 382108) +OrderBookRemoveOrderMockTest:testRemoveOrderOnlyOwner(address,address,((address,uint8,uint256)[],(address,uint8,uint256)[],(address,bytes,uint256[]),bytes),address) (runs: 5096, μ: 2603652, ~: 2601305) +OrderBookWithdrawTest:testWithdrawEmptyVault(address,address,uint256,uint256) (runs: 5096, μ: 15316, ~: 15316) +OrderBookWithdrawTest:testWithdrawFailure(address,uint256,uint256,uint256) (runs: 5096, μ: 8937393460516719594, ~: 8937393460516700429) +OrderBookWithdrawTest:testWithdrawFullVault(address,uint256,uint256,uint256) (runs: 5096, μ: 41258, ~: 41256) +OrderBookWithdrawTest:testWithdrawMany((bool,address,address,uint256,uint248)[]) (runs: 5096, μ: 1448793, ~: 1371051) +OrderBookWithdrawTest:testWithdrawPartialVault(address,uint256,uint256,uint256) (runs: 5096, μ: 51929, ~: 51929) +OrderBookWithdrawTest:testWithdrawReentrant(address,uint256,uint256,address,address,uint256) (runs: 5096, μ: 506286, ~: 507997) +OrderBookWithdrawTest:testWithdrawZero(address,address,uint256) (runs: 5096, μ: 12787, ~: 12787) \ No newline at end of file diff --git a/meta/GenericPoolOrderBookFlashBorrower.rain.meta b/meta/GenericPoolOrderBookFlashBorrower.rain.meta index ee4ceacd8..aa664e479 100644 Binary files a/meta/GenericPoolOrderBookFlashBorrower.rain.meta and b/meta/GenericPoolOrderBookFlashBorrower.rain.meta differ diff --git a/meta/OrderBook.rain.meta b/meta/OrderBook.rain.meta index 9371654af..c375be5ef 100644 Binary files a/meta/OrderBook.rain.meta and b/meta/OrderBook.rain.meta differ diff --git a/script/DeployGenericPoolOrderBookFlashBorrower.sol b/script/DeployGenericPoolOrderBookFlashBorrower.sol index 6cec25388..2c1c29d4a 100644 --- a/script/DeployGenericPoolOrderBookFlashBorrower.sol +++ b/script/DeployGenericPoolOrderBookFlashBorrower.sol @@ -17,7 +17,7 @@ contract DeployGenericPoolOrderBookFlashBorrower is Script { // hardcoded from CI https://github.com/rainprotocol/rain-protocol/actions/runs/5365826502/jobs/9934721206 address i9rDeployer = 0x12CC9A83C200354bc35e19e6ad18a0F444aB5c86; - console2.log("meta hash:"); + console2.log("DeployGenericPoolOrderBookFlashBorrower meta hash:"); console2.logBytes32(keccak256(meta)); vm.startBroadcast(deployerPrivateKey); diff --git a/script/DeployOrderBook.sol b/script/DeployOrderBook.sol index 7b35e1815..3a3818894 100644 --- a/script/DeployOrderBook.sol +++ b/script/DeployOrderBook.sol @@ -11,7 +11,7 @@ contract DeployOrderBook is Script { // hardcoded from CI https://github.com/rainprotocol/rain-protocol/actions/runs/5365826502/jobs/9934721206 address i9rDeployer = 0x12CC9A83C200354bc35e19e6ad18a0F444aB5c86; - console2.log("meta hash:"); + console2.log("DeployOrderBook meta hash:"); console2.logBytes32(keccak256(meta)); vm.startBroadcast(deployerPrivateKey); diff --git a/src/abstract/OrderBookArbCommon.sol b/src/abstract/OrderBookArbCommon.sol new file mode 100644 index 000000000..2fdde39b6 --- /dev/null +++ b/src/abstract/OrderBookArbCommon.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: CAL +pragma solidity ^0.8.18; + +/// Thrown when the minimum output for the sender is not met after the arb. +/// @param minimum The minimum output expected by the sender. +/// @param actual The actual output that would be received by the sender. +error MinimumOutput(uint256 minimum, uint256 actual); + +/// Thrown when calling functions while the contract is still initializing. +error Initializing(); + +/// Thrown when the stack is not empty after the access control dispatch. +error NonZeroBeforeArbStack(); + +/// Thrown when the lender is not the trusted `OrderBook`. +/// @param badLender The untrusted lender calling `onFlashLoan`. +error BadLender(address badLender); diff --git a/src/abstract/OrderBookArbOrderTaker.sol b/src/abstract/OrderBookArbOrderTaker.sol new file mode 100644 index 000000000..910aae1c9 --- /dev/null +++ b/src/abstract/OrderBookArbOrderTaker.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.19; + +import {ERC165, IERC165} from "openzeppelin-contracts/contracts/utils/introspection/ERC165.sol"; +import {ReentrancyGuard} from "openzeppelin-contracts/contracts/security/ReentrancyGuard.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {Initializable} from "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; +import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "openzeppelin-contracts/contracts/utils/Address.sol"; +import { + DeployerDiscoverableMetaV2, + DeployerDiscoverableMetaV2ConstructionConfig, + LibMeta +} from "rain.interpreter/src/abstract/DeployerDiscoverableMetaV2.sol"; +import "rain.factory/src/interface/ICloneableV2.sol"; +import "rain.interpreter/src/lib/caller/LibContext.sol"; +import "rain.interpreter/src/lib/caller/LibEncodedDispatch.sol"; +import "rain.interpreter/src/lib/bytecode/LibBytecode.sol"; + +import "../interface/unstable/IOrderBookV3.sol"; +import "../interface/unstable/IOrderBookV3OrderTaker.sol"; + +import "./OrderBookArbCommon.sol"; + +/// Config for `OrderBookFlashOrderTakerConfigV1` to initialize. +/// @param orderBook The `IOrderBookV3` to use for `takeOrders`. +/// @param evaluableConfig The config to eval for access control to arb. +/// @param implementationData Arbitrary bytes to pass to the implementation in +/// the `beforeInitialize` hook. +struct OrderBookFlashOrderTakerConfigV1 { + address orderBook; + EvaluableConfigV2 evaluableConfig; + bytes implementationData; +} + +/// @dev "Before arb" is evaluabled before the arb is executed. Ostensibly this +/// is to allow for access control to the arb, the return values are ignored. +SourceIndex constant BEFORE_ARB_SOURCE_INDEX = SourceIndex.wrap(0); +/// @dev "Before arb" has no return values. +uint256 constant BEFORE_ARB_MIN_OUTPUTS = 0; +/// @dev "Before arb" has no return values. +uint16 constant BEFORE_ARB_MAX_OUTPUTS = 0; + +abstract contract OrderBookArbOrderTaker is + IOrderBookV3OrderTaker, + ReentrancyGuard, + Initializable, + ICloneableV2, + DeployerDiscoverableMetaV2, + ERC165 +{ + using SafeERC20 for IERC20; + + event Initialize(address sender, OrderBookFlashOrderTakerConfigV1 config); + + IOrderBookV3 public sOrderBook; + EncodedDispatch public sI9rDispatch; + IInterpreterV1 public sI9r; + IInterpreterStoreV1 public sI9rStore; + + constructor(bytes32 metaHash, DeployerDiscoverableMetaV2ConstructionConfig memory config) + DeployerDiscoverableMetaV2(metaHash, config) + { + _disableInitializers(); + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IOrderBookV3OrderTaker).interfaceId || interfaceId == type(ICloneableV2).interfaceId + || super.supportsInterface(interfaceId); + } + + function _beforeInitialize(bytes memory data) internal virtual {} + + /// Ensure the contract is not initializing. + modifier onlyNotInitializing() { + if (_isInitializing()) { + revert Initializing(); + } + _; + } + + function initialize(OrderBookFlashOrderTakerConfigV1 calldata) external pure returns (bytes32) { + revert InitializeSignatureFn(); + } + + /// @inheritdoc ICloneableV2 + function initialize(bytes memory data) external initializer nonReentrant returns (bytes32) { + OrderBookFlashOrderTakerConfigV1 memory config = abi.decode(data, (OrderBookFlashOrderTakerConfigV1)); + + // Dispatch the hook before any external calls are made. + _beforeInitialize(config.implementationData); + + // @todo this could be paramaterised on `arb`. + sOrderBook = IOrderBookV3(config.orderBook); + + // Emit events before any external calls are made. + emit Initialize(msg.sender, config); + + // If there are any sources to eval then initialize the dispatch, + // otherwise it will remain 0 and we can skip evaluation on `arb`. + if (LibBytecode.sourceCount(config.evaluableConfig.bytecode) > 0) { + address expression; + + uint256[] memory entrypoints = new uint256[](1); + entrypoints[SourceIndex.unwrap(BEFORE_ARB_SOURCE_INDEX)] = BEFORE_ARB_MIN_OUTPUTS; + + // We have to trust the deployer because it produces the expression + // address for dispatch anyway. + // All external functions on this contract have `onlyNotInitializing` + // modifier on them so can't be reentered here anyway. + //slither-disable-next-line reentrancy-benign + (sI9r, sI9rStore, expression) = config.evaluableConfig.deployer.deployExpression( + config.evaluableConfig.bytecode, config.evaluableConfig.constants, entrypoints + ); + sI9rDispatch = LibEncodedDispatch.encode(expression, BEFORE_ARB_SOURCE_INDEX, BEFORE_ARB_MAX_OUTPUTS); + } + + return ICLONEABLE_V2_SUCCESS; + } + + function arb(TakeOrdersConfigV2 calldata takeOrders, uint256 minimumSenderOutput) + external + nonReentrant + onlyNotInitializing + { + // Run the access control dispatch if it is set. + EncodedDispatch dispatch = sI9rDispatch; + if (EncodedDispatch.unwrap(dispatch) > 0) { + (uint256[] memory stack, uint256[] memory kvs) = sI9r.eval( + sI9rStore, + DEFAULT_STATE_NAMESPACE, + dispatch, + LibContext.build(new uint256[][](0), new SignedContextV1[](0)) + ); + // This can only happen if interpreter is broken. + if (stack.length > 0) { + revert NonZeroBeforeArbStack(); + } + // Persist any state changes from the expression. + if (kvs.length > 0) { + sI9rStore.set(DEFAULT_STATE_NAMESPACE, kvs); + } + } + + IERC20(takeOrders.output).safeApprove(address(sOrderBook), 0); + IERC20(takeOrders.output).safeApprove(address(sOrderBook), type(uint256).max); + (uint256 totalInput, uint256 totalOutput) = sOrderBook.takeOrders(takeOrders); + (totalInput, totalOutput); + IERC20(takeOrders.output).safeApprove(address(sOrderBook), 0); + + // Send all unspent input tokens to the sender. + uint256 inputBalance = IERC20(takeOrders.input).balanceOf(address(this)); + if (inputBalance > 0) { + IERC20(takeOrders.input).safeTransfer(msg.sender, inputBalance); + } + // Send all unspent output tokens to the sender. + uint256 outputBalance = IERC20(takeOrders.output).balanceOf(address(this)); + if (outputBalance < minimumSenderOutput) { + revert MinimumOutput(minimumSenderOutput, outputBalance); + } + if (outputBalance > 0) { + IERC20(takeOrders.output).safeTransfer(msg.sender, outputBalance); + } + + // Send any remaining gas to the sender. + // Slither false positive here. We want to send everything to the sender + // because this contract should be empty of all gas and tokens between + // uses. Anyone who sends tokens or gas to an arb contract without + // calling `arb` is going to lose their tokens/gas. + // See https://github.com/crytic/slither/issues/1658 + Address.sendValue(payable(msg.sender), address(this).balance); + } + + /// @inheritdoc IOrderBookV3OrderTaker + function onTakeOrders(address, address, uint256, uint256, bytes calldata) + public + virtual + override + onlyNotInitializing + { + if (msg.sender != address(sOrderBook)) { + revert BadLender(msg.sender); + } + } +} diff --git a/src/abstract/OrderBookFlashBorrower.sol b/src/abstract/OrderBookFlashBorrower.sol index 26a95b1c7..523368649 100644 --- a/src/abstract/OrderBookFlashBorrower.sol +++ b/src/abstract/OrderBookFlashBorrower.sol @@ -18,10 +18,7 @@ import "rain.interpreter/src/lib/bytecode/LibBytecode.sol"; import "../interface/unstable/IOrderBookV3.sol"; import "rain.factory/src/interface/ICloneableV2.sol"; - -/// Thrown when the lender is not the trusted `OrderBook`. -/// @param badLender The untrusted lender calling `onFlashLoan`. -error BadLender(address badLender); +import "./OrderBookArbCommon.sol"; /// Thrown when the initiator is not `ZeroExOrderBookFlashBorrower`. /// @param badInitiator The untrusted initiator of the flash loan. @@ -30,22 +27,11 @@ error BadInitiator(address badInitiator); /// Thrown when the flash loan fails somehow. error FlashLoanFailed(); -/// Thrown when calling functions while the contract is still initializing. -error Initializing(); - /// Thrown when the swap fails. error SwapFailed(); -/// Thrown when the minimum output for the sender is not met after the arb. -/// @param minimum The minimum output expected by the sender. -/// @param actual The actual output that would be received by the sender. -error MinimumOutput(uint256 minimum, uint256 actual); - -/// Thrown when the stack is not empty after the access control dispatch. -error NonZeroBeforeArbStack(); - /// Config for `OrderBookFlashBorrower` to initialize. -/// @param orderBook The `OrderBook` contract to arb against. +/// @param orderBook The `IOrderBookV3` contract to arb against. /// @param evaluableConfig The config to eval for access control to arb. /// @param implementationData Arbitrary bytes to pass to the implementation in /// the `beforeInitialize` hook. @@ -141,7 +127,7 @@ abstract contract OrderBookFlashBorrower is /// Type hints for the input encoding for the `initialize` function. /// Reverts ALWAYS with `InitializeSignatureFn` as per ICloneableV2. - function initialize(OrderBookFlashBorrowerConfigV2 memory) external pure returns (bytes32) { + function initialize(OrderBookFlashBorrowerConfigV2 calldata) external pure returns (bytes32) { revert InitializeSignatureFn(); } @@ -197,7 +183,7 @@ abstract contract OrderBookFlashBorrower is /// @param takeOrders As per `arb`. /// @param exchangeData As per `arb`. //slither-disable-next-line dead-code - function _exchange(TakeOrdersConfig memory takeOrders, bytes memory exchangeData) internal virtual {} + function _exchange(TakeOrdersConfigV2 memory takeOrders, bytes memory exchangeData) internal virtual {} /// @inheritdoc IERC3156FlashBorrower function onFlashLoan(address initiator, address, uint256, uint256, bytes calldata data) @@ -214,7 +200,8 @@ abstract contract OrderBookFlashBorrower is revert BadInitiator(initiator); } - (TakeOrdersConfig memory takeOrders, bytes memory exchangeData) = abi.decode(data, (TakeOrdersConfig, bytes)); + (TakeOrdersConfigV2 memory takeOrders, bytes memory exchangeData) = + abi.decode(data, (TakeOrdersConfigV2, bytes)); // Dispatch the `_exchange` hook to ensure we have the correct asset // type and amount to fill the orders. @@ -259,7 +246,7 @@ abstract contract OrderBookFlashBorrower is /// for decoding this data and defining how it controls interactions with /// the external liquidity. For example, `GenericPoolOrderBookFlashBorrower` /// uses this data as a literal encoded external call. - function arb(TakeOrdersConfig calldata takeOrders, uint256 minimumSenderOutput, bytes calldata exchangeData) + function arb(TakeOrdersConfigV2 calldata takeOrders, uint256 minimumSenderOutput, bytes calldata exchangeData) external nonReentrant onlyNotInitializing diff --git a/src/abstract/OrderBookFlashLender.sol b/src/abstract/OrderBookFlashLender.sol index a2fbb95f3..70e2acd1a 100644 --- a/src/abstract/OrderBookFlashLender.sol +++ b/src/abstract/OrderBookFlashLender.sol @@ -97,7 +97,11 @@ abstract contract OrderBookFlashLender is IERC3156FlashLender { /// @param token The token being sent or for the debt being paid. /// @param receiver The receiver of the token or holder of the debt. /// @param sendAmount The amount to send or repay. - function _decreaseFlashDebtThenSendToken(address token, address receiver, uint256 sendAmount) internal { + /// @return The final amount sent after any debt repayment. + function _decreaseFlashDebtThenSendToken(address token, address receiver, uint256 sendAmount) + internal + returns (uint256) + { // If this token transfer matches the active debt then prioritise // reducing debt over sending tokens. if (token == _sToken && receiver == address(_sReceiver)) { @@ -112,6 +116,7 @@ abstract contract OrderBookFlashLender is IERC3156FlashLender { if (sendAmount > 0) { IERC20(token).safeTransfer(receiver, sendAmount); } + return sendAmount; } /// @inheritdoc IERC3156FlashLender diff --git a/src/concrete/GenericPoolOrderBookArbOrderTaker.sol b/src/concrete/GenericPoolOrderBookArbOrderTaker.sol new file mode 100644 index 000000000..57dbdb2ef --- /dev/null +++ b/src/concrete/GenericPoolOrderBookArbOrderTaker.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.19; + +import "../abstract/OrderBookArbOrderTaker.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "openzeppelin-contracts/contracts/utils/Address.sol"; + +bytes32 constant CALLER_META_HASH = bytes32(0x00); + +contract GenericPoolOrderBookArbOrderTaker is OrderBookArbOrderTaker { + using SafeERC20 for IERC20; + using Address for address; + + constructor(DeployerDiscoverableMetaV2ConstructionConfig memory config) + OrderBookArbOrderTaker(CALLER_META_HASH, config) + {} + + /// @inheritdoc OrderBookArbOrderTaker + function onTakeOrders( + address inputToken, + address outputToken, + uint256 inputAmountSent, + uint256 totalOutputAmount, + bytes calldata takeOrdersData + ) public virtual override { + super.onTakeOrders(inputToken, outputToken, inputAmountSent, totalOutputAmount, takeOrdersData); + (address spender, address pool, bytes memory encodedFunctionCall) = + abi.decode(takeOrdersData, (address, address, bytes)); + + IERC20(inputToken).safeApprove(spender, 0); + IERC20(inputToken).safeApprove(spender, type(uint256).max); + bytes memory returnData = pool.functionCallWithValue(encodedFunctionCall, address(this).balance); + // Nothing can be done with returnData as `takeOrders` does not support + // it. + (returnData); + IERC20(inputToken).safeApprove(spender, 0); + } + + /// Allow receiving gas. + fallback() external onlyNotInitializing {} +} diff --git a/src/concrete/GenericPoolOrderBookFlashBorrower.sol b/src/concrete/GenericPoolOrderBookFlashBorrower.sol index 35ff17214..b9a1d9f5d 100644 --- a/src/concrete/GenericPoolOrderBookFlashBorrower.sol +++ b/src/concrete/GenericPoolOrderBookFlashBorrower.sol @@ -9,7 +9,7 @@ import "src/abstract/OrderBookFlashBorrower.sol"; /// @dev Metadata hash for `DeployerDiscoverableMetaV1`. /// - ABI for GenericPoolOrderBookFlashBorrower /// - Interpreter caller metadata V1 for GenericPoolOrderBookFlashBorrower -bytes32 constant CALLER_META_HASH = bytes32(0xa62ee776c94e335a2d59a5fa7d87ae282b35af1370107165adafe3464270a852); +bytes32 constant CALLER_META_HASH = bytes32(0x3d6909481820fc692906b0477e8f98248e84973bc8b8d5ac935132857d4f4125); /// @title GenericPoolOrderBookFlashBorrower /// Implements the OrderBookFlashBorrower interface for a external liquidity @@ -30,7 +30,7 @@ contract GenericPoolOrderBookFlashBorrower is OrderBookFlashBorrower { {} /// @inheritdoc OrderBookFlashBorrower - function _exchange(TakeOrdersConfig memory takeOrders, bytes memory exchangeData) internal virtual override { + function _exchange(TakeOrdersConfigV2 memory takeOrders, bytes memory exchangeData) internal virtual override { (address spender, address pool, bytes memory encodedFunctionCall) = abi.decode(exchangeData, (address, address, bytes)); diff --git a/src/concrete/OrderBook.sol b/src/concrete/OrderBook.sol index 6c54f563a..9ed1206cb 100644 --- a/src/concrete/OrderBook.sol +++ b/src/concrete/OrderBook.sol @@ -19,6 +19,7 @@ import { import "rain.interpreter/src/lib/bytecode/LibBytecode.sol"; import "../interface/unstable/IOrderBookV3.sol"; +import "../interface/unstable/IOrderBookV3OrderTaker.sol"; import "../lib/LibOrder.sol"; import "../abstract/OrderBookFlashLender.sol"; @@ -116,7 +117,7 @@ uint256 constant CONTEXT_VAULT_IO_BALANCE_DIFF = 4; uint256 constant CONTEXT_VAULT_IO_ROWS = 5; /// @dev Hash of the caller contract metadata for construction. -bytes32 constant CALLER_META_HASH = bytes32(0xd55ed91accdfd893ecc4028057ab2894d6eb88b88f59a27f0b73eaef92d20430); +bytes32 constant CALLER_META_HASH = bytes32(0xf0c79e4006636a71899066ac45a478da4eafaa3117769678b6f18d96138bc156); /// All information resulting from an order calculation that allows for vault IO /// to be calculated and applied, then the handle IO entrypoint to be dispatched. @@ -321,7 +322,7 @@ contract OrderBook is IOrderBookV3, ReentrancyGuard, Multicall, OrderBookFlashLe } /// @inheritdoc IOrderBookV3 - function takeOrders(TakeOrdersConfig calldata config) + function takeOrders(TakeOrdersConfigV2 calldata config) external nonReentrant returns (uint256 totalInput, uint256 totalOutput) @@ -381,13 +382,28 @@ contract OrderBook is IOrderBookV3, ReentrancyGuard, Multicall, OrderBookFlashLe revert MinimumInput(config.minimumInput, totalInput); } + // Prioritise paying down any active flash loans before sending any + // tokens to `msg.sender`. We send the tokens to `msg.sender` first + // adopting a similar pattern to Uniswap flash swaps. We call the caller + // before attempting to pull tokens from them in order to facilitate + // better integrations with external liquidity sources. This could be + // done by the caller using flash loans but this callback: + // - may be simpler for the caller to implement + // - allows the caller to call `takeOrders` _before_ placing external + // trades, which is important if the order logic itself is dependent on + // external data (e.g. prices) that could be modified by the caller's + // trades. + uint256 inputAmountSent = _decreaseFlashDebtThenSendToken(config.input, msg.sender, totalInput); + if (config.data.length > 0) { + IOrderBookV3OrderTaker(msg.sender).onTakeOrders( + config.input, config.output, inputAmountSent, totalOutput, config.data + ); + } + // We already updated vault balances before we took tokens from // `msg.sender` which is usually NOT the correct order of operations for // depositing to a vault. We rely on reentrancy guards to make this safe. IERC20(config.output).safeTransferFrom(msg.sender, address(this), totalOutput); - // Prioritise paying down any active flash loans before sending any - // tokens to `msg.sender`. - _decreaseFlashDebtThenSendToken(config.input, msg.sender, totalInput); } /// @inheritdoc IOrderBookV3 diff --git a/src/interface/unstable/IOrderBookV3.sol b/src/interface/unstable/IOrderBookV3.sol index a15c4d4db..ac82b6d89 100644 --- a/src/interface/unstable/IOrderBookV3.sol +++ b/src/interface/unstable/IOrderBookV3.sol @@ -78,13 +78,18 @@ struct Order { /// hit. Takers are expected to prioritise orders that appear to be offering /// better deals i.e. lower IO ratios. This prioritisation and sorting MUST /// happen offchain, e.g. via. some simulator. -struct TakeOrdersConfig { +/// @param data If nonzero length, triggers `onTakeOrders` on the caller of +/// `takeOrders` with this data. This allows the caller to perform arbitrary +/// onchain actions between receiving their input tokens, before having to send +/// their output tokens. +struct TakeOrdersConfigV2 { address output; address input; uint256 minimumInput; uint256 maximumInput; uint256 maximumIORatio; TakeOrderConfig[] orders; + bytes data; } /// Config for an individual take order from the overall list of orders in a @@ -536,7 +541,9 @@ interface IOrderBookV3 is IERC3156FlashLender, IInterpreterCallerV2 { /// vaults processed. /// @return totalOutput Total tokens taken from `msg.sender` and distributed /// between vaults. - function takeOrders(TakeOrdersConfig calldata config) external returns (uint256 totalInput, uint256 totalOutput); + function takeOrders(TakeOrdersConfigV2 calldata config) + external + returns (uint256 totalInput, uint256 totalOutput); /// Allows `msg.sender` to match two live orders placed earlier by /// non-interactive parties and claim a bounty in the process. The clearer is diff --git a/src/interface/unstable/IOrderBookV3OrderTaker.sol b/src/interface/unstable/IOrderBookV3OrderTaker.sol new file mode 100644 index 000000000..6244bcf7f --- /dev/null +++ b/src/interface/unstable/IOrderBookV3OrderTaker.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: CAL +pragma solidity ^0.8.18; + +interface IOrderBookV3OrderTaker { + /// @notice Called by `OrderBookV3` when `takeOrders` is called with non-zero + /// data, if it caused a non-zero input amount. I.e. if the order(s) taker + /// received some tokens. Input and output directions are relative to the + /// `IOrderBookV3OrderTaker` contract. If the order(s) taker had an active + /// debt from a flash loan then that debt will be paid _before_ calculating + /// any input amounts sent. + /// i.e. the debt is deducted from the input amount before this callback is + /// called. + /// @param inputToken The token that was sent to `IOrderBookV3OrderTaker`. + /// @param outputToken The token that `IOrderBookV3` will attempt to pull + /// from `IOrderBookV3OrderTaker` after this callback returns. + /// @param inputAmountSent The amount of `inputToken` that was sent to + /// `IOrderBookV3OrderTaker`. + /// @param totalOutputAmount The total amount of `outputToken` that + /// `IOrderBookV3` will attempt to pull from `IOrderBookV3OrderTaker` after + /// this callback returns. + /// @param takeOrdersData The data passed to `takeOrders` by the caller. + function onTakeOrders( + address inputToken, + address outputToken, + uint256 inputAmountSent, + uint256 totalOutputAmount, + bytes calldata takeOrdersData + ) external; +} diff --git a/test/concrete/GenericPoolOrderBookFlashBorrower.sender.t.sol b/test/concrete/GenericPoolOrderBookFlashBorrower.sender.t.sol index 58d3ddd6c..b2ab5f4ad 100644 --- a/test/concrete/GenericPoolOrderBookFlashBorrower.sender.t.sol +++ b/test/concrete/GenericPoolOrderBookFlashBorrower.sender.t.sol @@ -28,7 +28,7 @@ contract MockOrderBook is IOrderBookV3 { return true; } - function takeOrders(TakeOrdersConfig calldata) external pure returns (uint256 totalInput, uint256 totalOutput) { + function takeOrders(TakeOrdersConfigV2 calldata) external pure returns (uint256 totalInput, uint256 totalOutput) { return (0, 0); } @@ -76,6 +76,7 @@ contract GenericPoolOrderBookFlashBorrowerTest is Test { abi.encode(address(0), address(0), address(0)) ); bytes memory meta = vm.readFileBinary(GENERIC_POOL_ORDER_BOOK_FLASH_BORROWER_META_PATH); + console2.log("GenericPoolOrderBookFlashBorrowerTest meta hash:"); console2.logBytes32(keccak256(meta)); implementation = address( new GenericPoolOrderBookFlashBorrower(DeployerDiscoverableMetaV2ConstructionConfig( @@ -102,8 +103,8 @@ contract GenericPoolOrderBookFlashBorrowerTest is Test { ); arb_.arb( - TakeOrdersConfig( - address(output_), address(input_), 0, type(uint256).max, type(uint256).max, new TakeOrderConfig[](0) + TakeOrdersConfigV2( + address(output_), address(input_), 0, type(uint256).max, type(uint256).max, new TakeOrderConfig[](0), "" ), 0, abi.encode(address(proxy_), address(proxy_), "") @@ -131,8 +132,8 @@ contract GenericPoolOrderBookFlashBorrowerTest is Test { vm.expectRevert(abi.encodeWithSelector(MinimumOutput.selector, minimumOutput, mintAmount)); arb.arb( - TakeOrdersConfig( - address(output), address(input), 0, type(uint256).max, type(uint256).max, new TakeOrderConfig[](0) + TakeOrdersConfigV2( + address(output), address(input), 0, type(uint256).max, type(uint256).max, new TakeOrderConfig[](0), "" ), minimumOutput, abi.encode(address(proxy), address(proxy), "") diff --git a/test/concrete/OrderBook.withdraw.t.sol b/test/concrete/OrderBook.withdraw.t.sol index d0dd71e35..4f0e02150 100644 --- a/test/concrete/OrderBook.withdraw.t.sol +++ b/test/concrete/OrderBook.withdraw.t.sol @@ -146,6 +146,8 @@ contract OrderBookWithdrawTest is OrderBookExternalMockTest { assumeNotPrecompile(actions[i].token); // Avoid errors from attempting to etch the orderbook. vm.assume(actions[i].token != address(iOrderbook)); + // Avoid eching the vm. + vm.assume(actions[i].token != address(this)); } Action memory action; for (uint256 i = 0; i < actions.length; i++) { diff --git a/test/util/abstract/IOrderBookV3Stub.sol b/test/util/abstract/IOrderBookV3Stub.sol index 566e8c0de..1fcf4e7ec 100644 --- a/test/util/abstract/IOrderBookV3Stub.sol +++ b/test/util/abstract/IOrderBookV3Stub.sol @@ -48,7 +48,7 @@ abstract contract IOrderBookV3Stub is IOrderBookV3 { } /// @inheritdoc IOrderBookV3 - function takeOrders(TakeOrdersConfig calldata) external pure returns (uint256, uint256) { + function takeOrders(TakeOrdersConfigV2 calldata) external pure returns (uint256, uint256) { revert("takeOrders"); } diff --git a/test/util/abstract/OrderBookExternalMockTest.sol b/test/util/abstract/OrderBookExternalMockTest.sol index 7289a0720..61a83ea52 100644 --- a/test/util/abstract/OrderBookExternalMockTest.sol +++ b/test/util/abstract/OrderBookExternalMockTest.sol @@ -47,7 +47,7 @@ abstract contract OrderBookExternalMockTest is Test, IMetaV1, IOrderBookV3Stub { abi.encode(iInterpreter, iStore, address(0)) ); bytes memory meta = vm.readFileBinary(ORDER_BOOK_META_PATH); - console2.log("meta hash:"); + console2.log("OrderBookExternalMockTest meta hash:"); console2.logBytes(abi.encodePacked(keccak256(meta))); iOrderbook = IOrderBookV3(address(new OrderBook(DeployerDiscoverableMetaV2ConstructionConfig(address(iDeployer), meta))));