Stopping Front-Running with Commit-Reveal
*Part 4 of the DEXignation Series. Estimated read time: 7 min.*
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:
- Copy the calldata.
- Replace the owner with their own address.
- Multiply the gas price.
- 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:
- Was there a commit at all?
ts == 0⇒ never committed. - Has enough time passed?
ts + minCommitmentAge > now⇒ too new. - Has too much time passed?
ts + maxCommitmentAge <= now⇒ too old. - 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.dexbut 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.
minCommitmentAgeof 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