diff --git a/contracts/docs/payload-types.md b/contracts/docs/payload-types.md index 62564c77c..7643619c9 100644 --- a/contracts/docs/payload-types.md +++ b/contracts/docs/payload-types.md @@ -338,3 +338,58 @@ enum OrderQuantities { |-----|-----------| |`nonce: u64`|The order's nonce (can only be used once but do not have to be used in order).| |`deadline: u40`|The unix timestamp in seconds (inclusive) after which the order is considered invalid by the contract. | + +#### `TwapOrder` + +```rust +struct TwapOrder { + ref_id: u32, + use_internal: bool, + pair_index: u16, + min_price: u256, + recipient: Option
, + hook_data: Option>, + zero_for_one: bool, + twap_data: TwapData, + order_quantities: u128, + max_extra_fee_asset0: u128, + extra_fee_asset0: u128, + exact_in: bool, + signature: Signature +} + +struct TwapData { + nonce: u64, + start_time: u40, + total_parts: u32, + time_interval: u32, + window: u32 +} +``` + +**`TwapOrder`** + +|Field|Description| +|-----|-----------| +|`ref_id: uint32`|Opt-in tag for source of order flow. May opt the user into being charged extra fees beyond gas.| +|`use_internal: bool`|Whether to use angstrom internal balance (`true`) or actual ERC20 balance (`false`) to settle| +|`pair_index: u16`|The index into the `List` array that the order is trading in.| +|`min_price: u256`|The minimum price in asset out over asset in base units in RAY| +|`recipient: Option
`|Recipient for order output, `None` implies signer.| +|`hook_data: Option>`|Optional hook for composable orders, consisting of the hook address concatenated to the hook extra data.| +|`zero_for_one: bool`|Whether the order is swapping in the pair's `asset0` and getting out `asset1` (`true`) or the other way around (`false`)| +|`twap_data: TwapData`|Specifies how the order will be executed over time.| +|`order_quantities: u128`|Description of the quantities the order trades.| +|`max_extra_fee_asset0: u128`|The maximum gas + referral fee the user accepts to be charged (in asset0 base units)| +|`extra_fee_asset0: u128`|The actual extra fee the user ended up getting charged for their order (in asset0 base units)| +|`exact_in: bool`|Whether the specified quantity is the input or output.| +|`signature: Signature`|The signature validating the order.| + +**`TwapData`** +|Field|Description| +|-----|-----------| +|`nonce: u64`|The Twap order's nonce (it is expected to be unique; however, it may be reused if it is no longer active or has not been invalidated).| +|`start_time: u40`|The unix timestamp from which the order becomes valid (or, after which the order is considered active). | +|`total_parts: u32`| The maximum number of times the twap order can be executed. | +|`time_interval: u32`| Specifies the required period between consecutive twap orders. | +|`window: u32`| The bounded time interval, starting at each scheduled execution point during which twap orders can be executed, and attempts outside this window are treated as invalid. | \ No newline at end of file diff --git a/contracts/src/Angstrom.sol b/contracts/src/Angstrom.sol index 4502a4fca..fb40c1e99 100644 --- a/contracts/src/Angstrom.sol +++ b/contracts/src/Angstrom.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.26; -import {console} from "forge-std/console.sol"; import {EIP712} from "solady/src/utils/EIP712.sol"; import {TopLevelAuth} from "./modules/TopLevelAuth.sol"; import {Settlement} from "./modules/Settlement.sol"; @@ -26,6 +25,8 @@ import {ToBOrderBuffer} from "./types/ToBOrderBuffer.sol"; import {ToBOrderVariantMap} from "./types/ToBOrderVariantMap.sol"; import {UserOrderBuffer} from "./types/UserOrderBuffer.sol"; import {UserOrderVariantMap} from "./types/UserOrderVariantMap.sol"; +import {TWAPOrderBuffer} from "./types/TWAPOrderBuffer.sol"; +import {TWAPOrderVariantMap} from "./types/TWAPOrderVariantMap.sol"; /// @author philogy contract Angstrom is @@ -48,7 +49,6 @@ contract Angstrom is } function execute(bytes calldata encoded) external { - console.log("testing we got here"); _nodeBundleLock(); if (encoded.length > 0) { UNI_V4.unlock(encoded); @@ -65,16 +65,14 @@ contract Angstrom is PairArray pairs; (reader, pairs) = PairLib.readFromAndValidate(reader, assets, _configStore); - console.log("read pairs and assets"); _takeAssets(assets); - console.log("took assets"); reader = _updatePools(reader, pairs); - console.log("updated pools"); + reader = _validateAndExecuteToBOrders(reader, pairs); - console.log("exectued tob"); reader = _validateAndExecuteUserOrders(reader, pairs); - console.log("executed user"); + reader = _validateAndExecuteTWAPOrders(reader, pairs); + reader.requireAtEndOf(data); _saveAndSettle(assets); @@ -157,7 +155,6 @@ contract Angstrom is : SignatureLib.readAndCheckERC1271(reader, orderHash); _invalidateOrderHash(orderHash, from); - console.log(from); address to = buffer.recipient; assembly ("memory-safe") { @@ -260,6 +257,92 @@ contract Angstrom is return reader; } + function _validateAndExecuteTWAPOrders(CalldataReader reader, PairArray pairs) + internal + returns (CalldataReader) + { + TypedDataHasher typedHasher = _erc712Hasher(); + TWAPOrderBuffer memory buffer; + buffer.setTypeHash(); + + CalldataReader end; + (reader, end) = reader.readU24End(); + + // Purposefully devolve into an endless loop if the specified length isn't exactly used s.t. + // `reader == end` at some point. + while (reader != end) { + reader = _validateAndExecuteTWAPOrder(reader, buffer, typedHasher, pairs); + } + + return reader; + } + + function _validateAndExecuteTWAPOrder( + CalldataReader reader, + TWAPOrderBuffer memory buffer, + TypedDataHasher typedHasher, + PairArray pairs + ) internal returns (CalldataReader) { + TWAPOrderVariantMap variantMap; + // Load variant map, ref id and set use internal. + (reader, variantMap) = buffer.init(reader); + + // Load and lookup asset in/out and dependent values. + PriceOutVsIn price; + { + uint256 priceOutVsIn; + uint16 pairIndex; + (reader, pairIndex) = reader.readU16(); + (buffer.assetIn, buffer.assetOut, priceOutVsIn) = + pairs.get(pairIndex).getSwapInfo(variantMap.zeroForOne()); + price = PriceOutVsIn.wrap(priceOutVsIn); + } + + (reader, buffer.minPrice) = reader.readU256(); + if (price.into() < buffer.minPrice) revert LimitViolated(); + + (reader, buffer.recipient) = + variantMap.recipientIsSome() ? reader.readAddr() : (reader, address(0)); + + HookBuffer hook; + (reader, hook, buffer.hookDataHash) = HookBufferLib.readFrom(reader, variantMap.noHook()); + + reader = buffer.readTWAPOrderValidation(reader); + + AmountIn amountIn; + AmountOut amountOut; + (reader, amountIn, amountOut) = buffer.loadAndComputeQuantity(reader, variantMap, price); + + address from; + { + bytes32 orderHash = typedHasher.hashTypedData(buffer.hash()); + (reader, from) = variantMap.isEcdsa() + ? SignatureLib.readAndCheckEcdsa(reader, orderHash) + : SignatureLib.readAndCheckERC1271(reader, orderHash); + + _checkTWAPOrderData(buffer.timeInterval, buffer.totalParts, buffer.window); + _checkTWAPOrderDeadline( + _invalidatePartTWAPNonce(orderHash, from, buffer.nonce, buffer.totalParts), + buffer.startTime, + buffer.timeInterval, + buffer.window + ); + } + + // Push before hook as a potential loan. + address to = buffer.recipient; + assembly ("memory-safe") { + to := or(mul(iszero(to), from), to) + } + _settleOrderOut(to, buffer.assetOut, amountOut, buffer.useInternal); + + hook.tryTrigger(from); + + _settleOrderIn(from, buffer.assetIn, amountIn, buffer.useInternal); + + return reader; + } + function _domainNameAndVersion() internal pure diff --git a/contracts/src/modules/OrderInvalidation.sol b/contracts/src/modules/OrderInvalidation.sol index 76f37d759..8223b58e3 100644 --- a/contracts/src/modules/OrderInvalidation.sol +++ b/contracts/src/modules/OrderInvalidation.sol @@ -6,14 +6,69 @@ abstract contract OrderInvalidation { error NonceReuse(); error OrderAlreadyExecuted(); error Expired(); + error TWAPExpired(); + error InvalidTWAPOrder(); + error TWAPOrderNonceReuse(); /// @dev `keccak256("angstrom-v1_0.unordered-nonces.slot")[0:4]` uint256 private constant UNORDERED_NONCES_SLOT = 0xdaa050e9; + /// @dev `keccak256("angstrom-v1_0.twap-unordered-nonces.slot")[0:4]` + uint256 private constant UNORDERED_TWAP_NONCES_SLOT = 0x635a0808; + // type(uint24).max + uint256 private constant MAX_U24 = 0xffffff; + // type(uint32).max + uint256 private constant MAX_U32 = 0xffffffff; + // max upper limit of twap intervals = 31557600 (365.25 days) + uint256 private constant MAX_TWAP_INTERVAL = 31557600; + // min lower limit of twap intervals = 12 seconds + uint256 private constant MIN_TWAP_INTERVAL = 12; + // max no. of order parts = 6311520 (365.25 days / 5 seconds) + uint256 private constant MAX_TWAP_TOTAL_PARTS = 6311520; function invalidateNonce(uint64 nonce) external { _invalidateNonce(msg.sender, nonce); } + function invalidateTWAPOrderNonce(uint64 nonce) external { + assembly ("memory-safe") { + mstore(12, nonce) + mstore(4, UNORDERED_TWAP_NONCES_SLOT) + mstore(0, caller()) + + let partPtr := keccak256(12, 32) + let bitmap := sload(partPtr) + + if eq(and(bitmap, MAX_U24), MAX_U24) { + mstore(0x00, 0x264a877f /* TWAPOrderNonceReuse() */ ) + revert(0x1c, 0x04) + } + + sstore(partPtr, MAX_U24) + } + } + + function _checkTWAPOrderData(uint32 interval, uint32 twapParts, uint32 window) internal pure { + bool invalidInterval = (interval < MIN_TWAP_INTERVAL) || (interval > MAX_TWAP_INTERVAL); + bool invalidTwapParts = (twapParts == 0) || (twapParts > MAX_TWAP_TOTAL_PARTS); + bool invalidWindow = (window < MIN_TWAP_INTERVAL) || (window > interval); + + if (invalidInterval || invalidTwapParts || invalidWindow) { + revert InvalidTWAPOrder(); + } + } + + function _checkTWAPOrderDeadline( + uint256 fulfilledParts, + uint40 startTime, + uint32 interval, + uint32 window + ) internal view { + uint256 currentPartStart = startTime + (fulfilledParts * interval); + bool expired = + (block.timestamp < currentPartStart) || (block.timestamp > currentPartStart + window); + if (expired) revert TWAPExpired(); + } + function _checkDeadline(uint256 deadline) internal view { if (block.timestamp > deadline) revert Expired(); } @@ -38,6 +93,45 @@ abstract contract OrderInvalidation { } } + function _invalidatePartTWAPNonce( + bytes32 orderHash, + address owner, + uint64 nonce, + uint32 twapParts + ) internal returns (uint256 _cachedFulfilledParts) { + uint256 bitmap; + uint256 partPtr; + assembly ("memory-safe") { + mstore(12, nonce) + mstore(4, UNORDERED_TWAP_NONCES_SLOT) + mstore(0, owner) + partPtr := keccak256(12, 32) + + bitmap := sload(partPtr) + + // the probability that two order hashes collide in their lower 232 bits is 1 in 2^232. + // for orders tied to a specific address, the space of possible values is more limited, + // making the chance of collision even smaller. + if iszero(bitmap) { bitmap := shl(24, orderHash) } + } + + uint256 lowerHashBits = uint232(uint256(orderHash)) ^ bitmap >> 24; + if (lowerHashBits != 0) revert TWAPOrderNonceReuse(); + + _cachedFulfilledParts = bitmap & MAX_U24; + uint256 fulfilledParts = _cachedFulfilledParts + 1; + + if (fulfilledParts != twapParts) { + bitmap += 1; + } else { + bitmap = 0; + } + + assembly ("memory-safe") { + sstore(partPtr, bitmap) + } + } + function _invalidateOrderHash(bytes32 orderHash, address from) internal { assembly ("memory-safe") { mstore(20, from) diff --git a/contracts/src/modules/TopLevelAuth.sol b/contracts/src/modules/TopLevelAuth.sol index 31975f299..0154564df 100644 --- a/contracts/src/modules/TopLevelAuth.sol +++ b/contracts/src/modules/TopLevelAuth.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import {console} from "forge-std/console.sol"; import {IAngstromAuth} from "../interfaces/IAngstromAuth.sol"; import {UniConsumer} from "./UniConsumer.sol"; @@ -50,19 +49,13 @@ abstract contract TopLevelAuth is UniConsumer, IAngstromAuth { uint24 bundleFee, uint24 unlockedFee ) external { - console.log("cnt"); _onlyController(); if (assetA > assetB) (assetA, assetB) = (assetB, assetA); - console.log("store key"); StoreKey key = PoolConfigStoreLib.keyFromAssetsUnchecked(assetA, assetB); - console.log("setIntoNew"); _configStore = _configStore.setIntoNew(key, assetA, assetB, tickSpacing, bundleFee); - console.log("validating"); unlockedFee.validate(); - console.log("bit_math", assetA); _unlockedFeePackedSet[key] = (uint256(unlockedFee) << 1) | 1; - console.log("res", assetA, assetB); } function initializePool( diff --git a/contracts/src/periphery/ControllerV1.sol b/contracts/src/periphery/ControllerV1.sol index da6532639..f47c3b352 100644 --- a/contracts/src/periphery/ControllerV1.sol +++ b/contracts/src/periphery/ControllerV1.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {console} from "forge-std/console.sol"; import {IAngstromAuth} from "../interfaces/IAngstromAuth.sol"; import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import { @@ -99,7 +98,6 @@ contract ControllerV1 is Ownable2Step { pools[key] = Pool(asset0, asset1); emit PoolConfigured(asset0, asset1, tickSpacing, bundleFee, unlockedFee); - console.log("log this shit", uint256(0x10)); ANGSTROM.configurePool(asset0, asset1, tickSpacing, bundleFee, unlockedFee); } diff --git a/contracts/src/types/TWAPOrderBuffer.sol b/contracts/src/types/TWAPOrderBuffer.sol new file mode 100644 index 000000000..7c03cbe6c --- /dev/null +++ b/contracts/src/types/TWAPOrderBuffer.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {CalldataReader} from "./CalldataReader.sol"; +import {TWAPOrderVariantMap} from "./TWAPOrderVariantMap.sol"; +import {PriceAB as PriceOutVsIn, AmountA as AmountOut, AmountB as AmountIn} from "./Price.sol"; + +struct TWAPOrderBuffer { + bytes32 typeHash; + uint32 refId; + bool exactIn; + uint256 quantity; + uint256 maxExtraFeeAsset0; + uint256 minPrice; + bool useInternal; + address assetIn; + address assetOut; + address recipient; + bytes32 hookDataHash; + uint64 nonce; + uint40 startTime; + uint32 totalParts; + uint32 timeInterval; + uint32 window; +} + +using TWAPOrderBufferLib for TWAPOrderBuffer global; + +/// @author philogy +library TWAPOrderBufferLib { + error GasAboveMax(); + + uint256 internal constant BUFFER_BYTES = 512; + + uint256 internal constant VARIANT_MAP_BYTES = 1; + /// @dev Destination offset for direct calldatacopy of 4-byte ref ID (therefore not word aligned). + uint256 internal constant REF_ID_MEM_OFFSET = 0x20; + uint256 internal constant REF_ID_BYTES = 4; + uint256 internal constant NONCE_MEM_OFFSET = 0x160; + uint256 internal constant NONCE_BYTES = 8; + uint256 internal constant START_TIME_MEM_OFFSET = 0x180; + uint256 internal constant START_TIME_BYTES = 5; + uint256 internal constant PARTS_MEM_OFFSET = 0x1a0; + uint256 internal constant PARTS_BYTES = 4; + uint256 internal constant TIME_INTERVALS_MEM_OFFSET = 0x1c0; + uint256 internal constant TIME_INTERVALS_BYTES = 4; + uint256 internal constant WINDOW_MEM_OFFSET = 0x1e0; + uint256 internal constant WINDOW_BYTES = 4; + + /// forgefmt: disable-next-item + bytes32 internal constant TWAP_ORDER_TYPEHASH = keccak256( + "TimeWeightedAveragePriceOrder(" + "uint32 ref_id," + "bool exact_in," + "uint128 amount," + "uint128 max_extra_fee_asset0," + "uint256 min_price," + "bool use_internal," + "address asset_in," + "address asset_out," + "address recipient," + "bytes hook_data," + "uint64 nonce," + "uint40 start_time," + "uint32 total_parts," + "uint32 time_interval," + "uint32 window" + ")" + ); + + function setTypeHash(TWAPOrderBuffer memory self) internal pure { + self.typeHash = TWAP_ORDER_TYPEHASH; + } + + function init(TWAPOrderBuffer memory self, CalldataReader reader) + internal + pure + returns (CalldataReader, TWAPOrderVariantMap variantMap) + { + assembly ("memory-safe") { + variantMap := byte(0, calldataload(reader)) + reader := add(reader, VARIANT_MAP_BYTES) + // Copy `refId` from calldata directly to memory. + calldatacopy( + add(self, add(REF_ID_MEM_OFFSET, sub(0x20, REF_ID_BYTES))), reader, REF_ID_BYTES + ) + // Advance reader. + reader := add(reader, REF_ID_BYTES) + } + + self.useInternal = variantMap.useInternal(); + + return (reader, variantMap); + } + + function hash(TWAPOrderBuffer memory self) internal pure returns (bytes32 orderHash) { + assembly ("memory-safe") { + orderHash := keccak256(self, BUFFER_BYTES) + } + } + + function loadAndComputeQuantity( + TWAPOrderBuffer memory self, + CalldataReader reader, + TWAPOrderVariantMap variant, + PriceOutVsIn price + ) internal pure returns (CalldataReader, AmountIn quantityIn, AmountOut quantityOut) { + uint256 quantity; + (reader, quantity) = reader.readU128(); + self.exactIn = variant.exactIn(); + self.quantity = quantity; + + uint128 extraFeeAsset0; + uint128 maxExtraFeeAsset0; + (reader, maxExtraFeeAsset0) = reader.readU128(); + (reader, extraFeeAsset0) = reader.readU128(); + if (extraFeeAsset0 > maxExtraFeeAsset0) revert GasAboveMax(); + self.maxExtraFeeAsset0 = maxExtraFeeAsset0; + + if (variant.zeroForOne()) { + AmountIn fee = AmountIn.wrap(extraFeeAsset0); + if (variant.exactIn()) { + quantityIn = AmountIn.wrap(quantity); + quantityOut = price.convertDown(quantityIn - fee); + } else { + quantityOut = AmountOut.wrap(quantity); + quantityIn = price.convertUp(quantityOut) + fee; + } + } else { + AmountOut fee = AmountOut.wrap(extraFeeAsset0); + if (variant.exactIn()) { + quantityIn = AmountIn.wrap(quantity); + quantityOut = price.convertDown(quantityIn) - fee; + } else { + quantityOut = AmountOut.wrap(quantity); + quantityIn = price.convertUp(quantityOut + fee); + } + } + + return (reader, quantityIn, quantityOut); + } + + function readTWAPOrderValidation(TWAPOrderBuffer memory self, CalldataReader reader) + internal + pure + returns (CalldataReader) + { + // Copy slices directly from calldata into memory. + assembly ("memory-safe") { + calldatacopy( + add(self, add(NONCE_MEM_OFFSET, sub(0x20, NONCE_BYTES))), reader, NONCE_BYTES + ) + reader := add(reader, NONCE_BYTES) + calldatacopy( + add(self, add(START_TIME_MEM_OFFSET, sub(0x20, START_TIME_BYTES))), + reader, + START_TIME_BYTES + ) + reader := add(reader, START_TIME_BYTES) + calldatacopy( + add(self, add(PARTS_MEM_OFFSET, sub(0x20, PARTS_BYTES))), reader, PARTS_BYTES + ) + reader := add(reader, PARTS_BYTES) + calldatacopy( + add(self, add(TIME_INTERVALS_MEM_OFFSET, sub(0x20, TIME_INTERVALS_BYTES))), + reader, + TIME_INTERVALS_BYTES + ) + reader := add(reader, TIME_INTERVALS_BYTES) + calldatacopy( + add(self, add(WINDOW_MEM_OFFSET, sub(0x20, WINDOW_BYTES))), reader, WINDOW_BYTES + ) + reader := add(reader, WINDOW_BYTES) + } + return reader; + } +} diff --git a/contracts/src/types/TWAPOrderVariantMap.sol b/contracts/src/types/TWAPOrderVariantMap.sol new file mode 100644 index 000000000..e627886a1 --- /dev/null +++ b/contracts/src/types/TWAPOrderVariantMap.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +type TWAPOrderVariantMap is uint8; + +using TWAPOrderVariantMapLib for TWAPOrderVariantMap global; + +/// @author philogy +library TWAPOrderVariantMapLib { + uint256 internal constant USE_INTERNAL_BIT = 0x01; + uint256 internal constant HAS_RECIPIENT_BIT = 0x02; + uint256 internal constant HAS_HOOK_BIT = 0x04; + uint256 internal constant ZERO_FOR_ONE_BIT = 0x08; + uint256 internal constant IS_EXACT_IN_BIT = 0x10; + uint256 internal constant IS_ECDSA_BIT = 0x20; + + function useInternal(TWAPOrderVariantMap variant) internal pure returns (bool) { + return TWAPOrderVariantMap.unwrap(variant) & USE_INTERNAL_BIT != 0; + } + + function recipientIsSome(TWAPOrderVariantMap variant) internal pure returns (bool) { + return TWAPOrderVariantMap.unwrap(variant) & HAS_RECIPIENT_BIT != 0; + } + + function noHook(TWAPOrderVariantMap variant) internal pure returns (bool) { + return TWAPOrderVariantMap.unwrap(variant) & HAS_HOOK_BIT == 0; + } + + function zeroForOne(TWAPOrderVariantMap variant) internal pure returns (bool) { + return TWAPOrderVariantMap.unwrap(variant) & ZERO_FOR_ONE_BIT != 0; + } + + function exactIn(TWAPOrderVariantMap variant) internal pure returns (bool) { + return TWAPOrderVariantMap.unwrap(variant) & IS_EXACT_IN_BIT != 0; + } + + function isEcdsa(TWAPOrderVariantMap variant) internal pure returns (bool) { + return TWAPOrderVariantMap.unwrap(variant) & IS_ECDSA_BIT != 0; + } +} diff --git a/contracts/test/Angstrom.t.sol b/contracts/test/Angstrom.t.sol index 70fe3e408..03ffa8f2a 100644 --- a/contracts/test/Angstrom.t.sol +++ b/contracts/test/Angstrom.t.sol @@ -8,7 +8,11 @@ import {Bundle} from "test/_reference/Bundle.sol"; import {Asset, AssetLib} from "test/_reference/Asset.sol"; import {Pair, PairLib} from "test/_reference/Pair.sol"; import {UserOrder, UserOrderLib} from "test/_reference/UserOrder.sol"; -import {PartialStandingOrder, ExactFlashOrder} from "test/_reference/OrderTypes.sol"; +import { + PartialStandingOrder, + ExactFlashOrder, + TimeWeightedAveragePriceOrder +} from "test/_reference/OrderTypes.sol"; import {PriceAB as Price10} from "src/types/Price.sol"; import {MockERC20} from "super-sol/mocks/MockERC20.sol"; @@ -113,6 +117,94 @@ contract AngstromTest is BaseTest { angstrom.execute(payload); } + function test_twapOrderWithFees() public { + uint256 fee = 0.002e6; + + vm.prank(controller); + angstrom.configurePool(asset0, asset1, 1, uint24(fee), 0); + + console.log("asset0: %s", asset0); + console.log("asset1: %s", asset1); + + Account memory user1 = makeAccount("user_1"); + MockERC20(asset0).mint(user1.addr, 100.0e18); + vm.prank(user1.addr); + MockERC20(asset0).approve(address(angstrom), type(uint256).max); + + Account memory user2 = makeAccount("user_2"); + MockERC20(asset1).mint(user2.addr, 100.0e18); + vm.prank(user2.addr); + MockERC20(asset1).approve(address(angstrom), type(uint256).max); + + Price10 price = Price10.wrap(1e27); + + Bundle memory bundle; + + bundle.addAsset(asset0).addAsset(asset1).addPair(asset0, asset1, price); + + uint256 startTime = block.timestamp; + uint256 timeInterval; + + { + TimeWeightedAveragePriceOrder memory order; + order.exactIn = true; + order.amount = 10.0e18; + order.maxExtraFeeAsset0 = 1.3e18; + order.minPrice = 0.1e27; + order.assetIn = asset0; + order.assetOut = asset1; + order.nonce = 18446744073709551615; + order.startTime = u40(block.timestamp); + order.totalParts = 3; + order.timeInterval = 12 seconds; + order.window = order.timeInterval; + sign(user1, order.meta, digest712(order.hash())); + order.extraFeeAsset0 = 1.0e18; + bundle.addTwap(order); + timeInterval = order.timeInterval; + } + + { + TimeWeightedAveragePriceOrder memory order; + order.exactIn = true; + order.amount = 9.200400801603206413e18; + order.maxExtraFeeAsset0 = 0.2e18; + order.minPrice = 0.1e27; + order.assetIn = asset1; + order.assetOut = asset0; + order.nonce = 18446744073709551515; + order.startTime = u40(block.timestamp); + order.totalParts = 3; + order.timeInterval = 12 seconds; + order.window = order.timeInterval; + sign(user2, order.meta, digest712(order.hash())); + order.extraFeeAsset0 = 0.2e18; + bundle.addTwap(order); + } + + bundle.assets[0].save += 1.018e18; + bundle.assets[1].save += 0.218400801603206413e18; + bundle.assets[1].take += 10.0e18; + bundle.assets[1].settle += 10.0e18; + + bytes memory payload = bundle.encode(rawGetConfigStore(address(angstrom))); + vm.startPrank(node); + + angstrom.execute(payload); + + // only one bundle per block. + vm.roll(block.number + 1); + vm.warp(startTime + timeInterval); + angstrom.execute(payload); + + // only one bundle per block. + vm.roll(block.number + 2); + vm.warp(startTime + 2 * (timeInterval)); + angstrom.execute(payload); + + vm.stopPrank(); + } + function digest712(bytes32 structHash) internal view returns (bytes32) { return erc712Hash(domainSeparator, structHash); } diff --git a/contracts/test/_helpers/Utils.sol b/contracts/test/_helpers/Utils.sol index 24d9044bd..6f1fa9e36 100644 --- a/contracts/test/_helpers/Utils.sol +++ b/contracts/test/_helpers/Utils.sol @@ -18,4 +18,21 @@ library Utils { bx := xor(shl(64, dirt), x) } } + + // https://github.com/ethereum/solidity/issues/15144 + function brutalizeU40(uint40 y) internal view returns (uint40 cx) { + assembly ("memory-safe") { + mstore(0x00, gas()) + let dirt := keccak256(0, 32) + cx := xor(shl(40, dirt), y) + } + } + + function brutalizeU32(uint32 z) internal view returns (uint32 dx) { + assembly ("memory-safe") { + mstore(0x00, gas()) + let dirt := keccak256(0, 32) + dx := xor(shl(32, dirt), z) + } + } } diff --git a/contracts/test/_mocks/OpenAngstrom.sol b/contracts/test/_mocks/OpenAngstrom.sol index b08cda1b4..63281915e 100644 --- a/contracts/test/_mocks/OpenAngstrom.sol +++ b/contracts/test/_mocks/OpenAngstrom.sol @@ -12,6 +12,8 @@ import {ToBOrderBuffer} from "src/types/ToBOrderBuffer.sol"; import {ToBOrderVariantMap} from "src/types/ToBOrderVariantMap.sol"; import {UserOrderBuffer} from "src/types/UserOrderBuffer.sol"; import {UserOrderVariantMap} from "src/types/UserOrderVariantMap.sol"; +import {TWAPOrderBuffer} from "src/types/TWAPOrderBuffer.sol"; +import {TWAPOrderVariantMap} from "src/types/TWAPOrderVariantMap.sol"; import {PoolId} from "v4-core/src/types/PoolId.sol"; import {Position} from "src/types/Positions.sol"; import {PoolConfigStore} from "src/libraries/PoolConfigStore.sol"; @@ -85,6 +87,22 @@ contract OpenAngstrom is Angstrom { reader.requireAtEndOf(userOrderPayload); } + /// @custom:pade (List, List, UserOrder) + function validateAndExecuteTWAPOrder(bytes calldata userOrderPayload) public { + CalldataReader reader = CalldataReaderLib.from(userOrderPayload); + + AssetArray assets; + (reader, assets) = AssetLib.readFromAndValidate(reader); + PairArray pairs; + (reader, pairs) = PairLib.readFromAndValidate(reader, assets, _configStore); + + TWAPOrderBuffer memory buffer; + buffer.setTypeHash(); + reader = _validateAndExecuteTWAPOrder(reader, buffer, _erc712Hasher(), pairs); + + reader.requireAtEndOf(userOrderPayload); + } + /// @dev custom:pade List function saveAndSettle(bytes calldata assetsPayload) public { CalldataReader reader = CalldataReaderLib.from(assetsPayload); diff --git a/contracts/test/_reference/Bundle.sol b/contracts/test/_reference/Bundle.sol index 28efc995a..7d57d3d23 100644 --- a/contracts/test/_reference/Bundle.sol +++ b/contracts/test/_reference/Bundle.sol @@ -5,7 +5,7 @@ import {UserOrder, UserOrderLib} from "./UserOrder.sol"; import {Asset, AssetLib} from "./Asset.sol"; import {Pair, PairLib} from "./Pair.sol"; import {PriceAB as Price10} from "src/types/Price.sol"; -import {TopOfBlockOrder, OrdersLib} from "./OrderTypes.sol"; +import {TopOfBlockOrder, TimeWeightedAveragePriceOrder, OrdersLib} from "./OrderTypes.sol"; import {PoolUpdate, PoolUpdateLib} from "./PoolUpdate.sol"; import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; @@ -15,6 +15,7 @@ struct Bundle { PoolUpdate[] poolUpdates; TopOfBlockOrder[] toBOrders; UserOrder[] userOrders; + TimeWeightedAveragePriceOrder[] twapOrders; } using BundleLib for Bundle global; @@ -22,6 +23,7 @@ using BundleLib for Bundle global; /// @author philogy library BundleLib { using OrdersLib for TopOfBlockOrder[]; + using OrdersLib for TimeWeightedAveragePriceOrder[]; using UserOrderLib for UserOrder[]; using AssetLib for Asset[]; using PairLib for Pair[]; @@ -35,7 +37,8 @@ library BundleLib { self.pairs.encode(self.assets, configStore), self.poolUpdates.encode(self.pairs), self.toBOrders.encode(self.pairs), - self.userOrders.encode(self.pairs) + self.userOrders.encode(self.pairs), + self.twapOrders.encode(self.pairs) ); } @@ -119,6 +122,24 @@ library BundleLib { return self; } + function addTwap(Bundle memory self, TimeWeightedAveragePriceOrder memory twap) + internal + pure + returns (Bundle memory) + { + // self.addPair(twap.assetIn, twap.assetOut); + + TimeWeightedAveragePriceOrder[] memory newTwapOrders = + new TimeWeightedAveragePriceOrder[](self.twapOrders.length + 1); + for (uint256 i = 0; i < self.twapOrders.length; i++) { + newTwapOrders[i] = self.twapOrders[i]; + } + newTwapOrders[self.twapOrders.length] = twap; + self.twapOrders = newTwapOrders; + + return self; + } + function addDeltas(Bundle memory self, uint256 index0, uint256 index1, BalanceDelta deltas) internal pure diff --git a/contracts/test/_reference/OrderTypes.sol b/contracts/test/_reference/OrderTypes.sol index df9877a15..7b18ee45c 100644 --- a/contracts/test/_reference/OrderTypes.sol +++ b/contracts/test/_reference/OrderTypes.sol @@ -12,11 +12,11 @@ import { ExactStandingOrder as SignedExactStandingOrder, PartialFlashOrder as SignedPartialFlashOrder, ExactFlashOrder as SignedExactFlashOrder, - TopOfBlockOrder as SignedTopOfBlockOrder + TopOfBlockOrder as SignedTopOfBlockOrder, + TimeWeightedAveragePriceOrder as SignedTimeWeightedAveragePriceOrder } from "./SignedTypes.sol"; import {FormatLib} from "super-sol/libraries/FormatLib.sol"; -import {console} from "forge-std/console.sol"; struct OrderMeta { bool isEcdsa; @@ -109,12 +109,34 @@ struct TopOfBlockOrder { uint128 gasUsedAsset0; } +struct TimeWeightedAveragePriceOrder { + uint32 refId; + bool exactIn; + uint128 amount; + uint128 maxExtraFeeAsset0; + uint256 minPrice; + bool useInternal; + address assetIn; + address assetOut; + address recipient; + address hook; + bytes hookPayload; + uint64 nonce; + uint40 startTime; + uint32 totalParts; + uint32 timeInterval; + uint32 window; + OrderMeta meta; + uint128 extraFeeAsset0; +} + using OrdersLib for OrderMeta global; using OrdersLib for PartialStandingOrder global; using OrdersLib for ExactStandingOrder global; using OrdersLib for PartialFlashOrder global; using OrdersLib for ExactFlashOrder global; using OrdersLib for TopOfBlockOrder global; +using OrdersLib for TimeWeightedAveragePriceOrder global; library OrdersLib { using PairLib for *; @@ -200,6 +222,26 @@ library OrdersLib { ).hash(); } + function hash(TimeWeightedAveragePriceOrder memory order) internal pure returns (bytes32) { + return SignedTimeWeightedAveragePriceOrder( + order.refId, + order.exactIn, + order.amount, + order.maxExtraFeeAsset0, + order.minPrice, + order.useInternal, + order.assetIn, + order.assetOut, + order.recipient, + _toHookData(order.hook, order.hookPayload), + order.nonce, + order.startTime, + order.totalParts, + order.timeInterval, + order.window + ).hash(); + } + /// @dev WARNING: Assumes `pairs` are sorted. function encode(PartialStandingOrder memory order, Pair[] memory pairs) internal @@ -376,6 +418,55 @@ library OrdersLib { ); } + function encode(TimeWeightedAveragePriceOrder[] memory orders, Pair[] memory pairs) + internal + pure + returns (bytes memory b) + { + for (uint256 i = 0; i < orders.length; i++) { + b = bytes.concat(b, orders[i].encode(pairs)); + } + b = bytes.concat(bytes3(b.length.toUint24()), b); + } + + function toVariantMap(TimeWeightedAveragePriceOrder memory order, bool zeroForOne) + internal + pure + returns (uint8 varMap) + { + varMap = (order.useInternal ? 1 : 0) | (order.recipient != address(0) ? 2 : 0) + | (order.hook != address(0) ? 4 : 0) | (zeroForOne ? 8 : 0) | (order.exactIn ? 16 : 0) + | (order.meta.isEcdsa ? 32 : 0); + } + + function encode(TimeWeightedAveragePriceOrder memory order, Pair[] memory pairs) + internal + pure + returns (bytes memory) + { + (uint16 pairIndex, bool zeroForOne) = pairs.getIndex(order.assetIn, order.assetOut); + + return bytes.concat( + bytes.concat( + bytes1(order.toVariantMap(zeroForOne)), + bytes4(order.refId), + bytes2(pairIndex), + bytes32(order.minPrice), + _encodeRecipient(order.recipient), + _encodeHookData(order.hook, order.hookPayload), + bytes8(order.nonce) + ), + bytes5(order.startTime), + bytes4(order.totalParts), + bytes4(order.timeInterval), + bytes4(order.window), + bytes16(order.amount), + bytes16(order.maxExtraFeeAsset0), + bytes16(order.extraFeeAsset0), + _encodeSig(order.meta) + ); + } + function toStr(PartialStandingOrder memory o) internal pure returns (string memory str) { str = string.concat( "PartialStandingOrder {", @@ -512,6 +603,50 @@ library OrdersLib { ); } + function toStr(TimeWeightedAveragePriceOrder memory o) + internal + pure + returns (string memory str) + { + str = string.concat( + "ExactStandingOrder {", + "\n exactIn: ", + o.exactIn.toStr(), + ",\n amount: ", + o.amount.toStr(), + ",\n minPrice: ", + o.minPrice.toStr(), + ",\n useInternal: ", + o.useInternal.toStr(), + ",\n assetIn: ", + o.assetIn.toStr(), + ",\n assetOut: ", + o.assetOut.toStr() + ); + str = string.concat( + str, + ",\n recipient: ", + o.recipient.toStr(), + ",\n hook: ", + o.hook.toStr(), + ",\n hookPayload: ", + o.hookPayload.toStr(), + ",\n nonce: ", + o.nonce.toStr(), + ",\n startTime: ", + o.startTime.toStr(), + ",\n totalParts: ", + o.totalParts.toStr(), + ",\n timeInterval: ", + o.timeInterval.toStr(), + ",\n window: ", + o.window.toStr(), + ",\n meta: ", + o.meta.toStr(), + "\n}" + ); + } + function toStr(OrderMeta memory meta) internal pure returns (string memory) { return string.concat( "OrderMeta { isEcdsa: ", diff --git a/contracts/test/_reference/SignedTypes.sol b/contracts/test/_reference/SignedTypes.sol index 319662119..a9c4daa6c 100644 --- a/contracts/test/_reference/SignedTypes.sol +++ b/contracts/test/_reference/SignedTypes.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; import {UserOrderBufferLib} from "src/types/UserOrderBuffer.sol"; import {ToBOrderBufferLib} from "src/types/ToBOrderBuffer.sol"; +import {TWAPOrderBufferLib} from "src/types/TWAPOrderBuffer.sol"; struct PartialStandingOrder { uint32 ref_id; @@ -73,11 +74,30 @@ struct TopOfBlockOrder { uint64 valid_for_block; } +struct TimeWeightedAveragePriceOrder { + uint32 ref_id; + bool exact_in; + uint128 amount; + uint128 max_extra_fee_asset0; + uint256 min_price; + bool use_internal; + address asset_in; + address asset_out; + address recipient; + bytes hook_data; + uint64 nonce; + uint40 start_time; + uint32 total_parts; + uint32 time_interval; + uint32 window; +} + using SignedTypesLib for ExactStandingOrder global; using SignedTypesLib for PartialStandingOrder global; using SignedTypesLib for ExactFlashOrder global; using SignedTypesLib for PartialFlashOrder global; using SignedTypesLib for TopOfBlockOrder global; +using SignedTypesLib for TimeWeightedAveragePriceOrder global; /// @author philogy library SignedTypesLib { @@ -174,4 +194,45 @@ library SignedTypesLib { ) ); } + + struct TimeWeightedAveragePriceOrderMem { + bytes32 type_hash; + uint32 ref_id; + bool exact_in; + uint128 amount; + uint128 max_extra_fee_asset0; + uint256 min_price; + bool use_internal; + address asset_in; + address asset_out; + address recipient; + bytes32 hook_data_hash; + uint64 nonce; + uint40 start_time; + uint32 total_parts; + uint32 time_interval; + uint32 window; + } + + function hash(TimeWeightedAveragePriceOrder memory self) internal pure returns (bytes32) { + TimeWeightedAveragePriceOrderMem memory orderToMem = TimeWeightedAveragePriceOrderMem({ + type_hash: TWAPOrderBufferLib.TWAP_ORDER_TYPEHASH, + ref_id: self.ref_id, + exact_in: self.exact_in, + amount: self.amount, + max_extra_fee_asset0: self.max_extra_fee_asset0, + min_price: self.min_price, + use_internal: self.use_internal, + asset_in: self.asset_in, + asset_out: self.asset_out, + recipient: self.recipient, + hook_data_hash: keccak256(self.hook_data), + nonce: self.nonce, + start_time: self.start_time, + total_parts: self.total_parts, + time_interval: self.time_interval, + window: self.window + }); + return keccak256(abi.encode(orderToMem)); + } } diff --git a/contracts/test/benchmark/TWAPOrder.b.sol b/contracts/test/benchmark/TWAPOrder.b.sol new file mode 100644 index 000000000..9eedbf830 --- /dev/null +++ b/contracts/test/benchmark/TWAPOrder.b.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {BaseTest} from "test/_helpers/BaseTest.sol"; +import {OpenAngstrom} from "test/_mocks/OpenAngstrom.sol"; +import {Pair, PairLib} from "test/_reference/Pair.sol"; +import {Asset, AssetLib} from "test/_reference/Asset.sol"; +import {Bundle} from "test/_reference/Bundle.sol"; +import {PoolConfigStore} from "src/libraries/PoolConfigStore.sol"; +import {MockERC20} from "super-sol/mocks/MockERC20.sol"; +import {PoolManager} from "v4-core/src/PoolManager.sol"; +import {TimeWeightedAveragePriceOrder} from "../_reference/OrderTypes.sol"; +import {PriceAB} from "src/types/Price.sol"; + +import {console} from "forge-std/console.sol"; + +/// @author philogy +contract TWAPOrderBenchmarkTest is BaseTest { + using AssetLib for *; + using PairLib for *; + + OpenAngstrom angstrom; + PoolManager uni; + + address asset0; + address asset1; + + address fee_master = makeAddr("fee_master"); + address controller = makeAddr("controller"); + address node = makeAddr("the_one"); + + function setUp() public { + uni = new PoolManager(address(0)); + angstrom = OpenAngstrom(deployAngstrom(type(OpenAngstrom).creationCode, uni, controller)); + (asset0, asset1) = deployTokensSorted(); + vm.startPrank(controller); + angstrom.configurePool(asset0, asset1, 1, 0, 0); + angstrom.toggleNodes(addressArray(abi.encode(node))); + vm.stopPrank(); + } + + function test_benchmark_TwapOrder() public { + Account memory user = makeAccount("user"); + + uint128 balance = 3.3e18; + MockERC20(asset0).mint(user.addr, balance); + uint128 other = 34_000e18; + MockERC20(asset1).mint(user.addr, other); + vm.startPrank(user.addr); + MockERC20(asset0).approve(address(angstrom), type(uint256).max); + angstrom.deposit(asset0, balance); + MockERC20(asset1).approve(address(angstrom), type(uint256).max); + angstrom.deposit(asset1, other); + vm.stopPrank(); + + TimeWeightedAveragePriceOrder memory order; + order.exactIn = true; + order.amount = 1e18; + order.maxExtraFeeAsset0 = 0; + order.minPrice = 10.0e27; + order.useInternal = true; + order.assetIn = asset0; + order.assetOut = asset1; + order.nonce = 18446744073709551615; + order.startTime = u40(block.timestamp); + order.totalParts = 3; + order.timeInterval = 12 seconds; + order.window = order.timeInterval; + sign(user, order.meta, erc712Hash(computeDomainSeparator(address(angstrom)), order.hash())); + + Asset[] memory assets = new Asset[](2); + assets[0].addr = asset0; + assets[1].addr = asset1; + Pair[] memory pair = new Pair[](1); + pair[0] = Pair(asset0, asset1, PriceAB.wrap(11.5e27)); + + bytes memory payload = bytes.concat( + assets.encode(), + pair.encode(assets, PoolConfigStore.unwrap(angstrom.configStore())), + order.encode(pair) + ); + + angstrom.validateAndExecuteTWAPOrder(payload); + vm.warp(order.startTime + order.timeInterval); + angstrom.validateAndExecuteTWAPOrder(payload); + vm.warp(order.startTime + 2 * (order.timeInterval)); + angstrom.validateAndExecuteTWAPOrder(payload); + } +} diff --git a/contracts/test/modules/OrderInvalidation.t.sol b/contracts/test/modules/OrderInvalidation.t.sol index ae45518d2..0e2a83559 100644 --- a/contracts/test/modules/OrderInvalidation.t.sol +++ b/contracts/test/modules/OrderInvalidation.t.sol @@ -10,10 +10,114 @@ contract InvalidationManagerTest is Test, OrderInvalidation { using Utils for *; bytes4 internal constant NONCES_SLOT = bytes4(keccak256("angstrom-v1_0.unordered-nonces.slot")); + bytes4 internal constant TWAP_NONCES_SLOT = 0x635a0808; + uint256 private constant MAX_TWAP_INTERVAL = 31557600; + uint256 private constant MIN_TWAP_INTERVAL = 12; + uint256 private constant MAX_TWAP_TOTAL_PARTS = 6311520; + uint256 private constant MAX_U32_VAL = 4294967295; function test_fuzzing_revertsUponReuse(address owner, uint64 nonce) public { _invalidateNonce(owner.brutalize(), nonce.brutalize()); vm.expectRevert(OrderInvalidation.NonceReuse.selector); _invalidateNonce(owner.brutalize(), nonce.brutalize()); } + + function test_fuzzing_revertsUponInvalidatedOrder(uint64 nonce) public { + this.invalidateTWAPOrderNonce(nonce.brutalize()); + vm.expectRevert(OrderInvalidation.TWAPOrderNonceReuse.selector); + this.invalidateTWAPOrderNonce(nonce.brutalize()); + } + + function test_fuzzing_revertsUponExpiry( + uint256 fulfilledParts, + uint40 startTime, + uint32 interval, + uint32 window + ) public { + interval = uint32(bound(uint256(interval), MIN_TWAP_INTERVAL, MAX_TWAP_INTERVAL)); + window = uint32(bound(uint256(interval), MIN_TWAP_INTERVAL, interval)); + fulfilledParts = bound(fulfilledParts, 1, MAX_TWAP_TOTAL_PARTS); + + vm.warp(startTime + (interval * fulfilledParts)); + _checkTWAPOrderDeadline(fulfilledParts, startTime, interval, window); + + vm.warp(startTime + (interval * fulfilledParts) + window); + _checkTWAPOrderDeadline(fulfilledParts, startTime, interval, window); + + vm.warp(startTime + (interval * fulfilledParts) - 1); + vm.expectRevert(OrderInvalidation.TWAPExpired.selector); + _checkTWAPOrderDeadline(fulfilledParts, startTime, interval, window); + + vm.warp(startTime + (interval * fulfilledParts) + window + 1); + vm.expectRevert(OrderInvalidation.TWAPExpired.selector); + _checkTWAPOrderDeadline(fulfilledParts, startTime, interval, window); + } + + function test_fuzzing_revertsUponInvalidTWAPData( + uint32 interval, + uint32 twapParts, + uint32 window + ) public { + interval = uint32(bound(uint256(interval), MIN_TWAP_INTERVAL, MAX_TWAP_INTERVAL)); + twapParts = uint32(bound(uint256(twapParts), 1, MAX_TWAP_TOTAL_PARTS)); + window = uint32(bound(uint256(interval), MIN_TWAP_INTERVAL, interval)); + _checkTWAPOrderData(interval, twapParts, window); + + interval = uint32(bound(uint256(interval), 0, MIN_TWAP_INTERVAL - 1)); + vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); + _checkTWAPOrderData(interval, twapParts, window); + + interval = uint32(bound(uint256(interval), MIN_TWAP_INTERVAL + 1, MAX_U32_VAL)); + vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); + _checkTWAPOrderData(interval, twapParts, window); + + interval = uint32(bound(uint256(interval), MIN_TWAP_INTERVAL, MAX_TWAP_INTERVAL)); + twapParts = uint32(bound(uint256(twapParts), MAX_TWAP_TOTAL_PARTS + 1, MAX_U32_VAL)); + vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); + _checkTWAPOrderData(interval, twapParts, window); + + twapParts = 0; + vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); + _checkTWAPOrderData(interval, twapParts, window); + + twapParts = uint32(bound(uint256(twapParts), 1, MAX_TWAP_TOTAL_PARTS)); + window = uint32(bound(uint256(interval), interval + 1, MAX_U32_VAL)); + vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); + _checkTWAPOrderData(interval, twapParts, window); + + window = uint32(bound(uint256(interval), 0, interval - 1)); + vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); + _checkTWAPOrderData(interval, twapParts, window); + } + + function test_fuzzing_revertsUponPartsTWAPNonceReuse( + bytes32 orderHash, + address owner, + uint64 nonce, + uint32 twapParts + ) public { + twapParts = uint32(bound(uint256(twapParts), 0, 25)); + + for (uint256 i = twapParts; i != 0; i--) { + _invalidatePartTWAPNonce( + orderHash, owner.brutalize(), nonce.brutalize(), twapParts.brutalizeU32() + ); + bytes32 _orderHash = keccak256(abi.encode(orderHash)); + vm.expectRevert(OrderInvalidation.TWAPOrderNonceReuse.selector); + _invalidatePartTWAPNonce( + _orderHash, owner.brutalize(), nonce.brutalize(), twapParts.brutalizeU32() + ); + } + + uint256 part; + assembly ("memory-safe") { + mstore(12, nonce) + mstore(4, TWAP_NONCES_SLOT) + mstore(0, owner) + let partPtr := keccak256(12, 32) + part := sload(partPtr) + } + + assertEq(part, 0); + } } diff --git a/contracts/test/periphery/ControllerV1.t.sol b/contracts/test/periphery/ControllerV1.t.sol index 06c64f33d..15f727bc1 100644 --- a/contracts/test/periphery/ControllerV1.t.sol +++ b/contracts/test/periphery/ControllerV1.t.sol @@ -232,7 +232,7 @@ contract ControllerV1Test is BaseTest { function _isNode(address node) internal returns (bool) { bumpBlock(); vm.prank(node); - try angstrom.execute(new bytes(15)) { + try angstrom.execute(new bytes(18)) { return true; } catch (bytes memory error) { require(keccak256(error) == keccak256(abi.encodePacked(TopLevelAuth.NotNode.selector))); diff --git a/contracts/test/types/TWAPOrderBuffer.t.sol b/contracts/test/types/TWAPOrderBuffer.t.sol new file mode 100644 index 000000000..caf593fed --- /dev/null +++ b/contracts/test/types/TWAPOrderBuffer.t.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {BaseTest} from "test/_helpers/BaseTest.sol"; +import {TWAPOrderBuffer, TWAPOrderBufferLib} from "src/types/TWAPOrderBuffer.sol"; +import {TimeWeightedAveragePriceOrder} from "test/_reference/OrderTypes.sol"; +import {TWAPOrderVariantMap} from "src/types/TWAPOrderVariantMap.sol"; +import {OrderVariant} from "test/_reference/OrderVariant.sol"; +import {CalldataReader, CalldataReaderLib} from "src/types/CalldataReader.sol"; + +/// @author philogy +contract TWAPOrderBufferTest is BaseTest { + function setUp() public {} + + function test_fuzzing_referenceEqBuffer_TWAPOrder(TimeWeightedAveragePriceOrder memory order) + public + view + { + assertEq(bufferHash(order), order.hash()); + } + + function test_ffi_fuzzing_bufferPythonEquivalence_TWAPOrder( + TimeWeightedAveragePriceOrder memory order + ) public { + assertEq(bufferHash(order), ffiPythonEIP712Hash(order)); + } + + function bufferHash(TimeWeightedAveragePriceOrder memory order) + internal + view + returns (bytes32) + { + return this._bufferHashTWAPOrder( + order, + bytes.concat( + bytes1(order.toVariantMap(false)), + bytes4(order.refId), + bytes8(order.nonce), + bytes5(order.startTime), + bytes4(order.totalParts), + bytes4(order.timeInterval), + bytes4(order.window) + ) + ); + } + + function _bufferHashTWAPOrder( + TimeWeightedAveragePriceOrder memory order, + bytes calldata dataStart + ) external pure returns (bytes32) { + CalldataReader reader = CalldataReaderLib.from(dataStart); + TWAPOrderBuffer memory buffer; + TWAPOrderVariantMap varMap; + buffer.setTypeHash(); + (reader, varMap) = buffer.init(reader); + + buffer.exactIn = order.exactIn; + buffer.quantity = order.amount; + buffer.maxExtraFeeAsset0 = order.maxExtraFeeAsset0; + buffer.minPrice = order.minPrice; + buffer.useInternal = order.useInternal; + buffer.assetIn = order.assetIn; + buffer.assetOut = order.assetOut; + buffer.recipient = order.recipient; + buffer.hookDataHash = keccak256( + order.hook == address(0) + ? new bytes(0) + : bytes.concat(bytes20(order.hook), order.hookPayload) + ); + buffer.readTWAPOrderValidation(reader); + return buffer.hash(); + } + + function ffiPythonEIP712Hash(TimeWeightedAveragePriceOrder memory order) + internal + returns (bytes32) + { + string[] memory args = new string[](17); + args[0] = "test/_reference/eip712.py"; + args[1] = "test/_reference/SignedTypes.sol:TimeWeightedAveragePriceOrder"; + uint256 i = 2; + args[i++] = vm.toString(order.refId); + args[i++] = vm.toString(order.exactIn); + args[i++] = vm.toString(order.amount); + args[i++] = vm.toString(order.maxExtraFeeAsset0); + args[i++] = vm.toString(order.minPrice); + args[i++] = vm.toString(order.useInternal); + args[i++] = vm.toString(order.assetIn); + args[i++] = vm.toString(order.assetOut); + args[i++] = vm.toString(order.recipient); + args[i++] = vm.toString( + order.hook == address(0) + ? new bytes(0) + : bytes.concat(bytes20(order.hook), order.hookPayload) + ); + args[i++] = vm.toString(order.nonce); + args[i++] = vm.toString(order.startTime); + args[i++] = vm.toString(order.totalParts); + args[i++] = vm.toString(order.timeInterval); + args[i++] = vm.toString(order.window); + return bytes32(ffiPython(args)); + } +}