Writing A First Smart Contract
You write, compile and deploy two small Solidity contracts using the Remix browser editor, first on a practice VM and then on the Sepolia testnet. You finish able to call your contract and read the results on a block explorer.
You Will Be Able To
- Read and write a basic Solidity contract with state variables, functions and events.
- Compile and deploy a contract in Remix on the Remix VM and on Sepolia.
- Call functions that read state for free and functions that change state with a transaction.
- Protect a function with a require check on msg.sender.
- Explain why deployed contracts cannot be edited and what that means for testing.
Where We Are And What We Will Build
In week 5 you learned that Ethereum stores programs called smart contracts at addresses, that the EVM runs them identically on every node and that each action costs gas. This week you write one. The language is Solidity, the most widely used language for the EVM, and the tool is Remix, a free editor that runs in your browser at remix.ethereum.org. Nothing needs installing.
We build two contracts. The first, Counter, stores a number that anyone can increase and that only its owner can reset. The second, TipJar, accepts ether from anyone and lets only its owner withdraw the balance. Together they cover the essentials: state, functions, events, access control and handling ether.
Use the learning wallet and Sepolia test ETH from week 5. All deployments in this module go to a test network, so mistakes cost nothing.
Solidity In Five Minutes
A Solidity file begins with a licence comment and a pragma line that states which compiler versions may build it. A contract is declared with the keyword contract and looks like a class in other languages. Inside it you declare state variables, which live in the contract's permanent storage, and functions, which read or change that state.
Common types include uint256 (an unsigned integer between 0 and a very large maximum), address (a 20-byte account address), bool and string. A mapping is a table from keys to values, for example mapping(address => uint256). Since version 0.8, arithmetic reverts on overflow, which removes an entire family of older bugs.
Functions have a visibility. External functions can be called from outside the contract. Public functions can be called from outside and inside. A function marked view promises not to change state, so calling it from outside costs no gas. A function marked payable can receive ether. Three global values appear constantly: msg.sender is the address that called the function, msg.value is the ether sent with the call, and block.timestamp is the time of the current block.
- State variables persist between calls and live in storage.
- view functions read state and are free to call from outside.
- payable functions can receive ether.
- require(condition, message) reverts the whole call when the condition is false.
- events write entries to the transaction log for apps and explorers to read.
| Type | Holds | Example |
|---|---|---|
| uint256 | Whole number, zero or more | A counter value |
| address | A 20 byte account address | msg.sender |
| bool | True or false | A paused flag |
| string | Text | A token name |
| mapping | Key to value lookup | Balance per address |
Solidity is statically typed, so every variable declares its type. Source: Solidity documentation: Types
Contract One: Counter
The Counter contract below stores a count and an owner. The constructor runs once, at deployment, and records the deployer as owner. The increment function adds one and emits an event. The reset function sets the count to zero but only when called by the owner. The public keyword on a state variable makes Solidity create a free getter function automatically.
Copy the code into a new file named Counter.sol in Remix. Type it if you can, so you notice each line. The statement msg.sender == owner inside require is the simplest form of access control: the network has already checked the caller's signature, so msg.sender is trustworthy, and the contract only compares it with a stored address.
- // SPDX-License-Identifier: MIT
- pragma solidity ^0.8.20;
- contract Counter { uint256 public count; address public owner; event Incremented(address indexed by, uint256 newCount);
- constructor() { owner = msg.sender; }
- function increment() external { count += 1; emit Incremented(msg.sender, count); }
- function reset() external { require(msg.sender == owner, "only owner"); count = 0; } }
Compile And Deploy In Remix
Open Remix and create Counter.sol in the file explorer. Go to the Solidity Compiler tab, choose a compiler version of 0.8.20 or higher that matches the pragma, and press Compile. A green check appears when the code is valid. Errors show line numbers, so read them closely: a missing semicolon is the most common first mistake.
Go to the Deploy and Run Transactions tab. The Environment menu chooses where the contract goes. Start with Remix VM, a simulated chain inside your browser. It has funded test accounts, confirms transactions instantly and costs nothing. Select Counter in the contract menu and press Deploy. The deployed contract appears at the bottom of the panel.
Expand it. The blue buttons, count and owner, are free reads. Press count and see 0. The orange button, increment, sends a transaction. Press it, then press count again and see 1. Open the terminal output to inspect the transaction, its gas used and the emitted event. Switch to a second account in the Account menu and press reset. The call fails with the message only owner.
- Write Solidity
- Compile to bytecode
- Deploy with a transaction
- Contract gets an address
- Call its functions
Remix performs the compile and deploy steps in the browser.
Contract Two: TipJar
TipJar teaches how a contract holds and moves ether. The tip function is payable, so it accepts ether, and it records how much each address has given. The withdraw function sends the full balance to the owner. A contract does not need to ask a bank to hold funds: its own balance is part of the world state.
Notice two details. The call to owner.call with a value is the recommended way to send ether in current Solidity, and the code checks the returned boolean and reverts if the transfer fails. And the contract uses require on msg.sender so only the owner can withdraw. Without that line, anyone could take the funds. Access control mistakes like this have caused real losses on main networks.
- contract TipJar { address public owner; mapping(address => uint256) public tips;
- constructor() { owner = msg.sender; }
- function tip() external payable { require(msg.value > 0, "send some ETH"); tips[msg.sender] += msg.value; }
- function withdraw() external { require(msg.sender == owner, "only owner");
- (bool ok, ) = owner.call{value: address(this).balance}(""); require(ok, "transfer failed"); } }
| Contract | Purpose | Key ideas |
|---|---|---|
| Counter | Store and change a number | State variable, functions |
| TipJar | Receive and withdraw ether | payable, require, owner check |
Two small contracts cover most first concepts. Source: Course contracts
Deploy To Sepolia
Once the contract behaves in the Remix VM, move to a real test network. In Remix, change Environment to Injected Provider with MetaMask. Your wallet asks permission to connect. Confirm that the network shown is Sepolia, and that your learning account holds test ETH.
Press Deploy. Your wallet shows a contract creation transaction. Look at the fields from week 5: there is no recipient because this is a creation, the data holds the compiled code, and the gas estimate is much higher than 21,000. Confirm, wait for inclusion and watch the deployed contract appear in Remix.
Copy the contract address and paste it into sepolia.etherscan.io. You will see your creation transaction, the contract's balance and its bytecode. Call increment from Remix, approve the transaction in the wallet and refresh the explorer to see the new transaction and the Incremented event in the logs. The explorer can also verify your source code so that visitors read your Solidity next to the bytecode. Use the Verify and Publish option if you wish.
Immutability And Early Testing Habits
A deployed contract's code cannot be edited. If you find a bug, you deploy a new contract at a new address and move on. This shapes how professionals work. They write automated tests, deploy first to test networks, request independent reviews called audits for contracts that hold real value and keep contracts small and simple.
Several habits help from the start. Keep the compiler pragma pinned to a version you tested. Check access control on every function that changes important state. Emit events for meaningful actions so that apps can follow them. Prefer well-reviewed libraries, such as OpenZeppelin Contracts, over writing standard pieces yourself. Treat any contract that holds ether as a target.
Remember what a contract cannot do. It cannot call a website, schedule itself to run later or hide its code and data from readers. Storage marked private is still readable by anyone who inspects the chain. Never place secrets in a contract.
What You Built And What Comes Next
In six weeks you went from the question of why digital money is hard to deploying code that runs on a public network. You can explain hashes, signatures, UTXOs, consensus and gas, and you have used a wallet, a faucet, an explorer, a Python script and a smart contract.
Your contracts store numbers and ether. The next step is a contract that stores something people treat as an asset: a token. Tokens are contracts that keep a table of balances and follow a shared interface so wallets and exchanges can work with them. Week 7 introduces tokens, starting with the ERC-20 standard, building directly on the mapping and function patterns you used here.
Key Terms
- Solidity
- A programming language for writing smart contracts that run on the EVM.
- Remix
- A free browser editor for writing, compiling, deploying and testing Solidity contracts.
- State variable
- A value declared in a contract that is saved in permanent storage between calls.
- msg.sender
- The address that called the current function.
- require
- A check that reverts the whole call with a message when its condition is false.
- Event
- A log entry emitted by a contract that apps and explorers can read.
- View function
- A function that reads state without changing it, so reading it from outside costs no gas.
- Payable
- A function marker that lets the function receive ether.
- Deployment
- The transaction that places a contract's code on the blockchain at a new address.
Exercise: Deploy Counter And TipJar, Then Call Them
- Open remix.ethereum.org, create Counter.sol, paste the Counter contract and compile it with Solidity 0.8.20 or higher.
- Deploy Counter on the Remix VM. Press increment three times, read count, then try reset from a second account and note the error message.
- Create TipJar.sol, compile it and deploy it on the Remix VM. Send 1 ether through the tip function using the Value field, then withdraw as the owner and watch the balance change.
- Switch Environment to Injected Provider with MetaMask on Sepolia and deploy Counter. Call increment and approve the transaction in your wallet.
- Open the contract address on sepolia.etherscan.io and find the creation transaction and the Incremented event. Record the gas used for deployment and for increment.
- Add a new function decrement that subtracts one but reverts with require when count is zero. Redeploy and test it on the Remix VM.
Further Reading
- Solidity documentation, Solidity team, docs.soliditylang.org (introduction and the Solidity by Example pages)
- Remix IDE documentation, Ethereum Foundation, remix-ide.readthedocs.io
- Mastering Ethereum, Andreas M. Antonopoulos and Gavin Wood, O'Reilly Media, chapters on smart contracts and Solidity
- OpenZeppelin Contracts documentation, OpenZeppelin, docs.openzeppelin.com/contracts
- Ethereum smart contract security best practices, ConsenSys Diligence, consensys.github.io/smart-contract-best-practices