Skip to content
53 changes: 53 additions & 0 deletions contracts/docs/payload-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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.|
|`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. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not entirely clear what this is just from the docs start_time + window is end time?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but more specifically at each interval.

93 changes: 92 additions & 1 deletion contracts/src/Angstrom.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't be there in the first place but remove the console.log

reader = _validateAndExecuteUserOrders(reader, pairs);
console.log("executed user");
reader = _validateAndExecuteTWAPOrders(reader, pairs);
console.log("executed twap");
reader.requireAtEndOf(data);
_saveAndSettle(assets);

Expand Down Expand Up @@ -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
Expand Down
124 changes: 124 additions & 0 deletions contracts/src/modules/OrderInvalidation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
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)
}
}
}
Comment thread
0xnonso marked this conversation as resolved.

function _checkDeadline(uint256 deadline) internal view {
if (block.timestamp > deadline) revert Expired();
}
Expand All @@ -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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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)
Expand Down
Loading