Implement automated Orders
Overview
This guide explains how to implement basic automated Orders—smart contracts that monitor prices and execute token swaps on Uniswap based on simple price thresholds.
Automated Orders use the following Warden modules:
You'll take these steps:
- Implement the core logic of Orders in the
BasicOrder
contract: create the logic for price monitoring and trade execution - Implement the creation of Orders in
BasicOrderFactory
: implement Order deployment, register and track the Order - Deploy an Order: specify the Order input, including the price pair and threshold, and monitor the result
This Order type serves as a foundation for building more advanced Orders with price prediction. When implementing them, you'll add such features as mupltiple price sources (predicted and oracle), strict inequality comparisons (<
, >
), and a 24-hour execution time window. In addition to the Warden and Slinky precompiles, you'll also use Async.
Prerequisites
Before you start implementing automated Orders, take these steps:
1. Implement Orders
In our example, the core logic of Orders resides in the BasicOrder
contract.
This contract implements the logic for price monitoring and trade execution using the Slinky and Warden precompiles. Once the price threshold is met, the Order will construct a swap transaction, send it for signing, and record the transaction the registry.
To create this logic, add BasicOrder
to the src/orders
directory and take these steps:
-
Declare the following state variables:
- The Order and execution data structures from
Types
andTypesV0
- References to the Slinky precompile and the registry
- State tracking (
_executed
,_unsignedTx
, etc.)
- The Order and execution data structures from
-
Create a
constructor
handling the following tasks:- Validate all inputs
- Set up a connection with
AbstractOrder
(the transaction signing service) - Validate the threshold price
- Initialize the Slinky price feed
- Store the Order and execution data
-
In the
canExecute()
function, implement the logic for monitoring prices. This function should check if the price meets a given condition:>=
or<=
than the threshold. See thePriceCondition
enum inTypes
. -
In the
execute()
function, implement the logic for executing trades. This function should do the following:- Verify the caller and conditions
- Pack the swap data for Uniswap
- Create and encode a transaction
- Request a signature through the Warden precompile
- Emit the
Executed()
event - Register the transaction in the registry
- Return the execution status
-
To test price conditions, use the following code:
function test_priceConditions() public {
// Test the GTE condition
vm.mockCall(
address(SLINKY_PRECOMPILE),
abi.encodeWithSelector(ISlinky.getPrice.selector),
abi.encode(price)
);
assertTrue(order.canExecute());
} -
To test the execution flow, use this:
function test_execution() public {
vm.startPrank(scheduler);
(bool success, bytes32 txHash) = order.execute(
1, // nonce
200000, // gas
0, // unused
2 gwei, // maxPriorityFeePerGas
100 gwei // maxFeePerGas
);
assertTrue(success);
}
- Reentrancy protection: The execution function can't be re-entered before the previous execution finishes.
function execute(...) external nonReentrant { ... }
- Access control: If the caller is unauthorized, the execution will be reverted.
if (msg.sender != scheduler) revert Unauthorized();
- State management: If the Order has already been executed, the execution will be reverted.
if (_executed) revert ExecutedError();
2. Implement Order creation
In our example, the creation of Orders is implemented in the BasicOrderFactory
contract.
BasicOrderFactory
, when triggered by OrderFactory
, deploys Orders (instances of BasicOrder
) and registers them in the registry. Orders are deployed with the CREATE3
opcode to provide front-running protection, salt-based deployment security, and deterministic address computation.
To create this logic, add BasicOrderFactory
to the src/factories
directory and take the following steps:
-
Include a
createBasicOrder()
function implementing the deployment of Orders. It should do the following:- Create the deployment bytecode
- Deploy an Orders with an precomputed address using the
CREATE3
opcode - Verify the contract address
- Register and track the Order
- Emit the
BasicOrderCreated()
event
-
Include a
computeOrderAddress()
function for previewing the deterministic address of an Order without deploying it. -
To test the contract, you can use the following code:
contract BasicOrderFactoryTest is Test {
function test_CreateOrder() public {
// Test basic creation
bytes32 salt = bytes32("test");
address expected = factory.computeOrderAddress(
address(this),
salt
);
address actual = factory.createBasicOrder(
orderData,
executionData,
fees,
scheduler,
salt
);
assertEq(actual, expected);
assertTrue(Registry(registry).isRegistered(actual));
}
function test_PreventSaltReuse() public {
bytes32 salt = bytes32("test");
factory.createBasicOrder(...);
vm.expectRevert(SaltAlreadyUsed.selector);
factory.createBasicOrder(...);
}
}
- Salt management: Salts are guarded by
tx.origin
to prevent front-running. Each salt can only be used once per creator.address origin = tx.origin;
bytes32 guardedSalt = keccak256(
abi.encodePacked(uint256(uint160(origin)), salt)
if (usedSalts[guardedSalt]) {
revert SaltAlreadyUsed();
}
3. Deploy an Order
To deploy an Order, take these steps:
-
Create an
.env
file with your environment configuration:# Network settings
RPC_URL="http://127.0.0.1:8545"
CHAIN_ID="12345"
# Account settings
MNEMONIC="your mnemonic phrase here"
SCHEDULER_ADDRESS="0x6EA8AC1673402989E7B653AE4E83B54173719C30"
FACTORY_OWNER_ADDRESS="0x6EA8AC1673402989E7B653AE4E83B54173719C30" -
Install dependencies and compile all contracts:
yarn install
forge build -
Load the environment:
source .env
-
Deploy the infrastructure by using the main deployment script:
forge script script/Deploy.s.sol:Deploy \
--rpc-url $RPC_URL \
--broadcast \
--chain-id $CHAIN_IDThis will deploy the
OrderFactory
andRegistry
contracts. -
Set the Order parameters:
THRESHOLD_PRICE="3324181371" # Target price in oracle decimals
PRICE_CONDITION="0" # 0 for LTE, 1 for GTE
PRICE_PAIR='("ETH","USD")' # Oracle price pair -
Set the transaction parameters:
TX_FIELDS="\
(100000000000000,\ # The value (in wei)
11155111,\ # The chain ID
0x467b...1f,\ # The target contract
0x7ff3...)` # Encoded swap data -
Deploy an Order by using the script for creating Orders:
forge script script/CreateOrder.s.sol:CreateOrder \
--rpc-url $RPC_URL \
--broadcast \
--sig "basic(uint256,uint8,(string,string),(uint256,uint256,address,bytes))" \
$THRESHOLD_PRICE \
$PRICE_CONDITION \
$PRICE_PAIR \
$TX_FIELDS
Utility commands
Monitor the Order state
- Check if the Order is executable:
cast call $ORDER_ADDRESS "canExecute()"
- Check if the Order is executed:
cast call $ORDER_ADDRESS "isExecuted()"
- Get the execution data:
cast call $ORDER_ADDRESS "executionData()"
Monitor events
- Monitor the execution:
cast logs $ORDER_ADDRESS "Executed()"
- Monitor new transactions:
cast logs $REGISTRY_ADDRESS "NewTx(address,bytes32)"
Monitor prices
- Get prices from the [oracle]:
cast call $SLINKY_PRECOMPILE "getPrice(string,string)" "ETH" "USD"
Check the transaction
- Get the unsigned transaction:
cast call $ORDER_ADDRESS "getTx()"
Get data from the registry
- Get the order creator from the [registry]:
cast call $REGISTRY_ADDRESS "executions(address)" $ORDER_ADDRESS
- Get the transaction details from the [registry]:
cast call $REGISTRY_ADDRESS "transactions(bytes32)" $TX_HASH
Troubleshooting
Here are some of the common deployment issues and solutions for them:
- The Order creation fails
Solution: Check the salt usage and verify the registry status.cast call $FACTORY_ADDRESS "usedSalts(bytes32)" $SALT
cast call $REGISTRY_ADDRESS "isRegistered(address)" $ORDER_ADDRESS - The Order execution fails
Solution: Check the price feeds and verify the scheduler permissions.cast call $SLINKY_PRECOMPILE "getPrice(string,string)" "ETH" "USD"
cast call $ORDER_ADDRESS "scheduler()"
Next steps
Now you can implement more advanced Orders with price prediction.