Unit 5: Solidity - Subjective Questions
CSC307 — Blockchain Architecture And Design-I • Practice Questions with Detailed Answers
20 questions
What is Remix IDE? Explain the steps involved in accessing and setting up Remix IDE as an online Solidity development environment.
Remix IDE is a powerful, open-source web and desktop application used for developing, deploying, debugging, and testing smart contracts written in Solidity. It is maintained by the Ethereum Foundation and requires no installation when used online.
Steps to access and set up Remix IDE:
- Open the browser and navigate to
https://remix.ethereum.org. - The IDE loads directly in the browser; no installation is required for the online version.
- File Explorer Panel: Create a new workspace or use the default one, and add a new file with a
.solextension. - Solidity Compiler Plugin: Activate it from the plugin manager to compile the contract. Select the appropriate compiler version matching the
pragmastatement. - Deploy & Run Transactions Plugin: Choose an environment such as:
- Remix VM (JavaScript VM) for local testing
- Injected Provider (MetaMask) for test/main networks
- External HTTP Provider for a local node
- Compile the contract using the Solidity Compiler, then deploy it.
- Interact with the deployed contract through the auto-generated UI buttons.
Key advantages:
- Browser-based, zero configuration
- Built-in debugger and static analysis
- Direct integration with MetaMask for real network deployment
Describe the main panels and features of the Remix IDE interface that assist in smart contract development.
Remix IDE provides an integrated environment with several specialized panels and features:
- File Explorer: Manages workspaces, folders, and
.solsource files. Supports creating, importing, and organizing project files. - Solidity Compiler: Allows selection of compiler version, enables auto-compilation, and shows warnings/errors. Generates the ABI and bytecode.
- Deploy & Run Transactions: Lets users select the execution environment (Remix VM, Injected Provider, etc.), set gas limits, choose accounts, and deploy contracts.
- Debugger: Steps through transactions opcode-by-opcode to inspect state changes, stack, memory, and storage.
- Terminal/Console: Displays transaction logs, errors, and allows running Web3/Ethers.js scripts.
- Plugin Manager: Enables adding features like static analyzers, unit testing, and flattener tools.
- Static Analysis Plugin: Detects security vulnerabilities and bad coding practices.
Benefits:
- All-in-one environment for coding, compiling, deploying, and debugging
- Immediate feedback through warnings and gas estimations
- Simplifies learning for beginners while supporting advanced workflows
Define a token standard. Why are token standards important in the Ethereum ecosystem?
A token standard is a set of predefined rules and function signatures (an interface) that a smart contract must implement so that tokens created on a blockchain behave in a consistent and interoperable way.
Importance of token standards:
- Interoperability: Wallets, exchanges, and dApps can interact with any token following the standard without custom code.
- Predictability: Developers know exactly which functions and events a token must support (e.g.,
transfer,balanceOf). - Reusability: Standard libraries (like OpenZeppelin) provide tested, secure implementations.
- Ecosystem Growth: Easy listing on exchanges and integration promotes adoption.
- Security: Well-audited standards reduce vulnerabilities.
Common Ethereum token standards:
- ERC-20: Fungible tokens (all units identical).
- ERC-721: Non-fungible tokens (unique, indivisible).
- ERC-1155: Multi-token standard supporting both fungible and non-fungible tokens.
Without standards, every token would require custom integration, making the ecosystem fragmented and difficult to scale.
Explain the ERC-20 token standard in detail, listing its mandatory functions and events.
The ERC-20 (Ethereum Request for Comments 20) standard defines a common interface for fungible tokens, where each token unit is identical and interchangeable (e.g., currencies, utility tokens).
Mandatory Functions:
totalSupply()— Returns the total number of tokens in existence.balanceOf(address account)— Returns the token balance of a given account.transfer(address to, uint256 amount)— Transfers tokens from caller to another address.approve(address spender, uint256 amount)— Allows a spender to withdraw tokens on behalf of the owner.allowance(address owner, address spender)— Returns the remaining number of tokens a spender is allowed to use.transferFrom(address from, address to, uint256 amount)— Transfers tokens on behalf of the owner using the allowance mechanism.
Mandatory Events:
Transfer(address indexed from, address indexed to, uint256 value)— Emitted when tokens are transferred.Approval(address indexed owner, address indexed spender, uint256 value)— Emitted whenapproveis called.
Optional Fields: name, symbol, and decimals improve usability.
Use cases: Stablecoins, governance tokens, utility tokens, and ICO tokens.
Explain the ERC-721 token standard and describe how it enables Non-Fungible Tokens (NFTs).
The ERC-721 standard defines an interface for Non-Fungible Tokens (NFTs), where each token is unique and non-interchangeable. Each token has a distinct tokenId that distinguishes it from all others.
Key Characteristics:
- Each token is unique and indivisible (cannot be split into fractions).
- Suitable for digital art, collectibles, real estate titles, gaming assets, and certificates.
Core Functions:
balanceOf(address owner)— Number of NFTs owned by an address.ownerOf(uint256 tokenId)— Returns the owner of a specific token.safeTransferFrom(address from, address to, uint256 tokenId)— Safely transfers ownership.transferFrom(address from, address to, uint256 tokenId)— Transfers token ownership.approve(address to, uint256 tokenId)— Approves another address to transfer a specific token.setApprovalForAll(address operator, bool approved)— Approves an operator for all tokens.getApproved(uint256 tokenId)— Returns approved address for a token.
Events: Transfer, Approval, ApprovalForAll.
Metadata Extension (ERC-721 Metadata): Provides name(), symbol(), and tokenURI() which links each token to its metadata (image, attributes) typically stored on IPFS.
This uniqueness enables provable ownership and scarcity of digital assets.
Distinguish between ERC-20 and ERC-721 token standards with suitable examples.
Both ERC-20 and ERC-721 are Ethereum token standards, but they serve fundamentally different purposes.
| Feature | ERC-20 | ERC-721 |
|---|---|---|
| Token Type | Fungible | Non-Fungible |
| Uniqueness | All tokens identical and interchangeable | Each token unique with a distinct tokenId |
| Divisibility | Divisible (supports decimals) |
Indivisible (whole units only) |
| Identification | Tracked by balance (balanceOf) |
Tracked by tokenId and ownerOf |
| Use Cases | Currencies, utility tokens, stablecoins | Digital art, collectibles, game items |
| Key Function | transfer(to, amount) |
safeTransferFrom(from, to, tokenId) |
| Metadata | Optional name, symbol, decimals |
tokenURI for unique metadata per token |
Examples:
- ERC-20: USDT, DAI, UNI tokens where 1 token equals any other token of the same type.
- ERC-721: CryptoPunks, Bored Ape Yacht Club NFTs where each item is distinct and may have different value.
Summary: ERC-20 is about quantity (how many tokens), while ERC-721 is about identity (which specific token).
Explain the structure of a basic Solidity smart contract with an example illustrating state variables, constructor, and functions.
A Solidity smart contract is a self-contained program deployed on the Ethereum blockchain. Its structure typically includes a license identifier, a pragma directive, the contract declaration, state variables, a constructor, and functions.
Example:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
// State variable (stored on blockchain)
uint256 private storedData;
address public owner;
// Constructor runs once at deployment
constructor() {
owner = msg.sender;
}
// Function to write data
function set(uint256 x) public {
storedData = x;
}
// Function to read data (view - no gas when called externally)
function get() public view returns (uint256) {
return storedData;
}
}
Components explained:
- SPDX License & Pragma: Specify licensing and compiler version.
- State Variables:
storedData,owner— permanently stored in contract storage. - Constructor: Executes once at deployment, here setting the deployer as
owner. - Functions:
setmodifies state (costs gas),getis aviewfunction that only reads state. - Visibility specifiers:
public,privatecontrol access.
Describe the various data types available in Solidity with examples.
Solidity is a statically typed language, so the type of each variable must be declared. Data types are broadly classified as value types and reference types.
1. Value Types (passed by value):
- Boolean:
bool isActive = true;— holdstrue/false. - Integer:
int(signed) anduint(unsigned), e.g.,uint256 count = 100;. Sizes from 8 to 256 bits. - Address:
address owner;— holds a 20-byte Ethereum address.address payablecan receive Ether. - Bytes (fixed):
bytes32 hash;— fixed-size byte arrays. - Enum: User-defined type:
enum State { Active, Inactive }.
2. Reference Types (passed by reference, require data location):
-
Arrays: Fixed
uint[5] fixedArr;or dynamicuint[] dynamicArr;. -
Strings:
string name = "Alice";— dynamic UTF-8 data. -
Structs: Custom grouped data:
solidity
struct Student { string name; uint age; } -
Mappings: Key-value storage:
mapping(address => uint) balances;.
Data Location Keywords: storage (persistent), memory (temporary), and calldata (read-only function arguments).
Choosing the smallest sufficient type (e.g., uint8 vs uint256) can help optimize storage in structs.
Explain function visibility specifiers in Solidity: public, private, internal, and external.
Visibility specifiers in Solidity control who can call a function or access a state variable. Choosing the correct visibility is critical for security.
-
public:- Accessible internally (within the contract), by derived contracts, and externally.
- For state variables, the compiler auto-generates a getter function.
- Example:
function getData() public view returns (uint) { ... }
-
private:- Accessible only within the contract that defines it.
- Not accessible by derived (child) contracts.
- Note: private does not mean the data is hidden on-chain; it is still readable from blockchain storage.
-
internal:- Accessible within the current contract and derived contracts.
- Cannot be called from outside. Default for state variables.
-
external:- Can be called only from outside the contract (or via
this.function()). - More gas-efficient for large array arguments as data comes from
calldata.
- Can be called only from outside the contract (or via
Summary Table:
| Specifier | Within Contract | Derived Contracts | External |
|---|---|---|---|
| public | Yes | Yes | Yes |
| private | Yes | No | No |
| internal | Yes | Yes | No |
| external | No* | No | Yes |
(external functions can be called internally only via this.*)
What are function modifiers in Solidity? Explain with an example how they are used to enforce access control.
A function modifier in Solidity is a reusable piece of code that can change the behavior of functions, typically used to add preconditions such as access checks before the function body executes.
Key points:
- Modifiers help avoid code duplication.
- The special symbol
_;indicates where the modified function's body is inserted.
Example — Access Control:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Owned {
address public owner;
constructor() {
owner = msg.sender;
}
// Modifier definition
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_; // function body executes here
}
function changeOwner(address newOwner) public onlyOwner {
owner = newOwner;
}
}
Explanation:
- The
onlyOwnermodifier checks that the caller is the owner usingrequire. - If the condition fails, the transaction reverts and the function body never runs.
changeOwnerapplies the modifier, so only the owner can change ownership.
Common uses: access restriction, input validation, reentrancy guards, and state checks (e.g., whenNotPaused).
Explain the concepts of require, assert, and revert in Solidity error handling. When should each be used?
Solidity provides three main error-handling mechanisms to validate conditions and revert transactions when something goes wrong. All of them undo state changes on failure.
-
require(condition, "message"):- Used to validate inputs, conditions, and external call results.
- If the condition is
false, it reverts and refunds remaining gas. - Example:
require(msg.value > 0, "Must send Ether"); - Use for: validating user inputs and preconditions.
-
assert(condition):- Used to check for internal errors and invariants that should never be false.
- A failing
assertindicates a bug in the contract. - In older versions it consumed all gas; modern versions revert.
- Use for: checking invariants (e.g., overflow conditions that should never occur).
-
revert("message")/revert CustomError():-
Explicitly aborts execution and reverts state, often inside complex
ifconditions. -
Example:
solidity
if (balance < amount) {
revert("Insufficient balance");
} -
Use for: complex conditional logic where
requireis less readable.
-
Summary:
- Use
requirefor input/condition validation. - Use
assertfor internal consistency/invariants. - Use
revertfor complex custom error flows.
Custom errors (Solidity ≥0.8.4) are gas-efficient: error Unauthorized();
What are events in Solidity? Explain their purpose and how they are used in smart contracts and user interfaces.
Events in Solidity provide a way for smart contracts to log information to the blockchain and notify external applications (such as front-end UIs) when something happens.
Purpose:
- Enable logging of significant actions (transfers, state changes).
- Allow off-chain applications to listen and react to on-chain activity.
- Provide a cheaper storage option — logged data is stored in transaction logs, not expensive contract storage.
Declaration and Emission:
solidity
contract Bank {
event Deposit(address indexed sender, uint256 amount);
function deposit() public payable {
// emit the event
emit Deposit(msg.sender, msg.value);
}
}
Key features:
- The
indexedkeyword (up to 3 parameters) allows efficient filtering/searching of logs by that parameter. - Events cannot be read back by smart contracts; they are for off-chain consumption.
Usage in User Interfaces:
-
Front-end libraries like Web3.js or Ethers.js subscribe to events:
javascript
contract.on("Deposit", (sender, amount) => {
console.log(sender, amount);
}); -
This lets dApps update the UI in real time, show transaction history, and provide user feedback.
Explain the complete lifecycle of smart contract management from writing code to deployment and interaction. (10 marks)
Smart contract management encompasses the entire lifecycle of a contract — development, testing, deployment, interaction, and maintenance.
1. Development (Writing):
- Write the contract in Solidity using an IDE such as Remix.
- Define state variables, functions, modifiers, and events.
- Follow secure coding standards (use OpenZeppelin libraries).
2. Compilation:
- The Solidity compiler (
solc) converts source code into:- Bytecode — executed by the Ethereum Virtual Machine (EVM).
- ABI (Application Binary Interface) — JSON describing functions/events for front-end interaction.
3. Testing:
- Test on a local blockchain (Remix VM, Ganache) or testnets (Sepolia).
- Write unit tests to verify correctness and edge cases.
- Run static analysis to detect vulnerabilities.
4. Deployment:
- Submit a deployment transaction containing the bytecode.
- The transaction, once mined, creates a contract account with a unique address.
- Deployment costs gas paid in ETH.
5. Interaction:
- Call functions via the ABI using Remix, Web3.js, or Ethers.js.
- Read (view) calls are free; write transactions cost gas and modify state.
6. Monitoring & Maintenance:
- Listen to emitted events to track activity.
- Since deployed code is immutable, upgrades require patterns like Proxy contracts or redeployment.
- Use
selfdestruct(deprecated/discouraged) or pausable patterns for emergency control.
Key Challenge: Immutability means bugs cannot be directly fixed, making thorough testing and upgradeability patterns essential.
Describe how a user interface (front-end) connects to and interacts with a smart contract. Explain the role of Web3.js/Ethers.js and the ABI.
A User Interface (UI) or decentralized application (dApp) front-end allows end users to interact with smart contracts without writing code. The connection relies on JavaScript libraries, a provider, and the contract's ABI.
Key Components:
-
ABI (Application Binary Interface):
- A JSON file generated at compilation describing the contract's functions, inputs, outputs, and events.
- Acts as the "instruction manual" the UI uses to encode/decode calls.
-
Provider:
- A connection to an Ethereum node (e.g., MetaMask, Infura, Alchemy).
- Allows reading blockchain data.
-
Signer:
- Represents the user's account (via MetaMask) used to sign and send transactions.
-
Web3.js / Ethers.js:
- JavaScript libraries that bridge the UI and the blockchain.
Interaction Flow:
import { ethers } from "ethers";
// 1. Connect to MetaMask
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const signer = provider.getSigner();
// 2. Create contract instance
const contract = new ethers.Contract(contractAddress, abi, signer);
// 3. Call a read function (free)
const value = await contract.get();
// 4. Send a write transaction (costs gas)
const tx = await contract.set(42);
await tx.wait();Steps summarized:
- UI connects to a wallet (MetaMask) for the provider/signer.
- A contract object is built from the address + ABI.
- Read calls fetch data; write calls prompt the user to sign and pay gas.
- The UI listens to events to update dynamically.
Explain the difference between view, pure, and payable functions in Solidity with examples.
Solidity uses these function state-mutability keywords to describe how functions interact with the blockchain state and Ether.
1. view functions:
- Can read state variables but cannot modify them.
- Do not cost gas when called externally (off-chain).
solidity
uint public count = 5;
function getCount() public view returns (uint) {
return count; // reads state
}
2. pure functions:
- Cannot read or modify state. Operate only on inputs.
- Used for calculations/utility logic.
solidity
function add(uint a, uint b) public pure returns (uint) {
return a + b; // no state access
}
3. payable functions:
- Can receive Ether along with the function call.
- Without
payable, sending Ether to a function reverts.
solidity
function deposit() public payable {
// msg.value holds the Ether sent
}
Comparison Table:
| Keyword | Read State | Modify State | Receive Ether |
|---|---|---|---|
| view | Yes | No | No |
| pure | No | No | No |
| payable | Yes | Yes | Yes |
| (default) | Yes | Yes | No |
Significance: Using view/pure improves clarity, allows the compiler to optimize, and signals intent to callers.
Write a simple ERC-20 token contract in Solidity (using OpenZeppelin) and explain each part. (10 marks)
A simple ERC-20 token can be created efficiently using the audited OpenZeppelin library, which implements the standard's logic.
Example Contract:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Import OpenZeppelin's ERC-20 implementation
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
// Constructor sets name, symbol, and mints initial supply
constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
_mint(msg.sender, initialSupply * (10 ** decimals()));
}
}
Explanation of each part:
- SPDX & Pragma: Declares license and compiler version.
- Import: Brings in the standard ERC-20 implementation, so functions like
transfer,balanceOf, andapproveare inherited automatically. contract MyToken is ERC20: Inherits all ERC-20 functionality.- Constructor:
ERC20("MyToken", "MTK")sets the token name and symbol._mint(msg.sender, ...)creates the initial supply and assigns it to the deployer.decimals()defaults to 18, so the supply is scaled by .
Inherited Functions available:
totalSupply(),balanceOf(),transfer(),approve(),allowance(),transferFrom().
Advantages of using OpenZeppelin:
- Secure, audited, and standard-compliant.
- Reduces development time and risk of bugs.
- Easy to extend with features like
Ownable,Pausable, andBurnable.
Explain the concept of mapping in Solidity. How is it used to maintain account balances in a token contract?
A mapping in Solidity is a reference data type that stores data as key-value pairs, similar to a hash table or dictionary.
Syntax:
solidity
mapping(KeyType => ValueType) visibility mappingName;
Characteristics:
- Keys can be value types (e.g.,
address,uint), but not reference types. - Values can be any type, including structs and other mappings.
- All possible keys exist and map to a default (zero) value until set.
- Mappings cannot be iterated directly (no length, no key list).
- They can only be stored in storage (state variables).
Use in a Token Contract (tracking balances):
solidity
contract Token {
mapping(address => uint256) public balances;
function mint(address to, uint256 amount) public {
balances[to] += amount;
}
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
Explanation:
balancesmaps eachaddressto the number of tokens it holds.balances[msg.sender]quickly retrieves a user's balance in time.- The public mapping auto-generates a getter so the UI can query any address's balance.
Nested mapping example (used for ERC-20 allowances):
solidity
mapping(address => mapping(address => uint256)) public allowance;
What is inheritance in Solidity? Explain how contracts can inherit from one another with an example.
Inheritance in Solidity allows a contract (child/derived) to acquire the properties and behavior (state variables and functions) of another contract (parent/base). This promotes code reuse and modular design.
Key points:
- Use the
iskeyword to inherit. - Solidity supports multiple inheritance (C3 linearization resolves ordering).
- Functions marked
virtualcan be overridden; overriding functions useoverride. internalandpublicmembers are accessible to child contracts;privateare not.
Example:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Base contract
contract Animal {
function sound() public pure virtual returns (string memory) {
return "Some sound";
}
}
// Derived contract
contract Dog is Animal {
// Override the base function
function sound() public pure override returns (string memory) {
return "Bark";
}
}
Explanation:
Dog is AnimalmeansDoginherits fromAnimal.- The
virtualkeyword in the base allows overriding. - The
overridekeyword inDogreplaces the base behavior.
Real-world use: Standards like OpenZeppelin's ERC20, Ownable, and Pausable are inherited to build feature-rich contracts quickly, e.g., contract MyToken is ERC20, Ownable { ... }.
Explain the concept of gas in Ethereum. Why is gas important when managing and deploying smart contracts?
Gas is the unit that measures the amount of computational effort required to execute operations on the Ethereum network. Every operation (computation, storage, transaction) consumes gas.
Key Concepts:
- Gas Units: Each EVM opcode has a fixed gas cost (e.g.,
SSTOREfor writing to storage is expensive). - Gas Price: Amount of ETH (in Gwei) the user is willing to pay per unit of gas.
- Transaction Fee: Calculated as:
- Gas Limit: The maximum gas a user allows a transaction to consume, preventing infinite loops from draining funds.
Why gas is important in smart contract management:
- Prevents abuse: Stops infinite loops and spam by making every computation cost money.
- Deployment cost: Deploying a contract consumes significant gas proportional to its bytecode size; larger contracts cost more.
- Optimization matters: Developers minimize storage writes, use efficient data types, and prefer
calldata/memoryappropriately to reduce costs. - Transaction prioritization: Higher gas prices get transactions mined faster.
- Out-of-gas errors: If gas runs out mid-execution, the transaction reverts but gas is still consumed.
Example optimization: Using uint256 packed structs, avoiding redundant storage reads, and using events instead of storage for logs all reduce gas usage.
Discuss the common security vulnerabilities in Solidity smart contracts and best practices to prevent them. (10 marks)
Because smart contracts are immutable and often manage valuable assets, security is paramount. Below are common vulnerabilities and their mitigations.
1. Reentrancy Attack:
- An external contract calls back into the vulnerable function before the first execution completes, draining funds (e.g., the DAO hack).
- Prevention: Use the Checks-Effects-Interactions pattern (update state before external calls) and
ReentrancyGuard.
2. Integer Overflow/Underflow:
- Arithmetic exceeding the type's range wraps around.
- Prevention: Solidity ≥0.8 reverts automatically; otherwise use SafeMath.
3. Access Control Issues:
- Sensitive functions callable by anyone.
- Prevention: Use modifiers like
onlyOwnerand role-based access (OpenZeppelinAccessControl).
4. tx.origin Authentication:
- Using
tx.originfor auth is phishable. - Prevention: Use
msg.senderinstead.
5. Unchecked External Calls:
- Low-level
callreturn values ignored. - Prevention: Always check return values; handle failures.
6. Denial of Service (DoS):
- Unbounded loops or failing external calls block functions.
- Prevention: Use pull-over-push for payments; avoid loops over dynamic arrays.
7. Front-Running:
- Attackers observe pending transactions and insert their own with higher gas.
- Prevention: Use commit-reveal schemes.
Best Practices Summary:
- Use audited libraries (OpenZeppelin).
- Follow Checks-Effects-Interactions.
- Conduct thorough testing and third-party audits.
- Keep contracts simple and use static analysis tools (Slither, MythX).
- Implement upgradeability (proxy) and pausable patterns for emergencies.
Security-focused design is essential since deployed bugs cannot be easily fixed.
What is Remix IDE? Explain the steps involved in accessing and setting up Remix IDE as an online Solidity development environment.
Remix IDE is a powerful, open-source web and desktop application used for developing, deploying, debugging, and testing smart contracts written in Solidity. It is maintained by the Ethereum Foundation and requires no installation when used online.
Steps to access and set up Remix IDE:
- Open the browser and navigate to
https://remix.ethereum.org. - The IDE loads directly in the browser; no installation is required for the online version.
- File Explorer Panel: Create a new workspace or use the default one, and add a new file with a
.solextension. - Solidity Compiler Plugin: Activate it from the plugin manager to compile the contract. Select the appropriate compiler version matching the
pragmastatement. - Deploy & Run Transactions Plugin: Choose an environment such as:
- Remix VM (JavaScript VM) for local testing
- Injected Provider (MetaMask) for test/main networks
- External HTTP Provider for a local node
- Compile the contract using the Solidity Compiler, then deploy it.
- Interact with the deployed contract through the auto-generated UI buttons.
Key advantages:
- Browser-based, zero configuration
- Built-in debugger and static analysis
- Direct integration with MetaMask for real network deployment
Did this save you a night before the exam?
LPU Notes is free, and it stays free. Ads cover part of the server bill. The rest comes out of a student's own pocket: the domain, the storage, and keeping the site up through the weeks everyone needs it at once.
The payment button didn't load. An ad blocker or a filtered network is the usual reason. to try again.
Nothing here is ever locked, and nothing unlocks. Chip in only if it was worth it. What it pays for →