How to manage the fee parameters of your Orbit chain
Different fees get collected for every transaction as part of an Orbit chain activity. These fees are collected as a single amount (the transaction fees) but split internally into different components depending on their purpose. Each component is transferrable to a different fee collector address that is configurable on your chain.
This guide describes the different collected fees and explains how to specify the fee collector address on your chain for each fee type. This guide describes the different fees collected on your chain, how to configure them, and how to specify the fee collector address for each type.
What fees are collected on an Orbit chain?
There are four fee types that are collected on every transaction of an Orbit chain:
-
Orbit base fee: fees paid for executing the transaction on the chain based on the minimum base price configured.
-
Orbit surplus fee: if the chain is congested (i.e., the base price paid for the transaction is higher than the minimum base price), these fees account for executing the transaction on the chain based on any gas price paid above the minimum base price configured.
-
Parent chain base fee: relative fees paid for posting the transaction on the parent chain. This amount is calculated based on the transaction's estimated size and the current view of the parent chain's base fee.
-
Parent chain surplus fee: if configured, these are extra fees rewarded to the batch poster.
You can find more detailed information about these fee types in these pages:
- L2 fees for the Orbit base fee and surplus fee
- L1 fees for the Parent chain base fee and surplus fee
How to configure the fees collected?
Let's see in what ways we can configure each fee type:
Orbit (minimum) base fee
Your chain is configured with a minimum base fee for execution. This value can be obtained by calling the method getMinimumGasPrice()(uint256)
of the ArbGasInfo precompile.
cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006C "getMinimumGasPrice() (uint256)"
Alternatively, you can use the Orbit SDK to retrieve the minimum Orbit base fee configured:
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbGasInfoPublicActions);
const orbitMinimumBaseFee = await orbitChainClient.arbGasInfoReadContract({
functionName: 'getMinimumGasPrice',
});
::: note This minimum base fee defines the minimum value that the chain's base fee can have. However, in periods of congestion, the actual base fee might be higher than this minimum. Check the next section "Orbit surplus fee" for more information. :::
To set a new minimum base fee, use the method setMinimumL2BaseFee(uint256)
of the ArbOwner precompile, and pass the new minimum base fee in wei. For example:
cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setMinimumL2BaseFee(uint256) ()" $NEW_MINIMUM_BASE_FEE_IN_WEI
Or using the Orbit SDK:
const owner = privateKeyToAccount(<OwnerPrivateKey>);
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbOwnerPublicActions);
const transactionRequest = await orbitChainClient.arbOwnerPrepareTransactionRequest({
functionName: 'setMinimumL2BaseFee',
args: [<NewMinimumBaseFeeInWei>],
upgradeExecutor: false,
account: owner.address,
});
await orbitChainClient.sendRawTransaction({
serializedTransaction: await owner.signTransaction(transactionRequest),
});
Orbit surplus fee
In periods of congestion, the actual base fee of your Orbit chain might be higher than the configured minimum. You can see the current base fee of your chain by calling the method getPricesInWei()(uint256,uint256,uint256,uint256,uint256,uint256)
of the ArbGasInfo precompile, and check the last result of the returned tuple.
cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006C "getPricesInWei() (uint256,uint256,uint256,uint256,uint256,uint256)"
You can then calculate the current Orbit surplus fees as currentBaseFee - minimumBaseFee
.
Note: getPricesInWei()
also returns the correspondent fees due to congestion in the second-to-last result of the returned tuple.
Orbit chains automatically adjust the Orbit surplus fee based on the traffic of the chain. If the gas consumed goes over the speed limit, the chain's base fee will start increasing. Likewise, the base fee will gradually go down if demand of gas returns to below the configured speed limit, until it reaches the minimum base fee configured.
Parent chain base fee
To obtain the current parent chain base fee of your chain, you can call the method getL1BaseFeeEstimate()(uint256)
of the ArbGasInfo precompile.
cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006C "getL1BaseFeeEstimate() (uint256)"
Alternatively, you can use the Orbit SDK to retrieve the current parent chain base fee:
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbGasInfoPublicActions);
const parentChainBaseFee = await orbitChainClient.arbGasInfoReadContract({
functionName: 'getL1BaseFeeEstimate',
});
You can modify the current estimate of the parent chain base fee by calling the method setL1PricePerUnit(uint256)
of the ArbOwner precompile. For example:
cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setL1PricePerUnit(uint256) ()" $NEW_PARENT_CHAIN_BASE_FEE
Or using the Orbit SDK:
const owner = privateKeyToAccount(<OwnerPrivateKey>);
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbOwnerPublicActions);
const transactionRequest = await orbitChainClient.arbOwnerPrepareTransactionRequest({
functionName: 'setL1PricePerUnit',
args: [<NewParentChainBaseFee>],
upgradeExecutor: false,
account: owner.address,
});
await orbitChainClient.sendRawTransaction({
serializedTransaction: await owner.signTransaction(transactionRequest),
});
Orbit chains are configured to automatically adjust the current parent chain base fee estimation based on the batch poster reports sent from the parent chain. That means that even though you can set a new parent chain base fee, the chain will automatically adjust it based on the reports received afterwards.
Orbit chains that use a custom gas token should have their parent chain base fees disabled (set to 0), to avoid charging users for a non-existent parent chain's base fee, as explained in How to use a custom gas token.
Parent chain surplus fee
The parent chain surplus fee collected is based on a reward rate configured in the chain. To obtain this parameter, you can call the method getL1RewardRate()(uint64)
of the ArbGasInfo precompile. This function will return the amount of wei per gas unit paid to the appropriate fee collector. For example:
cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006C "getL1RewardRate() (uint64)"
Alternatively, you can obtain this information using the Orbit SDK:
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbGasInfoPublicActions);
const parentChainRewardRate = await orbitChainClient.arbGasInfoReadContract({
functionName: 'getL1RewardRate',
});
To change the reward rate, you can use the method setL1PricingRewardRate(uint64)
of the ArbOwner precompile and pass the amount of wei per gas unit to reward. For example:
cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setL1PricingRewardRate(uint64) ()" $NEW_REWARD_RATE
Or using the Orbit SDK:
const owner = privateKeyToAccount(<OwnerPrivateKey>);
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbOwnerPublicActions);
const transactionRequest = await orbitChainClient.arbOwnerPrepareTransactionRequest({
functionName: 'setL1PricingRewardRate',
args: [<NewRewardRate>],
upgradeExecutor: false,
account: owner.address,
});
await orbitChainClient.sendRawTransaction({
serializedTransaction: await owner.signTransaction(transactionRequest),
});
How to configure the fee collector addresses?
Let's now look at how to configure the collector addresses for each fee type.
Orbit base fee
Orbit base fees are paid to the infraFeeAccount
configured in your chain. You can retrieve the current configured address by calling the method getInfraFeeAccount()(address)
of the ArbOwnerPublic precompile. For example:
cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006B "getInfraFeeAccount() (address)"
Note: The ArbOwner precompile also has a getInfraFeeAccount()(address)
method that can be used, but only by the owner of the chain.
Alternatively, you can use the Orbit SDK to retrieve the current address configured as infraFeeAccount
:
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbOwnerPublicActions);
const infraFeeAccount = await orbitChainClient.arbOwnerReadContract({
functionName: 'getInfraFeeAccount',
});
To set a new infraFeeAccount
, use the method setInfraFeeAccount(address)
of the ArbOwner precompile. For example:
cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setInfraFeeAccount(address) ()" $NEW_INFRAFEEACCOUNT_ADDRESS
Or using the Orbit SDK:
const owner = privateKeyToAccount(<OwnerPrivateKey>);
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbOwnerPublicActions);
const transactionRequest = await orbitChainClient.arbOwnerPrepareTransactionRequest({
functionName: 'setInfraFeeAccount',
args: [<NewInfraFeeAccountAddress>],
upgradeExecutor: false,
account: owner.address,
});
await orbitChainClient.sendRawTransaction({
serializedTransaction: await owner.signTransaction(transactionRequest),
});
Orbit surplus fee
Orbit surplus fees are paid to the networkFeeAccount
configured in your chain. You can retrieve the current configured address by calling the method getNetworkFeeAccount()(address)
of the ArbOwnerPublic precompile. For example:
cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006B "getNetworkFeeAccount() (address)"
Note: The ArbOwner precompile also has a getNetworkFeeAccount()(address)
method that can be used, but only by the owner of the chain.
Alternatively, you can use the Orbit SDK to retrieve the current address configured as networkFeeAccount
:
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbOwnerPublicActions);
const networkFeeAccount = await orbitChainClient.arbOwnerReadContract({
functionName: 'getNetworkFeeAccount',
});
To set a new networkFeeAccount
, use the method setNetworkFeeAccount(address)
of the ArbOwner precompile. For example:
cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setNetworkFeeAccount(address) ()" $NEW_NETWORKFEEACCOUNT_ADDRESS
Or using the Orbit SDK:
const owner = privateKeyToAccount(<OwnerPrivateKey>);
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbOwnerPublicActions);
const transactionRequest = await orbitChainClient.arbOwnerPrepareTransactionRequest({
functionName: 'setNetworkFeeAccount',
args: [<NewNetworkFeeAccountAddress>],
upgradeExecutor: false,
account: owner.address,
});
await orbitChainClient.sendRawTransaction({
serializedTransaction: await owner.signTransaction(transactionRequest),
});
Parent chain base fee
Parent chain base fees are paid to the fee collector of the active batch poster configured in your chain. The current configured batch posters can be obtained by calling the method getBatchPosters()(address[])
of the ArbAggregator precompile. For example:
cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006D "getBatchPosters() (address[])"
This list has to also be verified against the SequencerInbox
contract living on the parent chain. This contract needs to have any of those addresses in its isBatchPoster
mapping. To verify a specific address, you can check the mapping directly like this:
cast call --rpc-url $PARENT_CHAIN_RPC $SEQUENCER_INBOX_ADDRESS "isBatchPoster(address) (bool)" $BATCH_POSTER_ADDRESS
Alternatively, you can use the Orbit SDK to retrieve the current configured batch posters. This method will not use the ArbAggregator precompile, but instead will rely on events emitted when configuring a new batch poster in the SequencerInbox contract.
const parentChainClient = createPublicClient({
chain: <ParentChainDefinition>,
transport: http(),
});
const batchPosters = await getBatchPosters(parentChainClient, {
rollup: rollupAddress,
sequencerInbox: sequencerInboxAddress,
});
Once you have the current batch poster, you can obtain the fee collector address configured for that batch poster by calling the method getFeeCollector(address)(address)
of the ArbAggregator precompile and passing the address of the batch poster.
cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006D "getFeeCollector(address) (address)" $BATCH_POSTER_ADDRESS
You can also use the Orbit SDK to retrieve the current fee collector configured for a specific batch poster:
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbAggregatorActions);
const networkFeeAccount = await orbitChainClient.arbAggregatorReadContract({
functionName: 'getFeeCollector',
args: [<BatchPosterAddress>],
});
To set a new fee collector for a specific batch poster, use the method setFeeCollector(address, address)
of the ArbAggregator precompile and pass the address of the batch poster and the address of the new fee collector.
cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x000000000000000000000000000000000000006D "setFeeCollector(address,address) ()" $BATCH_POSTER_ADDRESS $NEW_FEECOLLECTOR_ADDRESS
Or using the Orbit SDK:
const owner = privateKeyToAccount(<OwnerPrivateKey>);
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbAggregatorReadContract);
const transactionRequest = await orbitChainClient.arbAggregatorPrepareTransactionRequest({
functionName: 'setFeeCollector',
args: [<BatchPosterAddress>, <NewFeeCollectorAddress>],
upgradeExecutor: false,
account: owner.address,
});
await orbitChainClient.sendRawTransaction({
serializedTransaction: await owner.signTransaction(transactionRequest),
});
Finally, if you want to set a new batch poster, you can call the method addBatchPoster(address)
of the ArbAggregator precompile and pass the address of the new batch poster, and later call the method setIsBatchPoster(address,bool)
of the SequencerInbox contract on the parent chain.
cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x000000000000000000000000000000000000006D "addBatchPoster(address) ()" $NEW_BATCH_POSTER_ADDRESS
cast send --rpc-url $PARENT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY $SEQUENCER_INBOX_ADDRESS "setIsBatchPoster(address,bool) ()" $NEW_BATCH_POSTER_ADDRESS true
Note: When setting a new batch poster, its fee collector will be configured to the same address by default.
Parent chain surplus fee
Parent chain surplus fees are paid to a specific L1RewardRecipient
address that is configured individually per chain. The current fee collector address can be obtained by calling the method getL1RewardRecipient()(address)
of the ArbGasInfo precompile. For example:
cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006C "getL1RewardRecipient() (address)"
Alternatively, you can obtain this information using the Orbit SDK:
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbGasInfoPublicActions);
const parentChainRewardRecipient = await orbitChainClient.arbGasInfoReadContract({
functionName: 'getL1RewardRecipient',
});
To set a new L1RewardRecipient
address, you can call the method setL1PricingRewardRecipient(address)
of the ArbOwner precompile, and pass the address of the new reward recipient. For example:
cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setL1PricingRewardRecipient(address) ()" $NEW_L1REWARDRECIPIENT_ADDRESS
Alternatively, you can use the Orbit SDK to set the new address:
const owner = privateKeyToAccount(<OwnerPrivateKey>);
const orbitChainClient = createPublicClient({
chain: <OrbitChainDefinition>,
transport: http(),
}).extend(arbOwnerPublicActions);
const transactionRequest = await orbitChainClient.arbOwnerPrepareTransactionRequest({
functionName: 'setL1PricingRewardRecipient',
args: [<NewL1RewardRecipientAddress>],
upgradeExecutor: false,
account: owner.address,
});
await orbitChainClient.sendRawTransaction({
serializedTransaction: await owner.signTransaction(transactionRequest),
});
How to use the fee distribution contracts?
In the previous section we described how to set the individual collector addresses for each fee type. Some chains may require multiple addresses to receive the collected fees of any of the available types. In those cases, there's the possibility of using a distributor contract that can gather all fees of a specific type and distribute those among multiple addresses.
This section shows how to configure a distributor contract to manage the fees of a specific type.
This section will explain the process of deploying and configuring a distribution contract manually, but the Orbit SDK includes an example to perform this process through a script.
Step 1. Deploy the distributor contract
An example implementation of a distributor contract can be found here. You'll have to deploy this contract on your Orbit chain.
Step 2. Set the contract address as the desired fee type collector address
Use the instructions provided in the previous section to set the address of the deployed distributor contract as the collector of the desired fee type. For example, if you want the distributor contract to manage the Orbit surplus fees, set the networkFeeAccount
to the address of the deployed contract.
Step 3. Configure the recipients of fees in the contract
Now you can set the different addresses that will be receiving fees from that distributor contract. To do that, you can call the method setRecipients(address[], uint256[])
of the distributor contract, and specify the list of addresses that will be receiving fees, and the proportion of fees for each address.
For example, if you want to set two addresses as receivers, with the first one receiving 80% of the fees and the second one receiving 20% of the fees, you'll use the following parameters:
cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY $DISTRIBUTOR_CONTRACT_ADDRESS "setRecipients(address[],uint256[]) ()" "[$RECEIVER_1, $RECEIVER_2]" "[8000, 2000]"
Step 4. Trigger the distribution of fees
With the recipients configured in the distributor contract, and with the contract having collected some fees, you can now trigger the distribution of fees to the recipients by using the method distributeRewards(address[], uint256[])
of the distributor contract, and specifying the list of addresses that are configured, and the proportion of fees for each address. The parameters passed must match the information that is set in the contract (i.e., you can't specify different addresses or proportions than what's been configured beforehand).
For example, if you want to distribute the fees to the two addresses specified before, you'll use the following parameters:
cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY $DISTRIBUTOR_CONTRACT_ADDRESS "distributeRewards(address[],uint256[]) ()" "[$RECEIVER_1, $RECEIVER_2]" "[8000, 2000]"