Lesson 9 Notes
Learning Writing NatSpec (need to download Solidity extension by Juan Blanco)
use /// or /** + Enter/ to have a natspec template for writing clear information for a contract or function
| Tag | Use Case |
|---|---|
@title |
Short title for a contract/interface |
@author |
Who wrote the code |
@notice |
What users (end-users) should know about this item |
@dev |
Technical details for developers |
@param |
Document each parameter (@param name description) |
@return |
Document return values (@return valueName description) |
@inheritdoc |
Inherit NatSpec from parent (saves repetition) |
@custom:tag |
Custom tags (e.g. @custom:security) |
// Layout of Contract:
// version
// imports
// errors
// interfaces, libraries, contracts
// Type declarations
// State variables
// Events
// Modifiers
// Functions
// Layout of Functions:
// constructor
// receive function (if exists)
// fallback function (if exists)
// external
// public
// internal
// private
// internal & private view & pure functions
// external & public view & pure functions error ExampleRevert__Error();
function revertWithError() public pure { ---> costs 142 gas
if(false) {
revert ExampleRevert__Error();
}
}
function reverWithRequire() public pure { ---> costs 161 gas
if(false) {
require(true, "ExampleRevert__Error");
}
}
// more gas efficient using custom errors. (^0.8.4)Naming Practice: Name the error's prefix with your contract name.
if(msg.value < i_entranceFee) {
revert Raffle__SendMoreToEnterRaffle(); // ---> Contract: Raffle.sol
}The reason emitting events makes life easier for the frontend is because of how Ethereum (and other EVM chains) work:
-
Efficient "indexing":
- Events are logged in transaction receipts and stored in the blockchain log system.
- Unlike storage variables, events are indexed by topics, so you can quickly search/filter by event type or arguments.
- Example:
RaffleEnter(address indexed player)lets you query only entries from a specific player directly via an RPC provider (e.g. Alchemy, Infura, or TheGraph).
-
Cheaper to query than storage:
- Reading state (e.g.
s_players) requires a call to the contract → more expensive and sometimes requires off-chain parsing. - Events are designed for off-chain consumption, so they’re lightweight to fetch and filter.
- Reading state (e.g.
-
Frontend reactivity:
- Your dApp can "listen" for
RaffleEnterevents in real time (using ethers.js or wagmi hooks). (This is super important!) - This means the UI updates automatically when someone enters the raffle — without needing constant
calls to checks_players.
- Your dApp can "listen" for
-
Migration / transparency:
- If you redeploy the contract (new address), the historical log of who entered still exists in the event logs, even if
s_playersgets reset. - The frontend can reconstruct past activity from logs.
- If you redeploy the contract (new address), the historical log of who entered still exists in the event logs, even if
contract.on("RaffleEnter", (player) => {
console.log("New entry:", player);
updateUI();
});That way the frontend reacts instantly when someone calls the function — no need to poll the chain for changes.
In short: Emitting events = easy filtering, fast lookups, and reactive frontends. Without them, you’d have to constantly read contract storage (expensive + less reliable).
In Solidity, the indexed keyword marks event parameters so they get stored as topics in the transaction logs.
-
Each event log has:
- 1 topic = the event signature (
keccak256("EventName(type1,type2,...)")) - Up to 3 additional topics = values of
indexedparameters - All other parameters (non-indexed) are stored in the data field of the log
- 1 topic = the event signature (
So max = 4 topics total (event signature + 3 indexed params).
event RaffleEnter(address indexed player, uint256 indexed round, uint256 amount);playerandroundgo into topics (easy to filter by address or round number).amountgoes into the data field (you can still read it, but can’t filter directly by it when querying).
With indexed, the frontend can filter directly:
// Ethers.js example
contract.queryFilter(
contract.filters.RaffleEnter("0x1234..."), // filter by player
fromBlock,
toBlock
);// Wagmi Example
import { useEffect } from "react";
import { useWatchContractEvent } from "wagmi";
const contractConfig = {
address: "0xYourContractAddress",
abi: [
{
type: "event",
name: "RaffleEnter",
inputs: [
{ name: "player", type: "address", indexed: true },
{ name: "round", type: "uint256", indexed: true },
{ name: "amount", type: "uint256", indexed: false },
],
},
],
};
export function RaffleListener() {
useWatchContractEvent({
...contractConfig,
eventName: "RaffleEnter",
onLogs(logs) {
logs.forEach((log) => {
console.log(" New entry:", log.args);
});
},
});
return <div>Listening for raffle entries...</div>;
}If amount were indexed → you could filter by it too, but remember only 3 params can be indexed.
- Index identifiers: e.g.
address,id,round - Don’t index large data like
stringorbytes(costly in gas) - Use non-indexed params for values you’ll usually just read, not filter by
// In constructor: s_lastTimeStamp = block.timestamp;
// This locks in the deployment timestamp as the starting point. So s_lastTimeStamp is “when the raffle opened.”
bool timePassed = (block.timestamp - s_lastTimeStamp) > i_interval;So, let say i_interval be 1000 seconds.
- At block.timestamp (1550) - s_lastTimeStamp (1000) = 550 seconds (timePassed False -> not triggering Upkeep)
- At block.timestamp (2550) - s_lastTimeStamp (1000) = 1550 seconds (timePassed True -> triggering Upkeep)
After upkeep runs, you reset:
s_lastTimeStamp = block.timestamp;