An Introduction to Force Inclusion

DevRel in web3. Retired 10x engineer. Developer DAO enjoyer
In these last few years, layer two blockchains (L2) have become central to Ethereum's scaling efforts. L2s are fast, cheap, and inherit Ethereum's strong security guarantees. Ethereum serves as the settlement layer, and L2s serve as the execution layer. This is great.
It is important to pause here, though, and ask:
What does it mean that these L2s inherit Ethereum's security guarantees?
What does it mean for an L2 to be secure?
This article won’t cover all the different L2 security details, but it will discuss one of the most important ones: censorship resistance.
What is Censorship Resistance?
An ideal L2 should be censorship-resistant. This means that no single entity or group (including the network operators, especially the network operators) should be able to prevent users from accessing the network or using its services.
Most L2s use highly centralised sequencer sets for speed, though. This is the usual lifecycle of an L2 transaction. For example, you send some ETH to a friend on Base:

It is not hard to spot that there is a potential single point of failure in this system. The sequencer can censor your transaction and refuse to include it in the next batch.
This is not good.
But the cool thing about real L2s is that this is not the end of the story. They are designed to protect users in exactly this scenario.
What is Force Inclusion?
When you suspect that an L2 sequencer is misbehaving, you can bypass it entirely and get your transaction included anyway. Whether the sequencer likes it or not, it does not have a choice.
This sequencer bypass is called force inclusion and looks like this:

How is Force Inclusion Possible?
In very simple terms, an Ethereum L2 is just a sophisticated set of smart contracts living on Ethereum L1. When you send a transaction on an L2, it is eventually posted to one of these smart contracts on Ethereum L1. The transaction is only considered truly 'final' once that happens. Unless you are using an optimistic rollup, in which case the finality can take up to 7 days.
This is why L2s inherit Ethereum's security. The source of truth for L2s is Ethereum L1, not the L2 sequencers or L2 nodes. The sequencer abstracts away the complexity of creating a valid L2 transaction and posting it properly to Ethereum. Any user can do this, too, but it is obviously more complicated.
As an educational exercise, let's explore how you can actually send a transaction to an L2 without using the sequencer.
Trying Out Force Inclusion
Most major L2s support force inclusion. For this example, we will aim to get a transaction force-included on Base mainnet. To keep things simple, we will attempt to send some ETH from one address to another, but the same methods work for transactions of any level of complexity.
I will be running a simple Bun script here. Let's initialize the project and install the one dependency we will need:
bun init
bun add ethers dotenv
We will also need a wallet's private key to send our transaction out. Note that this wallet needs to be funded on both Ethereum L1 and Base L2. We need ETH on L1 to pay the gas for the L1 transaction, and on L2 to pay for the ETH transfer.
Create an .env file and paste the private key there:
WALLET_PRIVATE_KEY=0x...
Let's open up index.ts and start setting up the script now.
import { ethers } from "ethers";
import "dotenv/config";
const l1Provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY!, l1Provider);
const portal = new ethers.Contract(
"0x49048044D57e1C92A77f79988d21Fa8fAF74E97e", // OptimismPortal contract address on Ethereum (used by Base)
[
"function depositTransaction(address to,uint256 value,uint64 gasLimit,bool isCreation,bytes data) payable",
],
wallet
);
Base is an OP Stack rollup. This means that it uses a similar smart contract structure to Optimism and other OP Stack rollups.
Remember how we discussed earlier that users have the ability to send transactions to one of the rollup contracts on Ethereum if they want to get a transaction force-included? This smart contract is called OptimismPortal for Base. To put things very simply, any valid L2 transactions deposited to this contract will be force-included on Base L2. This mechanism is slightly different from other rollup stacks like Arbitrum, but the core idea is the same.
If you are curious, you can check out the contract on Etherscan.
You might have also noticed the depositTransaction function signature that we added to our contract initialization code. This is the function we will be calling to deposit our transaction to the rollup contract.
The next step is to construct the actual transaction that we wish to send.
const to = "0x0ED6Cec17F860fb54E21D154b49DAEFd9Ca04106";
const value = ethers.parseEther("0.001");
const gasLimit = 200000n; // conservative
const data = "0x";
const tx = await portal.depositTransaction(
to,
value,
gasLimit,
false,
data,
{ value } // pay ETH
);
console.log("L1 tx hash:", tx.hash);
We need to specify five properties:
to: This is the address of the L2 wallet that we want to send the ETH to. If you were calling a smart contract, this would be the address of the contract you want to call.value: This is the amount of ETH we wish to send to the L2 wallet.gasLimit: This is the maximum amount of gas we are willing to pay for the transaction. Libraries likeviemprovide helpers that can calculate this for you. For now, we will use a conservative estimate.isCreation: This is a boolean value that indicates whether the transaction is creating a new contract or not. If you are sending ETH to an existing contract, this should be set tofalse. We have set this tofalse.data: The calldata for the transaction. Since we are doing a simple ETH transfer, we can leave this as0x. If you were interacting with a smart contract, this would be the encoded function call data.
Notice how we are not interacting with any Base RPC endpoints or contracts at all. Everything is being sent to Ethereum directly.
.env is funded on both L1 and L2, and check the receiving address on L2 (the to field). This is the address that will receive the funds on Base.For reference, you can find the complete code on GitHub.
Run the script, and check the transaction hash on Etherscan:
bun run index.ts
Once the transaction is confirmed on L1, you might need to wait around 2 epochs (~13 minutes) for the Ethereum chain to be finalised before the transaction is included in an L2 block.
After waiting, you should see a transaction pop up on Base L2 for the receiving address. You should receive the exact amount of ETH that you specified in the value field.
Congratulations!
You just sent some ETH from one address to another on an Ethereum L2 without interacting with the L2 sequencer.
Conclusion
I hope this article served as a very basic primer on why force inclusion is important and how you can use this security feature yourself if need be.
I am currently working on an SDK around this with some friends to make similar rollup security features more accessible to developers. You can check it out on GitHub!
Also, I’m currently looking for my next role in crypto. My X DMs are open, hmu!





