Skip to main content

Setting royalties


Single Royalty Recipients: Immutable's Preset Contracts

Immutable's preset ERC721 (NFT) and ERC1155 (SFT) contract is designed to allocate royalty payments to a single recipient for each collection. Consequently, all assets minted under this collection will direct their royalty proceeds to the designated payee account at the specified percentage of the transaction price.

To configure the royalties payee address and percentage for your collection, refer to the royalty setup instructions in the deploy preset contract section of our "Create in-game assets" guide.

View a collection's royalty information

To view the royalty fee instructions on an existing collection follow these steps. Price has to be specified in the request as royalty may change based on price.

  1. Go to the root directory of your project
  2. Create a scripts directory
  3. Create get-royalties.ts file
  4. Insert the following code with the below variables:
  • CONTRACT_ADDRESS (The address of the collection)
  • TOKEN_ID (The ID of the NFT/SFT)
  • SALE_PRICE (The proposed sales price for the NFT/SFT)
  • COLLECTION_SYMBOL (The symbol of your collection - typically 2-3 characters long)
get-royalties.ts
import { getContract, http, createWalletClient, defineChain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { ImmutableERC721Abi } from '@imtbl/contracts';

const PRIVATE_KEY = 'YOUR_PRIVATE_KEY'; // should be read from environment variable
const CONTRACT_ADDRESS = '0xYOUR_CONTRACT_ADDRESS'; // should be of type `0x${string}`
const TOKEN_ID = BigInt(1);
const SALE_PRICE = BigInt(1);

const immutableTestnet = defineChain({
id: 13473,
name: 'imtbl-zkevm-testnet',
nativeCurrency: { name: 'IMX', symbol: 'IMX', decimals: 18 },
rpcUrls: {
default: {
http: ['https://rpc.testnet.immutable.com'],
},
},
});

const walletClient = createWalletClient({
chain: immutableTestnet,
transport: http(),
account: privateKeyToAccount(`0x${PRIVATE_KEY}`),
});

const contract = getContract({
address: CONTRACT_ADDRESS,
abi: ImmutableERC721Abi,
client: walletClient,
});

const getRoyalty = async (): Promise<[string, BigInt]> => {
const [receiver, royaltyAmount] = await contract.read.royaltyInfo([
TOKEN_ID,
SALE_PRICE,
]);

return [receiver, royaltyAmount];
};

getRoyalty();
  1. Run the script:
./node_modules/.bin/ts-node get-royalties.ts

Multiple Royalty Recipients: Immutable's Preset Fee Splitting Contract

Immutable's fee splitting contract is designed to manage the collection and distribution of ERC20 tokens for royalty fees in Web3 gaming. While Immutable's collections are typically set up to support a single royalty recipient, the fee splitting contract allows for more complex arrangements by enabling multiple recipients.

This contract is particularly beneficial for games with intricate royalty structures. It allows each collection to set a default royalty address, often the fee splitting contract's address, so that all assets minted from these collections automatically direct their royalties there.

Customizing Royalty Distribution

For collections requiring varied royalty distributions among individual assets, the royalty address for each asset can be customized after minting. This ensures that royalty payments are allocated precisely as needed for each specific asset within a collection.

The diagram below illustrates scenarios where multiple fee splitting contracts might be necessary:

  • Different Recipients: A unique fee splitter contract is required for each distinct combination of royalty recipients. For example, if NFT X allocates royalties to wallets A and B, and NFT Y allocates royalties to wallets A and C, two different fee splitter contracts are needed.
  • Varied Distribution Percentages: A unique fee splitter contract is needed for different percentage allocations to the same recipients. For instance, if one asset requires a 50/50 royalty split and another a 70/30 split between the same recipients' wallets, two different fee splitter contracts are necessary.
Fee Splitting Contract Splitting Fees

Using Immutable's Preset Fee Splitter Contract for Royalties

Creating a Fee Splitting Contract

Immutable's preset fee splitting contract must be updated by the content creator (i.e. game studio) to specify the required fee allocation for the collection.

Each recipient's wallet can be specified in the contract as well as the % allocation that recipient is expected to receive.

See the below example:

forge create --rpc-url <RPC_URL> \
--private-key <PRIVATE_KEY> \
contracts/payment-splitter/PaymentSplitter.sol:PaymentSplitter \
--constructor-args <ADMIN_ADDRESS> \
--constructor-args <REGISTRAR_ADDRESS> \
--constructor-args <FUNDS_ADMIN_ADDRESS> \
--gas-price 20000000000 --priority-gas-price 20000000000

Setting a Fee Splitter Contract as the Default Royalty Recipient for a Collection

The content creator (i.e., game studio) must update Immutable's preset fee splitting contract to specify the required fee allocation for the collection. Each recipient's wallet and the percentage allocation expected for each recipient can be specified in the contract.

Setting a Fee Splitter Contract as the Default Royalty Recipient

When deploying a collection using Immutable's preset contracts, a royalty recipient can be assigned from the outset. By designating the address of a pre-deployed fee splitter contract as the recipient for all royalty fees, the collected royalties will be divided among the recipients according to the contract's configuration, with funds released via the releaseAll() function.

Deploying a Collection via Immutable Hub (Default Royalty Splitting Allocation)

If deploying your collection with Immutable's hub, please refer to the tutorial for a step-by-step guide. Enter the contract address of your royalty fee splitting contract, as well as the percentage of the transaction price allocated for all royalties (e.g. , if a collection is set to allocate a 5% royalty fee for the sale of each asset, this 5% will be directed to the fee splitter contract. When prompted the contract will distribute this 5% among the specified recipients based on the pre-defined percentage splits (e.g., 50/50, 70/30) set within the fee splitter contract).

Adding fee splitter address to hub deployed contract
Customizing Royalty Allocations for Specific Assets

For assets within a collection that require distinct royalty allocations differing from the default setting, game studios can specify these variations post-minting. The process involves:

  1. Deploying a New Royalty Splitting Contract: Deploy a separate royalty splitting contract tailored to the unique royalty requirements of the group of assets. Record the contract address once deployed.

  2. Assigning Assets to the New Royalty Splitting Contract: Develop a script to assign the new royalty structure dictated by the new fee splitter contract. This script will reassign the royalty fee allocation for already minted NFTs/SFTs, replacing the default collection royalty fee splitter contract address with the new contract address.

An example of such a script is below:

import { getContract, http, createWalletClient, defineChain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { ImmutableERC721Abi } from '@imtbl/contracts';

const PRIVATE_KEY = 'YOUR_PRIVATE_KEY'; // should be read from environment variable
const CONTRACT_ADDRESS = '0xYOUR_CONTRACT_ADDRESS'; // should be of type `0x${string}`
const TOKEN_IDS = [BigInt(1), BigInt(2)]; // should be of type `BigInt`
const ROYALTY_RECEIVER = '0xPAYMENT_SPLITTER_ADDRESS'; // should be of type `0x${string}`
const FEE_NUMERATOR = BigInt(100); // should be of type `BigInt`

const immutableTestnet = defineChain({
id: 13473,
name: 'imtbl-zkevm-testnet',
nativeCurrency: { name: 'IMX', symbol: 'IMX', decimals: 18 },
rpcUrls: {
default: {
http: ['https://rpc.testnet.immutable.com'],
},
},
});

const walletClient = createWalletClient({
chain: immutableTestnet,
transport: http(),
account: privateKeyToAccount(`0x${PRIVATE_KEY}`),
});

const contract = getContract({
address: CONTRACT_ADDRESS,
abi: ImmutableERC721Abi,
client: walletClient,
});

const setNFTRoyaltyReceiverBatch = async (): Promise<void> => {
await contract.write.setNFTRoyaltyReceiverBatch([TOKEN_IDS, ROYALTY_RECEIVER, FEE_NUMERATOR]);
};

setNFTRoyaltyReceiverBatch();

By following these steps, game studios can effectively manage and tailor royalty distributions for individual assets within the same collection.

Releasing Collected Fees to Recipients

To distribute the ERC20 tokens accumulated in a fee splitter smart contract, call the releaseAll() function. The native token (IMX for zkEVM) is automatically released with this function and does not need explicit mention.

This distribution can be scheduled through a periodic script or initiated by a specific user. Importantly, executing the releaseAll() function only affects the release of funds and does not alter the pre-established allocation. Therefore, broad access to this function does not pose a security risk, as the caller cannot modify the recipients or their shares.

note

The party that triggers the releaseAll() function is responsible for covering the gas fees associated with the transaction. This means if an individual content creator initiates this function, they will incur the gas costs not just for their own royalty distribution but also for all other recipients included in the transaction.

Granting Access to the releaseAll() Function

The grantReleaseFundsRole() function grants access to the releaseAll() function. This access can be given to all royalty recipients or only to an admin user, depending on the game studio's requirements.

This can be given to all royalty recipients, or only an admin user, depending on the game studio's requirements.

Below is an example of granting access using the grantReleaseFundsRole() function.

import { getContract, http, createWalletClient, defineChain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { PaymentSplitterAbi } from '@imtbl/contracts';

const PRIVATE_KEY = 'YOUR_PRIVATE_KEY'; // should be read from environment variable
const CONTRACT_ADDRESS = '0xYOUR_CONTRACT_ADDRESS'; // should be of type `0x${string}`
const RELEASE_FUNDS_ROLE_ADDRESS = '0xRELEASE_FUNDS_ROLE_ADDRESS' // should be of type `0x${string}`

const immutableTestnet = defineChain({
id: 13473,
name: 'imtbl-zkevm-testnet',
nativeCurrency: { name: 'IMX', symbol: 'IMX', decimals: 18 },
rpcUrls: {
default: {
http: ['https://rpc.testnet.immutable.com'],
},
},
});

const walletClient = createWalletClient({
chain: immutableTestnet,
transport: http(),
account: privateKeyToAccount(`0x${PRIVATE_KEY}`),
});

const contract = getContract({
address: CONTRACT_ADDRESS,
abi: PaymentSplitterAbi,
client: walletClient,
});

const grantReleaseFundsRole = async (): Promise<void> => {
await contract.write.grantReleaseFundsRole([RELEASE_FUNDS_ROLE_ADDRESS]);
};

grantReleaseFundsRole();

Changing the Fee Allocation of a Previously Deployed Fee Splitter Contract

Over time, the royalty distribution associated with an asset may need modification. This can be achieved through the following methods:

  • Amending the Collection's Default Royalty Recipient: Deploy a new fee splitter contract with updated allocations and change the collection's default royalty configuration to be directed to this new contract. Assets with a non-default fee splitting configuration will remain unaffected by this change and must be individually redirected to the new contract.
  • Amending a Fee Splitter Contract: The admin user can modify an existing fee splitter contract allocation by updating the number of recipients or adjusting the allocation percentages for each recipient.

Admin users should exclusively handle these changes, as opposed to the more accessible releaseAll() function.

Amending the Collection's Default Royalty Recipient

Below is an example script to change the default royalty recipient of a collection to a new fee splitting contract. Alternatively, an individual wallet can be used if the collection no longer requires multiple royalty recipients.

import { getContract, http, createWalletClient, defineChain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { ImmutableERC721Abi } from '@imtbl/contracts';

const PRIVATE_KEY = 'YOUR_PRIVATE_KEY'; // should be read from environment variable
const CONTRACT_ADDRESS = '0xYOUR_CONTRACT_ADDRESS'; // should be of type `0x${string}`
const ROYALTY_RECEIVER = '0xPAYMENT_SPLITTER_ADDRESS'; // should be of type `0x${string}`
const FEE_NUMERATOR = BigInt(100); // should be of type `BigInt`

const immutableTestnet = defineChain({
id: 13473,
name: 'imtbl-zkevm-testnet',
nativeCurrency: { name: 'IMX', symbol: 'IMX', decimals: 18 },
rpcUrls: {
default: {
http: ['https://rpc.testnet.immutable.com'],
},
},
});

const walletClient = createWalletClient({
chain: immutableTestnet,
transport: http(),
account: privateKeyToAccount(`0x${PRIVATE_KEY}`),
});

const contract = getContract({
address: CONTRACT_ADDRESS,
abi: ImmutableERC721Abi,
client: walletClient,
});

const setDefaultRoyaltyReceiver = async (): Promise<void> => {
await contract.write.setDefaultRoyaltyReceiver([ROYALTY_RECEIVER, FEE_NUMERATOR]);
};

setDefaultRoyaltyReceiver();

Amending the Fee Splitter Contract

The overridePayees() function can be used to adjust the fee splitting allocation of a previously deployed smart contract. It can only be run by the admin user.

The script below illustrates how to alter the royalty allocation in an existing fee splitting contract using the overridePayees() function.

caution

After overriding a contract, all fees in the old fee splitting contract will be distributed according to the new allocation configuration. The contract does not retain memory of previous allocations for funds collected before the adjustment. Therefore, it's advisable to execute the releaseAll() function before modifying the contract's allocation settings to ensure accurate distribution of previously collected funds.

import { getContract, http, createWalletClient, defineChain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { PaymentSplitterAbi } from '@imtbl/contracts';

const PRIVATE_KEY = 'YOUR_PRIVATE_KEY'; // should be read from environment variable
const CONTRACT_ADDRESS = '0xYOUR_CONTRACT_ADDRESS'; // should be of type `0x${string}`
const PAYEES: `0x${string}`[] = ['0xPAYEE_1', '0xPAYEE_2']; // should be of type `0x${string}`
const SHARES: bigint[] = [BigInt(2), BigInt(8)]; // should be of type `BigInt`

const immutableTestnet = defineChain({
id: 13473,
name: 'imtbl-zkevm-testnet',
nativeCurrency: { name: 'IMX', symbol: 'IMX', decimals: 18 },
rpcUrls: {
default: {
http: ['https://rpc.testnet.immutable.com'],
},
},
});

const walletClient = createWalletClient({
chain: immutableTestnet,
transport: http(),
account: privateKeyToAccount(`0x${PRIVATE_KEY}`),
});

const contract = getContract({
address: CONTRACT_ADDRESS,
abi: PaymentSplitterAbi,
client: walletClient,
});

const overridePayees = async (): Promise<void> => {
await contract.write.overridePayees([PAYEES, SHARES]);
};

overridePayees();