Skip to content

Use the Call Permit Precompile to Send Gasless Transactions

by Erin Shaben

Introduction

To interact with dApps on Moonbeam, users typically need to hold GLMR, Moonbeam's native token, in order to pay for transaction fees. This requirement creates an obstacle for dApps in terms of user experience, as a user needs to ensure they keep a balance of the native token to interact with the dApp.

One solution to this problem is gasless transactions, also known as meta transactions. Gasless transactions are a type of transaction that does not require the user to pay for the gas required to execute the transaction. The gas for these transactions can be covered by a third-party service or it can be deducted from the user's balance of a different token, depending on the implementation. For example, a user could simply sign a message that represents the transaction to be submitted to the network, and then a third-party could submit the transaction and pay the transaction fees for the user.

A regular transaction may have the following flow:

Flow of a transaction

Whereas a gasless transaction may look something like this:

Flow of a gasless transaction

Gasless transactions can be especially beneficial for users that make small transactions frequently, as is the case with gaming dApps like Damned Pirates Society (DPS). In DPS, users go on voyages in search of treasure and with the goal of growing their fleet. There are two in-game currencies that are used in DPS: Treasure Maps (TMAP) and Doubloons (DBL). TMAP are used to buy voyages, and DBL are used to maintain flagships and buy support ships and can be earned while on voyages. Currently, if a user wants to start a voyage, they'll need TMAP to buy the voyage and GLMR to pay for transaction fees. Wouldn't it be ideal to lower the barrier to entry by implementing gasless transactions so users wouldn't need to worry about keeping a GLMR balance on top of their TMAP and DBL balances? From a dApp's perspective, it would keep users on their platform, as their users wouldn't need to leave the dApp to fund their GLMR balance; they could keep on gaming.

Gasless transactions can be implemented using Moonbeam's Call Permit Precompile, which is a Solidity interface that allows a user to sign a permit, an EIP-712 signed message, that can then be dispatched by your dApp. The Call Permit Precompile can be used to execute any EVM call. The best part is that you don't need to modify your existing contracts!

In this tutorial, we'll walk through the process of implementing gasless transactions in a dApp. More specifically, we'll take a closer look at how we can implement gasless transactions to buy a voyage in DPS, as an example. We'll go over building an EIP-712 signed message, signing it, and dispatching it with the Call Permit Precompile.

What are EIP-712 Signed Messages?

An EIP-712 signed message is a message that is structured, hashed, and signed in a standardized way. The benefit of the EIP-712 standardization is that message data can be displayed in a much more human-readable way for users signing these messages, so they can better understand what exactly they're signing. Before this standardization existed, users had to sign off on unreadable and difficult-to-decode hexadecimal strings, which made it easy for users to misplace their trust and sign off on messages with malicious data.

The EIP-712 standard specifies how the message data should be structured by requiring developers to define a JSON structure of the message data that users will sign off on and specifying a domain separator. The main goal of the domain separator is to prevent replay attacks. We'll cover both of these requirements in the following sections.

Checking Prerequisites

For this tutorial, you'll need the following:

  • An account with funds. You can get DEV tokens for testing on Moonbase Alpha once every 24 hours from the Moonbase Alpha Faucet
  • A project with Ethers installed:

    npm i ethers
    

    To test out the examples in this guide on Moonbeam or Moonriver, you will need to have your own endpoint and API key, which you can get from one of the supported Endpoint Providers

Configure your Project

To get started, make sure you have a project with Ethers installed, as specified in the prerequisites. To configure Ethers for Moonbeam, you'll need to:

  1. Import ethers
  2. Define the network configurations
  3. Create an ethers provider
// 1. Import ethers
import { ethers } from 'ethers';

// 2. Define network configurations
const providerRPC = {
  moonbeam: {
    name: 'moonbeam',
    rpc: 'INSERT_RPC_API_ENDPOINT', // Insert your RPC URL here
    chainId: 1284, // 0x504 in hex,
  },
};
// 3. Create ethers provider
const provider = new ethers.JsonRpcProvider(providerRPC.moonbeam.rpc, {
  chainId: providerRPC.moonbeam.chainId,
  name: providerRPC.moonbeam.name,
});
// 1. Import ethers
import { ethers } from 'ethers';

// 2. Define network configurations
const providerRPC = {
  moonriver: {
    name: 'moonriver',
    rpc: 'INSERT_RPC_API_ENDPOINT', // Insert your RPC URL here
    chainId: 1285, // 0x505 in hex,
  },
};
// 3. Create ethers provider
const provider = new ethers.JsonRpcProvider(providerRPC.moonriver.rpc, {
  chainId: providerRPC.moonriver.chainId,
  name: providerRPC.moonriver.name,
});
// 1. Import ethers
import { ethers } from 'ethers';

// 2. Define network configurations
const providerRPC = {
  moonbase: {
    name: 'moonbase-alpha',
    rpc: 'https://rpc.api.moonbase.moonbeam.network',
    chainId: 1287, // 0x507 in hex,
  },
};
// 3. Create ethers provider
const provider = new ethers.JsonRpcProvider(providerRPC.moonbase.rpc, {
  chainId: providerRPC.moonbase.chainId,
  name: providerRPC.moonbase.name,
});
// 1. Import ethers
import { ethers } from 'ethers';

// 2. Define network configurations
const providerRPC = {
  dev: {
    name: 'moonbeam-development',
    rpc: 'http://127.0.0.1:9944',
    chainId: 1281, // 0x501 in hex,
  },
};
// 3. Create ethers provider
const provider = new ethers.JsonRpcProvider(providerRPC.dev.rpc, {
  chainId: providerRPC.dev.chainId,
  name: providerRPC.dev.name,
});

As previously mentioned, there are several ways to set up gasless transactions. For the purposes of this tutorial, we'll assume that there is a third-party account that pays the fees. As such, you'll need to have a signer for the user of the dApp, which is connected to your user's wallet, and a signer for the third-party account paying for the transaction fees. This tutorial assumes that you already have these signers in place, but if needed, you can set up the following generic signers for testing purposes:

const userSigner = new ethers.Wallet('INSERT_PRIVATE_KEY', provider);
const thirdPartyGasSigner = new ethers.Wallet('INSERT_PRIVATE_KEY', provider);

Remember

Never store your private keys in a JavaScript or TypeScript file.

Now that we've set up the initial configurations, let's dive into building the EIP-712 signed message.

Build an EIP-712 Typed Message

There are three components that we'll need to build an EIP-712 typed message: the domain separator, the typed data structure for the data that users will sign, and the actual message data.

The domain separator and the typed data structure will be based on the Call Permit Precompile. The steps to build both of these components will always be the same, regardless of the data that is being signed. The actual message data will change depending on your individual use case.

Define the Domain Separator

We'll first start off with the domain separator, which will define the Call Permit Precompile as the signing domain. Permits will get dispatched by calling the dispatch function of the Call Permit Precompile, which is why the Call Permit Precompile is always going to be the signing domain. As previously mentioned, the goal of the domain separator is to avoid replay attacks.

The domain separator is defined in the EIP-712 standard and is calculated as:

keccak256(PERMIT_DOMAIN, name, version, chain_id, address)

The parameters of the hash can be broken down as follows:

  • PERMIT_DOMAIN - is the keccak256 of EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
  • name - is the name of the signing domain and must be 'Call Permit Precompile' exactly
  • version - is the version of the signing domain. For this case version is set to 1
  • chainId - is the chain ID of the network
  • verifyingContract - is the address of the contract that will verify the signature. In this case, the Call Permit Precompile address

We're using Ethers in this example, which requires the domain separator to be in the format specified by the TypedDataDomain interface, but if desired, you could generate the domain separator as a bytes32 representation using the DOMAIN_SEPARATOR() function of the Call Permit Precompile.

The domain separator for each Moonbeam network is as follows:

const domain = {
  name: 'Call Permit Precompile',
  version: '1',
  chainId: 1284,
  verifyingContract: '0x000000000000000000000000000000000000080a',
};
const domain = {
  name: 'Call Permit Precompile',
  version: '1',
  chainId: 1285,
  verifyingContract: '0x000000000000000000000000000000000000080a',
};
const domain = {
  name: 'Call Permit Precompile',
  version: '1',
  chainId: 1287,
  verifyingContract: '0x000000000000000000000000000000000000080a',
};
const domain = {
  name: 'Call Permit Precompile',
  version: '1',
  chainId: 1281,
  verifyingContract: '0x000000000000000000000000000000000000080a',
};

Define the Typed Data Structure

Next, we'll need to define the typed data structure. The typed data structure defines the acceptable types of data that our users will be signing. We'll go into detail on the actual data in the following section.

If you take a look at the dispatch function of the Call Permit Precompile, you'll see that the data that we need to send, along with the associated types, is as follows:

function dispatch(
    address from,
    address to,
    uint256 value,
    bytes memory data,
    uint64 gaslimit,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) external returns (bytes memory output);

We'll need to add each of the above parameters to our typed data structure, with a couple of modifications. We don't need to include the signature-related parameters, but we do need to include a parameter for the nonce of the from account, which will be a uint256. The signature-related parameters aren't needed at this point because we're building the message data for the users to sign. We'll circle back to the signature-related parameters after we've finished building the message and requested the signature.

So, if we grab the rest of the parameters, we can start to build our data structure. Some implementations of EIP-712 require a type for EIP712Domain to be specified, but this is not the case when using Ethers as it computes it for you! For our implementation, the only type we'll need is the CallPermit type. The CallPermit type will be an array of objects that correspond to each of the parameters and define the name and type for each one:

const types = {
  CallPermit: [
    { name: 'from', type: 'address' },
    { name: 'to', type: 'address' },
    { name: 'value', type: 'uint256' },
    { name: 'data', type: 'bytes' },
    { name: 'gaslimit', type: 'uint64' },
    { name: 'nonce', type: 'uint256' },
    { name: 'deadline', type: 'uint256' },
  ],
};

Define the Message Data

Since we are going to implement gasless transactions for buying a voyage, we're going to be interacting with the Cartographer V1 contract, which is located at this address: 0xD1A9bA3e61Ac676f58B29EA0a09Cf5D7f4f35138 on Moonbeam.

So, let's start by going over the arguments required to build the message data:

  • from - your user's address, which you can easily get from your user's Ethers signer using signer.address
  • to - the contract address that you want to interact with. For this example, we'll use the address of DPS's Cartographer V1 contract
  • value - the value to be transferred from the from account. This will be 0 as TMAP are used to buy voyages, not GLMR
  • data - the calldata to be executed, which we'll calculate in the following steps
  • gaslimit- the gas limit the call requires
  • nonce - the nonce of the from account. This isn't your standard nonce, but the nonce for permits dispatched through the Call Permit Precompile specifically. To get this nonce, you can call the Call Permit Precompile's nonces function and pass in the address of the from account
  • deadline - the deadline in UNIX seconds after which the permit will expire and no longer be valid

The message will resemble the following:

const message = {
  from: userSigner.address,
  to: '0xD1A9bA3e61Ac676f58B29EA0a09Cf5D7f4f35138', // Cartographer V1 contract
  value: 0,
  data: 'TODO: Calculate the data that will buy a voyage',
  gaslimit: 'TODO: Estimate the gas',
  nonce: 'TODO: Use the Call Permit Precompile to get the nonce of the from account',
  deadline: '1714762357000', // Randomly created deadline in the future
};

Now, let's dig a little bit deeper and tackle the TODO items.

Get the Encoded Call Data for Buying a Voyage

We'll start off by calculating the data value. We can programmatically calculate the data value with Ethers by creating an interface of the Cartographer V1 contract and using the interface.encodeFunctionData function.

If you take a look at the DPSCartographer.sol contract's code, you'll see the buyVoyages function. The buyVoyages function accepts three parameters:

  • uint16 _voyageType - specifies the type of voyage to buy, i.e., easy, medium, hard, etc. This value corresponds to the index of the voyage in the VOYAGE_TYPE enum. For this example, we'll do an easy voyage, so we'll pass in 0 as the value
  • uint256 _amount - corresponds to the number of voyages to buy. We'll buy one voyage
  • DPSVoyageIV2 _voyage - represents the address of the DPSVoyageV2.sol contract, which is: 0x72A33394f0652e2Bf15d7901f3Cd46863d968424 on Moonbeam

To create an interface using Ethers, we'll need to get the ABI of the Cartographer V1 contract. You can retrieve it in full from Moonscan, or for simplicity, you can use the following snippet, which is the part of the ABI we need for this example:

const cartographerAbi = [
  {
    inputs: [
      { internalType: 'uint16', name: '_voyageType', type: 'uint16' },
      { internalType: 'uint256', name: '_amount', type: 'uint256' },
      {
        internalType: 'contract DPSVoyageIV2',
        name: '_voyage',
        type: 'address',
      },
    ],
    name: 'buyVoyages',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
];

Then we can create the interface using the ABI and get the encoded data using the values we specified for each of the parameters of the buyVoyages function:

const cartographerInterface = new ethers.Interface(cartographerAbi);
const data = cartographerInterface.encodeFunctionData('buyVoyages', [
  0n, // Voyage type: Easy
  1n, // Number of voyages to buy
  '0x72A33394f0652e2Bf15d7901f3Cd46863d968424', // Voyage V2 contract
]);

This will provide us with the following value for data:

'0xdb76d5b30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000072a33394f0652e2bf15d7901f3cd46863d968424'

Estimate the Gas Required to Buy a Voyage

Now that we have the encoded call data for buying a voyage, we can use it to estimate the gas required for the transaction. We'll use the estimateGas method and pass in the user's address, the address of the Cartographer V1 contract, and the encoded call data:

const gasEstimate = await provider.estimateGas({
  from: userSigner.address,
  to: '0xD1A9bA3e61Ac676f58B29EA0a09Cf5D7f4f35138', // Cartographer V1 contraact
  data,
})

Note

For this example, you'll need to have a balance of at least 1 TMAP to be able to estimate the gas. Otherwise, you'll get a 'VM Exception while processing transaction: revert' error.

We'll add a little bit of a buffer to the gasEstimate value and set it as the gaslimit:

const message = {
  ...
  gaslimit: gasEstimate + 50000n,
  ...
}

We'll get the nonce in the next section, and then put all of the arguments together, and the message data will be complete.

Get the Signer's Nonce Using the Call Permit Precompile

Lastly, we'll need to get the nonce of the from account. As previously mentioned, we can use the nonces function of the Call Permit Precompile to get this value. To do so, you'll need to create a contract instance for the Call Permit Precompile:

  1. Create a new file in your project that contains the ABI of the Call Permit Precompile. You can find the ABI on GitHub
  2. Import the ABI into your Ethers file
  3. Create an instance of the Call Permit Precompile using the precompile's address and the ABI of the precompile. You can use either a provider or a signer. Since we are dispatching the permit later on in this tutorial, we'll use the signer associated with the third-party account for transaction fees, but if you only needed to access the nonces function, you could use a provider instead
  4. Call the nonces function and pass in the signer.account of the user, which is the same as the from account
...
import abi from './callPermitABI.js'

...

const callPermit = new ethers.Contract(
  '0x000000000000000000000000000000000000080a', 
  abi, 
  thirdPartyGasSigner,
);

const nonce = await callPermit.nonces(userSigner.address);
View the script so far
import { ethers } from 'ethers';
import abi from './callPermitABI.js'
import cartographerAbi from './cartographerAbi.js'

const providerRPC = {
  moonbeam: {
    name: 'moonbeam',
    rpc: 'INSERT_RPC_API_ENDPOINT', // Insert your RPC URL here
    chainId: 1284, // 0x504 in hex,
  },
};
const provider = new ethers.JsonRpcProvider(providerRPC.moonbeam.rpc, {
  chainId: providerRPC.moonbeam.chainId,
  name: providerRPC.moonbeam.name,
});

// Insert your own signer logic or use the following for testing purposes
const userSigner = new ethers.Wallet('INSERT_PRIVATE_KEY', provider);
const thirdPartyGasSigner = new ethers.Wallet('INSERT_PRIVATE_KEY', provider);

const domain = {
  name: 'Call Permit Precompile',
  version: '1',
  chainId: 1284,
  verifyingContract: '0x000000000000000000000000000000000000080a',
};

const types = {
  CallPermit: [
    { name: 'from', type: 'address' },
    { name: 'to', type: 'address' },
    { name: 'value', type: 'uint256' },
    { name: 'data', type: 'bytes' },
    { name: 'gaslimit', type: 'uint64' },
    { name: 'nonce', type: 'uint256' },
    { name: 'deadline', type: 'uint256' },
  ],
};

const cartographerInterface = new ethers.Interface(cartographerAbi);
const data = cartographerInterface.encodeFunctionData('buyVoyages', [
  0n, // Voyage type: Easy
  1n, // Number of voyages to buy
  '0x72A33394f0652e2Bf15d7901f3Cd46863d968424', // Voyage V2 contract
]);

const gasEstimate = await provider.estimateGas({
  from: userSigner.address,
  to: '0xD1A9bA3e61Ac676f58B29EA0a09Cf5D7f4f35138', // Cartographer V1 contraact
  data,
})

const callPermit = new ethers.Contract(
  '0x000000000000000000000000000000000000080a', 
  abi, 
  thirdPartyGasSigner,
);

const nonce = await callPermit.nonces(userSigner.address);

const message = {
  from: userSigner.address,
  to: '0xD1A9bA3e61Ac676f58B29EA0a09Cf5D7f4f35138', // Cartographer V1 contract
  value: 0,
  data,
  gaslimit: gasEstimate + 50000n,
  nonce,
  deadline: '1714762357000', // Randomly created deadline in the future
};

Remember

Never store your private keys in a JavaScript or TypeScript file.

So far, we've created the domain separator, defined the data structure of our EIP-712 message, and assembled the data for the message. Next, we'll need to request the signature for our EIP-712 typed message!

Get Signature for EIP-712 Typed Messages

For this next step, we're going to use our Ethers signer and the signer.signTypedData function to prompt our users to sign the EIP-712 typed message we've assembled. This signature will allow the third-party account for transaction fees to call the dispatch function of the Call Permit Precompile. The third-party account will pay the transaction fees for us, and a voyage will be bought on our behalf!

The signTypedData function will calculate a signature for our data using the following calculation:

sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message)))

The components of the hash can be broken down as follows:

  • \x19 - makes the encoding deterministic
  • \x01 - the version byte, which makes the hash compliant with EIP-191
  • domainSeparator - the 32-byte domain seperator, which was previously covered and can be easily retrieved using the DOMAIN_SEPARATOR function of the Call Permit Precompile
  • hashStruct(message) - the 32-byte data to sign, which is based on the typed data structure and the actual data. For more information, please refer to the EIP-712 specification

Now that we have an understanding of what the signTypedData function does, we can go ahead and pass in the data we've assembled in the previous sections:

const signature = await signer.signTypedData(
  domain, // The domain separator
  types, // The typed data structure
  message, // The message data
);
console.log(`Signature hash: ${signature}`);

A hash of the signature will print to the terminal. We'll use the user's signature to dispatch the permit from the third-party account using the Call Permit Precompile's dispatch function in the next section.

Dispatch a Signed EIP-712 Message

Before an EIP-712 signed message can be dispatched, we'll need to get the signature-related parameters, v, r, and s, from the signed message. The signTypedData function returned a hex string that contains each of these values, but to easily get these values individually, we're going to use Ethers' Signature.from function. This will create a new instance of Ether's Signature class, which will allow us to easily grab the v, r, and s values that we need in order to use the dispatch function.

const formattedSignature = ethers.Signature.from(signature);

Now that we can individually access the v, r, and s arguments needed to dispatch the permit, we can call the dispatch function of the Call Permit Precompile. The arguments passed to the dispatch function must be the exact same arguments that were passed in for the value parameter of the signTypedData function. You'll send the following function using an account associated with your dApp as the signer (not the signer associated with the user), and it will dispatch the permit that the user signed:

const dispatch = await callPermit.dispatch(
  message.from,
  message.to,
  message.value,
  message.data,
  message.gaslimit,
  message.deadline,
  formattedSignature.v,
  formattedSignature.r,
  formattedSignature.s,
);

await dispatch.wait();
console.log(`Transaction hash: ${dispatch.hash}`);
View the complete script
import { ethers } from 'ethers';
import abi from './callPermitABI.js';
import cartographerAbi from './cartographerAbi.js';

const providerRPC = {
  moonbeam: {
    name: 'moonbeam',
    rpc: 'INSERT_RPC_API_ENDPOINT', // Insert your RPC URL here
    chainId: 1284, // 0x504 in hex,
  },
};
const provider = new ethers.JsonRpcProvider(providerRPC.moonbeam.rpc, {
  chainId: providerRPC.moonbeam.chainId,
  name: providerRPC.moonbeam.name,
});

// Insert your own signer logic or use the following for testing purposes.
// For demo purposes only. Never store your private keys in a JavaScript file
const userSigner = new ethers.Wallet('INSERT_PRIVATE_KEY', provider);
const thirdPartyGasSigner = new ethers.Wallet('INSERT_PRIVATE_KEY', provider);

const domain = {
  name: 'Call Permit Precompile',
  version: '1',
  chainId: 1284,
  verifyingContract: '0x000000000000000000000000000000000000080a',
};

const types = {
  CallPermit: [
    { name: 'from', type: 'address' },
    { name: 'to', type: 'address' },
    { name: 'value', type: 'uint256' },
    { name: 'data', type: 'bytes' },
    { name: 'gaslimit', type: 'uint64' },
    { name: 'nonce', type: 'uint256' },
    { name: 'deadline', type: 'uint256' },
  ],
};

const cartographerInterface = new ethers.Interface(cartographerAbi);
const data = cartographerInterface.encodeFunctionData('buyVoyages', [
  0n, // Voyage type: Easy
  1n, // Number of voyages to buy
  '0x72A33394f0652e2Bf15d7901f3Cd46863d968424', // Voyage V2 contract
]);

const gasEstimate = await provider.estimateGas({
  from: userSigner.address,
  to: '0xD1A9bA3e61Ac676f58B29EA0a09Cf5D7f4f35138', // Cartographer V1 contract
  data,
});

const callPermit = new ethers.Contract(
  '0x000000000000000000000000000000000000080a', // Call Permit contract
  abi,
  thirdPartyGasSigner
);

const nonce = await callPermit.nonces(userSigner.address);

const message = {
  from: userSigner.address,
  to: '0xD1A9bA3e61Ac676f58B29EA0a09Cf5D7f4f35138', // Cartographer V1 contract
  value: 0,
  data,
  gaslimit: gasEstimate + 50000n,
  nonce,
  deadline: '1714762357000', // Randomly created deadline in the future
};

const signature = await userSigner.signTypedData(domain, types, message);
console.log(`Signature hash: ${signature}`);

const formattedSignature = ethers.Signature.from(signature);

// This gets dispatched using the dApps signer
const dispatch = await callPermit.dispatch(
  message.from,
  message.to,
  message.value,
  message.data,
  message.gaslimit,
  message.deadline,
  formattedSignature.v,
  formattedSignature.r,
  formattedSignature.s
);

await dispatch.wait();
console.log(`Transaction hash: ${dispatch.hash}`);

Remember

Never store your private keys in a JavaScript or TypeScript file.

Once the transaction goes through, the gas fees will be deducted from the GLMR balance of the third-party account, 1 TMAP will be deducted from the user's balance, and a voyage will be purchased on behalf of the user. As you can see, the user doesn't need to worry about having a GLMR balance!

You can view the transaction for the example that we covered in this guide on Moonscan. You'll notice the following:

  • The from account is the third-party account: 0xd0ccb8d33530456f1d37e91a6ef5503b5dcd2ebc
  • The contract interacted with is the Call Permit Precompile: 0x000000000000000000000000000000000000080a
  • A TMAP has been deducted from the user's account: 0xa165c7970886d4064b6cec9ab1db9d03202bda37
  • A voyage with ID 622646 has been sent to the user's account

Review the transaction details

And that's it! Congrats! You've learned how to implement gasless transactions using the Call Permit Precompile on Moonbeam. You can now adapt the logic in this tutorial for your own dApp!

This tutorial is for educational purposes only. As such, any contracts or code created in this tutorial should not be used in production.
The information presented herein has been provided by third parties and is made available solely for general information purposes. Moonbeam does not endorse any project listed and described on the Moonbeam Doc Website (https://docs.moonbeam.network/). Moonbeam Foundation does not warrant the accuracy, completeness or usefulness of this information. Any reliance you place on such information is strictly at your own risk. Moonbeam Foundation disclaims all liability and responsibility arising from any reliance placed on this information by you or by anyone who may be informed of any of its contents. All statements and/or opinions expressed in these materials are solely the responsibility of the person or entity providing those materials and do not necessarily represent the opinion of Moonbeam Foundation. The information should not be construed as professional or financial advice of any kind. Advice from a suitably qualified professional should always be sought in relation to any particular matter or circumstance. The information herein may link to or integrate with other websites operated or content provided by third parties, and such other websites may link to this website. Moonbeam Foundation has no control over any such other websites or their content and will have no liability arising out of or related to such websites or their content. The existence of any such link does not constitute an endorsement of such websites, the content of the websites, or the operators of the websites. These links are being provided to you only as a convenience and you release and hold Moonbeam Foundation harmless from any and all liability arising from your use of this information or the information provided by any third-party website or service.
Last update: January 25, 2024
| Created: April 5, 2023