Solidity & Smart Contract Development
In short: Solidity is a statically typed, contract-oriented programming language for writing smart contracts that run on the Ethereum Virtual Machine.
- The EVM is the sandboxed, deterministic, stack-based virtual machine that every Ethereum node runs to execute contract bytecode identically, metering each operation in gas.
- Solidity compiles to EVM bytecode plus an ABI used by clients to call contract functions.
In short: public — callable internally and externally (state variables get automatic getters).
external— callable only from outside the contract (or viathis.).internal— this contract and contracts that inherit from it (default for state variables).private— only this contract.- Note:
privatedoesn't hide data — all on-chain storage is publicly readable.
In short: view functions can read state but not modify it; pure functions neither read nor modify state (only use their inputs and local variables).
- Both are free when called off-chain via
eth_call, but cost gas when called from within a transaction. - The compiler enforces these restrictions.
In short: They're data locations.
storageis persistent contract state on the blockchain (expensive to write).memoryis temporary, modifiable data that exists for the duration of a function call.calldatais a read-only, non-persistent area holding the arguments of external calls — the cheapest choice for external function array/string/struct parameters.- Assigning storage to a local
storagevariable creates a reference; tomemorycreates a copy.
In short: msg.sender is the immediate caller of the current function — an EOA or another contract.
tx.originis always the EOA that originally signed the transaction.- Using
tx.originfor authorisation is a vulnerability: a malicious contract the user interacts with can call your contract and pass atx.origin == ownercheck. - Always use
msg.sender.
In short: Events (event Transfer(address indexed from, address indexed to, uint256 value)) emitted with emit are stored in transaction logs, not contract storage — much cheaper.
- Off-chain apps (frontends, indexers, The Graph, .NET backends) subscribe to and filter them (up to 3
indexedtopics). - Contracts can't read logs.
- Use them for activity history, notifications and indexing.
In short: When a contract makes an external call (e.g. sending Ether) before updating its own state, the recipient's code (fallback/receive or a token hook) can call back into the contract and repeat the action — for example withdrawing again before the balance is reduced (The DAO hack, 2016, ~3.6M ETH).
- Prevent it with the Checks-Effects-Interactions pattern (validate, update state, then call out), OpenZeppelin
ReentrancyGuard(nonReentrant), pull-payment designs, and care with token callbacks (ERC-777/721/1155 hooks).
In short: require(cond, "msg") validates inputs/conditions and reverts with an Error(string).
revert CustomError(args)(orrevert("msg")) aborts explicitly, often after anif.- Custom errors (0.8.4+) are cheaper (4-byte selector + args instead of strings) and carry structured data.
assert(cond)checks internal invariants that should never be false; failure raises a Panic error and indicates a bug.- All revert all state changes in the call.
In short: transfer forwards 2,300 gas and reverts on failure;
sendforwards 2,300 gas and returns false on failure;call{value: x}("")forwards all available gas (configurable) and returns (success, data).- Since gas costs changed (EIP-1884) and smart-contract wallets need more gas,
callwith a checked return value is recommended — combined with reentrancy protection and the pull-payment pattern.
In short: Arithmetic is checked by default: overflow/underflow reverts (Panic 0x11) instead of silently wrapping, making SafeMath unnecessary;
unchecked { }blocks opt out for gas savings where safe.- ABI coder v2 became the default.
- Later 0.8.x versions added custom errors (0.8.4), user-defined value types,
bytes.concat/string.concat, named mapping parameters, user-defined operators and transient storage (0.8.28).
In short: An interface declares only external function signatures (no state, no implementation, no constructor) — used to interact with other contracts and define standards (IERC20).
- An abstract contract can contain state and implemented functions but has at least one unimplemented function or is marked abstract — it can't be deployed, only inherited.
- A library holds reusable functions without state (internal functions are inlined;
- public ones are deployed and called via delegatecall), attached to types with
using X for Y.
In short: Reusable code that runs before and/or after a function body, where _ marks where the body executes.
- Typical uses: access control (
onlyOwner,onlyRole), state checks (whenNotPaused), reentrancy guards, input validation. - Example:
modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; }.
In short: A proxy contract holds the state and forwards calls using delegatecall to an implementation contract, which runs in the proxy's storage context;
- upgrading means pointing the proxy to a new implementation (Transparent, UUPS, Beacon patterns).
- Risks: storage layout collisions if variables are reordered, uninitialised implementations being taken over (use
_disableInitializers), forgetting to protect the upgrade function, a malicious or compromised admin (mitigate with multisig + timelock), and loss of the "code is law" guarantee for users.
In short: Reduce storage writes and reads (cache in memory/stack), pack variables into 32-byte slots, use constant/immutable, calldata for external inputs, custom errors, mappings instead of array searches, avoid unbounded loops, use unchecked where overflow can't happen, emit events instead of storing off-chain-only data, batch operations, transient storage for per-transaction flags, and tune the optimizer.
- Measure with gas reports/snapshots and never trade away safety.
In short: Everything on-chain is deterministic and visible: block.timestamp, blockhash and block.prevrandao can be predicted or influenced by validators/attackers, and users can revert transactions with unfavourable outcomes.
- Use a verifiable random function oracle (Chainlink VRF) that provides randomness with a cryptographic proof, or commit-reveal schemes among multiple parties.
In short: call executes code in the target contract's context (its storage; msg.sender becomes the caller contract) and can send Ether.
delegatecallexecutes the target's code in the caller's context (caller's storage,msg.senderandmsg.valuepreserved) — used by proxies and libraries;- dangerous with untrusted targets.
staticcallis like call but reverts on any state modification — used for view calls.
In short: Use OpenZeppelin ERC-20 and SafeERC20 (safeTransferFrom for USDC);
- handle decimals correctly (USDC has 6, your token 18);
- checks-effects-interactions plus
nonReentrant; - caps per wallet and a total cap;
- start/end times;
- pausable by a multisig;
- no reliance on spot prices;
- no unbounded loops (let users claim — pull, not push);
- withdrawal of raised funds only by the treasury multisig;
- events for every purchase;
- comprehensive unit/fuzz tests including edge cases (zero amounts, rounding);
- an external audit;
- and legal/compliance review (securities rules, KYC).
In short: Act immediately: if it has a pause or emergency-withdraw function controlled by a multisig, use it;
- otherwise consider a white-hat rescue (exploit it yourself first to move funds to safety, coordinated with security firms such as SEAL 911), notify users and integrators to stop interacting, revoke approvals guidance, coordinate with exchanges/bridges to freeze stolen funds where possible, then deploy a fixed contract, migrate state from a snapshot, compensate users if possible, and publish a post-mortem.
- Prevention: audits, bug bounties, pausability, rate limits and monitoring.
In short: Optimise the mint: avoid storing per-token data that can be computed or kept off-chain (use a base URI + token ID), use ERC721A or similar batch-mint-optimised implementations for multi-mints, pack storage, use custom errors, and remove loops over all holders;
- use a Merkle-proof allowlist instead of storing whitelists on-chain;
- deploy on a layer 2 (Base, Arbitrum) for much lower fees;
- and test gas usage with Foundry gas reports before launch.