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