Skip to content
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,
Comment thread
0xnonso marked this conversation as resolved.
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