Smart contracts are the backbone of decentralized applications on blockchain platforms like Ethereum. Solidity, as the most widely used language for writing smart contracts, offers powerful features to implement complex logic securely and transparently. This guide walks through several foundational Solidity contract examples—ranging from voting systems to payment channels—highlighting core concepts such as struct usage, modifiers, events, cryptographic signatures, and secure design patterns.
These examples not only demonstrate syntax but also best practices in decentralized logic, trustless interactions, and state management. Whether you're building a token, auction system, or multi-step transaction channel, understanding these patterns is essential.
👉 Discover how to deploy and interact with Solidity contracts using advanced blockchain tools
Voting Contract
The following example demonstrates a complete decentralized voting system where voting rights are assigned by the chairperson, and voters can either vote directly or delegate their vote to someone else. The outcome is fully automatic and transparent.
pragma solidity >=0.7.0 <0.9.0;
contract Ballot {
struct Voter {
uint weight;
bool voted;
address delegate;
uint vote;
}
struct Proposal {
bytes32 name;
uint voteCount;
}
address public chairperson;
mapping(address => Voter) public voters;
Proposal[] public proposals;
constructor(bytes32[] memory proposalNames) {
chairperson = msg.sender;
voters[chairperson].weight = 1;
for (uint i = 0; i < proposalNames.length; i++) {
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
function giveRightToVote(address voter) external {
require(msg.sender == chairperson, "Only chairperson can give voting rights.");
require(!voters[voter].voted, "Voter already voted.");
require(voters[voter].weight == 0);
voters[voter].weight = 1;
}
function delegate(address to) external {
Voter storage sender = voters[msg.sender];
require(sender.weight != 0, "No right to vote");
require(!sender.voted, "Already voted.");
require(to != msg.sender, "Self-delegation is disallowed.");
while (voters[to].delegate != address(0)) {
to = voters[to].delegate;
require(to != msg.sender, "Found loop in delegation.");
}
Voter storage delegate_ = voters[to];
require(delegate_.weight >= 1);
sender.voted = true;
sender.delegate = to;
if (delegate_.voted) {
proposals[delegate_.vote].voteCount += sender.weight;
} else {
delegate_.weight += sender.weight;
}
}
function vote(uint proposal) external {
Voter storage sender = voters[msg.sender];
require(sender.weight != 0, "Has no right to vote");
require(!sender.voted, "Already voted.");
sender.voted = true;
sender.vote = proposal;
proposals[proposal].voteCount += sender.weight;
}
function winningProposal() public view returns (uint winningProposal_) {
uint winningVoteCount = 0;
for (uint p = 0; p < proposals.length; p++) {
if (proposals[p].voteCount > winningVoteCount) {
winningVoteCount = proposals[p].voteCount;
winningProposal_ = p;
}
}
}
function winnerName() external view returns (bytes32 winnerName_) {
winnerName_ = proposals[winningProposal()].name;
}
}Key Features
- Delegation: Voters can assign their voting power to others.
- Weighted Voting: Chairperson can grant multiple votes per person.
- Transparency: All data is on-chain; results are verifiable.
- Immutability: Once votes are cast, they cannot be altered.
Possible Optimizations
- Batch assigning voting rights via merkle proofs instead of individual transactions.
- Handle ties in
winningProposal()by returning multiple winners or triggering a runoff.
Blind Auction (Secret Bidding)
This example shows how to build a secure blind auction system in Solidity. First, we introduce a simple open auction and then extend it into a sealed-bid auction where bids remain hidden until revealed.
Simple Open Auction
In an open auction, bidders send ether along with their bid. If outbid, the previous highest bidder can withdraw funds later.
pragma solidity ^0.8.4;
contract SimpleAuction {
address payable public beneficiary;
uint public auctionEndTime;
address public highestBidder;
uint public highestBid;
mapping(address => uint) pendingReturns;
bool ended;
event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);
error AuctionAlreadyEnded();
error BidNotHighEnough(uint highestBid);
error AuctionNotYetEnded();
error AuctionEndAlreadyCalled();
constructor(uint biddingTime, address payable beneficiaryAddress) {
beneficiary = beneficiaryAddress;
auctionEndTime = block.timestamp + biddingTime;
}
function bid() external payable {
if (block.timestamp > auctionEndTime)
revert AuctionAlreadyEnded();
if (msg.value <= highestBid)
revert BidNotHighEnough(highestBid);
if (highestBid != 0) {
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit HighestBidIncreased(msg.sender, msg.value);
}
function withdraw() external returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
pendingReturns[msg.sender] = 0;
if (!payable(msg.sender).send(amount)) {
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
function auctionEnd() external {
if (block.timestamp < auctionEndTime)
revert AuctionNotYetEnded();
if (ended)
revert AuctionEndAlreadyCalled();
ended = true;
emit AuctionEnded(highestBidder, highestBid);
beneficiary.transfer(highestBid);
}
}👉 Learn how to test auction logic with real-time blockchain simulations
Blind Auction Extension
To hide bid values during the bidding phase:
- Bidders submit a hash of
(value, secret)instead of revealing value directly. - After bidding ends, users reveal their secrets and values.
- Only valid and highest bids are rewarded.
This prevents last-minute sniping and enhances fairness.
Secure Remote Purchase
This contract implements a trust-minimized escrow system for remote goods trading.
Both buyer and seller deposit funds equal to twice the item value. Upon delivery confirmation:
- Buyer gets half their deposit back.
- Seller receives triple the base value (their deposit + item value).
Motivation is aligned: both parties lose money if the deal stalls.
contract Purchase {
uint public value;
address payable public seller;
address payable public buyer;
enum State { Created, Locked, Release, Inactive }
State public state;
modifier condition(bool condition_) { require(condition_); _; }
modifier onlyBuyer() { require(msg.sender == buyer); _; }
modifier onlySeller() { require(msg.sender == seller); _; }
modifier inState(State state_) { require(state == state_); _; }
event Aborted();
event PurchaseConfirmed();
event ItemReceived();
event SellerRefunded();
constructor() payable {
seller = payable(msg.sender);
value = msg.value / 2;
if ((2 * value) != msg.value) revert ValueNotEven();
}
function abort() external onlySeller inState(State.Created) {
emit Aborted();
state = State.Inactive;
seller.transfer(address(this).balance);
}
function confirmPurchase() external inState(State.Created) condition(msg.value == (2 * value)) payable {
emit PurchaseConfirmed();
buyer = payable(msg.sender);
state = State.Locked;
}
function confirmReceived() external onlyBuyer inState(State.Locked) {
emit ItemReceived();
state = State.Release;
buyer.transfer(value);
}
function refundSeller() external onlySeller inState(State.Release) {
emit SellerRefunded();
state = State.Inactive;
seller.transfer(3 * value);
}
}Micro Payment Channels
Payment channels allow off-chain transactions with final settlement on-chain. They're ideal for frequent micropayments (e.g., streaming services).
Core Components
- Channel Setup: One party funds a contract.
- Signed Messages: Off-chain signed updates reflect current balance.
- Settlement: Recipient closes channel with latest signed state.
- Timeout Recovery: Sender reclaims funds if receiver doesn’t close.
contract SimplePaymentChannel {
address payable public sender;
address payable public recipient;
uint256 public expiration;
constructor(address payable recipientAddress, uint256 duration) payable {
sender = payable(msg.sender);
recipient = recipientAddress;
expiration = block.timestamp + duration;
}
function close(uint256 amount, bytes memory signature) external {
require(msg.sender == recipient);
require(isValidSignature(amount, signature));
recipient.transfer(amount);
selfdestruct(sender);
}
function claimTimeout() external {
require(block.timestamp >= expiration);
selfdestruct(sender);
}
function isValidSignature(uint256 amount, bytes memory signature)
internal view returns (bool)
{
bytes32 message = prefixed(keccak256(abi.encodePacked(this, amount)));
return recoverSigner(message, signature) == sender;
}
function prefixed(bytes32 hash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}
function recoverSigner(bytes32 message, bytes memory sig)
internal pure returns (address)
{
(uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);
return ecrecover(message, v, r, s);
}
function splitSignature(bytes memory sig)
internal pure returns (uint8 v, bytes32 r, bytes32 s)
{
require(sig.length == 65);
assembly {
r := mload(add(sig, 32))
s := mload(add(sig, 64))
v := byte(0, mload(add(sig, 96)))
}
}
}✅ Best Practice: Use OpenZeppelin’s ECDSA library for production-grade signature handling.
Modular Contracts Using Libraries
Modular design improves code maintainability and security. Libraries like Balances encapsulate reusable logic.
library Balances {
function move(mapping(address => uint256) storage balances, address from, address to, uint amount) internal {
require(balances[from] >= amount);
require(balances[to] + amount >= balances[to]);
balances[from] -= amount;
balances[to] += amount;
}
}
contract Token {
mapping(address => uint256) balances;
using Balances for *;
function transfer(address to, uint amount) external returns (bool success) {
balances.move(msg.sender, to, amount);
emit Transfer(msg.sender, to, amount);
return true;
}
}Benefits:
- Isolated logic testing.
- Prevents integer overflow/underflow.
- Encourages code reuse.
Frequently Asked Questions (FAQ)
Q: What is the purpose of ecrecover in Solidity?
A: It recovers the Ethereum address that signed a given message hash. Used for off-chain authentication without gas costs.
Q: Why use selfdestruct in payment channels?
A: It cleans up contract storage and sends remaining funds back to the owner—critical for trustless fund recovery after timeout.
Q: How do blind auctions prevent bid manipulation?
A: By separating bidding into two phases—commit and reveal—bidders cannot adjust based on others’ visible offers.
Q: Can anyone call giveRightToVote in the voting contract?
A: No. Only the chairperson can assign voting rights to ensure controlled participation.
Q: What happens if a bidder doesn’t withdraw after being outbid?
A: Their funds remain in pendingReturns indefinitely. They must manually call withdraw() to reclaim them.
Q: Are Solidity libraries upgradeable?
A: Libraries themselves aren’t upgradeable unless deployed as delegate proxies. However, they’re typically used for pure logic with no state.
Final Thoughts
Solidity enables powerful decentralized applications when used correctly. These examples cover key patterns: secure voting, time-bound auctions, escrow systems, off-chain payments, and modular design. Always follow security best practices—use established libraries, test thoroughly, and audit critical contracts before deployment.
👉 Start building secure Solidity contracts with integrated development tools