
RustyChains
975 posts

RustyChains
@RustyChains101
Blockchain, Solidity, DeFi 🪿
Se unió Kasım 2024
687 Siguiendo1.1K Seguidores

Solidity Interview Q: What does codesize() return if called within the constructor? What about outside the constructor?
This is a nuanced EVM behavior with a real security implication
Outside the constructor (normal calls):
codesize() returns the size of the contract's deployed bytecode — the runtime code stored on-chain. For any deployed contract, this is always > 0
Inside the constructor:
codesize() returns the size of the initcode — the deployment bytecode, which includes the constructor logic itself plus the runtime code it will deploy. This is also > 0, but it's a different value entirely
The critical distinction: EXTCODESIZE vs. CODESIZE
EXTCODESIZE(addr) checks another address's deployed code size. A classic anti-contract check uses:
require(msg.sender.code.length == 0);
During a constructor call, the contract being deployed has no runtime code yet — EXTCODESIZE returns 0. So this check passes for contracts calling you from their constructor, silently breaking the "EOA only" assumption
codesize() (no argument) always refers to the currently executing code — initcode during deployment, runtime code afterward. It's not useful as an EOA check
The takeaway:
- codesize() inside constructor → size of initcode
- codesize() outside constructor → size of runtime bytecode
- EXTCODESIZE == 0 does NOT reliably prove the caller is an EOA
English

Solidity Interview Q: Without using the % operator, how can you determine if a number is even or odd?
One bitwise AND operation:
bool isOdd = (x & 1) == 1;
Here's why it works
All integers in binary have a least significant bit (LSB) — the rightmost bit. By definition:
- If LSB is 0 → the number is even
- If LSB is 1 → the number is odd
AND-ing any number with 1 isolates that LSB entirely, zeroing out all other bits:
5 = 0b...00000101 & 1 = 1 → odd
8 = 0b...00001000 & 1 = 0 → even
Beyond correctness, this matters for gas
The EVM's AND opcode costs 3 gas. The MOD opcode (%) also costs 5 gas — but more importantly, bitwise operations signal intent to the optimizer and are idiomatic in low-level EVM code
In Yul or heavily optimized Solidity, you'll see (x & 1) everywhere for this reason. It's not just a trick — it's the canonical low-level pattern for parity checks across virtually every computing environment, from embedded systems to the EVM
English

Solidity Interview Q: What is the danger of using return in assembly out of a Solidity function that has a modifier?
This is a subtle but critical trap that can silently break modifier logic
First, understand how modifiers work under the hood. Solidity compiles modifiers by inlining the function body at the position of the _ placeholder. The modifier's post-_ code runs after the function body returns
function foo() external myModifier {
// function body inlined here
}
modifier myModifier() {
// pre-checks
_; // function body executes here
// post-logic — runs after function body
}
Now here's the problem
A Solidity-level return respects this inlining. It exits the function body and hands control back to the modifier, which continues executing its post-_ logic
An assembly-level return does not
assembly { return(ptr, size) } is a raw EVM RETURN opcode. It immediately terminates the entire call frame — no cleanup, no continuation. The modifier's post-_ code never executes
This means:
- Reentrancy guards that reset a flag after _ - never reset, looks locked forever or worse
- Access control post-checks - silently skipped
- State cleanup logic - never runs
The contract appears to work correctly in testing — but under specific conditions, the modifier's guarantees are completely voided
Rule: never use assembly return inside a function with modifiers that have post-_ logic. Use Solidity-level returns, or restructure to avoid the conflict
English

Solidity Interview Q: Describe how to compute the 9th root of a number on-chain in Solidity
There's no native root opcode in the EVM. But you have two solid approaches
Option 1: Newton's Method (iterative approximation)
Newton's method converges on a root by iteratively refining a guess:
x_new = ((n-1) * x_old + val / x_old^(n-1)) / n
For the 9th root (n=9):
- Start with an initial guess (e.g. the value itself or a binary search estimate)
- Each iteration: x = (8 * x + val / x^8) / 9
- Repeat until x_new >= x_old (convergence)
In Solidity with fixed-point integers, this converges in ~100 iterations for uint256. Gas-intensive but fully on-chain and exact to integer precision
Option 2: Binary Search
Search for the largest integer x such that x^9 <= val:
- lo = 0, hi = val
- mid = (lo + hi) / 2
- If mid^9 <= val: lo = mid, else hi = mid
- Repeat until convergence
Computingmid^9 naively risks overflow — use mulmod or a checked exponentiation helper. Binary search is simpler to reason about but slower to converge than Newton's method
Option 3: Verify off-chain, check on-chain
The most gas-efficient pattern: compute the root off-chain, pass it in, and verify on-chain:
require(root**9 <= val && (root+1)**9 > val);
One exponentiation check instead of a full iterative loop. This is the production-grade approach for most protocols
For most on-chain use cases, verify-don't-compute is the right default
English

Solidity Interview Q: Why shouldn’t you get price from slot0 in Uniswap V3?
slot0 is the first storage slot of a Uniswap V3 pool. It contains sqrtPriceX96 — the current price of the pool. It's right there, cheap to read, and tempting to use
Don't
slot0 reflects the price after the last swap in the current block. That makes it trivially manipulable within a single transaction:
- Attacker takes a large flash loan
- Executes a massive swap, moving slot0 price significantly
- Calls your contract, which reads slot0 as "the price"
- Your protocol misprices collateral, mints excess tokens, or liquidates incorrectly
- Attacker reverses the swap and repays the flash loan
All in one transaction. No capital required. This exact vector has been exploited in multiple DeFi hacks
The correct alternative: use Uniswap V3's built-in TWAP oracle
observe() returns time-weighted average prices over a configurable window. A 30-minute TWAP requires an attacker to sustain price manipulation across many blocks — enormously expensive and almost always unprofitable
The tradeoff: TWAPs lag real-time price. For latency-sensitive use cases, consider Chainlink or a hybrid approach
Rule: slot0 is for internal pool logic. For any external price consumption — collateral, liquidations, minting — always use a TWAP or a dedicated oracle
English

Solidity Interview Q: What is SECP256K1?
SECP256K1 is the elliptic curve that underpins public/private key cryptography in both Bitcoin and Ethereum. Every wallet, every signature, every address derivation relies on it
The name breaks down as:
- SEC: Standards for Efficient Cryptography
- P: prime field
- 256: 256-bit key size
- K: Koblitz curve (a specific curve family)
- 1: first curve of this type in the standard
The curve is defined over a prime field by the equation:
y² = x³ + 7
This specific form (a=0, b=7) is what makes it a Koblitz curve — and gives it performance advantages over more generic curves like NIST's P-256
How it's used in Ethereum:
- Your private key is a random 256-bit integer
- Your public key is a point on the curve: pubKey = privKey × G (where G is the generator point)
- Your address is the last 20 bytes of keccak256(pubKey)
The security assumption: given a public key, computing the private key requires solving the elliptic curve discrete logarithm problem — computationally infeasible with current technology
One nuance: SECP256K1 was relatively obscure before Bitcoin. Most standards bodies favored NIST curves. Satoshi's choice of it — possibly to avoid curves with opaque NIST parameter selection — is one of the more consequential decisions in crypto history
English

Solidity Interview Q: What is a nullifier in the context of zero knowledge, and what is it used for?
In ZK protocols, a nullifier is a public value derived from a private secret that proves a specific action has occurred — without revealing who performed it or what the secret is
The classic problem it solves: double spending in privacy-preserving systems
Consider Zcash or Tornado Cash. A user deposits funds and receives a private note (a secret). Later they withdraw by proving, via a ZK proof, that they own a valid note. But without a nullifier, the same note could be used to withdraw multiple times — and since everything is private, no one would know
The nullifier fixes this:
- It's computed deterministically from the private note: nullifier = hash(secret)
- It's revealed publicly at withdrawal time
- The contract stores all used nullifiers
- If a nullifier was already seen - reject. If not - allow and record it
Critically, the nullifier reveals nothing about the original secret or the depositor's identity. It's just a unique fingerprint of the private note — unlinkable to the deposit transaction
Nullifiers are foundational in: private payments (Zcash, Tornado Cash), ZK identity systems, private voting, and anonymous credential schemes
Privacy without double-spend protection is broken. Nullifiers are what make it whole
English

Solidity Interview Q: What does a metaproxy do?
A metaproxy (EIP-3448) is a minimal proxy pattern — like EIP-1167 clones — but with one key upgrade: it appends arbitrary immutable data to the bytecode at deployment time
A standard EIP-1167 clone delegates all calls to a fixed implementation address. Every clone is identical. If you need instance-specific configuration, you'd have to store it in storage — which costs gas on every read
A metaproxy solves this by encoding instance-specific data directly into the contract's bytecode as a suffix. That data is read via CODECOPY, not SLOAD — making it cheaper to access and immutable by design
The deployed bytecode looks like:
[minimal proxy logic] + [ABI-encoded immutable data]
At runtime, the implementation contract reads its own code to retrieve the appended data:
address implementation = ...; // fixed in bytecode
bytes memory data = ...; // copied from code suffix
This is useful when:
- You're deploying thousands of instances with slightly different configs
- The config never changes after deployment
- You want to avoid per-instance storage costs
Real-world use cases: factory-deployed pools, per-user vaults, or any pattern where a single implementation serves many parameterized instances
eips.ethereum.org/EIPS/eip-3448
English

Solidity Interview Q: What is a zk-friendly hash function and how does it differ from a non-zk-friendly hash function?
Zero-knowledge proof systems (SNARKs, STARKs) don't execute code like the EVM does. They work by expressing computation as arithmetic circuits over a finite field — essentially systems of equations with additions and multiplications
Traditional hash functions like SHA-256 or Keccak-256 are designed for hardware and software efficiency. They rely heavily on bitwise operations (XOR, AND, bit shifts) — operations that are cheap on CPUs but translate into enormous arithmetic circuits. A single SHA-256 hash can require tens of thousands of constraints in a ZK circuit
ZK-friendly hash functions — Poseidon, MiMC, Pedersen — are designed from the ground up to minimize arithmetic constraints. They operate natively over the same prime fields ZK proof systems use, replacing bitwise ops with field additions and multiplications. The result: Poseidon uses ~200-300 constraints vs. ~25,000+ for SHA-256 in a ZK circuit
The tradeoff: ZK-friendly hashes are slower and less battle-tested outside ZK contexts. Keccak remains the standard for EVM compatibility
English

Solidity Interview Q: Why do negative numbers in calldata cost more gas?
It comes down to zero bytes vs. non-zero bytes
EIP-2028 defines calldata pricing:
- Zero bytes (0x00) cost 4 gas each
- Non-zero bytes cost 16 gas each
Small positive integers are cheap because they ABI-encode to mostly zero bytes. uint256(1) is:
0x0000000000000000000000000000000000000000000000000000000000000001
31 zero bytes + 1 non-zero byte = 31×4 + 1×16 = 140 gas
Negative integers, following two's complement, are padded with 0xFF — non-zero bytes. int256(-1) is:
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
32 non-zero bytes = 32×16 = 512 gas
That's 3.6× more expensive for the same 32-byte slot
Practical takeaway: if your protocol passes signed integers in calldata frequently, consider encoding strategies that minimize non-zero bytes — or reconsider whether signed types are necessary at all
English

Solidity Interview Q: What is the use of the signextend opcode?
The EVM natively operates on 256-bit words. But Solidity supports smaller signed integers: int8, int32, int128, and so on
When you load a small signed integer into a 256-bit stack slot, the upper bytes are zero-padded. That's fine for unsigned values — but wrong for negative signed ones
Consider int8(-1): in 8-bit two's complement it's 0xFF. But in a 256-bit word, 0x00...00FF is +255, not -1
SIGNEXTEND fixes this. It takes:
- b: the byte size minus 1 (e.g. 0 for int8, 1 for int16)
- x: the value to extend
And propagates the sign bit across all upper bytes:
0xFF -> 0xFFFF...FFFF (-1 in 256-bit two's complement)
Solidity emits SIGNEXTEND automatically when casting or operating on signed integer types smaller than 256 bits. You'll rarely call it directly in Yul — but it's what makes int8 arithmetic semantically correct at the EVM level
Without it, signed arithmetic on sub-256 types would silently produce wrong results
English

Solidity Interview Q: What does an int256 variable that stores -1 look like in hex?
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
That's 32 bytes — 64 F's. And it's not a coincidence.
Solidity uses two's complement to represent signed integers, just like most CPUs
In two's complement:
- Flip all bits of the positive value
- Add 1
For -1: flip all bits of 0x000...001 -> 0xFFF...FFE, then add 1 -> 0xFFF...FFF
The result is all bits set to 1 — which is -1 for any signed integer width, whether it's int8 or int256
This has practical implications in Solidity:
- type(int256).max == 0x7FFFFFFF...FFFF (MSB = 0)
- type(int256).min == 0x80000000...0000 (MSB = 1)
- -1 unchecked cast to uint256 == type(uint256).max
That last point is a classic source of bugs. If you ever cast a negative int to a uint without checking, you get a massive number — not an error
English

Solidity Interview Q: How much gas can be forwarded in a call to another smart contract?
By default, when you make an external call in Solidity, nearly all remaining gas is forwarded. But "nearly all" has a precise definition: EIP-150 (the 63/64 rule) caps it
The 63/64 rule: a call can forward at most 63/64 of the remaining gas. The caller always retains at least 1/64 as a safety buffer to handle the call returning
This applies recursively. Deep call stacks progressively reduce available gas at each level — which is exactly the point. It prevents call depth attacks that could drain gas entirely
You can also set gas explicitly:
- someContract.call{gas: 50000}(...) — forward exactly 50,000 gas
- transfer() and send() — hardcoded to 2,300 gas (enough for an event, nothing more)
One important edge case: if you manually specify more gas than the 63/64 limit allows, the EVM silently caps it. You won't get an error — you'll just forward less than you asked for
The takeaway: default calls forward ~all gas minus 1/64. Explicit gas limits give you control. And transfer()/send() are intentionally restrictive — which is why most modern contracts avoid them
English


Solidity Interview Q: What does the verbatim keyword do, and where can it be used?
verbatim is a Yul-only built-in that lets you inject raw bytecode directly into compiled output, bypassing the optimizer entirely
The syntax looks like this:
verbatim_2i_1o(hex"600a600b01", a, b)
The suffix encodes the stack effect: 2 inputs, 1 output. The optimizer treats it as a black box and won't touch it
Why does this matter?
The Solidity optimizer is powerful — but it can't reason about arbitrary bytecode. verbatim lets you insert opcodes that Yul doesn't natively expose, or guarantee exact bytecode output when it's critical (e.g. in interpreter loops or low-level precompile wrappers)
Constraints to know:
- Only available in Yul / inline assembly blocks, not in regular Solidity
- Not available in Solidity expressions — pure Yul only
- Stack inputs/outputs must be declared explicitly and match reality
- The optimizer will not reorder, remove, or simplify it
In practice, verbatim is a tool for compiler developers and protocol engineers working at the lowest level of the EVM — not something you'll reach for in everyday contract development
But knowing it exists tells you something important: Yul is designed to give you complete control, all the way down to the byte
English

Solidity Interview Q: Why did Solidity deprecate the “years” keyword?
It seems harmless — 1 years == 365 days, right?
The problem: that's not always true
Leap years exist. A calendar year is 365.25 days on average, meaning time-sensitive logic using "years" could silently drift by ~6 hours per year. For financial contracts or governance with year-long time locks, that's a real issue
Solidity's time unit keywords (seconds, minutes, hours, days, weeks) are all fixed-duration — unambiguous and safe. But "years" implied calendar accuracy it couldn't deliver
It was removed in Solidity 0.5.0 to prevent developers from building false assumptions into their contracts
The lesson: in smart contracts, time is just seconds. If you need calendar-aware logic, handle it explicitly — don't let a keyword do it for you
English

Solidity Interview Q: Under what conditions does the Openzeppelin Proxy.sol overwrite the free memory pointer? Why is it safe to do this?
In _delegate(), the entire function is written in raw assembly. It copies calldata directly to memory address 0x00, performs a delegatecall, then copies return data back to 0x00:
- calldatacopy(0, 0, calldatasize())
- delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
- returndatacopy(0, 0, returndatasize())
This deliberately ignores the free memory pointer at 0x40
Why is it safe?
- The function is 100% assembly. No Solidity variables or ABI encoding rely on 0x40 being valid
- Execution never returns to Solidity. The function always terminates with return() or revert() — so no Solidity code ever sees the corrupted pointer
- 0x00–0x3f is scratch space by design. The EVM memory layout reserves it for short-term use. It's only Solidity's allocator that needs 0x40 to be correct
The free memory pointer is a Solidity convention, not an EVM rule. In a fully self-contained assembly context, it simply doesn't apply
English

@RustyChains101 good work, keep going. we’re hiring DevRel ambassadors for paid campaigns. fill the form in my pinned post
English

Solidity Interview Q: How can you validate on-chain that another smart contract emitted an event, without using an oracle?
Short answer: not directly. EVM events live in transaction receipts, outside the state trie, so the EVM can't read them at runtime
But there are trustless alternatives:
- Read storage instead of events. If you control both contracts, expose a getter function. Events are for off-chain consumers; storage is for on-chain consumers
- Callback pattern. Have the source contract call back into your verifier at execution time. Simple and oracle-free
- Storage proofs. Prove the state of another contract at a specific block using Merkle-Patricia proofs. Used in bridges and cross-chain protocols
- ZK proofs. Tools like Axiom or Risc Zero let you prove historical on-chain execution and verify it trustlessly on-chain
The most common mistake? Treating events as an on-chain data source. They're not — they're logs for off-chain indexers
If you need cross-contract verification, design around state, not events
English
