Smart contracts are self-executing programs deployed on a blockchain that automatically enforce the terms of an agreement when predefined conditions are met. On Ethereum, smart contracts are written in Solidity, a statically-typed language designed specifically for the Ethereum Virtual Machine (EVM). At StrikingWeb, we have been building smart contracts for DeFi platforms, token launches, and enterprise blockchain solutions. Here is what you need to know to get started.
Understanding the Ethereum Virtual Machine
Before writing Solidity code, it is essential to understand the environment your code will run in. The EVM is a sandboxed, deterministic virtual machine that executes smart contract bytecode across every node in the Ethereum network. Key characteristics include:
- Immutability: Once deployed, smart contract code cannot be changed. Bugs are permanent unless you design upgrade mechanisms in advance.
- Gas costs: Every computation and storage operation costs gas (paid in ETH). Inefficient code literally costs money, making optimisation critical.
- Determinism: The same input must always produce the same output across all nodes. This means no random number generation, no external API calls, and no floating-point arithmetic.
- Storage: Persistent storage on the blockchain is extremely expensive. Storing data on-chain should be minimised wherever possible.
Solidity Basics
Solidity's syntax is influenced by JavaScript, C++, and Python. Here is a simple example of a token contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleToken {
string public name = "SimpleToken";
string public symbol = "STK";
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * 10 ** uint256(decimals);
balanceOf[msg.sender] = totalSupply;
}
function transfer(address _to, uint256 _value) public returns (bool) {
require(balanceOf[msg.sender] >= _value, "Insufficient balance");
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
}
Key Concepts
- State variables are stored permanently on the blockchain. Use them sparingly due to gas costs.
- Functions can be
public,private,internal, orexternal. View and pure functions do not modify state and cost no gas when called externally. - Modifiers add reusable preconditions to functions. The common
onlyOwnerpattern restricts access to the contract deployer. - Events emit logs that external applications can listen to. They are far cheaper than storage and are the primary way DApps track contract activity.
- Mappings provide key-value storage. Unlike arrays, you cannot iterate over mappings -- they provide O(1) lookup by key.
Development Environment Setup
A modern Solidity development workflow typically uses one of two frameworks:
- Truffle: The original Ethereum development framework, offering compilation, migration scripts, testing with Mocha, and integration with Ganache for local blockchain simulation.
- Hardhat: A newer alternative that has gained rapid adoption thanks to its superior debugging experience (stack traces for Solidity errors), flexible plugin system, and built-in Hardhat Network for local testing.
At StrikingWeb, we currently prefer Hardhat for new projects due to its better developer experience and more active development pace. A basic Hardhat project structure looks like this:
my-contract/
contracts/ # Solidity source files
scripts/ # Deployment scripts
test/ # Test files
hardhat.config.js
Testing Smart Contracts
Testing is non-negotiable for smart contracts. Unlike traditional software, deployed contracts cannot be patched, and bugs can result in permanent loss of funds. Write tests for every function, every edge case, and every access control scenario.
const { expect } = require("chai");
describe("SimpleToken", function () {
let token, owner, addr1;
beforeEach(async function () {
const Token = await ethers.getContractFactory("SimpleToken");
[owner, addr1] = await ethers.getSigners();
token = await Token.deploy(1000000);
});
it("Should assign total supply to owner", async function () {
const ownerBalance = await token.balanceOf(owner.address);
expect(await token.totalSupply()).to.equal(ownerBalance);
});
it("Should transfer tokens between accounts", async function () {
await token.transfer(addr1.address, 50);
expect(await token.balanceOf(addr1.address)).to.equal(50);
});
it("Should fail if sender has insufficient balance", async function () {
await expect(
token.connect(addr1).transfer(owner.address, 1)
).to.be.revertedWith("Insufficient balance");
});
});
Security Considerations
Smart contract security is paramount. The most common vulnerability patterns include:
- Reentrancy: An attacker's contract calls back into your contract before the first execution completes, potentially draining funds. Use the checks-effects-interactions pattern and consider OpenZeppelin's ReentrancyGuard.
- Integer overflow/underflow: In Solidity versions before 0.8.0, arithmetic operations could silently overflow. Solidity 0.8+ includes built-in overflow checks, but be aware when working with older contracts.
- Access control: Ensure administrative functions are properly restricted. Use OpenZeppelin's Ownable or AccessControl contracts rather than building your own.
- Front-running: Transactions in the mempool are visible to everyone. Designs that rely on transaction ordering can be exploited by miners or MEV bots.
Never deploy a smart contract that handles significant value without a professional security audit. The cost of an audit is always cheaper than the cost of an exploit.
Deployment
Deploying to Ethereum mainnet involves compiling your contracts, estimating gas costs, and broadcasting the deployment transaction. Always deploy to a testnet (Ropsten, Rinkeby, or Goerli) first and thoroughly test before mainnet deployment. Verify your contract source code on Etherscan so users can independently confirm what the contract does.
Getting Started
Smart contract development is a specialised discipline that requires careful attention to security, gas optimisation, and the unique constraints of blockchain execution. If you are building a DApp, token, or DeFi protocol and need experienced Solidity developers, we are here to help you build it right.