From 31cb9a4d78db865b54749b1954e4d0b95ae6947c Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Mon, 17 Feb 2025 15:58:45 +0100 Subject: [PATCH 01/15] Impl TWAP order type --- contracts/src/Angstrom.sol | 85 ++++++++++ contracts/src/modules/OrderInvalidation.sol | 109 ++++++++++++ contracts/src/types/TWAPOrderBuffer.sol | 159 ++++++++++++++++++ contracts/src/types/TWAPOrderVariantMap.sol | 41 +++++ contracts/test/_helpers/BaseTest.sol | 2 +- contracts/test/_helpers/Utils.sol | 19 ++- contracts/test/_reference/Bundle.sol | 11 +- contracts/test/_reference/OrderTypes.sol | 132 ++++++++++++++- contracts/test/_reference/SignedTypes.sol | 58 +++++++ .../test/modules/OrderInvalidation.t.sol | 46 +++++ contracts/test/types/TWAPOrderBuffer.t.sol | 95 +++++++++++ 11 files changed, 750 insertions(+), 7 deletions(-) create mode 100644 contracts/src/types/TWAPOrderBuffer.sol create mode 100644 contracts/src/types/TWAPOrderVariantMap.sol create mode 100644 contracts/test/types/TWAPOrderBuffer.t.sol diff --git a/contracts/src/Angstrom.sol b/contracts/src/Angstrom.sol index 4502a4fca..aa190738b 100644 --- a/contracts/src/Angstrom.sol +++ b/contracts/src/Angstrom.sol @@ -26,6 +26,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 @@ -260,6 +262,89 @@ contract Angstrom is return reader; } + function _validateAndExecuteTWAPOrders(CalldataReader reader, PairArray pairs) + internal + returns (CalldataReader) + { + TypedDataHasher typedHasher = _erc712Hasher(); + TWAPOrderBuffer memory buffer; + + CalldataReader end; + (reader, end) = reader.readU24End(); + + 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.readOrderValidation(reader); + + AmountIn amountIn; + AmountOut amountOut; + (reader, amountIn, amountOut) = buffer.loadAndComputeQuantity(reader, variantMap, price); + + bytes32 orderHash = typedHasher.hashTypedData(buffer.hash()); + + address from; + (reader, from) = variantMap.isEcdsa() + ? SignatureLib.readAndCheckEcdsa(reader, orderHash) + : SignatureLib.readAndCheckERC1271(reader, orderHash); + + _checkTWAPOrderData(buffer.timeInterval, buffer.totalParts); + _invalidatePartTWAPNonceAndCheckDeadline( + from, + buffer.nonce, + buffer.startTime, + buffer.timeInterval, + buffer.totalParts + ); + + // 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..b0786c2d6 100644 --- a/contracts/src/modules/OrderInvalidation.sol +++ b/contracts/src/modules/OrderInvalidation.sol @@ -6,14 +6,72 @@ abstract contract OrderInvalidation { error NonceReuse(); error OrderAlreadyExecuted(); error Expired(); + error TWAPNonceReuse(); + error TWAPExpired(); + error InvalidTWAPNonce(); + error InvalidTWAPOrder(); /// @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(uint32).max + uint256 private constant MASK_U32 = 4294967295; + // type(uint40).max + uint256 private constant MASK_U40 = 1099511627775; + // type(uint64).max + uint256 private constant MASK_U64 = 18446744073709551615; + // type(uint232).max + uint256 private constant MASK_U232 = 6901746346790563787434755862277025452451108972170386555162524223799295; + // max upper limit of twap intervals = once very 365.25 days + uint256 private constant MAX_TWAP_INTERVAL = 0x1e187e0; + // max no. of order parts = 365.25 days / 5 seconds + uint256 private constant MAX_TWAP_TOTAL_PARTS = 0x604e60; function invalidateNonce(uint64 nonce) external { _invalidateNonce(msg.sender, nonce); } + function invalidateTWAPNonce(uint64 nonce) external { + assembly ("memory-safe") { + nonce := and(nonce, MASK_U64) + mstore(12, div(nonce, 232)) + mstore(4, UNORDERED_TWAP_NONCES_SLOT) + mstore(0, caller()) + + let bitmapPtr := keccak256(12, 32) + let flag := shl(mod(nonce, 232), 1) + let bitmapVal := sload(bitmapPtr) + let updated := xor(and(bitmapVal, MASK_U232) , flag) + let twapNonce := iszero(and(updated, flag)) + let fParts := shr(232, bitmapVal) + + if xor(iszero(iszero(fParts)), twapNonce) { + mstore(0x00, 0xcfa42043 /* InvalidTWAPNonce() */ ) + revert(0x1c, 0x04) + } + + if eq(fParts, 0xffffff) { + mstore(0x00, 0x9a495418 /* TWAPNonceReuse() */ ) + revert(0x1c, 0x04) + } + + sstore(bitmapPtr, or(updated, shl(232, 0xffffff))) + } + } + + function _checkTWAPOrderData(uint32 interval, uint32 tParts) internal pure { + bool validInterval = interval != 0 && interval < MAX_TWAP_INTERVAL; + bool validTParts = tParts != 0 && tParts < MAX_TWAP_TOTAL_PARTS; + + assembly { + if iszero(and(validInterval, validTParts)){ + mstore(0x00, 0x51e490f3 /* InvalidTWAPOrder() */ ) + revert(0x1c, 0x04) + } + } + } + function _checkDeadline(uint256 deadline) internal view { if (block.timestamp > deadline) revert Expired(); } @@ -38,6 +96,57 @@ abstract contract OrderInvalidation { } } + function _invalidatePartTWAPNonceAndCheckDeadline(address owner, uint64 nonce, uint40 sTime, uint32 interval, uint32 tParts) + internal + { + uint256 _fParts; + assembly ("memory-safe") { + nonce := and(nonce, MASK_U64) + mstore(12, div(nonce, 232)) + mstore(4, UNORDERED_TWAP_NONCES_SLOT) + mstore(0, owner) + + let bitmapPtr := keccak256(12, 32) + let flag := shl(mod(nonce, 232), 1) + let bitmapVal := sload(bitmapPtr) + let updated := xor(and(bitmapVal, MASK_U232), flag) + let twapNonce := iszero(and(updated, flag)) + + // part to fulfill + let fParts := shr(232, bitmapVal) + _fParts := fParts + + if xor(iszero(iszero(fParts)), twapNonce) { + mstore(0x00, 0xcfa42043 /* InvalidTWAPNonce() */ ) + revert(0x1c, 0x04) + } + + fParts := add(fParts, 1) + tParts:= and(tParts, MASK_U32) + + if gt(fParts, tParts) { + mstore(0x00, 0x9a495418 /* TWAPNonceReuse() */ ) + revert(0x1c, 0x04) + } + + updated := or(shl(232, fParts), flag) + + if iszero(sub(tParts, fParts)) { + updated := or(updated, shl(232, 0xffffff)) + } + sstore(bitmapPtr, updated) + } + + assembly ("memory-safe") { + let cPartStart := add(and(sTime, MASK_U40), mul(_fParts, and(interval, MASK_U32))) + + if or(lt(timestamp(), cPartStart), gt(timestamp(), add(cPartStart, interval))) { + mstore(0x00, 0x982c606d /* TWAPExpired() */ ) + revert(0x1c, 0x04) + } + } + } + function _invalidateOrderHash(bytes32 orderHash, address from) internal { assembly ("memory-safe") { mstore(20, from) diff --git a/contracts/src/types/TWAPOrderBuffer.sol b/contracts/src/types/TWAPOrderBuffer.sol new file mode 100644 index 000000000..e46512363 --- /dev/null +++ b/contracts/src/types/TWAPOrderBuffer.sol @@ -0,0 +1,159 @@ +// 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; +} + +using TWAPOrderBufferLib for TWAPOrderBuffer global; + +/// @author philogy +library TWAPOrderBufferLib { + error GasAboveMax(); + + uint256 internal constant BUFFER_BYTES = 480; + + 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; + + /// 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" + ")" + ); + + 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.typeHash = TWAP_ORDER_TYPEHASH; + + 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(); + // how is this actually used. + // 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; + + quantityIn = AmountIn.wrap(quantity); + if (variant.zeroForOne()) { + AmountIn fee = AmountIn.wrap(extraFeeAsset0); + quantityOut = price.convertDown(quantityIn - fee); + } else { + AmountOut fee = AmountOut.wrap(extraFeeAsset0); + quantityOut = price.convertDown(quantityIn) - fee; + } + + return (reader, quantityIn, quantityOut); + } + + function readOrderValidation( + 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) + } + return reader; + } +} \ No newline at end of file diff --git a/contracts/src/types/TWAPOrderVariantMap.sol b/contracts/src/types/TWAPOrderVariantMap.sol new file mode 100644 index 000000000..1ffbd8f96 --- /dev/null +++ b/contracts/src/types/TWAPOrderVariantMap.sol @@ -0,0 +1,41 @@ +// 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/_helpers/BaseTest.sol b/contracts/test/_helpers/BaseTest.sol index fa52ecaaa..9813a4781 100644 --- a/contracts/test/_helpers/BaseTest.sol +++ b/contracts/test/_helpers/BaseTest.sol @@ -110,7 +110,7 @@ contract BaseTest is Test, HookDeployer { function pythonRunCmd() internal pure returns (string[] memory args) { args = new string[](1); - args[0] = ".venv/bin/python3.12"; + args[0] = ".venv/bin/python3.13"; } function ffiPython(string[] memory args) internal returns (bytes memory) { diff --git a/contracts/test/_helpers/Utils.sol b/contracts/test/_helpers/Utils.sol index 24d9044bd..b64a24880 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) + } + } +} \ No newline at end of file diff --git a/contracts/test/_reference/Bundle.sol b/contracts/test/_reference/Bundle.sol index 28efc995a..c94e00604 100644 --- a/contracts/test/_reference/Bundle.sol +++ b/contracts/test/_reference/Bundle.sol @@ -5,7 +5,11 @@ 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 +19,7 @@ struct Bundle { PoolUpdate[] poolUpdates; TopOfBlockOrder[] toBOrders; UserOrder[] userOrders; + // TimeWeightedAveragePriceOrder[] twapOrders; } using BundleLib for Bundle global; @@ -22,6 +27,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[]; @@ -36,6 +42,7 @@ library BundleLib { self.poolUpdates.encode(self.pairs), self.toBOrders.encode(self.pairs), self.userOrders.encode(self.pairs) + // self.twapOrders.encode(self.pairs) ); } @@ -126,4 +133,4 @@ library BundleLib { self.assets[index0].addDelta(deltas.amount0()); self.assets[index1].addDelta(deltas.amount1()); } -} +} \ No newline at end of file diff --git a/contracts/test/_reference/OrderTypes.sol b/contracts/test/_reference/OrderTypes.sol index df9877a15..8b08c4710 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,33 @@ 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; + 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 +221,25 @@ 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 + ).hash(); + } + /// @dev WARNING: Assumes `pairs` are sorted. function encode(PartialStandingOrder memory order, Pair[] memory pairs) internal @@ -376,6 +416,54 @@ 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), + 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 +600,44 @@ 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 meta: ", + o.meta.toStr(), + "\n}" + ); + } + function toStr(OrderMeta memory meta) internal pure returns (string memory) { return string.concat( "OrderMeta { isEcdsa: ", @@ -561,4 +687,4 @@ library OrdersLib { ); } } -} +} \ No newline at end of file diff --git a/contracts/test/_reference/SignedTypes.sol b/contracts/test/_reference/SignedTypes.sol index 319662119..6957bed1f 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,29 @@ 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; +} + 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 +193,43 @@ 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; + } + + 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 + }); + return keccak256(abi.encode(orderToMem)); + } } diff --git a/contracts/test/modules/OrderInvalidation.t.sol b/contracts/test/modules/OrderInvalidation.t.sol index ae45518d2..dc78f4ef5 100644 --- a/contracts/test/modules/OrderInvalidation.t.sol +++ b/contracts/test/modules/OrderInvalidation.t.sol @@ -10,10 +10,56 @@ contract InvalidationManagerTest is Test, OrderInvalidation { using Utils for *; bytes4 internal constant NONCES_SLOT = bytes4(keccak256("angstrom-v1_0.unordered-nonces.slot")); + uint256 private constant MAX_TWAP_INTERVAL = 0x1e187e0; + uint256 private constant MAX_TWAP_TOTAL_PARTS = 0x604e60; 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_revertsUponTWAPNonceReuse(uint64 nonce) public { + this.invalidateTWAPNonce(nonce.brutalize()); + vm.expectRevert(OrderInvalidation.TWAPNonceReuse.selector); + this.invalidateTWAPNonce(nonce.brutalize()); + } + + function test_fuzzing_revertsUponInvalidTWAPData(uint32 interval, uint32 tParts) public view { + interval = uint32(bound(uint256(interval), 1, MAX_TWAP_INTERVAL)); + tParts = uint32(bound(uint256(tParts), 1, MAX_TWAP_TOTAL_PARTS)); + _checkTWAPOrderData(interval.brutalizeU32(), tParts.brutalizeU32()); + } + + /// forge-config: default.allow_internal_expect_revert = true + function test_fuzzing_revertsUponPartsTWAPNonceReuse( + address owner, + uint64 nonce, + uint32 interval, + uint32 tParts + ) public { + uint40 sTime = uint40(block.timestamp); + interval = uint32(bound(uint256(interval), 0, MAX_TWAP_INTERVAL)); + tParts = uint32(bound(uint256(tParts), 0, 20)); + + for(uint256 i = tParts; i != 0; i--){ + _invalidatePartTWAPNonceAndCheckDeadline( + owner.brutalize(), + nonce.brutalize(), + sTime.brutalizeU40(), + interval.brutalizeU32(), + tParts.brutalizeU32() + ); + uint256 warpedTime = sTime + ((tParts-(i-1)) * interval); + vm.warp(warpedTime); + } + vm.expectRevert(OrderInvalidation.TWAPNonceReuse.selector); + _invalidatePartTWAPNonceAndCheckDeadline( + owner.brutalize(), + nonce.brutalize(), + sTime.brutalizeU40(), + interval.brutalizeU32(), + tParts.brutalizeU32() + ); + } } diff --git a/contracts/test/types/TWAPOrderBuffer.t.sol b/contracts/test/types/TWAPOrderBuffer.t.sol new file mode 100644 index 000000000..0b4a23151 --- /dev/null +++ b/contracts/test/types/TWAPOrderBuffer.t.sol @@ -0,0 +1,95 @@ +// 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) + ) + ); + } + + function _bufferHashTWAPOrder( + TimeWeightedAveragePriceOrder memory order, + bytes calldata dataStart + ) external pure returns (bytes32) { + CalldataReader reader = CalldataReaderLib.from(dataStart); + TWAPOrderBuffer memory buffer; + TWAPOrderVariantMap varMap; + (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.readOrderValidation(reader); + return buffer.hash(); + } + + function ffiPythonEIP712Hash(TimeWeightedAveragePriceOrder memory order) internal returns (bytes32) { + string[] memory args = new string[](16); + 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); + return bytes32(ffiPython(args)); + } +} \ No newline at end of file From 50fe844507f52f04e573afcbbd51c9b8542eff12 Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Mon, 17 Feb 2025 23:16:17 +0100 Subject: [PATCH 02/15] fix: execute twap orders in `unlockCallback()` --- contracts/src/Angstrom.sol | 6 +++++- contracts/test/_reference/Bundle.sol | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/src/Angstrom.sol b/contracts/src/Angstrom.sol index aa190738b..0cc28cc25 100644 --- a/contracts/src/Angstrom.sol +++ b/contracts/src/Angstrom.sol @@ -74,9 +74,11 @@ contract Angstrom is reader = _updatePools(reader, pairs); console.log("updated pools"); reader = _validateAndExecuteToBOrders(reader, pairs); - console.log("exectued tob"); + console.log("executed tob"); reader = _validateAndExecuteUserOrders(reader, pairs); console.log("executed user"); + reader = _validateAndExecuteTWAPOrders(reader, pairs); + console.log("executed twap"); reader.requireAtEndOf(data); _saveAndSettle(assets); @@ -272,6 +274,8 @@ contract Angstrom is 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); } diff --git a/contracts/test/_reference/Bundle.sol b/contracts/test/_reference/Bundle.sol index c94e00604..7619a6f94 100644 --- a/contracts/test/_reference/Bundle.sol +++ b/contracts/test/_reference/Bundle.sol @@ -19,7 +19,7 @@ struct Bundle { PoolUpdate[] poolUpdates; TopOfBlockOrder[] toBOrders; UserOrder[] userOrders; - // TimeWeightedAveragePriceOrder[] twapOrders; + TimeWeightedAveragePriceOrder[] twapOrders; } using BundleLib for Bundle global; @@ -41,8 +41,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.twapOrders.encode(self.pairs) + self.userOrders.encode(self.pairs), + self.twapOrders.encode(self.pairs) ); } From d999d11852df8dd1da626a4b7f8ff5e177e322f3 Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Mon, 17 Feb 2025 23:21:36 +0100 Subject: [PATCH 03/15] cleanup --- contracts/src/modules/OrderInvalidation.sol | 71 +++++++++++---------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/contracts/src/modules/OrderInvalidation.sol b/contracts/src/modules/OrderInvalidation.sol index b0786c2d6..97e25e4df 100644 --- a/contracts/src/modules/OrderInvalidation.sol +++ b/contracts/src/modules/OrderInvalidation.sol @@ -16,16 +16,18 @@ abstract contract OrderInvalidation { /// @dev `keccak256("angstrom-v1_0.twap-unordered-nonces.slot")[0:4]` uint256 private constant UNORDERED_TWAP_NONCES_SLOT = 0x635a0808; // type(uint32).max - uint256 private constant MASK_U32 = 4294967295; + uint256 private constant MASK_U32 = 0xffffffff; // type(uint40).max - uint256 private constant MASK_U40 = 1099511627775; + uint256 private constant MASK_U40 = 0xffffffffff; // type(uint64).max - uint256 private constant MASK_U64 = 18446744073709551615; + uint256 private constant MASK_U64 = 0xffffffffffffffff; // type(uint232).max - uint256 private constant MASK_U232 = 6901746346790563787434755862277025452451108972170386555162524223799295; - // max upper limit of twap intervals = once very 365.25 days + uint256 private constant MASK_U232 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + // max twap nonce bit + uint256 private constant MAX_TWAP_NONCE_SIZE = 232; + // max upper limit of twap intervals = 365.25 days uint256 private constant MAX_TWAP_INTERVAL = 0x1e187e0; - // max no. of order parts = 365.25 days / 5 seconds + // max no. of order parts = 6311520 (365.25 days / 5 seconds) uint256 private constant MAX_TWAP_TOTAL_PARTS = 0x604e60; function invalidateNonce(uint64 nonce) external { @@ -35,36 +37,36 @@ abstract contract OrderInvalidation { function invalidateTWAPNonce(uint64 nonce) external { assembly ("memory-safe") { nonce := and(nonce, MASK_U64) - mstore(12, div(nonce, 232)) + mstore(12, div(nonce, MAX_TWAP_NONCE_SIZE)) mstore(4, UNORDERED_TWAP_NONCES_SLOT) mstore(0, caller()) let bitmapPtr := keccak256(12, 32) - let flag := shl(mod(nonce, 232), 1) + let flag := shl(mod(nonce, MAX_TWAP_NONCE_SIZE), 1) let bitmapVal := sload(bitmapPtr) let updated := xor(and(bitmapVal, MASK_U232) , flag) let twapNonce := iszero(and(updated, flag)) - let fParts := shr(232, bitmapVal) + let fulfilledParts := shr(MAX_TWAP_NONCE_SIZE, bitmapVal) - if xor(iszero(iszero(fParts)), twapNonce) { + if xor(iszero(iszero(fulfilledParts)), twapNonce) { mstore(0x00, 0xcfa42043 /* InvalidTWAPNonce() */ ) revert(0x1c, 0x04) } - if eq(fParts, 0xffffff) { + if eq(fulfilledParts, 0xffffff) { mstore(0x00, 0x9a495418 /* TWAPNonceReuse() */ ) revert(0x1c, 0x04) } - sstore(bitmapPtr, or(updated, shl(232, 0xffffff))) + sstore(bitmapPtr, or(updated, shl(MAX_TWAP_NONCE_SIZE, 0xffffff))) } } - function _checkTWAPOrderData(uint32 interval, uint32 tParts) internal pure { - bool validInterval = interval != 0 && interval < MAX_TWAP_INTERVAL; - bool validTParts = tParts != 0 && tParts < MAX_TWAP_TOTAL_PARTS; + function _checkTWAPOrderData(uint32 interval, uint32 twapParts) internal pure { + bool validInterval = interval != 0 && interval <= MAX_TWAP_INTERVAL; + bool validTParts = twapParts != 0 && twapParts <= MAX_TWAP_TOTAL_PARTS; - assembly { + assembly("memory-safe") { if iszero(and(validInterval, validTParts)){ mstore(0x00, 0x51e490f3 /* InvalidTWAPOrder() */ ) revert(0x1c, 0x04) @@ -96,51 +98,54 @@ abstract contract OrderInvalidation { } } - function _invalidatePartTWAPNonceAndCheckDeadline(address owner, uint64 nonce, uint40 sTime, uint32 interval, uint32 tParts) + function _invalidatePartTWAPNonceAndCheckDeadline( + address owner, + uint64 nonce, + uint40 startTime, + uint32 interval, + uint32 twapParts + ) internal { - uint256 _fParts; assembly ("memory-safe") { nonce := and(nonce, MASK_U64) - mstore(12, div(nonce, 232)) + mstore(12, div(nonce, MAX_TWAP_NONCE_SIZE)) mstore(4, UNORDERED_TWAP_NONCES_SLOT) mstore(0, owner) let bitmapPtr := keccak256(12, 32) - let flag := shl(mod(nonce, 232), 1) + let flag := shl(mod(nonce, MAX_TWAP_NONCE_SIZE), 1) let bitmapVal := sload(bitmapPtr) let updated := xor(and(bitmapVal, MASK_U232), flag) let twapNonce := iszero(and(updated, flag)) // part to fulfill - let fParts := shr(232, bitmapVal) - _fParts := fParts + let fulfilledParts := shr(MAX_TWAP_NONCE_SIZE, bitmapVal) + let _cachedFulfilledParts := fulfilledParts - if xor(iszero(iszero(fParts)), twapNonce) { + if xor(iszero(iszero(fulfilledParts)), twapNonce) { mstore(0x00, 0xcfa42043 /* InvalidTWAPNonce() */ ) revert(0x1c, 0x04) } - fParts := add(fParts, 1) - tParts:= and(tParts, MASK_U32) + fulfilledParts := add(fulfilledParts, 1) + twapParts:= and(twapParts, MASK_U32) - if gt(fParts, tParts) { + if gt(fulfilledParts, twapParts) { mstore(0x00, 0x9a495418 /* TWAPNonceReuse() */ ) revert(0x1c, 0x04) } - updated := or(shl(232, fParts), flag) + updated := or(shl(MAX_TWAP_NONCE_SIZE, fulfilledParts), flag) - if iszero(sub(tParts, fParts)) { - updated := or(updated, shl(232, 0xffffff)) + if iszero(sub(twapParts, fulfilledParts)) { + updated := or(updated, shl(MAX_TWAP_NONCE_SIZE, 0xffffff)) } sstore(bitmapPtr, updated) - } - assembly ("memory-safe") { - let cPartStart := add(and(sTime, MASK_U40), mul(_fParts, and(interval, MASK_U32))) + let currentPartStart := add(and(startTime, MASK_U40), mul(_cachedFulfilledParts, and(interval, MASK_U32))) - if or(lt(timestamp(), cPartStart), gt(timestamp(), add(cPartStart, interval))) { + if or(lt(timestamp(), currentPartStart), gt(timestamp(), add(currentPartStart, interval))) { mstore(0x00, 0x982c606d /* TWAPExpired() */ ) revert(0x1c, 0x04) } From 4397949ff23514d74b34d2d7fd58ce4bbb40fc1d Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Tue, 18 Feb 2025 06:52:46 +0100 Subject: [PATCH 04/15] compute twap order quantity correctly --- contracts/src/types/TWAPOrderBuffer.sol | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/contracts/src/types/TWAPOrderBuffer.sol b/contracts/src/types/TWAPOrderBuffer.sol index e46512363..a9095e5f1 100644 --- a/contracts/src/types/TWAPOrderBuffer.sol +++ b/contracts/src/types/TWAPOrderBuffer.sol @@ -102,8 +102,7 @@ library TWAPOrderBufferLib { ) internal pure returns (CalldataReader, AmountIn quantityIn, AmountOut quantityOut) { uint256 quantity; (reader, quantity) = reader.readU128(); - // how is this actually used. - // self.exactIn = variant.exactIn(); + self.exactIn = variant.exactIn(); self.quantity = quantity; uint128 extraFeeAsset0; @@ -113,13 +112,25 @@ library TWAPOrderBufferLib { if (extraFeeAsset0 > maxExtraFeeAsset0) revert GasAboveMax(); self.maxExtraFeeAsset0 = maxExtraFeeAsset0; - quantityIn = AmountIn.wrap(quantity); + if (variant.zeroForOne()) { AmountIn fee = AmountIn.wrap(extraFeeAsset0); - quantityOut = price.convertDown(quantityIn - fee); + 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); - quantityOut = price.convertDown(quantityIn) - fee; + 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); From 15b640af8d2df0022835cb57a50d5c39e42a1553 Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Tue, 18 Feb 2025 14:41:40 +0100 Subject: [PATCH 05/15] =?UTF-8?q?=F0=9F=AA=9F=20add=20support=20for=20twap?= =?UTF-8?q?=20order=20execution=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/Angstrom.sol | 16 ++--- contracts/src/modules/OrderInvalidation.sol | 20 +++--- contracts/src/types/TWAPOrderBuffer.sol | 14 ++++- contracts/test/_reference/OrderTypes.sol | 7 ++- contracts/test/_reference/SignedTypes.sol | 5 +- .../test/modules/OrderInvalidation.t.sol | 62 ++++++++++++++----- contracts/test/types/TWAPOrderBuffer.t.sol | 6 +- 7 files changed, 95 insertions(+), 35 deletions(-) diff --git a/contracts/src/Angstrom.sol b/contracts/src/Angstrom.sol index 0cc28cc25..f4a515e3b 100644 --- a/contracts/src/Angstrom.sol +++ b/contracts/src/Angstrom.sol @@ -319,20 +319,22 @@ contract Angstrom is AmountOut amountOut; (reader, amountIn, amountOut) = buffer.loadAndComputeQuantity(reader, variantMap, price); - bytes32 orderHash = typedHasher.hashTypedData(buffer.hash()); - address from; - (reader, from) = variantMap.isEcdsa() - ? SignatureLib.readAndCheckEcdsa(reader, orderHash) - : SignatureLib.readAndCheckERC1271(reader, orderHash); + { + bytes32 orderHash = typedHasher.hashTypedData(buffer.hash()); + (reader, from) = variantMap.isEcdsa() + ? SignatureLib.readAndCheckEcdsa(reader, orderHash) + : SignatureLib.readAndCheckERC1271(reader, orderHash); + } - _checkTWAPOrderData(buffer.timeInterval, buffer.totalParts); + _checkTWAPOrderData(buffer.timeInterval, buffer.totalParts, buffer.window); _invalidatePartTWAPNonceAndCheckDeadline( from, buffer.nonce, buffer.startTime, buffer.timeInterval, - buffer.totalParts + buffer.totalParts, + buffer.window ); // Push before hook as a potential loan. diff --git a/contracts/src/modules/OrderInvalidation.sol b/contracts/src/modules/OrderInvalidation.sol index 97e25e4df..6da5be10a 100644 --- a/contracts/src/modules/OrderInvalidation.sol +++ b/contracts/src/modules/OrderInvalidation.sol @@ -24,9 +24,11 @@ abstract contract OrderInvalidation { // type(uint232).max uint256 private constant MASK_U232 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; // max twap nonce bit - uint256 private constant MAX_TWAP_NONCE_SIZE = 232; - // max upper limit of twap intervals = 365.25 days + uint256 private constant MAX_TWAP_NONCE_SIZE = 0xe8; + // max upper limit of twap intervals = 31557600 (365.25 days) uint256 private constant MAX_TWAP_INTERVAL = 0x1e187e0; + // min lower limit of twap intervals = 5 seconds + uint256 private constant MIN_TWAP_INTERVAL = 0x05; // max no. of order parts = 6311520 (365.25 days / 5 seconds) uint256 private constant MAX_TWAP_TOTAL_PARTS = 0x604e60; @@ -62,12 +64,13 @@ abstract contract OrderInvalidation { } } - function _checkTWAPOrderData(uint32 interval, uint32 twapParts) internal pure { - bool validInterval = interval != 0 && interval <= MAX_TWAP_INTERVAL; - bool validTParts = twapParts != 0 && twapParts <= MAX_TWAP_TOTAL_PARTS; + function _checkTWAPOrderData(uint32 interval, uint32 twapParts, uint32 window) internal pure { + bool validInterval = interval < MIN_TWAP_INTERVAL || interval > MAX_TWAP_INTERVAL; + bool validTParts = twapParts == 0 || twapParts > MAX_TWAP_TOTAL_PARTS; + bool validWindow = window < MIN_TWAP_INTERVAL || window > interval; assembly("memory-safe") { - if iszero(and(validInterval, validTParts)){ + if or(or(validInterval, validTParts), validWindow){ mstore(0x00, 0x51e490f3 /* InvalidTWAPOrder() */ ) revert(0x1c, 0x04) } @@ -103,7 +106,8 @@ abstract contract OrderInvalidation { uint64 nonce, uint40 startTime, uint32 interval, - uint32 twapParts + uint32 twapParts, + uint32 window ) internal { @@ -145,7 +149,7 @@ abstract contract OrderInvalidation { let currentPartStart := add(and(startTime, MASK_U40), mul(_cachedFulfilledParts, and(interval, MASK_U32))) - if or(lt(timestamp(), currentPartStart), gt(timestamp(), add(currentPartStart, interval))) { + if or(lt(timestamp(), currentPartStart), gt(timestamp(), add(currentPartStart, and(window, MASK_U32)))) { mstore(0x00, 0x982c606d /* TWAPExpired() */ ) revert(0x1c, 0x04) } diff --git a/contracts/src/types/TWAPOrderBuffer.sol b/contracts/src/types/TWAPOrderBuffer.sol index a9095e5f1..51dcdf1e5 100644 --- a/contracts/src/types/TWAPOrderBuffer.sol +++ b/contracts/src/types/TWAPOrderBuffer.sol @@ -21,6 +21,7 @@ struct TWAPOrderBuffer { uint40 startTime; uint32 totalParts; uint32 timeInterval; + uint32 window; } using TWAPOrderBufferLib for TWAPOrderBuffer global; @@ -29,7 +30,7 @@ using TWAPOrderBufferLib for TWAPOrderBuffer global; library TWAPOrderBufferLib { error GasAboveMax(); - uint256 internal constant BUFFER_BYTES = 480; + 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). @@ -43,6 +44,8 @@ library TWAPOrderBufferLib { 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( @@ -60,7 +63,8 @@ library TWAPOrderBufferLib { "uint64 nonce," "uint40 start_time," "uint32 total_parts," - "uint32 time_interval" + "uint32 time_interval," + "uint32 window" ")" ); @@ -164,6 +168,12 @@ library TWAPOrderBufferLib { 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/test/_reference/OrderTypes.sol b/contracts/test/_reference/OrderTypes.sol index 8b08c4710..a6f6392d4 100644 --- a/contracts/test/_reference/OrderTypes.sol +++ b/contracts/test/_reference/OrderTypes.sol @@ -125,6 +125,7 @@ struct TimeWeightedAveragePriceOrder { uint40 startTime; uint32 totalParts; uint32 timeInterval; + uint32 window; OrderMeta meta; uint128 extraFeeAsset0; } @@ -236,7 +237,8 @@ library OrdersLib { order.nonce, order.startTime, order.totalParts, - order.timeInterval + order.timeInterval, + order.window ).hash(); } @@ -457,6 +459,7 @@ library OrdersLib { bytes5(order.startTime), bytes4(order.totalParts), bytes4(order.timeInterval), + bytes4(order.window), bytes16(order.amount), bytes16(order.maxExtraFeeAsset0), bytes16(order.extraFeeAsset0), @@ -632,6 +635,8 @@ library OrdersLib { o.totalParts.toStr(), ",\n timeInterval: ", o.timeInterval.toStr(), + ",\n window: ", + o.window.toStr(), ",\n meta: ", o.meta.toStr(), "\n}" diff --git a/contracts/test/_reference/SignedTypes.sol b/contracts/test/_reference/SignedTypes.sol index 6957bed1f..a9c4daa6c 100644 --- a/contracts/test/_reference/SignedTypes.sol +++ b/contracts/test/_reference/SignedTypes.sol @@ -89,6 +89,7 @@ struct TimeWeightedAveragePriceOrder { uint40 start_time; uint32 total_parts; uint32 time_interval; + uint32 window; } using SignedTypesLib for ExactStandingOrder global; @@ -210,6 +211,7 @@ library SignedTypesLib { uint40 start_time; uint32 total_parts; uint32 time_interval; + uint32 window; } function hash(TimeWeightedAveragePriceOrder memory self) internal pure returns (bytes32) { @@ -228,7 +230,8 @@ library SignedTypesLib { nonce: self.nonce, start_time: self.start_time, total_parts: self.total_parts, - time_interval: self.time_interval + time_interval: self.time_interval, + window: self.window }); return keccak256(abi.encode(orderToMem)); } diff --git a/contracts/test/modules/OrderInvalidation.t.sol b/contracts/test/modules/OrderInvalidation.t.sol index dc78f4ef5..5e7da4008 100644 --- a/contracts/test/modules/OrderInvalidation.t.sol +++ b/contracts/test/modules/OrderInvalidation.t.sol @@ -11,7 +11,9 @@ contract InvalidationManagerTest is Test, OrderInvalidation { bytes4 internal constant NONCES_SLOT = bytes4(keccak256("angstrom-v1_0.unordered-nonces.slot")); uint256 private constant MAX_TWAP_INTERVAL = 0x1e187e0; + uint256 private constant MIN_TWAP_INTERVAL = 5; uint256 private constant MAX_TWAP_TOTAL_PARTS = 0x604e60; + uint256 private constant MAX_U32_VAL = type(uint32).max; function test_fuzzing_revertsUponReuse(address owner, uint64 nonce) public { _invalidateNonce(owner.brutalize(), nonce.brutalize()); @@ -25,10 +27,38 @@ contract InvalidationManagerTest is Test, OrderInvalidation { this.invalidateTWAPNonce(nonce.brutalize()); } - function test_fuzzing_revertsUponInvalidTWAPData(uint32 interval, uint32 tParts) public view { - interval = uint32(bound(uint256(interval), 1, MAX_TWAP_INTERVAL)); - tParts = uint32(bound(uint256(tParts), 1, MAX_TWAP_TOTAL_PARTS)); - _checkTWAPOrderData(interval.brutalizeU32(), tParts.brutalizeU32()); + /// forge-config: default.allow_internal_expect_revert = true + 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.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); + + interval = uint32(bound(uint256(interval), 0, MIN_TWAP_INTERVAL - 1)); + vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); + _checkTWAPOrderData(interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); + + interval = uint32(bound(uint256(interval), MIN_TWAP_INTERVAL + 1, MAX_U32_VAL)); + vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); + _checkTWAPOrderData(interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); + + 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.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); + + twapParts = 0; + vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); + _checkTWAPOrderData(interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); + + 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.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); + + window = uint32(bound(uint256(interval), 0, interval - 1)); + vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); + _checkTWAPOrderData(interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); } /// forge-config: default.allow_internal_expect_revert = true @@ -36,30 +66,34 @@ contract InvalidationManagerTest is Test, OrderInvalidation { address owner, uint64 nonce, uint32 interval, - uint32 tParts + uint32 twapParts, + uint32 window ) public { - uint40 sTime = uint40(block.timestamp); - interval = uint32(bound(uint256(interval), 0, MAX_TWAP_INTERVAL)); - tParts = uint32(bound(uint256(tParts), 0, 20)); + uint40 startTime = uint40(block.timestamp); + interval = uint32(bound(uint256(interval), MIN_TWAP_INTERVAL, MAX_TWAP_INTERVAL)); + twapParts = uint32(bound(uint256(twapParts), 0, 25)); + window = uint32(bound(uint256(window), MIN_TWAP_INTERVAL, interval)); - for(uint256 i = tParts; i != 0; i--){ + for(uint256 i = twapParts; i != 0; i--){ _invalidatePartTWAPNonceAndCheckDeadline( owner.brutalize(), nonce.brutalize(), - sTime.brutalizeU40(), + startTime.brutalizeU40(), interval.brutalizeU32(), - tParts.brutalizeU32() + twapParts.brutalizeU32(), + window.brutalizeU32() ); - uint256 warpedTime = sTime + ((tParts-(i-1)) * interval); + uint256 warpedTime = startTime + ((twapParts-(i-1)) * interval); vm.warp(warpedTime); } vm.expectRevert(OrderInvalidation.TWAPNonceReuse.selector); _invalidatePartTWAPNonceAndCheckDeadline( owner.brutalize(), nonce.brutalize(), - sTime.brutalizeU40(), + startTime.brutalizeU40(), interval.brutalizeU32(), - tParts.brutalizeU32() + twapParts.brutalizeU32(), + window.brutalizeU32() ); } } diff --git a/contracts/test/types/TWAPOrderBuffer.t.sol b/contracts/test/types/TWAPOrderBuffer.t.sol index 0b4a23151..d53beb755 100644 --- a/contracts/test/types/TWAPOrderBuffer.t.sol +++ b/contracts/test/types/TWAPOrderBuffer.t.sol @@ -36,7 +36,8 @@ contract TWAPOrderBufferTest is BaseTest { bytes8(order.nonce), bytes5(order.startTime), bytes4(order.totalParts), - bytes4(order.timeInterval) + bytes4(order.timeInterval), + bytes4(order.window) ) ); } @@ -68,7 +69,7 @@ contract TWAPOrderBufferTest is BaseTest { } function ffiPythonEIP712Hash(TimeWeightedAveragePriceOrder memory order) internal returns (bytes32) { - string[] memory args = new string[](16); + string[] memory args = new string[](17); args[0] = "test/_reference/eip712.py"; args[1] = "test/_reference/SignedTypes.sol:TimeWeightedAveragePriceOrder"; uint256 i = 2; @@ -90,6 +91,7 @@ contract TWAPOrderBufferTest is BaseTest { 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)); } } \ No newline at end of file From 8b4609ad7a7437145476f14b8c14dc07a8b2a21d Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Wed, 19 Feb 2025 06:33:50 +0100 Subject: [PATCH 06/15] =?UTF-8?q?=E2=9C=A8=20adjust=20interval=20limit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/modules/OrderInvalidation.sol | 12 ++++++++---- contracts/test/modules/OrderInvalidation.t.sol | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/contracts/src/modules/OrderInvalidation.sol b/contracts/src/modules/OrderInvalidation.sol index 6da5be10a..cfe0748e3 100644 --- a/contracts/src/modules/OrderInvalidation.sol +++ b/contracts/src/modules/OrderInvalidation.sol @@ -23,12 +23,12 @@ abstract contract OrderInvalidation { uint256 private constant MASK_U64 = 0xffffffffffffffff; // type(uint232).max uint256 private constant MASK_U232 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; - // max twap nonce bit + // max twap nonce bit = 232 uint256 private constant MAX_TWAP_NONCE_SIZE = 0xe8; // max upper limit of twap intervals = 31557600 (365.25 days) uint256 private constant MAX_TWAP_INTERVAL = 0x1e187e0; - // min lower limit of twap intervals = 5 seconds - uint256 private constant MIN_TWAP_INTERVAL = 0x05; + // min lower limit of twap intervals = 12 seconds + uint256 private constant MIN_TWAP_INTERVAL = 0x0c; // max no. of order parts = 6311520 (365.25 days / 5 seconds) uint256 private constant MAX_TWAP_TOTAL_PARTS = 0x604e60; @@ -50,6 +50,8 @@ abstract contract OrderInvalidation { let twapNonce := iszero(and(updated, flag)) let fulfilledParts := shr(MAX_TWAP_NONCE_SIZE, bitmapVal) + // Reverts if `fulfilledParts` is empty while `twapNonce` is not empty, + // or if `fulfilledParts` is not empty while `twapNonce` is empty. if xor(iszero(iszero(fulfilledParts)), twapNonce) { mstore(0x00, 0xcfa42043 /* InvalidTWAPNonce() */ ) revert(0x1c, 0x04) @@ -60,7 +62,7 @@ abstract contract OrderInvalidation { revert(0x1c, 0x04) } - sstore(bitmapPtr, or(updated, shl(MAX_TWAP_NONCE_SIZE, 0xffffff))) + sstore(bitmapPtr, or(flag, shl(MAX_TWAP_NONCE_SIZE, 0xffffff))) } } @@ -127,6 +129,8 @@ abstract contract OrderInvalidation { let fulfilledParts := shr(MAX_TWAP_NONCE_SIZE, bitmapVal) let _cachedFulfilledParts := fulfilledParts + // Reverts if `fulfilledParts` is empty while `twapNonce` is not empty, + // or if `fulfilledParts` is not empty while `twapNonce` is empty. if xor(iszero(iszero(fulfilledParts)), twapNonce) { mstore(0x00, 0xcfa42043 /* InvalidTWAPNonce() */ ) revert(0x1c, 0x04) diff --git a/contracts/test/modules/OrderInvalidation.t.sol b/contracts/test/modules/OrderInvalidation.t.sol index 5e7da4008..2a6a1d8f2 100644 --- a/contracts/test/modules/OrderInvalidation.t.sol +++ b/contracts/test/modules/OrderInvalidation.t.sol @@ -11,7 +11,7 @@ contract InvalidationManagerTest is Test, OrderInvalidation { bytes4 internal constant NONCES_SLOT = bytes4(keccak256("angstrom-v1_0.unordered-nonces.slot")); uint256 private constant MAX_TWAP_INTERVAL = 0x1e187e0; - uint256 private constant MIN_TWAP_INTERVAL = 5; + uint256 private constant MIN_TWAP_INTERVAL = 12; uint256 private constant MAX_TWAP_TOTAL_PARTS = 0x604e60; uint256 private constant MAX_U32_VAL = type(uint32).max; From 59909838b68ae259e41fae820a1ff63eef639c78 Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Wed, 19 Feb 2025 15:02:12 +0100 Subject: [PATCH 07/15] =?UTF-8?q?=F0=9F=A7=AA=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/Angstrom.sol | 4 +- contracts/src/modules/OrderInvalidation.sol | 6 +- contracts/test/Angstrom.t.sol | 94 ++++++++++++++++++++- contracts/test/_mocks/OpenAngstrom.sol | 17 ++++ contracts/test/_reference/Bundle.sol | 17 ++++ contracts/test/benchmark/TWAPOrder.b.sol | 89 +++++++++++++++++++ 6 files changed, 222 insertions(+), 5 deletions(-) create mode 100644 contracts/test/benchmark/TWAPOrder.b.sol diff --git a/contracts/src/Angstrom.sol b/contracts/src/Angstrom.sol index f4a515e3b..425b0be6b 100644 --- a/contracts/src/Angstrom.sol +++ b/contracts/src/Angstrom.sol @@ -326,7 +326,7 @@ contract Angstrom is ? SignatureLib.readAndCheckEcdsa(reader, orderHash) : SignatureLib.readAndCheckERC1271(reader, orderHash); } - + _checkTWAPOrderData(buffer.timeInterval, buffer.totalParts, buffer.window); _invalidatePartTWAPNonceAndCheckDeadline( from, @@ -347,7 +347,7 @@ contract Angstrom is hook.tryTrigger(from); _settleOrderIn(from, buffer.assetIn, amountIn, buffer.useInternal); - + console.log("end test"); return reader; } diff --git a/contracts/src/modules/OrderInvalidation.sol b/contracts/src/modules/OrderInvalidation.sol index cfe0748e3..39b413530 100644 --- a/contracts/src/modules/OrderInvalidation.sol +++ b/contracts/src/modules/OrderInvalidation.sol @@ -23,6 +23,8 @@ abstract contract OrderInvalidation { uint256 private constant MASK_U64 = 0xffffffffffffffff; // type(uint232).max uint256 private constant MASK_U232 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + // upper 24 bits mask + uint256 private constant UPPER_PART_MASK = 0xffffff0000000000000000000000000000000000000000000000000000000000; // max twap nonce bit = 232 uint256 private constant MAX_TWAP_NONCE_SIZE = 0xe8; // max upper limit of twap intervals = 31557600 (365.25 days) @@ -62,7 +64,7 @@ abstract contract OrderInvalidation { revert(0x1c, 0x04) } - sstore(bitmapPtr, or(flag, shl(MAX_TWAP_NONCE_SIZE, 0xffffff))) + sstore(bitmapPtr, or(flag, UPPER_PART_MASK)) } } @@ -147,7 +149,7 @@ abstract contract OrderInvalidation { updated := or(shl(MAX_TWAP_NONCE_SIZE, fulfilledParts), flag) if iszero(sub(twapParts, fulfilledParts)) { - updated := or(updated, shl(MAX_TWAP_NONCE_SIZE, 0xffffff)) + updated := or(updated, UPPER_PART_MASK) } sstore(bitmapPtr, updated) diff --git a/contracts/test/Angstrom.t.sol b/contracts/test/Angstrom.t.sol index 70fe3e408..322e5066c 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/_mocks/OpenAngstrom.sol b/contracts/test/_mocks/OpenAngstrom.sol index b08cda1b4..c46792e28 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,21 @@ 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; + 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 7619a6f94..a05ac23bc 100644 --- a/contracts/test/_reference/Bundle.sol +++ b/contracts/test/_reference/Bundle.sol @@ -126,6 +126,23 @@ 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/benchmark/TWAPOrder.b.sol b/contracts/test/benchmark/TWAPOrder.b.sol new file mode 100644 index 000000000..675c3fc03 --- /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); + } +} From 135ceda1d0b23c96e73046d72ec87d8f1ef6e96e Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Wed, 19 Feb 2025 15:03:24 +0100 Subject: [PATCH 08/15] =?UTF-8?q?=E2=9C=A8=20update=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/docs/payload-types.md | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/contracts/docs/payload-types.md b/contracts/docs/payload-types.md index 62564c77c..9e3d21bb2 100644 --- a/contracts/docs/payload-types.md +++ b/contracts/docs/payload-types.md @@ -338,3 +338,56 @@ 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, + 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.| +|`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 order's nonce (can only be used once but do not have to be used in order).| +|`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`| The required period between consecutive twap orders. | +|`window: u32`| The specified period when twap orders can be executed. | \ No newline at end of file From 0a798bac62b5663521cc69a3bbde6c1a6ca42c2a Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Wed, 19 Feb 2025 15:04:59 +0100 Subject: [PATCH 09/15] revert back to python3.12 --- contracts/test/_helpers/BaseTest.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/_helpers/BaseTest.sol b/contracts/test/_helpers/BaseTest.sol index 9813a4781..fa52ecaaa 100644 --- a/contracts/test/_helpers/BaseTest.sol +++ b/contracts/test/_helpers/BaseTest.sol @@ -110,7 +110,7 @@ contract BaseTest is Test, HookDeployer { function pythonRunCmd() internal pure returns (string[] memory args) { args = new string[](1); - args[0] = ".venv/bin/python3.13"; + args[0] = ".venv/bin/python3.12"; } function ffiPython(string[] memory args) internal returns (bytes memory) { From 92cc602ecc30939d9885afa1f3db70343c8d72d8 Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Thu, 20 Feb 2025 03:29:19 +0100 Subject: [PATCH 10/15] fix controller test --- contracts/test/periphery/ControllerV1.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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))); From d2d606608ab72574eb2311faa82284db4eee8a33 Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Sun, 23 Feb 2025 21:57:33 +0100 Subject: [PATCH 11/15] fix: Order Invalidation Logic --- contracts/src/Angstrom.sol | 25 ++-- contracts/src/modules/OrderInvalidation.sol | 130 +++++++----------- contracts/src/types/TWAPOrderBuffer.sol | 29 ++-- contracts/test/_mocks/OpenAngstrom.sol | 1 + .../test/modules/OrderInvalidation.t.sol | 78 ++++++----- contracts/test/types/TWAPOrderBuffer.t.sol | 18 ++- 6 files changed, 138 insertions(+), 143 deletions(-) diff --git a/contracts/src/Angstrom.sol b/contracts/src/Angstrom.sol index 425b0be6b..ef608ea24 100644 --- a/contracts/src/Angstrom.sol +++ b/contracts/src/Angstrom.sol @@ -270,6 +270,7 @@ contract Angstrom is { TypedDataHasher typedHasher = _erc712Hasher(); TWAPOrderBuffer memory buffer; + buffer.setTypeHash(); CalldataReader end; (reader, end) = reader.readU24End(); @@ -292,6 +293,7 @@ contract Angstrom is TWAPOrderVariantMap variantMap; // Load variant map, ref id and set use internal. (reader, variantMap) = buffer.init(reader); + console.log("buffer_typehash: ", uint256(buffer.typeHash)); // Load and lookup asset in/out and dependent values. PriceOutVsIn price; @@ -313,7 +315,7 @@ contract Angstrom is HookBuffer hook; (reader, hook, buffer.hookDataHash) = HookBufferLib.readFrom(reader, variantMap.noHook()); - reader = buffer.readOrderValidation(reader); + reader = buffer.readTWAPOrderValidation(reader); AmountIn amountIn; AmountOut amountOut; @@ -325,17 +327,16 @@ contract Angstrom is (reader, from) = variantMap.isEcdsa() ? SignatureLib.readAndCheckEcdsa(reader, orderHash) : SignatureLib.readAndCheckERC1271(reader, orderHash); + + _checkTWAPOrderData(buffer.timeInterval, buffer.totalParts, buffer.window); + _invalidatePartTWAPAndCheckDeadline( + _computeTWAPOrderSlot(orderHash, from), + buffer.startTime, + buffer.timeInterval, + buffer.totalParts, + buffer.window + ); } - - _checkTWAPOrderData(buffer.timeInterval, buffer.totalParts, buffer.window); - _invalidatePartTWAPNonceAndCheckDeadline( - from, - buffer.nonce, - buffer.startTime, - buffer.timeInterval, - buffer.totalParts, - buffer.window - ); // Push before hook as a potential loan. address to = buffer.recipient; @@ -347,7 +348,7 @@ contract Angstrom is hook.tryTrigger(from); _settleOrderIn(from, buffer.assetIn, amountIn, buffer.useInternal); - console.log("end test"); + return reader; } diff --git a/contracts/src/modules/OrderInvalidation.sol b/contracts/src/modules/OrderInvalidation.sol index 39b413530..e792b4c1e 100644 --- a/contracts/src/modules/OrderInvalidation.sol +++ b/contracts/src/modules/OrderInvalidation.sol @@ -6,75 +6,58 @@ abstract contract OrderInvalidation { error NonceReuse(); error OrderAlreadyExecuted(); error Expired(); - error TWAPNonceReuse(); + error TWAPOrderAlreadyExecuted(); error TWAPExpired(); - error InvalidTWAPNonce(); error InvalidTWAPOrder(); /// @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(uint32).max uint256 private constant MASK_U32 = 0xffffffff; // type(uint40).max uint256 private constant MASK_U40 = 0xffffffffff; - // type(uint64).max - uint256 private constant MASK_U64 = 0xffffffffffffffff; - // type(uint232).max - uint256 private constant MASK_U232 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; - // upper 24 bits mask - uint256 private constant UPPER_PART_MASK = 0xffffff0000000000000000000000000000000000000000000000000000000000; - // max twap nonce bit = 232 - uint256 private constant MAX_TWAP_NONCE_SIZE = 0xe8; + // max upper limit of twap intervals = 31557600 (365.25 days) - uint256 private constant MAX_TWAP_INTERVAL = 0x1e187e0; + uint256 private constant MAX_TWAP_INTERVAL = 31557600; // min lower limit of twap intervals = 12 seconds - uint256 private constant MIN_TWAP_INTERVAL = 0x0c; + 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 = 0x604e60; + uint256 private constant MAX_TWAP_TOTAL_PARTS = 6311520; function invalidateNonce(uint64 nonce) external { _invalidateNonce(msg.sender, nonce); } - function invalidateTWAPNonce(uint64 nonce) external { + function invalidateTWAPOrder(bytes32 orderHash) external { assembly ("memory-safe") { - nonce := and(nonce, MASK_U64) - mstore(12, div(nonce, MAX_TWAP_NONCE_SIZE)) - mstore(4, UNORDERED_TWAP_NONCES_SLOT) - mstore(0, caller()) - - let bitmapPtr := keccak256(12, 32) - let flag := shl(mod(nonce, MAX_TWAP_NONCE_SIZE), 1) - let bitmapVal := sload(bitmapPtr) - let updated := xor(and(bitmapVal, MASK_U232) , flag) - let twapNonce := iszero(and(updated, flag)) - let fulfilledParts := shr(MAX_TWAP_NONCE_SIZE, bitmapVal) - - // Reverts if `fulfilledParts` is empty while `twapNonce` is not empty, - // or if `fulfilledParts` is not empty while `twapNonce` is empty. - if xor(iszero(iszero(fulfilledParts)), twapNonce) { - mstore(0x00, 0xcfa42043 /* InvalidTWAPNonce() */ ) - revert(0x1c, 0x04) - } + mstore(20, caller()) + mstore(0, orderHash) + let partPtr := keccak256(0, 52) + + let fulfilledParts := sload(partPtr) if eq(fulfilledParts, 0xffffff) { - mstore(0x00, 0x9a495418 /* TWAPNonceReuse() */ ) + mstore(0x00, 0xceb2041c /* TWAPOrderAlreadyExecuted() */ ) revert(0x1c, 0x04) } - sstore(bitmapPtr, or(flag, UPPER_PART_MASK)) + sstore(partPtr, 0xffffff) } } function _checkTWAPOrderData(uint32 interval, uint32 twapParts, uint32 window) internal pure { - bool validInterval = interval < MIN_TWAP_INTERVAL || interval > MAX_TWAP_INTERVAL; - bool validTParts = twapParts == 0 || twapParts > MAX_TWAP_TOTAL_PARTS; - bool validWindow = window < MIN_TWAP_INTERVAL || window > interval; + assembly ("memory-safe") { + interval := and(interval, MASK_U32) + twapParts := and(twapParts, MASK_U32) + window := and(window, MASK_U32) + + let validInterval := + or(lt(interval, MIN_TWAP_INTERVAL), gt(interval, MAX_TWAP_INTERVAL)) + let validTwapParts := or(iszero(twapParts), gt(twapParts, MAX_TWAP_TOTAL_PARTS)) + let validWindow := or(lt(window, MIN_TWAP_INTERVAL), gt(window, interval)) - assembly("memory-safe") { - if or(or(validInterval, validTParts), validWindow){ + if or(or(validInterval, validTwapParts), validWindow) { mstore(0x00, 0x51e490f3 /* InvalidTWAPOrder() */ ) revert(0x1c, 0x04) } @@ -105,57 +88,48 @@ abstract contract OrderInvalidation { } } - function _invalidatePartTWAPNonceAndCheckDeadline( - address owner, - uint64 nonce, - uint40 startTime, - uint32 interval, - uint32 twapParts, - uint32 window - ) - internal + function _computeTWAPOrderSlot(bytes32 orderHash, address owner) + internal + pure + returns (bytes32 partPtr) { assembly ("memory-safe") { - nonce := and(nonce, MASK_U64) - mstore(12, div(nonce, MAX_TWAP_NONCE_SIZE)) - mstore(4, UNORDERED_TWAP_NONCES_SLOT) - mstore(0, owner) - - let bitmapPtr := keccak256(12, 32) - let flag := shl(mod(nonce, MAX_TWAP_NONCE_SIZE), 1) - let bitmapVal := sload(bitmapPtr) - let updated := xor(and(bitmapVal, MASK_U232), flag) - let twapNonce := iszero(and(updated, flag)) + mstore(20, owner) + mstore(0, orderHash) + partPtr := keccak256(0, 52) + } + } + function _invalidatePartTWAPAndCheckDeadline( + bytes32 partPtr, + uint40 startTime, + uint32 interval, + uint32 twapParts, + uint32 window + ) internal { + assembly ("memory-safe") { // part to fulfill - let fulfilledParts := shr(MAX_TWAP_NONCE_SIZE, bitmapVal) + let fulfilledParts := sload(partPtr) let _cachedFulfilledParts := fulfilledParts - // Reverts if `fulfilledParts` is empty while `twapNonce` is not empty, - // or if `fulfilledParts` is not empty while `twapNonce` is empty. - if xor(iszero(iszero(fulfilledParts)), twapNonce) { - mstore(0x00, 0xcfa42043 /* InvalidTWAPNonce() */ ) - revert(0x1c, 0x04) - } - fulfilledParts := add(fulfilledParts, 1) - twapParts:= and(twapParts, MASK_U32) + twapParts := and(twapParts, MASK_U32) if gt(fulfilledParts, twapParts) { - mstore(0x00, 0x9a495418 /* TWAPNonceReuse() */ ) + mstore(0x00, 0xceb2041c /* TWAPOrderAlreadyExecuted() */ ) revert(0x1c, 0x04) } - updated := or(shl(MAX_TWAP_NONCE_SIZE, fulfilledParts), flag) + if eq(twapParts, fulfilledParts) { fulfilledParts := 0xffffff } + sstore(partPtr, fulfilledParts) - if iszero(sub(twapParts, fulfilledParts)) { - updated := or(updated, UPPER_PART_MASK) - } - sstore(bitmapPtr, updated) + let currentPartStart := + add(and(startTime, MASK_U40), mul(_cachedFulfilledParts, and(interval, MASK_U32))) - let currentPartStart := add(and(startTime, MASK_U40), mul(_cachedFulfilledParts, and(interval, MASK_U32))) - - if or(lt(timestamp(), currentPartStart), gt(timestamp(), add(currentPartStart, and(window, MASK_U32)))) { + if or( + lt(timestamp(), currentPartStart), + gt(timestamp(), add(currentPartStart, and(window, MASK_U32))) + ) { mstore(0x00, 0x982c606d /* TWAPExpired() */ ) revert(0x1c, 0x04) } diff --git a/contracts/src/types/TWAPOrderBuffer.sol b/contracts/src/types/TWAPOrderBuffer.sol index 51dcdf1e5..7c03cbe6c 100644 --- a/contracts/src/types/TWAPOrderBuffer.sol +++ b/contracts/src/types/TWAPOrderBuffer.sol @@ -68,6 +68,10 @@ library TWAPOrderBufferLib { ")" ); + function setTypeHash(TWAPOrderBuffer memory self) internal pure { + self.typeHash = TWAP_ORDER_TYPEHASH; + } + function init(TWAPOrderBuffer memory self, CalldataReader reader) internal pure @@ -83,13 +87,10 @@ library TWAPOrderBufferLib { // Advance reader. reader := add(reader, REF_ID_BYTES) } - - self.typeHash = TWAP_ORDER_TYPEHASH; - + self.useInternal = variantMap.useInternal(); return (reader, variantMap); - } function hash(TWAPOrderBuffer memory self) internal pure returns (bytes32 orderHash) { @@ -116,7 +117,6 @@ library TWAPOrderBufferLib { if (extraFeeAsset0 > maxExtraFeeAsset0) revert GasAboveMax(); self.maxExtraFeeAsset0 = maxExtraFeeAsset0; - if (variant.zeroForOne()) { AmountIn fee = AmountIn.wrap(extraFeeAsset0); if (variant.exactIn()) { @@ -140,10 +140,11 @@ library TWAPOrderBufferLib { return (reader, quantityIn, quantityOut); } - function readOrderValidation( - TWAPOrderBuffer memory self, - CalldataReader reader - ) internal pure returns (CalldataReader) { + function readTWAPOrderValidation(TWAPOrderBuffer memory self, CalldataReader reader) + internal + pure + returns (CalldataReader) + { // Copy slices directly from calldata into memory. assembly ("memory-safe") { calldatacopy( @@ -157,9 +158,7 @@ library TWAPOrderBufferLib { ) reader := add(reader, START_TIME_BYTES) calldatacopy( - add(self, add(PARTS_MEM_OFFSET, sub(0x20, PARTS_BYTES))), - reader, - PARTS_BYTES + add(self, add(PARTS_MEM_OFFSET, sub(0x20, PARTS_BYTES))), reader, PARTS_BYTES ) reader := add(reader, PARTS_BYTES) calldatacopy( @@ -169,12 +168,10 @@ library TWAPOrderBufferLib { ) reader := add(reader, TIME_INTERVALS_BYTES) calldatacopy( - add(self, add(WINDOW_MEM_OFFSET, sub(0x20, WINDOW_BYTES))), - reader, - WINDOW_BYTES + add(self, add(WINDOW_MEM_OFFSET, sub(0x20, WINDOW_BYTES))), reader, WINDOW_BYTES ) reader := add(reader, WINDOW_BYTES) } return reader; } -} \ No newline at end of file +} diff --git a/contracts/test/_mocks/OpenAngstrom.sol b/contracts/test/_mocks/OpenAngstrom.sol index c46792e28..63281915e 100644 --- a/contracts/test/_mocks/OpenAngstrom.sol +++ b/contracts/test/_mocks/OpenAngstrom.sol @@ -97,6 +97,7 @@ contract OpenAngstrom is Angstrom { (reader, pairs) = PairLib.readFromAndValidate(reader, assets, _configStore); TWAPOrderBuffer memory buffer; + buffer.setTypeHash(); reader = _validateAndExecuteTWAPOrder(reader, buffer, _erc712Hasher(), pairs); reader.requireAtEndOf(userOrderPayload); diff --git a/contracts/test/modules/OrderInvalidation.t.sol b/contracts/test/modules/OrderInvalidation.t.sol index 2a6a1d8f2..879c987c3 100644 --- a/contracts/test/modules/OrderInvalidation.t.sol +++ b/contracts/test/modules/OrderInvalidation.t.sol @@ -10,9 +10,9 @@ contract InvalidationManagerTest is Test, OrderInvalidation { using Utils for *; bytes4 internal constant NONCES_SLOT = bytes4(keccak256("angstrom-v1_0.unordered-nonces.slot")); - uint256 private constant MAX_TWAP_INTERVAL = 0x1e187e0; + uint256 private constant MAX_TWAP_INTERVAL = 31557600; uint256 private constant MIN_TWAP_INTERVAL = 12; - uint256 private constant MAX_TWAP_TOTAL_PARTS = 0x604e60; + uint256 private constant MAX_TWAP_TOTAL_PARTS = 6311520; uint256 private constant MAX_U32_VAL = type(uint32).max; function test_fuzzing_revertsUponReuse(address owner, uint64 nonce) public { @@ -21,51 +21,69 @@ contract InvalidationManagerTest is Test, OrderInvalidation { _invalidateNonce(owner.brutalize(), nonce.brutalize()); } - function test_fuzzing_revertsUponTWAPNonceReuse(uint64 nonce) public { - this.invalidateTWAPNonce(nonce.brutalize()); - vm.expectRevert(OrderInvalidation.TWAPNonceReuse.selector); - this.invalidateTWAPNonce(nonce.brutalize()); + function test_fuzzing_revertsUponAlreadyExecutedOrder(bytes32 orderHash) public { + this.invalidateTWAPOrder(orderHash); + vm.expectRevert(OrderInvalidation.TWAPOrderAlreadyExecuted.selector); + this.invalidateTWAPOrder(orderHash); } /// forge-config: default.allow_internal_expect_revert = true - function test_fuzzing_revertsUponInvalidTWAPData(uint32 interval, uint32 twapParts, uint32 window) public { + 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.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); + _checkTWAPOrderData( + interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() + ); interval = uint32(bound(uint256(interval), 0, MIN_TWAP_INTERVAL - 1)); vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); - _checkTWAPOrderData(interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); - + _checkTWAPOrderData( + interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() + ); + interval = uint32(bound(uint256(interval), MIN_TWAP_INTERVAL + 1, MAX_U32_VAL)); vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); - _checkTWAPOrderData(interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); + _checkTWAPOrderData( + interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() + ); 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.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); + _checkTWAPOrderData( + interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() + ); twapParts = 0; vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); - _checkTWAPOrderData(interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); + _checkTWAPOrderData( + interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() + ); 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.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); + _checkTWAPOrderData( + interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() + ); window = uint32(bound(uint256(interval), 0, interval - 1)); vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); - _checkTWAPOrderData(interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32()); + _checkTWAPOrderData( + interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() + ); } /// forge-config: default.allow_internal_expect_revert = true - function test_fuzzing_revertsUponPartsTWAPNonceReuse( + function test_fuzzing_revertsUponPartsTWAPAlreadyExecuted( + bytes32 orderHash, address owner, - uint64 nonce, - uint32 interval, + uint32 interval, uint32 twapParts, uint32 window ) public { @@ -74,24 +92,22 @@ contract InvalidationManagerTest is Test, OrderInvalidation { twapParts = uint32(bound(uint256(twapParts), 0, 25)); window = uint32(bound(uint256(window), MIN_TWAP_INTERVAL, interval)); - for(uint256 i = twapParts; i != 0; i--){ - _invalidatePartTWAPNonceAndCheckDeadline( - owner.brutalize(), - nonce.brutalize(), - startTime.brutalizeU40(), - interval.brutalizeU32(), + for (uint256 i = twapParts; i != 0; i--) { + _invalidatePartTWAPAndCheckDeadline( + _computeTWAPOrderSlot(orderHash, owner.brutalize()), + startTime.brutalizeU40(), + interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() ); - uint256 warpedTime = startTime + ((twapParts-(i-1)) * interval); + uint256 warpedTime = startTime + ((twapParts - (i - 1)) * interval); vm.warp(warpedTime); } - vm.expectRevert(OrderInvalidation.TWAPNonceReuse.selector); - _invalidatePartTWAPNonceAndCheckDeadline( - owner.brutalize(), - nonce.brutalize(), - startTime.brutalizeU40(), - interval.brutalizeU32(), + vm.expectRevert(OrderInvalidation.TWAPOrderAlreadyExecuted.selector); + _invalidatePartTWAPAndCheckDeadline( + _computeTWAPOrderSlot(orderHash, owner.brutalize()), + startTime.brutalizeU40(), + interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() ); diff --git a/contracts/test/types/TWAPOrderBuffer.t.sol b/contracts/test/types/TWAPOrderBuffer.t.sol index d53beb755..caf593fed 100644 --- a/contracts/test/types/TWAPOrderBuffer.t.sol +++ b/contracts/test/types/TWAPOrderBuffer.t.sol @@ -19,15 +19,17 @@ contract TWAPOrderBufferTest is BaseTest { 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) { + function bufferHash(TimeWeightedAveragePriceOrder memory order) + internal + view + returns (bytes32) + { return this._bufferHashTWAPOrder( order, bytes.concat( @@ -49,6 +51,7 @@ contract TWAPOrderBufferTest is BaseTest { CalldataReader reader = CalldataReaderLib.from(dataStart); TWAPOrderBuffer memory buffer; TWAPOrderVariantMap varMap; + buffer.setTypeHash(); (reader, varMap) = buffer.init(reader); buffer.exactIn = order.exactIn; @@ -64,11 +67,14 @@ contract TWAPOrderBufferTest is BaseTest { ? new bytes(0) : bytes.concat(bytes20(order.hook), order.hookPayload) ); - buffer.readOrderValidation(reader); + buffer.readTWAPOrderValidation(reader); return buffer.hash(); } - function ffiPythonEIP712Hash(TimeWeightedAveragePriceOrder memory order) internal returns (bytes32) { + 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"; @@ -94,4 +100,4 @@ contract TWAPOrderBufferTest is BaseTest { args[i++] = vm.toString(order.window); return bytes32(ffiPython(args)); } -} \ No newline at end of file +} From 3b15f87e1e5fa15bad0c207be5512c38d3698fa1 Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Sun, 23 Feb 2025 22:01:07 +0100 Subject: [PATCH 12/15] =?UTF-8?q?=E2=9C=A8=20chore:=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/docs/payload-types.md | 8 +++++--- contracts/src/types/TWAPOrderVariantMap.sol | 1 - contracts/test/Angstrom.t.sol | 6 +++--- contracts/test/_helpers/Utils.sol | 2 +- contracts/test/_reference/Bundle.sol | 11 ++++------- contracts/test/_reference/OrderTypes.sol | 14 +++++++++----- contracts/test/benchmark/TWAPOrder.b.sol | 2 +- 7 files changed, 23 insertions(+), 21 deletions(-) diff --git a/contracts/docs/payload-types.md b/contracts/docs/payload-types.md index 9e3d21bb2..03e477f9c 100644 --- a/contracts/docs/payload-types.md +++ b/contracts/docs/payload-types.md @@ -351,6 +351,7 @@ struct TwapOrder { 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, @@ -378,6 +379,7 @@ struct TwapData { |`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.| @@ -386,8 +388,8 @@ struct TwapData { **`TwapData`** |Field|Description| |-----|-----------| -|`nonce: u64`|The order's nonce (can only be used once but do not have to be used in order).| +|`nonce: u64`|The Twap order's nonce (not strictly required to be unique, because order uniqueness is enforced using the order hash).| |`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`| The required period between consecutive twap orders. | -|`window: u32`| The specified period when twap orders can be executed. | \ No newline at end of file +|`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/types/TWAPOrderVariantMap.sol b/contracts/src/types/TWAPOrderVariantMap.sol index 1ffbd8f96..e627886a1 100644 --- a/contracts/src/types/TWAPOrderVariantMap.sol +++ b/contracts/src/types/TWAPOrderVariantMap.sol @@ -14,7 +14,6 @@ library TWAPOrderVariantMapLib { 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; } diff --git a/contracts/test/Angstrom.t.sol b/contracts/test/Angstrom.t.sol index 322e5066c..03ffa8f2a 100644 --- a/contracts/test/Angstrom.t.sol +++ b/contracts/test/Angstrom.t.sol @@ -9,7 +9,7 @@ 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, + PartialStandingOrder, ExactFlashOrder, TimeWeightedAveragePriceOrder } from "test/_reference/OrderTypes.sol"; @@ -143,7 +143,7 @@ contract AngstromTest is BaseTest { bundle.addAsset(asset0).addAsset(asset1).addPair(asset0, asset1, price); uint256 startTime = block.timestamp; - uint256 timeInterval; + uint256 timeInterval; { TimeWeightedAveragePriceOrder memory order; @@ -199,7 +199,7 @@ contract AngstromTest is BaseTest { // only one bundle per block. vm.roll(block.number + 2); - vm.warp(startTime + 2*(timeInterval)); + vm.warp(startTime + 2 * (timeInterval)); angstrom.execute(payload); vm.stopPrank(); diff --git a/contracts/test/_helpers/Utils.sol b/contracts/test/_helpers/Utils.sol index b64a24880..6f1fa9e36 100644 --- a/contracts/test/_helpers/Utils.sol +++ b/contracts/test/_helpers/Utils.sol @@ -35,4 +35,4 @@ library Utils { dx := xor(shl(32, dirt), z) } } -} \ No newline at end of file +} diff --git a/contracts/test/_reference/Bundle.sol b/contracts/test/_reference/Bundle.sol index a05ac23bc..7d57d3d23 100644 --- a/contracts/test/_reference/Bundle.sol +++ b/contracts/test/_reference/Bundle.sol @@ -5,11 +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, - TimeWeightedAveragePriceOrder, - 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"; @@ -133,7 +129,8 @@ library BundleLib { { // self.addPair(twap.assetIn, twap.assetOut); - TimeWeightedAveragePriceOrder[] memory newTwapOrders = new TimeWeightedAveragePriceOrder[](self.twapOrders.length + 1); + TimeWeightedAveragePriceOrder[] memory newTwapOrders = + new TimeWeightedAveragePriceOrder[](self.twapOrders.length + 1); for (uint256 i = 0; i < self.twapOrders.length; i++) { newTwapOrders[i] = self.twapOrders[i]; } @@ -150,4 +147,4 @@ library BundleLib { self.assets[index0].addDelta(deltas.amount0()); self.assets[index1].addDelta(deltas.amount1()); } -} \ No newline at end of file +} diff --git a/contracts/test/_reference/OrderTypes.sol b/contracts/test/_reference/OrderTypes.sol index a6f6392d4..7b18ee45c 100644 --- a/contracts/test/_reference/OrderTypes.sol +++ b/contracts/test/_reference/OrderTypes.sol @@ -432,11 +432,11 @@ library OrdersLib { function toVariantMap(TimeWeightedAveragePriceOrder memory order, bool zeroForOne) internal pure - returns(uint8 varMap) + 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); + | (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) @@ -603,7 +603,11 @@ library OrdersLib { ); } - function toStr(TimeWeightedAveragePriceOrder memory o) internal pure returns (string memory str) { + function toStr(TimeWeightedAveragePriceOrder memory o) + internal + pure + returns (string memory str) + { str = string.concat( "ExactStandingOrder {", "\n exactIn: ", @@ -692,4 +696,4 @@ library OrdersLib { ); } } -} \ No newline at end of file +} diff --git a/contracts/test/benchmark/TWAPOrder.b.sol b/contracts/test/benchmark/TWAPOrder.b.sol index 675c3fc03..9eedbf830 100644 --- a/contracts/test/benchmark/TWAPOrder.b.sol +++ b/contracts/test/benchmark/TWAPOrder.b.sol @@ -83,7 +83,7 @@ contract TWAPOrderBenchmarkTest is BaseTest { angstrom.validateAndExecuteTWAPOrder(payload); vm.warp(order.startTime + order.timeInterval); angstrom.validateAndExecuteTWAPOrder(payload); - vm.warp(order.startTime + 2*(order.timeInterval)); + vm.warp(order.startTime + 2 * (order.timeInterval)); angstrom.validateAndExecuteTWAPOrder(payload); } } From 52bdb4f3848408d37c578bc072594be212ad71ef Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Mon, 24 Feb 2025 11:47:45 +0100 Subject: [PATCH 13/15] =?UTF-8?q?=E2=9C=A8more=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/Angstrom.sol | 12 ++---------- contracts/src/modules/TopLevelAuth.sol | 7 ------- contracts/src/periphery/ControllerV1.sol | 2 -- 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/contracts/src/Angstrom.sol b/contracts/src/Angstrom.sol index ef608ea24..24268f3ff 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"; @@ -50,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); @@ -67,18 +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("executed tob"); reader = _validateAndExecuteUserOrders(reader, pairs); - console.log("executed user"); reader = _validateAndExecuteTWAPOrders(reader, pairs); - console.log("executed twap"); + reader.requireAtEndOf(data); _saveAndSettle(assets); @@ -161,7 +155,6 @@ contract Angstrom is : SignatureLib.readAndCheckERC1271(reader, orderHash); _invalidateOrderHash(orderHash, from); - console.log(from); address to = buffer.recipient; assembly ("memory-safe") { @@ -293,7 +286,6 @@ contract Angstrom is TWAPOrderVariantMap variantMap; // Load variant map, ref id and set use internal. (reader, variantMap) = buffer.init(reader); - console.log("buffer_typehash: ", uint256(buffer.typeHash)); // Load and lookup asset in/out and dependent values. PriceOutVsIn price; 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); } From 730170e41e7359dec55e8d93007123c7a8fad9cc Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Wed, 26 Feb 2025 21:10:38 +0100 Subject: [PATCH 14/15] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20re-use=20twap=20nonc?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/docs/payload-types.md | 2 +- contracts/src/Angstrom.sol | 5 +- contracts/src/modules/OrderInvalidation.sol | 125 +++++++++--------- .../test/modules/OrderInvalidation.t.sol | 91 ++++++------- 4 files changed, 108 insertions(+), 115 deletions(-) diff --git a/contracts/docs/payload-types.md b/contracts/docs/payload-types.md index 03e477f9c..7643619c9 100644 --- a/contracts/docs/payload-types.md +++ b/contracts/docs/payload-types.md @@ -388,7 +388,7 @@ struct TwapData { **`TwapData`** |Field|Description| |-----|-----------| -|`nonce: u64`|The Twap order's nonce (not strictly required to be unique, because order uniqueness is enforced using the order hash).| +|`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. | diff --git a/contracts/src/Angstrom.sol b/contracts/src/Angstrom.sol index 24268f3ff..fb40c1e99 100644 --- a/contracts/src/Angstrom.sol +++ b/contracts/src/Angstrom.sol @@ -321,11 +321,10 @@ contract Angstrom is : SignatureLib.readAndCheckERC1271(reader, orderHash); _checkTWAPOrderData(buffer.timeInterval, buffer.totalParts, buffer.window); - _invalidatePartTWAPAndCheckDeadline( - _computeTWAPOrderSlot(orderHash, from), + _checkTWAPOrderDeadline( + _invalidatePartTWAPNonce(orderHash, from, buffer.nonce, buffer.totalParts), buffer.startTime, buffer.timeInterval, - buffer.totalParts, buffer.window ); } diff --git a/contracts/src/modules/OrderInvalidation.sol b/contracts/src/modules/OrderInvalidation.sol index e792b4c1e..2811f9508 100644 --- a/contracts/src/modules/OrderInvalidation.sol +++ b/contracts/src/modules/OrderInvalidation.sol @@ -6,18 +6,18 @@ abstract contract OrderInvalidation { error NonceReuse(); error OrderAlreadyExecuted(); error Expired(); - error TWAPOrderAlreadyExecuted(); 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 MASK_U32 = 0xffffffff; - // type(uint40).max - uint256 private constant MASK_U40 = 0xffffffffff; - + 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 @@ -29,41 +29,46 @@ abstract contract OrderInvalidation { _invalidateNonce(msg.sender, nonce); } - function invalidateTWAPOrder(bytes32 orderHash) external { + function invalidateTWAPOrderNonce(uint64 nonce) external { assembly ("memory-safe") { - mstore(20, caller()) - mstore(0, orderHash) - let partPtr := keccak256(0, 52) + mstore(12, nonce) + mstore(4, UNORDERED_TWAP_NONCES_SLOT) + mstore(0, caller()) - let fulfilledParts := sload(partPtr) + let partPtr := keccak256(12, 32) + let bitmap := sload(partPtr) - if eq(fulfilledParts, 0xffffff) { - mstore(0x00, 0xceb2041c /* TWAPOrderAlreadyExecuted() */ ) + if eq(and(bitmap, MAX_U24), MAX_U24) { + mstore(0x00, 0x264a877f /* TWAPOrderNonceReuse() */ ) revert(0x1c, 0x04) } - sstore(partPtr, 0xffffff) + sstore(partPtr, MAX_U24) } } function _checkTWAPOrderData(uint32 interval, uint32 twapParts, uint32 window) internal pure { - assembly ("memory-safe") { - interval := and(interval, MASK_U32) - twapParts := and(twapParts, MASK_U32) - window := and(window, MASK_U32) - - let validInterval := - or(lt(interval, MIN_TWAP_INTERVAL), gt(interval, MAX_TWAP_INTERVAL)) - let validTwapParts := or(iszero(twapParts), gt(twapParts, MAX_TWAP_TOTAL_PARTS)) - let validWindow := or(lt(window, MIN_TWAP_INTERVAL), gt(window, interval)) + 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 or(or(validInterval, validTwapParts), validWindow) { - mstore(0x00, 0x51e490f3 /* InvalidTWAPOrder() */ ) - revert(0x1c, 0x04) - } + 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(); } @@ -88,51 +93,43 @@ abstract contract OrderInvalidation { } } - function _computeTWAPOrderSlot(bytes32 orderHash, address owner) - internal - pure - returns (bytes32 partPtr) - { + function _invalidatePartTWAPNonce( + bytes32 orderHash, + address owner, + uint256 nonce, + uint32 twapParts + ) internal returns (uint256 _cachedFulfilledParts) { + uint256 bitmap; + uint256 partPtr; assembly ("memory-safe") { - mstore(20, owner) - mstore(0, orderHash) - partPtr := keccak256(0, 52) - } - } + mstore(12, nonce) + mstore(4, UNORDERED_TWAP_NONCES_SLOT) + mstore(0, owner) + partPtr := keccak256(12, 32) - function _invalidatePartTWAPAndCheckDeadline( - bytes32 partPtr, - uint40 startTime, - uint32 interval, - uint32 twapParts, - uint32 window - ) internal { - assembly ("memory-safe") { // part to fulfill - let fulfilledParts := sload(partPtr) - let _cachedFulfilledParts := fulfilledParts + bitmap := sload(partPtr) - fulfilledParts := add(fulfilledParts, 1) - twapParts := and(twapParts, MASK_U32) + // 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) } + } - if gt(fulfilledParts, twapParts) { - mstore(0x00, 0xceb2041c /* TWAPOrderAlreadyExecuted() */ ) - revert(0x1c, 0x04) - } + uint256 lowerHashBits = uint232(uint256(orderHash)) ^ bitmap >> 24; + if (lowerHashBits != 0) revert TWAPOrderNonceReuse(); - if eq(twapParts, fulfilledParts) { fulfilledParts := 0xffffff } - sstore(partPtr, fulfilledParts) + _cachedFulfilledParts = bitmap & MAX_U24; + uint256 fulfilledParts = _cachedFulfilledParts + 1; - let currentPartStart := - add(and(startTime, MASK_U40), mul(_cachedFulfilledParts, and(interval, MASK_U32))) + if (fulfilledParts != twapParts) { + bitmap += 1; + } else { + bitmap = 0; + } - if or( - lt(timestamp(), currentPartStart), - gt(timestamp(), add(currentPartStart, and(window, MASK_U32))) - ) { - mstore(0x00, 0x982c606d /* TWAPExpired() */ ) - revert(0x1c, 0x04) - } + assembly ("memory-safe") { + sstore(partPtr, bitmap) } } diff --git a/contracts/test/modules/OrderInvalidation.t.sol b/contracts/test/modules/OrderInvalidation.t.sol index 879c987c3..bc1f4d2af 100644 --- a/contracts/test/modules/OrderInvalidation.t.sol +++ b/contracts/test/modules/OrderInvalidation.t.sol @@ -21,10 +21,35 @@ contract InvalidationManagerTest is Test, OrderInvalidation { _invalidateNonce(owner.brutalize(), nonce.brutalize()); } - function test_fuzzing_revertsUponAlreadyExecutedOrder(bytes32 orderHash) public { - this.invalidateTWAPOrder(orderHash); - vm.expectRevert(OrderInvalidation.TWAPOrderAlreadyExecuted.selector); - this.invalidateTWAPOrder(orderHash); + function test_fuzzing_revertsUponAlreadyExecutedOrder(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); } /// forge-config: default.allow_internal_expect_revert = true @@ -36,80 +61,52 @@ contract InvalidationManagerTest is Test, OrderInvalidation { 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.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() - ); + _checkTWAPOrderData(interval, twapParts, window); interval = uint32(bound(uint256(interval), 0, MIN_TWAP_INTERVAL - 1)); vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); - _checkTWAPOrderData( - interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() - ); + _checkTWAPOrderData(interval, twapParts, window); interval = uint32(bound(uint256(interval), MIN_TWAP_INTERVAL + 1, MAX_U32_VAL)); vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); - _checkTWAPOrderData( - interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() - ); + _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.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() - ); + _checkTWAPOrderData(interval, twapParts, window); twapParts = 0; vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); - _checkTWAPOrderData( - interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() - ); + _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.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() - ); + _checkTWAPOrderData(interval, twapParts, window); window = uint32(bound(uint256(interval), 0, interval - 1)); vm.expectRevert(OrderInvalidation.InvalidTWAPOrder.selector); - _checkTWAPOrderData( - interval.brutalizeU32(), twapParts.brutalizeU32(), window.brutalizeU32() - ); + _checkTWAPOrderData(interval, twapParts, window); } /// forge-config: default.allow_internal_expect_revert = true - function test_fuzzing_revertsUponPartsTWAPAlreadyExecuted( + function test_fuzzing_revertsUponPartsResetTWAPNonce( bytes32 orderHash, address owner, - uint32 interval, - uint32 twapParts, - uint32 window + uint64 nonce, + uint32 twapParts ) public { - uint40 startTime = uint40(block.timestamp); - interval = uint32(bound(uint256(interval), MIN_TWAP_INTERVAL, MAX_TWAP_INTERVAL)); twapParts = uint32(bound(uint256(twapParts), 0, 25)); - window = uint32(bound(uint256(window), MIN_TWAP_INTERVAL, interval)); for (uint256 i = twapParts; i != 0; i--) { - _invalidatePartTWAPAndCheckDeadline( - _computeTWAPOrderSlot(orderHash, owner.brutalize()), - startTime.brutalizeU40(), - interval.brutalizeU32(), - twapParts.brutalizeU32(), - window.brutalizeU32() + _invalidatePartTWAPNonce( + orderHash, owner.brutalize(), nonce.brutalize(), twapParts.brutalizeU32() ); - uint256 warpedTime = startTime + ((twapParts - (i - 1)) * interval); - vm.warp(warpedTime); } - vm.expectRevert(OrderInvalidation.TWAPOrderAlreadyExecuted.selector); - _invalidatePartTWAPAndCheckDeadline( - _computeTWAPOrderSlot(orderHash, owner.brutalize()), - startTime.brutalizeU40(), - interval.brutalizeU32(), - twapParts.brutalizeU32(), - window.brutalizeU32() + vm.expectRevert(OrderInvalidation.TWAPOrderNonceReuse.selector); + _invalidatePartTWAPNonce( + orderHash, owner.brutalize(), nonce.brutalize(), twapParts.brutalizeU32() ); } } From aff85101add49af06a350fdf79a214794eceea3e Mon Sep 17 00:00:00 2001 From: 0xNONSO <0xnonso@gmail.com> Date: Thu, 27 Feb 2025 00:05:26 +0100 Subject: [PATCH 15/15] update tests --- contracts/src/modules/OrderInvalidation.sol | 3 +- .../test/modules/OrderInvalidation.t.sol | 29 +++++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/contracts/src/modules/OrderInvalidation.sol b/contracts/src/modules/OrderInvalidation.sol index 2811f9508..8223b58e3 100644 --- a/contracts/src/modules/OrderInvalidation.sol +++ b/contracts/src/modules/OrderInvalidation.sol @@ -96,7 +96,7 @@ abstract contract OrderInvalidation { function _invalidatePartTWAPNonce( bytes32 orderHash, address owner, - uint256 nonce, + uint64 nonce, uint32 twapParts ) internal returns (uint256 _cachedFulfilledParts) { uint256 bitmap; @@ -107,7 +107,6 @@ abstract contract OrderInvalidation { mstore(0, owner) partPtr := keccak256(12, 32) - // part to fulfill bitmap := sload(partPtr) // the probability that two order hashes collide in their lower 232 bits is 1 in 2^232. diff --git a/contracts/test/modules/OrderInvalidation.t.sol b/contracts/test/modules/OrderInvalidation.t.sol index bc1f4d2af..0e2a83559 100644 --- a/contracts/test/modules/OrderInvalidation.t.sol +++ b/contracts/test/modules/OrderInvalidation.t.sol @@ -10,10 +10,11 @@ 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 = type(uint32).max; + uint256 private constant MAX_U32_VAL = 4294967295; function test_fuzzing_revertsUponReuse(address owner, uint64 nonce) public { _invalidateNonce(owner.brutalize(), nonce.brutalize()); @@ -21,7 +22,7 @@ contract InvalidationManagerTest is Test, OrderInvalidation { _invalidateNonce(owner.brutalize(), nonce.brutalize()); } - function test_fuzzing_revertsUponAlreadyExecutedOrder(uint64 nonce) public { + function test_fuzzing_revertsUponInvalidatedOrder(uint64 nonce) public { this.invalidateTWAPOrderNonce(nonce.brutalize()); vm.expectRevert(OrderInvalidation.TWAPOrderNonceReuse.selector); this.invalidateTWAPOrderNonce(nonce.brutalize()); @@ -52,7 +53,6 @@ contract InvalidationManagerTest is Test, OrderInvalidation { _checkTWAPOrderDeadline(fulfilledParts, startTime, interval, window); } - /// forge-config: default.allow_internal_expect_revert = true function test_fuzzing_revertsUponInvalidTWAPData( uint32 interval, uint32 twapParts, @@ -90,8 +90,7 @@ contract InvalidationManagerTest is Test, OrderInvalidation { _checkTWAPOrderData(interval, twapParts, window); } - /// forge-config: default.allow_internal_expect_revert = true - function test_fuzzing_revertsUponPartsResetTWAPNonce( + function test_fuzzing_revertsUponPartsTWAPNonceReuse( bytes32 orderHash, address owner, uint64 nonce, @@ -103,10 +102,22 @@ contract InvalidationManagerTest is Test, OrderInvalidation { _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() + ); } - 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); } }