πŸ’³
Metatransactions
Making DIMO transactions seamless for end users.
The DIMO Network covers user gas costs on the Polygon blockchain for many essential transactions. Metatransactions allow this to be done in a way that does not compromise user control. In short:
  1. 1.
    The user signs a command message with their wallet.
  2. 2.
    A relay server wraps this message in a normal transaction and submits it to the blockchain, covering any gas fees with a wallet the server controls.
  3. 3.
    The contract unwraps the user's command, verifies the signature, and the executes the command.
Let's consider an example from further along in the DIMO roadmap, and assume that one can transfer their DIMO vehicle to someone else:
contract VehicleToken {
// Other token functions.
function transfer(address to, uint256 vehicleTokenId) {
require(ownerOf(vehicleTokenId) == msg.sender);
_transfer(to, vehicleTokenId);
}
}
To convert this into a metatransaction, we must decide what command message the user is signing. In some implementations the user signs raw calldata, which is easy to interpret in Solidity but appears opaque to the user. EIP-712 offers a way to encode structured data that is displayed pleasantly in most software wallets. In JavaScript we might create our command message as
{
// Some schema-defining fields.
primaryType: "Transfer",
domain: {
name: "VehicleToken",
version: "1",
chainId: 137,
verifyingContract: "0x7c7379531b2aEE82e4Ca06D4175D13b9CBEafd49"
},
message: {
to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
tokenId: 3264,
nonce: 4
}
}
The domain fields serve to prevent a signed command from being replayed against a different deployment of the contract. The nonce field in the message prevents replay against the same deployment. One then asks the user to sign the command with eth_signTypedData_v4. This appears as
This will return a 65-byte signature. Unfortunately, the contract has to change a bit:
contract VehicleToken {
// Other functions.
function transferMeta(
address from,
address to,
uint256 tokenId,
bytes calldata signature
) external {
require(ownerOf(tokenId) == from);
nonces[from]++;
uint256 nonce = nonces[from];
bytes32 hash = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(
abi.encode(
TRANSFER_TYPEHASH,
to,
tokenId,
nonce
)
)
)
);
require(from == ECDSA.recover(hash, signature));
_transfer(to, tokenId);
}
}
The constants DOMAIN_SEPARATOR and TRANSFER_TYPEHASH are computed as specified in the standard. This is much more code and use of gas: we need to track nonces, compute a hash, and perform elliptic curve public key recovery.
Finally, we say a bit about the broader topic of automated transaction submission. DIMO, Inc. maintains worker containers that each control a hardware wallet for signing transactions and paying gas fees. Each metatransaction request is assigned to one of these workers, which submits the request to an blockchain node's JSON-RPC endpoint. The transaction enters the "Submitted" state below:
We make note of the block number at the time of submission and start listening for new blocks. For each new block, the worker checks to see if the transaction was mined; once that happens, we wait for 7 more confirmations before declaring success.
Of course, problems may arise:
  1. 1.
    The transaction never gets mined because of a gas price spike. After a few blocks of this the worker will resubmit the transaction with a new gas price.
  2. 2.
    The transaction gets mined but then kicked out of the canonical chain after a reorganization. Again, the strategy is to resubmit.
  3. 3.
    The transaction reverts because of a logical error or because we run out of gas. In this case we return an error to the user.
Care must also be taken to manage the nonces of the relay's wallets: skipping or reusing nonces will cause transaction submission to grind to a halt.
Copy link