Cross-chain transactions introduce additional UX friction around gas payments by requiring that gas be paid on multiple chains, with multiple currencies. It is important that we design an experience that abstracts this reality away from the user. It should "just work".
Single message design
To illustrate how we might do this, we start by describing a solution that allows a user to pay on the source chain for the processing of a single cross-chain message.
Process bounties
In short, the user pays for the gas to dispatch the message, and funds a processBounty
for that message. When the message is processed on the destination chain, the Replica
records who processed the message. The processor is then able to dispatch a message back to the source chain, redeeming the bounty.
We do this by building a ProcessBounty
xApp. On the source chain, users call ProcessBounty.post(msgHash, value)
, which places a bounty of value
on the message with hash msgHash
. On the destination chain, processors call ProcessBounty.claim(msgHash)
, which checks the Replica
to confirm that the message was processed, and dispatches a message back to the source chain ProcessBounty
instance. The processor processes that message to claim the bounty. Replicas
will need to store who processed a particular message, which can be done for additional cost over the present system by overloading the Replica.messages
mapping.
Multi message design
While most (all?) cross-chain transactions of today pass only a single message between chains, transactions of the future are likely to spawn multiple cross-chain messages. This significantly complicates the problem of paying for message processing only on the source chain, for two reasons:
- Putting a bounty on the original message hash is insufficient, as there may be many other messages associated with this transaction.
- The bounty will need to be divided fairly between multiple processors, to each according to their contribution to processing the messages associated with this transaction.
Message context
To solve (1), we introduce the concept of message context. An additional bytes32 context
storage variable is added to the Home
contract. Before a Replica
processes a message, it sets context
equal to the message hash of the message that is about to be processed. When the Replica
is done processing a message, it clears context
.
When a xApp sends a cross-chain message to the Home
contract, the current context
is automatically added to the message, much like origin
and sender
are. This means that all messages spawned as part of the same cross-chain transaction will contain a commitment to the history of messages within the same cross-chain transaction that proceeded them.
Storing message processing costs
To solve (2), when a message is processed, Replicas
store a mapping of msgHash => keccak(processor, gas*gasPrice, messageContext)
.
Putting it all together
When a user initiates a cross-chain transaction, they call ProcessBounty.placeBounty(sourceMsgHash, value)
.
The bounty is valid for [one day], during which processors call ProcessBounty.fileClaim(processedMessageHash, sourceMessageHash, sourceMessageContext, processorAddress, gasCost, proofOfContext)
on the destination chain for each message that they processed as part of the user's cross-chain transaction. The proofOfContext
consists of all message hashes between sourceMessageHash
and processedMessageHash
which can be used along with sourceMessageContext
to reconstruct processedMessageContext
to confirm that processedMessage
is indeed downstream of sourceMesage
. ProcessBounty
can then confirm that processedMessage
was processed by processorAddress
at a cost of gasCost
.
The ProcessBounty
router on the domain on which the message was processed then sends the message ProcessBounty.acceptClaim(sourceMessageHash, processorAddress, gasCost, gasCurrency)
on the source domain.
The processor processes the message on the source domain, and the ProcessBounty
contract stores a commitment to (sourceMessageHash, processorAddress, gasCost, gasCurrency)
.
After the deadline to file claims has expired, anyone can purchase the bounty by paying a multiplier of [110%] of the total gas costs incurred in each token. Each processor gets the portion of these funds associated with the gas costs they paid, allowing the bounty to be split fairly without the need for oracle prices.
For example, if the transaction incurred fees of 0.1 CELO, 0.05 SOL, and 0.2 ETH, paid by processors A, B, and C respectively, anyone could purchase the bounty in exchange for sending 0.11 CELO to A, 0.055 SOL to B, and 0.22 ETH to C.
Bounty purchases can be made more efficient by using wrapped tokens that exist on the same chain as the bounty.
Further improvements
In order to make sure their messages get processed, the user will need to set a bounty higher than the cost of processing their messages plus the cost of claiming the bounty. This excess value can be thought of as split between the following three parties:
- The user
- The processors
- The purchaser
The design described in the previous section does not return any of this excess value to the user. Instead, the excess value is split between:
- The processors, who receive [10%] more tokens than they spent on processing the user's messages. The processors are not reimbursed for the cost of filing claims, and thus the [10%] must be set to be high enough to cover this.
- The purchaser, who receives the excess value remaining after the processors have taken their cut.
Refunding excess value to the user
Rather than allowing the purchaser to purchase the entire bounty, you could make the bounty available to the purchaser grow over time, starting at [50%] of the bounty specified by the user. Any remaining bounty that was not purchased is refunded to the user at the time of purchase.