Implement the creation of Orders
Overview
This article will guide you through creating the AdvancedOrderFactory contract. AdvancedOrderFactory, when triggered by OrderFactory, deploys Orders (instances of AdvancedOrder) and registers them in the registry.
This factory pattern supports deterministic address computation, front-running protection, and salt-based deployment security. Note that it extends the basic automated Orders creation.
You'll implement the following core components:
contract AdvancedOrderFactory is ReentrancyGuard {
Registry public immutable REGISTRY;
mapping(bytes32 salt => bool used) public usedSalts;
event AdvancedOrderCreated(
address indexed creator,
address orderAddress
);
event SaltUsed(
bytes32 indexed salt,
address indexed creator
);
}
Store AdvancedOrderFactory in the /src directory, alongside with other contracts.
You can find the full code on GitHub: /src/AdvancedOrderFactory.sol
1. Implement the Order creation logic
Implement the core function for deploying new Orders:
function createAdvancedOrder(
Types.AdvancedOrderData calldata orderData,
Types.CommonExecutionData calldata executionData,
CommonTypes.Coin[] calldata maxKeychainFees,
address scheduler,
bytes32 salt
) external nonReentrant returns (address orderAddress) {
// Front-running protection using tx.origin
address origin = tx.origin;
bytes32 guardedSalt = keccak256(
abi.encodePacked(uint256(uint160(origin)), salt)
);
if (usedSalts[guardedSalt]) {
revert SaltAlreadyUsed();
}
emit SaltUsed(guardedSalt, origin);
// Encode contract creation with prediction setup
bytes memory bytecode = abi.encodePacked(
type(AdvancedOrder).creationCode,
abi.encode(
orderData,
executionData,
maxKeychainFees,
scheduler,
address(REGISTRY)
)
);
// Deploy with the CREATE3 opcode
orderAddress = Create3.create3(guardedSalt, bytecode);
address expectedAddress = Create3.addressOf(guardedSalt);
if (orderAddress == address(0) ||
orderAddress != expectedAddress) {
revert OrderDeploymentFailed(guardedSalt);
}
// Register and track the Order
REGISTRY.register(orderAddress);
usedSalts[guardedSalt] = true;
emit AdvancedOrderCreated(msg.sender, orderAddress);
}
2. Implement data validation
Create functions for validating the Order data:
function _validateOraclePair(
Types.PricePair calldata pair
) internal pure returns (bool) {
return bytes(pair.base).length > 0 &&
bytes(pair.quote).length > 0;
}
function _validatePredictionPair(
Types.PricePair calldata pair
) internal pure returns (bool) {
return bytes(pair.base).length > 0 &&
bytes(pair.quote).length > 0;
}
function _validateAdvancedOrderData(
Types.AdvancedOrderData calldata data
) internal pure {
if (!_validateOraclePair(data.oraclePricePair)) {
revert InvalidOraclePair();
}
if (!_validatePredictionPair(data.predictPricePair)) {
revert InvalidPredictionPair();
}
if (data.priceCondition > Types.PriceCondition.GT) {
revert InvalidPriceCondition();
}
}
3. Add address computation
function computeOrderAddress(
address origin,
bytes32 salt
) external view returns (address) {
bytes32 guardedSalt = keccak256(
abi.encodePacked(uint256(uint160(origin)), salt)
);
return Create3.addressOf(guardedSalt);
}
4. Implement tests
Finally, implement tests:
contract AdvancedOrderFactoryTest is Test {
function test_CreateAdvancedOrder() public {
Types.AdvancedOrderData memory orderData = Types.AdvancedOrderData({
oraclePricePair: Types.PricePair({
base: "ETH",
quote: "USD"
}),
predictPricePair: Types.PricePair({
base: "ethereum",
quote: "tether"
}),
priceCondition: Types.PriceCondition.GT
});
bytes32 salt = bytes32("test");
address expected = factory.computeOrderAddress(
address(this),
salt
);
vm.expectEmit(true, true, false, false);
emit AdvancedOrderCreated(address(this), expected);
address actual = factory.createAdvancedOrder(
orderData,
executionData,
maxKeychainFees,
scheduler,
salt
);
assertEq(actual, expected);
assertTrue(registry.isRegistered(actual));
// Verify prediction setup
AdvancedOrder order = AdvancedOrder(actual);
assertTrue(order.futureId() > 0);
}
function test_InvalidPricePairs() public {
Types.AdvancedOrderData memory orderData = Types.AdvancedOrderData({
oraclePricePair: Types.PricePair({
base: "",
quote: "USD"
}),
predictPricePair: Types.PricePair({
base: "ethereum",
quote: "tether"
}),
priceCondition: Types.PriceCondition.GT
});
vm.expectRevert(InvalidOraclePair.selector);
factory.createAdvancedOrder(
orderData,
executionData,
maxKeychainFees,
scheduler,
bytes32("test")
);
}
function test_SaltReuse() public {
bytes32 salt = bytes32("test");
factory.createAdvancedOrder(...);
vm.expectRevert(SaltAlreadyUsed.selector);
factory.createAdvancedOrder(...);
}
}
Security measures
In the previous steps, you've implemented the following security measures:
- Price data validation
The contract will check if oracle and predictions price pairs are properly formatted and asset symbols match the expected formats.function _validateOraclePair(...)
function _validatePredictionPair(...)
function _validateAdvancedOrderData(...) - Salt management
Salts are guarded bytx.originto 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();
}
Next steps
After creating the AdvanceOrderFactory contract, you can deploy an Order.