← Back to list

Stopping Front-Running with Commit-Reveal

*Part 4 of the DEXignation Series. Estimated read time: 7 min.*

Roy in DEXignation · 2026-05-27 15:56 · 0 claps · 4.3 min read
#solidity #security #mev #web3 #blockchain
Open on Medium ↗
Wiki topics: CRY · Crypto & Web3 🏃 · Running & Endurance

Stopping Front-Running with Commit-Reveal

Part 4 of the DEXignation Series. Estimated read time: 7 min.

— -

You see a great .dex name in the mempool of a pending registration. You copy the transaction, replace the recipient address with yours, crank the gas price 10%, and submit. Block builder picks the higher-fee transaction first. You get the name. The original user pays gas for a failed transaction.

This is the front-running problem in two paragraphs. The standard mitigation is commit-reveal, and DEXignation uses the same pattern that ENS pioneered. This post explains why the pattern works and how to use it.

— -

The naive registration is unsafe

Imagine register(“alice”, msg.sender, …) in one transaction. The mempool — the public waiting room of pending transactions — exposes the label “alice” to anyone watching. A bot that sees the value of that name can:

  1. Copy the calldata.
  2. Replace the owner with their own address.
  3. Multiply the gas price.
  4. Submit.

Validators include transactions in order of fee. The bot wins.

This isn’t theoretical. ENS launched without commit-reveal in 2017, and the first month of public registration was a frenzy of bots sniping desirable names. ENS responded with commit-reveal in 2018, and it’s been the standard ever since.

— -

The commit-reveal idea

Split registration into two transactions, separated by time.

Transaction 1 (commit): the user submits a hash that depends on the label, owner, and a secret. The hash reveals nothing about any of the inputs — it’s just 32 bytes of randomness from the bot’s perspective.

Transaction 2 (reveal/register): the user submits the label, owner, duration, payment, and the same secret. The contract recomputes the hash, looks it up, and only proceeds if it was committed at least minCommitmentAge ago and at most maxCommitmentAge ago.

The bot can’t shortcut this. To register the same name, the bot would need to first commit its own hash, then wait minCommitmentAge (30 seconds in DEXignation). The real user, who committed earlier, will have already revealed by then.

— -

DEXignation’s implementation

The commitment hash:

function makeCommitment(
 string calldata name,
 address owner,
 bytes32 secret
) public pure override returns (bytes32) {
 return keccak256(abi.encode(name, owner, secret));
}

Notice abi.encode, not abi.encodePacked. With encodePacked and variable-length types like string, you can craft collisions across inputs. abi.encode left-pads each field, eliminating that class of attack.

Storage and the commit transaction:

mapping(bytes32 commitment => uint256 timestamp) public commitments;

function commit(bytes32 commitment) public override {
 if (commitments[commitment] + maxCommitmentAge >= block.timestamp) {
 revert UnexpiredCommitmentExists(commitment);
 }
 commitments[commitment] = block.timestamp;
}

The check prevents someone from “refreshing” a commitment by re-submitting it before the previous one has expired (which would otherwise create a way to chain commitments and bypass the time window).

The reveal-time consumption:

function _consumeCommitment(
 string calldata label,
 address owner,
 bytes32 secret
) internal {
 bytes32 commitment = makeCommitment(label, owner, secret);
 uint256 ts = commitments[commitment];
 if (ts == 0) revert CommitmentNotFound(commitment);
 if (ts + minCommitmentAge > block.timestamp) {
 revert CommitmentTooNew(commitment);
 }
 if (ts + maxCommitmentAge <= block.timestamp) {
 revert CommitmentTooOld(commitment);
 }
 delete commitments[commitment];
}

Four checks:

  1. Was there a commit at all? ts == 0 ⇒ never committed.
  2. Has enough time passed? ts + minCommitmentAge > now ⇒ too new.
  3. Has too much time passed? ts + maxCommitmentAge <= now ⇒ too old.
  4. One-time use. delete commitments[commitment] ⇒ can’t reveal twice.

— -

Choosing the time windows

DEXignation defaults:

uint256 public constant DEFAULT_MIN_COMMITMENT_AGE = 30; // 30 seconds
uint256 public constant DEFAULT_MAX_COMMITMENT_AGE = 1 hours; // 3600 seconds

The 30-second floor is the security floor: it must be long enough that a bot can’t watch the commit, immediately commit its own, and still beat the user to reveal. Even on Polygon’s ~2 second block times, 30 seconds gives 15 blocks of buffer.

The 1-hour ceiling is mostly UX: a user who walks away from their browser shouldn’t lose their slot indefinitely. After 1 hour they just commit again.

These can be tuned by owner via setCommitmentAgeSettings(minAge, maxAge), which also enforces minAge < maxAge:

function setCommitmentAgeSettings(
 uint256 minAge,
 uint256 maxAge
) external override onlyOwner {
 if (minAge >= maxAge) revert MaxCommitmentAgeTooLow();
 minCommitmentAge = minAge;
 maxCommitmentAge = maxAge;
}

— -

Generating the secret

The secret should be:

  • Random — 32 bytes from a CSPRNG, not derived from user input.
  • Single-use — never reused across registrations.
  • Confidential until reveal — keep it on the user’s device.

In a typical web frontend:

import { keccak256, encodeAbiParameters } from ‘viem’;

// Step 1 — generate locally
const secret = crypto.getRandomValues(new Uint8Array(32));
const secretHex = ‘0x’ + Buffer.from(secret).toString(‘hex’);

// Step 2 — compute commitment
const commitment = keccak256(
 encodeAbiParameters(
 [{ type: ‘string’ }, { type: ‘address’ }, { type: ‘bytes32’ }],
 [label, ownerAddress, secretHex]
 )
);

// Step 3 — send commit transaction
await controller.write.commit([commitment]);

// Step 4 — wait at least 30 seconds
await new Promise(r => setTimeout(r, 35_000));

// Step 5 — reveal
await controller.write.register(
 [label, ownerAddress, duration, resolverAddress, secretHex],
 { value: requiredPol }
);

The browser holds the secret between the two transactions. If the user closes the tab without persisting it, they lose their commitment. For mobile or wallet-app integrations, you’ll want to save the secret to local storage with a short TTL.

— -

What commit-reveal does NOT protect against

It’s not magic. The pattern stops one specific attack: front-running of publicly visible registration intent. It does not stop:

  • Bot registration of unclaimed names. If you want alice.dex but haven’t committed yet, a bot crawling potential names can grab it first. Commit-reveal only protects users who have already committed.

  • Social engineering. “Please send me your secret so I can register your name for you.” — please don’t.

  • Censorship by block proposers. A determined proposer could exclude your commit transaction from blocks. Commit-reveal doesn’t defend against this; trustless settlement does (eventually your tx gets in).

  • Re-org attacks. On chains with reorgs, a commitment that landed in a block can technically be reorged out. Polygon’s reorg risk is very low compared to L1, but it’s not zero. minCommitmentAge of 30 seconds gives enough cushion.

— -

Gas costs

Two transactions is two transactions. On Polygon:

| Step | Approx gas | Approx cost (30 gwei, POL ~$0.4) | | — -| — -:| — -:| | commit | ~50,000 | ~$0.0006 | | register | ~280,000 | ~$0.0034 | | Total | ~330,000 | ~$0.004 |

Negligible. The protocol fee dominates ($5+ per name) so the commit-reveal pattern adds almost nothing to the user’s bill.

— -

Previous: Part 3 — On-Chain SVG NFT Next: Part 5 — attoUSD + Chainlink Dual-Path Oracle

— -

🇰🇷 Korean version available at docs.dexignation.com/blog.](https://docs.dexignation.com/blog).*)


메타데이터
post_id
366d61ce1d6c
slug
stopping-front-running-with-commit-reveal-366d61ce1d6c
url
https://medium.com/dexignation/stopping-front-running-with-commit-reveal-366d61ce1d6c
canonical_url
https://medium.com/dexignation/stopping-front-running-with-commit-reveal-366d61ce1d6c
author_url
https://medium.com/@punditcode
status
ok
fetched_at
2026-06-12 07:40:50