Build a Limit Order contract
A Limit Order contract allows users to specify the exact price at which they are willing to buy or sell a particular asset. This contrasts with market orders, where trades are executed instantly at the current market price. Our example will focus on creating a Limit Order contract that enables users, like Eva, to place orders for trading Token C in exchange for Token D at her desired price point with limited price impact.
Getting Started
To begin development, run the following commands:
forge install
forge build
# Run test suite
forge test
The initial setup phase of the test involves deploying the necessary smart contracts for the operation:
- A Limit Order Contract, which facilitates the creation and execution of limit orders.
- A Mempool factory contract (
Laminator
) and a specific instance of LaminatedProxy (pusherLaminated
) to manage transactions. - Additionally, a smart contract for Alice’s Self-Checkout, acting as the swap protocol, is deployed.
Deployments
The deployment process for the Limit Order scenario involves several key steps to ensure the system is ready for operation:
-
Deploy the Limit Order Contract: This contract is the core of the system, enabling users to create and manage their limit orders for trading assets.
-
Deploy the Mempool Factory Contract (
Laminator
): TheLaminator
contract is responsible for managing the transactions in the mempool. It creates instances ofLaminatedProxy
to handle individual transactions. -
Deploy an Instance of LaminatedProxy (
pusherLaminated
): This specific instance ofLaminatedProxy
is used to manage the transactions related to the limit orders. It acts as a proxy for executing transactions in a secure and efficient manner. -
Deploy Alice’s Self-Checkout Contract: Although primarily used in the self-checkout tutorial, a similar contract is deployed here to act as the swap protocol. This contract facilitates the swapping of assets according to the specified limit orders.
Calls Pushed by the User
After the deployment, the user can initialize a swap by queuing up a call to the swapDAIForWETH
function. This function is part of the Alice's limit order contract and is responsible for executing the swap.
function userLand() public returns (uint256) {
pusherLaminated.transfer(1 ether);
CallObject[] memory pusherCallObjs = new CallObject[](2);
pusherCallObjs[0] = CallObject({
amount: 0,
addr: address(limitOrder),
gas: 1000000,
callvalue: abi.encodeWithSignature("swapDAIForWETH(uint256,uint160)", 100, 1)
});
// Including a tip for the callbreaker as an incentive.
pusherCallObjs[1] = CallObject({amount: _tipWei, addr: address(callbreaker), gas: 10000000, callvalue: ""});
// Pushing the swap operation to the proxy.
return laminator.pushToProxy(abi.encode(pusherCallObjs), 1);
}
The limit order contract itself uses the TimeTurner
to enforce slippage on the swap. The slippage is set to 1% in this example, but this can be changed based on the user's needs.
At the moment, the price is set to a static amount to demonstrate the slippage protection. In a real-world scenario, the price would be fetched from a MEV-time price oracle or similar source, and the TimeTurner would be similarly used to enforce price on the swap, similar to the tutorial using the self-checkout contract.
For now though, the swap calls are made but at the very end, the checkSlippage
function is called. This function is part of the limit order contract and is responsible for checking if the slippage is within the specified limits and will revert at MEV-time if it is not.
// use the timeturner to enforce slippage on a uniswap trade
// set slippage really high, let yourself slip, then use the timeturner to revert the trade if the price was above some number.
function swapDAIForWETH(uint256 _amountIn, uint256 slippagePercent) public {
uint256 amountIn = _amountIn * 1e18;
require(dai.transferFrom(msg.sender, address(this), amountIn), "transferFrom failed.");
_balanceOfDai += amountIn;
uint256 amountOut = (amountIn * _balanceOfWeth) / _balanceOfDai;
_balanceOfWeth -= amountOut;
require(weth.transfer(msg.sender, amountOut), "transferFrom failed.");
// check whether or not
CallObject[] memory callObjs = new CallObject[](1);
callObjs[0] = CallObject({
amount: 0,
addr: address(this),
gas: 10000000,
callvalue: abi.encodeWithSignature("checkSlippage(uint256)", slippagePercent)
});
assertFutureCallTo(callObjs[0]);
}
A solver will execute the call stack by calling pull
, which executes the calls that the user has pushed to the LaminatedProxy
. This is the same call that the user would make to execute the swap.
The solver finally calls the checkSlippage
function, which will revert the transaction if the slippage is above the specified limits at the time that the solver is committing to the swap. This call has to be made by the solver because the user's call to swapDAIForWETH
pushed a required call to checkSlippage
to the proxy, as specified earlier.
function solverLand(uint256 laminatorSequenceNumber, address filler, uint256 maxSlippage) public {
CallObject[] memory callObjs = new CallObject[](2);
ReturnObject[] memory returnObjs = new ReturnObject[](2);
callObjs[0] = CallObject({
amount: 0,
addr: pusherLaminated,
gas: 10000000,
callvalue: abi.encodeWithSignature("pull(uint256)", laminatorSequenceNumber)
});
ReturnObject[] memory returnObjsFromPull = new ReturnObject[](3);
returnObjsFromPull[0] = ReturnObject({returnvalue: abi.encode(true)});
returnObjsFromPull[1] = ReturnObject({returnvalue: ""});
returnObjsFromPull[2] = ReturnObject({returnvalue: ""});
returnObjs[0] = ReturnObject({returnvalue: abi.encode(abi.encode(returnObjsFromPull))});
callObjs[1] = CallObject({
amount: 0,
addr: address(daiWethPool),
gas: 10000000,
callvalue: abi.encodeWithSignature("checkSlippage(uint256)", maxSlippage)
});
returnObjs[1] = ReturnObject({returnvalue: ""});
AdditionalData[] memory associatedData = new AdditionalData[](3);
associatedData[0] =
AdditionalData({key: keccak256(abi.encodePacked("tipYourBartender")), value: abi.encodePacked(filler)});
associatedData[1] =
AdditionalData({key: keccak256(abi.encodePacked("pullIndex")), value: abi.encode(laminatorSequenceNumber)});
associatedData[2] = AdditionalData({key: keccak256(abi.encodePacked("hintdex")), value: abi.encode(2)});
AdditionalData[] memory hintdices = new AdditionalData[](2);
hintdices[0] = AdditionalData({key: keccak256(abi.encode(callObjs[0])), value: abi.encode(0)});
hintdices[1] = AdditionalData({key: keccak256(abi.encode(callObjs[1])), value: abi.encode(1)});
callbreaker.executeAndVerify(
abi.encode(callObjs), abi.encode(returnObjs), abi.encode(associatedData), abi.encode(hintdices)
);
}