Skip to content
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

add support for TWÅP orders #345

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
55 changes: 55 additions & 0 deletions contracts/docs/payload-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<address>,
hook_data: Option<List<bytes1>>,
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<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.|
|`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. |
99 changes: 91 additions & 8 deletions contracts/src/Angstrom.sol
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 <https://github.com/philogy>
contract Angstrom is
Expand All @@ -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);
Expand All @@ -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);

Expand Down Expand Up @@ -157,7 +155,6 @@ contract Angstrom is
: SignatureLib.readAndCheckERC1271(reader, orderHash);

_invalidateOrderHash(orderHash, from);
console.log(from);

address to = buffer.recipient;
assembly ("memory-safe") {
Expand Down Expand Up @@ -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
Expand Down
94 changes: 94 additions & 0 deletions contracts/src/modules/OrderInvalidation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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)
Expand Down
7 changes: 0 additions & 7 deletions contracts/src/modules/TopLevelAuth.sol
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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(
Expand Down
2 changes: 0 additions & 2 deletions contracts/src/periphery/ControllerV1.sol
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);
}

Expand Down
Loading