Use Template

Opens this plan in Hirezen, where one click makes it a position.

Blockchain Developer interview questionsCode review — the upgrade you cannot roll back round

A 60 min interview plan with a time-boxed script, what each question is for, and the signals to score against. Key skills: Reviewing a Solidity proxy upgrade before it ships: a storage layout collision, an initializer anyone can call first, a pause that blocks exits, and an upgrade key held partly by CI; testing the upgrade against the deployed contract on a fork; and deciding who may change a contract that users have funds in, and how fast..

The review

20 min
What this section is for

Purpose

Runs over a pull request handed over at the start. Build the repository the evening before so that everything below is true in it. `StakingV1` has been live for fourteen months behind an ERC-1967 proxy using the UUPS pattern, compiled against OpenZeppelin Contracts Upgradeable 5.x, with 8.4 million project tokens staked by 3,140 addresses and a docs page telling stakers they can withdraw at any time. Its own state variables, in order: `stakingToken`, `totalStaked`, `stakedOf`, `rewardDebt`, `accRewardPerShare`, `lastRewardTime`; rewards accrue per second since `lastRewardTime`. Pull request #212, 'StakingV2: lockups and an emergency pause', touches three files. `StakingV2.sol` declares a new `uint64 lockDuration` directly after `totalStaked`, which pushes every later variable one slot further along; appends `mapping(address => uint256) stakedAt` at the end; adds `PausableUpgradeable` as a parent; adds `initializeV2(uint64)` with `reinitializer(2)` and no access control; makes `withdraw` require `block.timestamp >= stakedAt[msg.sender] + lockDuration`; puts `whenNotPaused` on `withdraw` and `claim` but not on `stake`; keeps `_authorizeUpgrade` behind `onlyOwner`; and drops the `_disableInitializers()` call that V1 makes in its constructor. `script/UpgradeV2.s.sol` deploys the new implementation and prints two transactions for the owner Safe to execute one after the other: `upgradeToAndCall(newImplementation, "")`, then `initializeV2(604800)`. `test/StakingV2.t.sol` has fourteen passing tests, all against a V2 deployed fresh behind a new proxy. The owner is a 2-of-3 Safe whose signers are the CTO's hardware wallet, the lead developer's hardware wallet, and a deploy-bot key kept in the CI secret store so that releases do not wait on people; there is no timelock. Three things are deliberately fine: the new parent keeps its state in a namespaced storage slot and moves nothing, `reinitializer(2)` is the right modifier, and `stakedAt` is appended where it belongs. The missing `_disableInitializers()` is the red herring — worth a line in the review, not a blocker. Book 70 minutes; the close is outside the 60.

I'm [YOUR_NAME] and I review the contract changes at [COMPANY_NAME] that touch staked funds. You are the second approver on this pull request. The first approver has said yes, CI is green, and the signers are waiting on you.

What this section is for

Purpose

Puts the candidate in the seat where a review decides something, and tells them what a real approval carries with it: passing tests and a colleague who has already agreed.

Take the time you need with the diff and the script. Treat what you say as your review comments: what blocks, what you would ask for, and what you would let through.

What this section is for

Purpose

Makes triage part of the task. A reviewer who blocks on everything has not told anyone what matters.

Give me your review. What blocks this pull request, in the order you would write it up, and what happens to the stakers if it ships as it is?

What this question is for, and what to listen for

Purpose

Reads contract security in the shape upgrades give it: flaws that exist in neither contract alone, only in the move from one to the other and in the order two transactions run. The discriminator is whether the candidate reasons about the storage the proxy already holds and about the script's sequence, rather than about V2's source on its own.

Signals to score

  • Sees that inserting `lockDuration` after `totalStaked` shifts every later variable, and says what V2 will read from each moved slot
  • States the damage in stakers' terms: balances read out of the old reward-debt mapping, and reward accounting that restarts from a timestamp of zero
  • Notices that the upgrade and `initializeV2` are separate transactions, and that anyone can call the unprotected initializer in between
  • Says what a hostile `initializeV2` does — a duration near the `uint64` maximum locks every existing staker — and that `reinitializer(2)` then blocks a correction short of another upgrade
  • Fixes the gap by passing the encoded `initializeV2` call as the data of `upgradeToAndCall`
  • Flags the pause that stops `withdraw` but not `stake`, and the lockup that breaks the docs page's promise
  • Explains why fourteen green tests prove nothing here: none of them upgrades a proxy that already holds state
  • Gives the missing `_disableInitializers()` a low severity with a reason, rather than making it the headline
  • Does not report the new parent contract as a layout shift, or drops the concern when asked where that parent keeps its state

Follow-up questions

  • Take a staker with 10,000 tokens staked. After this upgrade, what does `stakedOf` return for them?
  • The Safe executes the upgrade, and three blocks later it executes `initializeV2`. What can happen in those three blocks?
  • `PausableUpgradeable` is a new parent. Does adding it move anything?
  • A teammate says the missing `_disableInitializers()` is the critical finding. Do you agree?
  • Suppose it shipped an hour ago. Can you roll back to V1?

A test against what is deployed

22 min
What this section is for

Purpose

The testing read. V2's suite is green and irrelevant, and the candidate writes the test that would have blocked the pull request without a reviewer having to be sharp that day: one that runs the real upgrade, from the real owner, over the real state. The test is written and talked through, not run, so nothing here needs a real deployment; if you want it to run, deploy V1 to a local node the evening before, seed a few hundred stakers with a script, and have the candidate fork that instead of mainnet.

Assume the layout and the initializer are fixed. I want the test that would have caught the original version before it reached a human reviewer.

What this section is for

Purpose

Moves from finding to preventing, and frames the deliverable as a check the next reviewer benefits from without being clever.

Write the test that runs this upgrade the way it will actually happen — against the proxy on mainnet, with the transactions the script produces — and tell me what it asserts and where it runs.

What this question is for, and what to listen for

Purpose

Separates testing a contract from testing a change to a deployed contract. A candidate who has shipped upgrades reaches for a fork test that replays the deployment and compares state before and after; one who has not writes more unit tests against a fresh V2.

Signals to score

  • Forks mainnet at a pinned block and tests the existing proxy address rather than a new deployment
  • Runs the exact calls the script prints, as the Safe, in the same order and as separate transactions
  • Chooses the stakers to compare on purpose — the largest, one with unclaimed rewards, one who has fully withdrawn — and says where the list comes from, since a mapping cannot be enumerated on chain
  • Asserts that every V1 variable reads the same before and after the upgrade for those stakers, along with the contract-wide totals
  • Performs a real withdraw and a real claim after the upgrade and checks the amounts received, not only that the calls succeed
  • Adds a storage-layout comparison that fails CI on any moved or retyped slot, and says how: the upgrades tooling's validation against V1, or the compiler's layout output for both contracts diffed
  • Asserts that an arbitrary address cannot call `initializeV2` at any point in the sequence, including between the script's transactions
  • Re-runs V1's invariant tests against the upgraded fork state
  • Pins the fork block and caches it, and says how an unavailable RPC provider is kept from blocking unrelated pull requests
  • Says what a failure blocks: the merge, and the Safe transactions generated from the same script

Follow-up questions

  • Your test deploys V2 behind a new proxy. What state is in that proxy?
  • Which stakers do you check, and how did you get their addresses?
  • Is the storage comparison something a person runs, or something that stops the merge?
  • Your fork test calls `upgradeToAndCall` directly. Is that what will happen on the day?
  • The RPC provider is down during CI. What happens to this pull request, and to one that only changes the docs?

Who can ship it, and how fast

18 min
What this section is for

Purpose

The key-custody read. Once a contract is upgradeable, its safety is only as good as the keys that can change it. The candidate says who really holds that power today, what stakers can do about a change they object to, and how an emergency can be fast without every change being fast.

Today an upgrade like this ships when the Safe approves it. Tell me who can actually make that happen, how long stakers get to react, and what you would change about the keys, the delay and the pause.

What this question is for, and what to listen for

Purpose

Tests whether the candidate reads a multisig as a threshold over people rather than as a number, and whether they design the power to change a contract as carefully as the contract itself. The deploy-bot signer is the discriminator: most candidates count three signers and stop.

Signals to score

  • Counts the real threshold: with a bot key in CI, one human plus a compromised or careless pipeline can upgrade
  • Takes the bot off the signer set, or limits it to proposing transactions that humans approve
  • Puts a timelock on upgrades with a delay longer than stakers need to exit, and says what that delay protects
  • Makes the timelock cover changes to itself and to the proxy's admin, not only upgrades
  • Separates a fast guardian that can only pause from a slow owner that can upgrade, each with its own keys
  • Bounds the pause: what it covers, that it expires unless renewed, and that exits stay open unless the incident runs through them
  • Has signers check decoded calldata on their devices rather than approving a hash shown in a browser
  • Monitors the proxy for queued and executed upgrades and admin changes, and announces queued upgrades publicly
  • Considers removing the upgrade right once the contract stops changing, and says what the team gives up

Follow-up questions

  • Three signers and a threshold of two. How many people have to be wrong for a malicious upgrade to go through?
  • Why not keep the bot as a signer and move its key into a hardware security module?
  • A critical bug is found on a Saturday and your timelock is 72 hours. How do stakers get protected?
  • What does a signer actually see when they approve this transaction?
  • Should this contract be upgradeable at all?

That is my side. What would you like to know about how changes to live contracts get approved here? Anything is fair — who signs, how long a queued upgrade waits, the last time we paused something and why.

What this section is for

Purpose

A candidate who has shipped upgrades asks about signers, delays and the last emergency; one who has not asks which audit firm we use. Offering the topics keeps the choice meaningful.

Before we finish, something true and not flattering: [name a real gap in how your team changes live contracts — a signer key nobody has rotated, an upgrade that shipped without a fork test, a pause nobody has rehearsed]. Fixing it would be part of the job.

What this section is for

Purpose

A concrete, current weakness is the best reason for the candidate this round is looking for to join, and an honest signal to one who is not. Confirm it is still accurate before saying it.

Blockchain Developer interviews — common questions

Who is this Blockchain Developer interview plan for?
It is written for the interviewer, not the candidate: the hiring manager, engineer or panel member running the Code review — the upgrade you cannot roll back round for a Blockchain Developer role. It gives you a 60 min script to follow in the conversation — 3 questions with what each one is for and the signals to score against — so you are not writing the round from scratch the night before.
What does the Code review — the upgrade you cannot roll back round assess?
This round is focused on: Reviewing a Solidity proxy upgrade before it ships: a storage layout collision, an initializer anyone can call first, a pause that blocks exits, and an upgrade key held partly by CI; testing the upgrade against the deployed contract on a fork; and deciding who may change a contract that users have funds in, and how fast.. It works through The review, A test against what is deployed and Who can ship it, and how fast, scoring against 28 observable signals, with follow-up prompts on all 3 questions for going deeper where an answer is thin.
How is the 60 min split up?
The review (20 min), A test against what is deployed (22 min), Who can ship it, and how fast (18 min). The timings are there so the round stays on schedule and every candidate gets the same shape of interview — which is what makes two candidates comparable afterwards.
What other rounds should I run for a Blockchain Developer?

A single round does not cover a whole role. The other rounds in this library for a Blockchain Developer: