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; 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 saves the order details in two mappings: orders and orderUpdates. The orders mapping stores all the 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 proveOrderFulfillment function. This function uses the onlyAllowedAddress 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 two key parameters: the orderId, which is being proven, and 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).

Before this step, the slots must be calculated off-chain and submitted to the Storage Proofs API. Once the API sends back a status: DONE to the webhook handler, you can then call this function to calculate the slots on-chain for verification. If the slots were correctly calculated off-chain and forwarded to the Storage Proofs API, the contract will access the same values on-chain. The function first verifies that the orderId is valid and stores an active order, and that the order is in the pending status currently. It then calculates the transfersMappingKey in the same way it is computed in the Payment Registry smart contract. Then using the transfersMappingSlot, which was pre-calculated based on the contract's storage layout before deployment, and the transfersMappingKey we can generate the slots that hold all the order information necessary to prove fulfillment on the destination chain. Once our slots are gathered, the order status is updated to PROVING in the orderUpdates mapping. The batch version of this process follows the same logic, marked by the batch keyword at the end of the function name.

The calculated slots contain the data that proves the fulfillment occurred. These slots are in bytes32 format, representing hex values. The function interacts with the Facts Registry smart contract deployed on the source chain, providing the Payment Registry’s address, the block number, and the slot from which to retrieve the value. After getting the slot values from the Facts Registry, the function uses convertBytes32toNative to turn the hex values back into usable data.

convertBytes32toNative converts the returned values from the Facts Registry, transforming the _orderId, _amount and _expirationTimestamp into uint256 values and the _dstAddress into the address type. The conversion of addresses is done using address(uint160(uint256())), which ensures the hex value is converted back into an proper address type, and the process moves forward.

Prove a Transaction and Withdrawal Process

The process continues with taking the native values related to the order and retrieving the original order details that were stored during order creation. The function then checks whether the user's destination address, the expiration timestamp and the amounts provided in the proof match the information stored in the contract. If the validation passes, it marks the order as PROVED and emits a ProveBridgeSuccess(_orderId).

If the proof fails—meaning the data provided does not match the contract’s stored information—the order is marked as PENDING, allowing it to be proved or fulfilled again.

Once the order is proven, the Market Maker (MM) must call the withdrawProved 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 withdrawProved function be called directly by proveOrderFulfillment, 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 withdrawProved function accepts only the _orderId as a parameter. It first retrieves the corresponding order details from the orders and orderUpdates mappings, ensuring that the order exists, is marked as PROVED, and that the msg.sender matches the allowedRelayAddress, which is a part of the SMM design architecture.

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 the _orderId as a parameter and ensures that the msg.sender matches the usrSrcAddress stored when the order was created. 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 processing and withdrawing. These functions, prefixed with batch, allow for proving and withdrawing multiple orders at once, functioning similarly to their single-order counterparts but optimized for handling multiple transactions in one go.

The contract features several smaller functions that enhance its functionality. It has two key modifiers: onlyOwner and onlyAllowedAddress, 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 onlyAllowedAddress 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.

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