Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f7a20f6
remove overloaded withdraw function and add sentinel value for allowi…
flooreyes Jun 15, 2025
e36436b
updated withdraw tests
flooreyes Jun 15, 2025
e66d879
updated withdraw tests
flooreyes Jun 15, 2025
b28df2f
emergency function improvements
flooreyes Jun 15, 2025
7ee5c5d
emergency function tests
flooreyes Jun 15, 2025
1af38ce
cancellation improvements
flooreyes Jun 15, 2025
395607f
update cancellation tests
flooreyes Jun 15, 2025
3c274e6
improved test coverage
flooreyes Jun 16, 2025
deee308
native token support
flooreyes Jun 16, 2025
736ee35
native token testing
flooreyes Jun 16, 2025
b0b0851
remove deprecated swap() function
flooreyes Jun 18, 2025
88fa43a
update tests
flooreyes Jun 18, 2025
1b3d4bb
more swap path tests
flooreyes Jun 18, 2025
ff76040
code quality improvements
flooreyes Jun 19, 2025
0b18c84
add nonReentrant to cancel()
flooreyes Jun 20, 2025
6cc7140
safe state updates in _settleOrder
flooreyes Jun 20, 2025
8c931f4
increment version
flooreyes Jun 30, 2025
e547cbc
increment version in tests
flooreyes Jun 30, 2025
658d3cd
Improve CEI Patterns
flooreyes Jun 30, 2025
64663ef
comments
flooreyes Jun 30, 2025
c0a6eb7
atomicity in _settleOrer
flooreyes Jul 3, 2025
869b4d1
require statement handling value expectations in dstHook execution
flooreyes Jul 3, 2025
331541e
remove signature input from depositNative
flooreyes Jul 7, 2025
0bab28f
update deposit native tests
flooreyes Jul 7, 2025
c023f0c
require erc-20 in deposit functions
flooreyes Jul 7, 2025
73cf4ca
wip
flooreyes Jul 8, 2025
1afcddc
update tests for extra options
flooreyes Jul 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@
# By default, the examples support both mnemonic-based and private key-based authentication
#
# You don't need to set both of these values, just pick the one that you prefer and set that one
MNEMONIC=
MNEMONIC=YOUR_MNEMONIC

# Contract owner's private key
# This is used for signing whitelist transactions
# Warning: Keep this secure and never commit to version control
PRIVATE_KEY=YOUR_PRIVATE_KEY_HERE
PRIVATE_KEY=YOUR_PRIVATE_KEY

#RPC URLS's

ETHEREUM_RPC_URL=YOUR_URL
BASE_RPC_URL=YOUR_URL
OPTIMISM_RPC_URL=YOUR_URL
ARBITRUM_RPC_URL=YOUR_URL
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ pnpm-error.log
# Editor and OS files
.idea
.vscode
cli/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

---

Aori is designed to securely facilitate omnichain trading, with low latency execution, amd trust minimized settlement. To accomplish this, Aori uses a combination of off-chain infrastructure, on-chain settlement contracts, and LayerZero messaging.
Aori is designed to securely facilitate omnichain trading, with low latency execution, and trust minimized settlement. To accomplish this, Aori uses a combination of off-chain infrastructure, on-chain settlement contracts, and LayerZero messaging.

Solvers can expose a simple API to ingest and process orderflow directly to their trading system. The Aori Protocol's smart contracts ensure that the user's intents are satisfied by the Solver on the destination chain according to the parameters of a user signed intent submitted on the source chain.

Expand Down
465 changes: 261 additions & 204 deletions contracts/Aori.sol

Large diffs are not rendered by default.

133 changes: 133 additions & 0 deletions contracts/AoriOptions.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import { OAppOptionsType3, EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

/**
* @title AoriOptions
* @dev Abstract contract that provides LayerZero enforced options functionality for Aori protocol
* @notice This contract implements LayerZero's enforced options pattern to ensure reliable cross-chain
* message delivery. It supports two message types: settlement and cancellation messages.
*/
abstract contract AoriOptions is OAppOptionsType3 {

/// @notice Message types for LayerZero operations
uint16 public constant SETTLEMENT_MSG_TYPE = 1;
uint16 public constant CANCELLATION_MSG_TYPE = 2;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ENFORCED OPTIONS MANAGEMENT */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/**
* @notice Sets enforced options for settlement messages to a specific destination
* @param dstEid The destination endpoint ID
* @param options The enforced options (e.g., gas limit, msg.value)
* @dev Only callable by the contract owner
*/
function setEnforcedSettlementOptions(uint32 dstEid, bytes calldata options) external onlyOwner {
EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](1);
enforcedOptions[0] = EnforcedOptionParam({
eid: dstEid,
msgType: SETTLEMENT_MSG_TYPE,
options: options
});
_setEnforcedOptions(enforcedOptions);
}

/**
* @notice Sets enforced options for cancellation messages to a specific destination
* @param dstEid The destination endpoint ID
* @param options The enforced options (e.g., gas limit, msg.value)
* @dev Only callable by the contract owner
*/
function setEnforcedCancellationOptions(uint32 dstEid, bytes calldata options) external onlyOwner {
EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](1);
enforcedOptions[0] = EnforcedOptionParam({
eid: dstEid,
msgType: CANCELLATION_MSG_TYPE,
options: options
});
_setEnforcedOptions(enforcedOptions);
}

/**
* @notice Sets enforced options for multiple destinations and message types
* @param enforcedOptions Array of enforced option parameters
* @dev Only callable by the contract owner. Allows batch configuration.
*/
function setEnforcedOptionsMultiple(EnforcedOptionParam[] calldata enforcedOptions) external onlyOwner {
_setEnforcedOptions(enforcedOptions);
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* VIEW FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/**
* @notice Gets the enforced options for a specific endpoint and message type
* @param eid The endpoint ID
* @param msgType The message type (0 for settlement, 1 for cancellation)
* @return The enforced options bytes
*/
function getEnforcedOptions(uint32 eid, uint8 msgType) external view returns (bytes memory) {
uint16 lzMsgType = _convertToLzMsgType(msgType);
return enforcedOptions[eid][lzMsgType];
}

/**
* @notice Gets the enforced options for settlement messages to a specific destination
* @param eid The destination endpoint ID
* @return The enforced options bytes for settlement messages
*/
function getEnforcedSettlementOptions(uint32 eid) external view returns (bytes memory) {
return enforcedOptions[eid][SETTLEMENT_MSG_TYPE];
}

/**
* @notice Gets the enforced options for cancellation messages to a specific destination
* @param eid The destination endpoint ID
* @return The enforced options bytes for cancellation messages
*/
function getEnforcedCancellationOptions(uint32 eid) external view returns (bytes memory) {
return enforcedOptions[eid][CANCELLATION_MSG_TYPE];
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INTERNAL FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/**
* @notice Gets enforced options for settlement messages to a specific destination
* @param dstEid The destination endpoint ID
* @return enforcedOptions The enforced options (empty bytes if none set)
*/
function _getSettlementOptions(uint32 dstEid) internal view returns (bytes memory) {
return enforcedOptions[dstEid][SETTLEMENT_MSG_TYPE];
}

/**
* @notice Gets enforced options for cancellation messages to a specific destination
* @param dstEid The destination endpoint ID
* @return enforcedOptions The enforced options (empty bytes if none set)
*/
function _getCancellationOptions(uint32 dstEid) internal view returns (bytes memory) {
return enforcedOptions[dstEid][CANCELLATION_MSG_TYPE];
}

/**
* @notice Converts public API message type to LayerZero message type
* @param msgType The public API message type (0 for settlement, 1 for cancellation)
* @return The LayerZero message type constant
*/
function _convertToLzMsgType(uint8 msgType) internal pure returns (uint16) {
if (msgType == 0) {
return SETTLEMENT_MSG_TYPE;
} else if (msgType == 1) {
return CANCELLATION_MSG_TYPE;
} else {
revert("Invalid message type");
}
}
}
138 changes: 89 additions & 49 deletions contracts/AoriUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.28;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ECDSA } from "solady/src/utils/ECDSA.sol";
import { IAori } from "./IAori.sol";

Expand Down Expand Up @@ -93,36 +94,8 @@ library ValidationUtils {
}

/**
* @notice Validates deposit and fill parameters for single-chain swaps
* @dev Combines validation for both deposit and fill in a single function
* @param order The order to validate
* @param signature The EIP712 signature to verify
* @param digest The EIP712 type hash digest of the order
* @param endpointId The current chain's endpoint ID
* @param orderStatus The status mapping function to check order status
* @return orderId The calculated order hash
*/
function validateSwap(
IAori.Order calldata order,
bytes calldata signature,
bytes32 digest,
uint32 endpointId,
function(bytes32) external view returns (IAori.OrderStatus) orderStatus
) internal view returns (bytes32 orderId) {
orderId = keccak256(abi.encode(order));
require(orderStatus(orderId) == IAori.OrderStatus.Unknown, "Order already exists");
// Signature validation
address recovered = ECDSA.recover(digest, signature);
require(recovered == order.offerer, "InvalidSignature");

// Order parameter validation
validateCommonOrderParams(order);
require(order.srcEid == endpointId && order.dstEid == endpointId, "Chain mismatch");
require(order.inputToken != order.outputToken, "Invalid Pair");
}

/**
* @notice Validates the cancellation of an order
* @notice Validates the cancellation of a cross-chain order from the destination chain
* @dev Allows whitelisted solvers (anytime), offerers (after expiry), and recipients (after expiry) to cancel
* @param order The order details to cancel
* @param orderId The hash of the order to cancel
* @param endpointId The current chain's endpoint ID
Expand All @@ -142,8 +115,9 @@ library ValidationUtils {
require(orderStatus(orderId) == IAori.OrderStatus.Unknown, "Order not active");
require(
(isAllowedSolver(sender)) ||
(sender == order.offerer && block.timestamp > order.endTime),
"Only whitelisted solver or offerer(after expiry) can cancel"
(sender == order.offerer && block.timestamp > order.endTime) ||
(sender == order.recipient && block.timestamp > order.endTime),
"Only whitelisted solver, offerer, or recipient (after expiry) can cancel"
);
}

Expand Down Expand Up @@ -171,20 +145,16 @@ library ValidationUtils {
// Verify order exists and is active
require(orderStatus(orderId) == IAori.OrderStatus.Active, "Order not active");

// For cross-chain orders: only solver can cancel, and only after expiry
if (order.srcEid != order.dstEid) {
require(
isAllowedSolver(sender) && block.timestamp > order.endTime,
"Cross-chain orders can only be cancelled by solver after expiry"
);
} else {
// For single-chain orders: solver can always cancel, offerer can cancel after expiry
require(
isAllowedSolver(sender) ||
(sender == order.offerer && block.timestamp > order.endTime),
"Only solver or offerer (after expiry) can cancel"
);
}
// Cross-chain orders cannot be cancelled from the source chain to prevent race conditions
// with settlement messages. Use emergencyCancel for emergency situations.
require(order.srcEid == order.dstEid, "Cross-chain orders must be cancelled from destination chain");

// For single-chain orders: solver can always cancel, offerer can cancel after expiry
require(
isAllowedSolver(sender) ||
(sender == order.offerer && block.timestamp > order.endTime),
"Only solver or offerer (after expiry) can cancel"
);
}

/**
Expand Down Expand Up @@ -431,17 +401,21 @@ library ExecutionUtils {
* @param target The target contract address to call
* @param data The calldata to send to the target
* @param observedToken The token address to observe balance changes for
* @return The balance change (typically positive if tokens are received)
* @return The balance change (positive if tokens increased, reverts if decreased)
*/
function observeBalChg(
address target,
bytes calldata data,
address observedToken
) internal returns (uint256) {
uint256 balBefore = IERC20(observedToken).balanceOf(address(this));
uint256 balBefore = NativeTokenUtils.balanceOf(observedToken, address(this));
(bool success, ) = target.call(data);
require(success, "Call failed");
uint256 balAfter = IERC20(observedToken).balanceOf(address(this));
uint256 balAfter = NativeTokenUtils.balanceOf(observedToken, address(this));

// Prevent underflow and provide clear error message
require(balAfter >= balBefore, "Hook decreased contract balance");

return balAfter - balBefore;
}
}
Expand Down Expand Up @@ -714,3 +688,69 @@ library PayloadSizeUtils {
}
}
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* NATIVE TOKEN UTILS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

// Native token address constant
address constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

/**
* @notice Library for native token operations
* @dev Provides utilities for handling native ETH alongside ERC20 tokens
*/
library NativeTokenUtils {
using SafeERC20 for IERC20;

/**
* @notice Checks if a token address represents native ETH
* @param token The token address to check
* @return True if the token is the native token address
*/
function isNativeToken(address token) internal pure returns (bool) {
return token == NATIVE_TOKEN;
}

/**
* @notice Safely transfers tokens (native or ERC20) to a recipient
* @param token The token address (use NATIVE_TOKEN for ETH)
* @param to The recipient address
* @param amount The amount to transfer
*/
function safeTransfer(address token, address to, uint256 amount) internal {
if (isNativeToken(token)) {
(bool success, ) = payable(to).call{value: amount}("");
require(success, "Native transfer failed");
} else {
IERC20(token).safeTransfer(to, amount);
}
}

/**
* @notice Gets the balance of a token for a specific address
* @param token The token address (use NATIVE_TOKEN for ETH)
* @param account The account to check balance for
* @return The token balance
*/
function balanceOf(address token, address account) internal view returns (uint256) {
if (isNativeToken(token)) {
return account.balance;
} else {
return IERC20(token).balanceOf(account);
}
}

/**
* @notice Validates that the contract has sufficient balance for a transfer
* @param token The token address (use NATIVE_TOKEN for ETH)
* @param amount The amount to validate
*/
function validateSufficientBalance(address token, uint256 amount) internal view {
if (isNativeToken(token)) {
require(address(this).balance >= amount, "Insufficient contract native balance");
} else {
require(IERC20(token).balanceOf(address(this)) >= amount, "Insufficient contract balance");
}
}
}
Loading