-
Notifications
You must be signed in to change notification settings - Fork 21
add support for TWÅP orders #345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 10 commits
31cb9a4
50fe844
d999d11
4397949
15b640a
8b4609a
5990983
135ceda
0a798ba
92cc602
d2d6066
3b15f87
52bdb4f
730170e
aff8510
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<address>, | ||
| hook_data: Option<List<bytes1>>, | ||
| 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<Pair>` 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<address>`|Recipient for order output, `None` implies signer.| | ||
| |`hook_data: Option<List<bytes1>>`|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. | | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not entirely clear what this is just from the docs
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, but more specifically at each interval. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 <https://github.com/philogy> | ||
| contract Angstrom is | ||
|
|
@@ -72,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"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't be there in the first place but remove the |
||
| reader = _validateAndExecuteUserOrders(reader, pairs); | ||
| console.log("executed user"); | ||
| reader = _validateAndExecuteTWAPOrders(reader, pairs); | ||
| console.log("executed twap"); | ||
| reader.requireAtEndOf(data); | ||
| _saveAndSettle(assets); | ||
|
|
||
|
|
@@ -260,6 +264,93 @@ 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(); | ||
|
|
||
| // 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.readOrderValidation(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); | ||
| _invalidatePartTWAPNonceAndCheckDeadline( | ||
| from, | ||
| buffer.nonce, | ||
| buffer.startTime, | ||
| buffer.timeInterval, | ||
| buffer.totalParts, | ||
| 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); | ||
| console.log("end test"); | ||
| return reader; | ||
| } | ||
|
|
||
| function _domainNameAndVersion() | ||
| internal | ||
| pure | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,14 +6,81 @@ 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 = 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; | ||
| // 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; | ||
|
0xnonso marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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, 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) | ||
| } | ||
|
|
||
| if eq(fulfilledParts, 0xffffff) { | ||
| mstore(0x00, 0x9a495418 /* TWAPNonceReuse() */ ) | ||
| revert(0x1c, 0x04) | ||
| } | ||
|
|
||
| sstore(bitmapPtr, or(flag, UPPER_PART_MASK)) | ||
| } | ||
| } | ||
|
|
||
| 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 or(or(validInterval, validTParts), validWindow){ | ||
| mstore(0x00, 0x51e490f3 /* InvalidTWAPOrder() */ ) | ||
| revert(0x1c, 0x04) | ||
| } | ||
| } | ||
| } | ||
|
0xnonso marked this conversation as resolved.
|
||
|
|
||
| function _checkDeadline(uint256 deadline) internal view { | ||
| if (block.timestamp > deadline) revert Expired(); | ||
| } | ||
|
|
@@ -38,6 +105,63 @@ abstract contract OrderInvalidation { | |
| } | ||
| } | ||
|
|
||
| function _invalidatePartTWAPNonceAndCheckDeadline( | ||
| address owner, | ||
| uint64 nonce, | ||
| uint40 startTime, | ||
| uint32 interval, | ||
| uint32 twapParts, | ||
| uint32 window | ||
| ) | ||
| internal | ||
| { | ||
| 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Besides the memory manipulation most of this does not have to be in assembly and in fact it makes it very hard to read |
||
|
|
||
| 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)) | ||
|
|
||
| // part to fulfill | ||
| 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) | ||
| } | ||
|
|
||
| fulfilledParts := add(fulfilledParts, 1) | ||
| twapParts:= and(twapParts, MASK_U32) | ||
|
|
||
| if gt(fulfilledParts, twapParts) { | ||
| mstore(0x00, 0x9a495418 /* TWAPNonceReuse() */ ) | ||
| revert(0x1c, 0x04) | ||
| } | ||
|
|
||
| updated := or(shl(MAX_TWAP_NONCE_SIZE, fulfilledParts), flag) | ||
|
|
||
| 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))) | ||
|
|
||
| if or(lt(timestamp(), currentPartStart), gt(timestamp(), add(currentPartStart, and(window, MASK_U32)))) { | ||
| mstore(0x00, 0x982c606d /* TWAPExpired() */ ) | ||
| revert(0x1c, 0x04) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function _invalidateOrderHash(bytes32 orderHash, address from) internal { | ||
| assembly ("memory-safe") { | ||
| mstore(20, from) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.