Escrow
The Escrow contract’s main role is to lock user funds intended for bridging, verify the proof of a transaction on the destination chain, and release funds once the transaction is proven.
Order Creation
The bridging process starts when a user creates an order to move their funds to another chain. The user can initiate the order by calling the createOrder
function, either through the easy-to-use interface on most.wtf or by interacting directly with the smart contract.
The createOrder
function takes in several parameters: _usrDstAddress
, which is the address that will receive the funds on the destination chain; _fee
, which incentivizes the market maker to facilitate the bridging, the _dstChainId which denotes where the user would like to receive their funds and the msg.value
, which is not passed as a parameter, represents the total amount being bridged. The amount that reaches the destination chain is the msg.value
minus the _fee
.
The function ensures that msg.value
is greater than zero to confirm that there are funds to bridge, and that msg.value
is greater than _fee
, so the fee doesn’t exceed the actual amount being transferred.
The function then hashes the order details, and saves them in two mappings: orders
and orderUpdates
. The orders
mapping stores all the hashed data that is present during the order creation phase, which remains unchanged throughout the bridging process and is used to verify the transaction on the destination chain. The orderUpdates
mapping, on the other hand, tracks the status of the order which is updated as the order progresses through it's checkpoints.
Generating Proof of Order Fulfillment
In order to withdraw the funds locked by the user, the order fulfillment must be proved. The process starts off with the proveHDPFulfillmentBatch
function. This function uses the onlyRelayAddress
modifier to ensure that only a trusted address can relay the necessary data back to the Escrow contract. In a multiple market maker (MMM) setup, this modifier could be removed, allowing any entity to submit proof to the contract. The function accepts three key parameters: the calldataOrders
, which are the orders being proven, the blockNumber
after which the order has been confirmed (this could be any block number after order fulfillment, as once the state has been changed, it will not be affected again), as well as the destinationChainId
. Thus is a batch function, however it retains the functionality of being able to prove a single order, if the MM chooses to do so. Our smart contracts are designed to be multichain, however each batch proven function call is only able to prove orders for one destination chain.
First, we create a taskInputs array, this is what we send to our custom HDP module to verify if the given orders have been fulfilled. The first three elements in this array are: the destination chain where the orders were fulfilled, the payment registry address on that chain (which holds the proof of fulfillment), and the block number at which we are certain those orders were already fulfilled.
Next, we hash all of the given order data and compare the calculated order hash with the stored order hash. This step is critical, as it ensures that the passed order data is valid, meaning an order was created with the exact same information. Once this check passes, we send the orders to our HDP module to verify their fulfillment status.
After committing our tasks to the HDP module, we wait for it to finalize before retrieving the proof that the orders have been fulfilled. Under the hood, the HDP module checks that the same order hashes we passed exist in the payment registry contract mapping. The only way an order hash gets into that mapping is through a transaction made via the payment registry using those exact order details, giving us cryptographic proof that the fulfillment occurred.
The security and accuracy of this system relies on a strict flow of order validation and proof generation. Here’s how each key point ensures the system remains robust:
Orders in the Escrow contract can only be created through the createOrder function. Similarly, an order can only be proven through the proveHDPFulfillmentBatch function, ensuring that no unauthorized or invalid orders can enter the system.
Any entry in the payment registry mapping must have been added via the transferTo function. This guarantees that an order recorded in the registry has gone through an actual fulfillment process and matches an on-chain transaction.
When order data is submitted to the proveHDPFulfillmentBatch function, the Escrow contract does not trust the input directly. Instead, it verifies that the submitted order hash matches an existing hash in the orders mapping. Since the order hash is generated from the full order details, this step ensures that the entire provided order data is correct.
Finally, if the same verified order hash stored in Escrow also exists in the PaymentRegistry mapping, that means the order was successfully fulfilled. At this point, we can confidently mark the order as “proved.”
This structured validation process ensures that only legitimate orders can be created, only fulfilled orders can be proven, and no false or manipulated order data can pass through the system.
Withdrawal Process
Once the order is proven, the Market Maker (MM) must call the withdrawProvedBatch
function to claim the funds. To fully withdraw funds, the MM needs to make two separate calls: one to prove the order and another to withdraw the funds. This separation is a deliberate safety feature.
While it is technically possible to have the withdrawProvedBatch
function be called directly by proving function, allowing the MM to prove and withdraw the funds in a single call, this approach presents risks. If an issue arises during the process—such as an out-of-gas error or a mid-call failure—the contract could be left in an inconsistent state, making recovery difficult. By splitting the calls, we minimize these risks, ensuring a safer and more reliable process for both proving and withdrawing funds.
The withdrawProvedBatch
function accepts only the calldataOrders
as a parameter. It first hashes the order details, and compares them against the stored order hash, in order to validate the order data passed. Then checks if the status of each order has been marked as PROVED
, thus enabling it for withdrawal.
The function then calculates the payout for the Market Maker (MM), including both the amount and the fee, and verifies that the contract has sufficient balance to make the payout—an extra safety check. Once confirmed, the order status is updated to COMPLETED
, the funds are transferred to the MM, and the bridging process is complete.
Refunding an order
If an order isn't fulfilled, the user can withdraw their locked assets after the order expires by calling the refundOrder
function. This function takes all of the order details that were set during order creation as parameters. By including the msg.sender, as a part of the function hash, the function ensures that only the individual that made the order originally, is able to call the refund function, otherwise the hashes will mismatch, and the function will revert. Once the hashes are matched, and the passed order data is verified, the function continues. It checks the order status and confirms that it has expired. Once verified, it marks the order as reclaimed, sends the locked funds and fee back to the user, and emits an OrderReclaimed
event, allowing the backend to update the database accordingly.
Additional Contract Features
The Escrow contract includes batching functions for more efficient order proving and withdrawing, these functions are a space efficiency instead of having two functions with the same logic for a single order call vs. a batch call, while the batch function has the capability of handling a single or multiple orders.
The contract features several smaller functions that enhance its functionality. It has two key modifiers: onlyOwner
and onlyRelayAddress
, along with setter functions that allow the owner to change the payment registry address and the allowed relay address.
The contract also implements a whenNotPaused
modifier from OpenZeppelin, which can pause certain functions, preventing them from being called when the contract is in a paused state. This pausing functionality is restricted to onlyRelayAddress
and is intended to be used by the Market Maker if something goes wrong on their end, serving as a safeguard to stop interactions with the contract. The refundOrder function is never pausable.
In addition, the contract incorporates OpenZeppelin’s ReentrancyGuard
for functions that are called externally. While this guard is almost redundant in the Single Market Maker (SMM) approach since the functions are also restricted to be only called from the allowed MM address, it provides significant added security in a Multiple Market Maker (MMM) setup, protecting against potential reentrancy attacks.
Last updated