How do you secure smart contracts and dApp interactions?
Web3 Developer
answer
I apply defense in depth: reentrancy is blocked with Checks-Effects-Interactions, pull-payment patterns, and ReentrancyGuard on state-changing externals. Signatures use EIP-712 typed data with per-user nonces, deadlines, chainId, and library-verified recovery (ECDSA/EIP-1271). For approvals, I avoid unlimited allowances: prefer EIP-2612 permit or Permit2 with scoped amount, spender, and expiry; otherwise do exact-value approvals, safeIncreaseAllowance, and auto-revoke on completion.
Long Answer
Securing smart contracts and dApp interactions requires layered controls that start at contract design and continue through wallets and front-end flows. I combine reentrancy-safe accounting, verifiable signatures, and least-privilege token approvals, then enforce these with tests, tooling, and on-chain monitoring.
1) Reentrancy: design out the bug, then guard it
I default to Checks-Effects-Interactions (CEI): validate inputs and balances, update state, and only then make external calls. Where funds are involved, I use pull payments (beneficiaries withdraw) instead of pushing value in the same transaction. For functions that perform external calls (ERC-20/721 transfers, callbacks, AMM/router hooks), I add nonReentrant via OpenZeppelin’s ReentrancyGuard. I avoid state writes after external calls and keep accounting idempotent. I also consider cross-function reentrancy—not just a single function—by grouping sensitive flows behind the same guard or by structuring them into internal steps that cannot be reentered into a vulnerable state. With token standards that can trigger hooks (e.g., ERC-777), I sandbox interactions or reject unexpected callbacks. Finally, I design settlement to tolerate failed external calls (e.g., withdrawal patterns), so a single malicious receiver cannot wedge the system.
2) Signature validation with EIP-712 and contract wallets
Off-chain authorization reduces on-chain cost and expands UX, but only when validated rigorously. I use EIP-712 typed data with a stable domain separator (name, version, chainId, verifyingContract, salt when needed). Every signed intent carries a nonce (per user and action type) and a deadline to constrain replay windows. I recover signers with OpenZeppelin ECDSA, enforcing s in the lower half order and strict v (27/28 or 0/1 mapping) to block malleability. For smart-contract wallets, I support EIP-1271 by calling isValidSignature and rejecting EOAs that masquerade as contracts. If a trusted forwarder (EIP-2771) or meta-tx relayer is used, I validate the forwarder, derive the correct user, and maintain separate nonces to prevent cross-channel replay. I also pin the chainId in typed data so signatures are not valid on forks or other networks, and I bind the exact function intent (struct type hash) so a permit cannot be replayed as a different operation.
3) Minimizing token approvals and blast radius
Unlimited allowances are a common source of loss. I first try to remove approvals entirely with single-use permissions:
- EIP-2612 permit on ERC-20: obtain an off-chain signature and spend exact amounts in the same transaction, with a short deadline and a fresh nonce.
- Permit2 (Uniswap) when available: grant scoped allowances (token, spender, amount, expiry) and track nonces per transfer.
- For NFTs, prefer operator filtering or session delegation mechanisms that expire quickly.
If standard approve must be used, I follow least privilege: approve only what is needed, for one spender, and with auto-revoke after use (approve(0) or safeDecreaseAllowance). I avoid the brittle “set from X to Y” pattern by using safeIncreaseAllowance/safeDecreaseAllowance to mitigate race conditions. On the dApp side, I detect stale infinite approvals and prompt users to reduce or revoke them (linking to revoke tools) after flows complete. Routers and escrows are allowlisted by address and version so users never approve arbitrary spenders surfaced by phishing pages.
4) Access control, pausability, and circuit breakers
Administrative functions use role-based access (OpenZeppelin AccessControl) or two-step ownership with a timelock. Sensitive parameters (oracle feeds, fee recipients) are guarded behind multisig and delayed execution to allow monitoring. A Pausable switch or per-feature circuit breaker lets us halt risky operations without locking funds. Rate limits and cool-downs protect from rapid-fire exploit chains.
5) Upgrades, storage safety, and initialization
When upgradable proxies are needed (Transparent/UUPS), I freeze storage layouts, use reserved gaps, and test upgrades with storage snapshots. Initializers are guarded (initializer/reinitializer) to prevent re-init attacks. Upgrade authorization is restricted to a timelocked multisig with clear runbooks and on-chain votes where governance is involved.
6) Tooling: static analysis, fuzzing, and invariants
I run Slither for static findings, use Foundry or Hardhat for fuzz testing and property-based tests, and add invariant tests (balances, conservation rules, no-loss properties). For deeper specs, Scribble annotations and solc SMT checks or tools like Certora/Manticore/Echidna help prove key invariants. Differential tests compare implementations against known-good references. Every external call path is covered by a reentrancy harness and by tests that simulate malicious tokens and callback contracts.
7) Front-end and wallet interaction hygiene
Security finishes at the UI: I render human-readable signing prompts for EIP-712 that clearly show token, amount, spender, deadline, chain, and intent. I block signature requests on unknown chains, pin RPC endpoints, and never request blanket approvals by default. I pre-fill slippage limits, show price impact, and require explicit confirmation for high-risk actions. I sanitize user inputs, isolate third-party scripts, and prefer hardware-wallet flows for large approvals. Post-tx, I display the effective allowance and offer one-click revoke.
8) Monitoring and response
Critical actions emit events with indexed fields for addresses and nonces. Off-chain watchers track anomalies (unexpected reverts, allowance spikes, oracle swings) and auto-page responders. Runbooks define how to pause, rotate keys, or roll out an upgrade or parameter change under a timelock.
Together, these strategies reduce exploit surface, narrow approval blast radius, and make signatures reliable, auditable proofs of user intent.
Table
Common Mistakes
Relying on nonReentrant alone while leaving state writes after external calls. Using raw ecrecover without ECDSA safety checks (malleable s, lax v), or skipping EIP-1271 so contract wallets cannot sign. Reusing nonces or omitting deadlines, enabling replay across chains when chainId is missing. Granting infinite approvals to routers and never revoking them. Increasing allowance from a nonzero value without first adjusting safely, enabling race conditions. Letting front-end ask for blanket approvals on page load. Deploying upgradable contracts without storage layout tests or initializer guards. Disabling pause because “we trust the code.” Skipping fuzz/invariant tests and never simulating malicious callbacks. Hiding intent in EIP-712 messages users cannot read, leading to phishing-like approvals.
Sample Answers (Junior / Mid / Senior)
Junior:
“I follow Checks-Effects-Interactions and add ReentrancyGuard on functions that transfer tokens. For signatures I use EIP-712 with per-user nonces and deadlines and recover with OpenZeppelin ECDSA. I avoid unlimited approvals by using permit when possible or exact-amount approvals, then revoke.”
Mid:
“I design pull-payment withdrawals and sandbox tokens that trigger hooks. EIP-712 includes chainId and verifyingContract; I support EIP-1271 for smart wallets. For approvals I prefer Permit2 with scoped amount and expiry, otherwise safeIncreaseAllowance and auto-revoke post-use. Tests include Slither, Foundry fuzzing, and invariant checks on balances.”
Senior:
“Defense in depth: CEI + cross-function guards; typed EIP-712 intents with domain separation, nonces, deadlines, and 1271. Approvals are least-privilege via 2612/Permit2, allowlisted spenders, and scheduled revocation. Admin is role-based with timelock and pausability; upgrades are storage-checked. CI gates for slither, coverage, invariants; UI shows human-readable permissions and slippage. Events feed monitoring for fast incident response.”
Evaluation Criteria
Strong candidates connect contract-level safety to wallet/UI flows. Look for CEI, pull-payments, and explicit ReentrancyGuard use, plus awareness of ERC-777/AMM callbacks. For signatures, they must articulate EIP-712 domain fields, per-user nonces, deadlines, low-s enforcement, and EIP-1271 support. For approvals, they should minimize scope via EIP-2612 or Permit2, use allowlists, and revoke on completion; mention safe allowance patterns. Governance (roles, timelock, pause) and upgradability hygiene are important. Red flags: infinite approvals by default, raw ecrecover, missing chainId, no replay protection, or thinking nonReentrant alone solves everything. Bonus: fuzz/invariant testing, malicious token harnesses, human-readable signing prompts, and event-driven monitoring tied to incident playbooks.
Preparation Tips
Implement a small vault with deposits/withdrawals using CEI and ReentrancyGuard. Add a token that calls back on transfer to test reentrancy defenses. Build an EIP-712 permit flow: include domain separator, nonce, and deadline; validate with ECDSA and add EIP-1271 for contract wallets. Integrate Permit2 and demonstrate scoped, expiring allowances. Write tests that fuzz inputs, simulate replay across chains, and try cross-function reentrancy. Add invariants: reserves never decrease unexpectedly; total balances are conserved. On the front end, render human-readable typed-data prompts and show effective allowance with a one-click revoke. Add a timelocked admin and a pause function; practice a mock incident where you pause, rotate keys, and resume operations. Ship CI steps for Slither, coverage, fuzz, and storage-layout checks, and require green gates before deploy.
Real-world Context
A DEX router exploit relied on infinite approvals. Migrating users to Permit2 with expiring, scoped allowances cut exposure surface dramatically, and adding revoke prompts after swaps reduced stale approvals by 80%. A lending protocol suffered a callback-driven reentrancy in an auxiliary reward function; moving to CEI + pull-payments and guarding cross-function paths eliminated the class entirely. A DAO had signatures replayed on a sidechain fork; binding chainId in EIP-712 and enforcing per-action nonces closed the gap. Another team shipped an upgradable proxy without storage checks and broke balances; adopting storage layout tests and two-step upgrades with timelock prevented recurrences. In all cases, pairing least-privilege approvals, intent-bound signatures, and reentrancy-safe accounting turned fragile designs into resilient systems that withstood targeted attacks and user-error-induced risks.
Key Takeaways
- Block reentrancy with CEI, pull-payments, and ReentrancyGuard; consider cross-function paths.
- Validate intent with EIP-712: domain separator, chainId, nonces, deadlines, ECDSA low-s, and EIP-1271.
- Minimize approvals with EIP-2612 or Permit2; scope by amount, spender, and expiry, and revoke after use.
- Govern with roles, timelock, and pausability; test upgrades and storage layout.
- Prove safety with static analysis, fuzz/invariants, malicious callback harnesses, and human-readable signing UX.
Practice Exercise
Scenario:
You are building a swapping dApp that routes through external AMMs and needs token spending rights. Users should not grant unlimited approvals, signatures must be replay-safe, and settlement must resist reentrancy, including callback-heavy tokens.
Tasks:
- Implement swap execution with CEI and nonReentrant; move payouts to a withdraw function. Add a malicious token mock that triggers callbacks and prove safety in tests.
- Add an EIP-712 typed “SwapIntent” struct (tokenIn, tokenOut, amountIn, minOut, deadline, nonce, chainId, router). Validate with ECDSA and EIP-1271. Reject expired or reused nonces.
- Integrate EIP-2612 or Permit2 so users authorize only the exact amount with an expiry. If approve is required, use safeIncreaseAllowance and auto-revoke on success.
- Create allowlists for routers/pools and block arbitrary external calls.
- Emit events for intents, executions, and revocations; build a watcher that alerts on unusual allowance spikes or failed callbacks.
- Write fuzz and invariant tests: conservation of value, no underflow/overflow, nonces strictly increasing, and “no external write after external call.”
- Add a timelocked admin with pause() for swaps; document an incident runbook.
Deliverable:
A prototype where a user signs an EIP-712 intent, submits a single transaction that uses permit/Permit2 to spend only what is needed, executes a swap with reentrancy-safe accounting, and auto-revokes residual allowance, with tests proving replay resistance and callback safety.

