Get Started with Solidity: Smart Contracts in Part 1

I will walk you through the process of writing a faucet smart contract 💸

Benjamin Duffield-Harding
Coinmonks
Published in
5 min readMar 22, 2024

--

Photo by John Schnobrich on Unsplash

Over three parts I will take you from “what’s a smart contract?” to deploying on testnet, explaining crypto jargon along the way…

Solidity is a programming language built for writing smart contracts on Ethereum. Smart contracts are programs stored on a blockchain. Users can trigger a transaction or state change by interacting with their crypto wallet.

A faucet is a smart contract that allows users to deposit and withdraw their tokens from the contract.

Table of content

The contract

A single light fitting hung loose above the table, it swayed as the room rumbled with the trains running above the apartment. Brick dust and cigarette ash covered the page of the contract that was put in front of me. This man was making me feel more and more uneasy. With two fingers he slammed the table, “Sign!” he said…

Source

Oh, no not that kind of contract…

First you’ll want to install Solidity — for example on Ubuntu:

sudo add-apt-repository ppa:ethereum/ethereum
sudo apt update
sudo apt install solc
solc --version

Create a directory for the project and open in your code editor. If you’re using VSCode I recommend installing the Solidity extension.

Then create a file for the faucet contract Faucet.sol.

Here’s the bones of our contract:

// language version
pragma solidity ^0.8.4;

// contract object
contract Faucet {

// send amount to the address that requested it
function withdraw(uint withdraw_amount) public {
payable(msg.sender).transfer(withdraw_amount);
}

// accept incoming payments
function deposit() public payable {}
}

A few notes on the contract…

  1. Solidity is a compiled language. At the top we specify the language version, the compiler reads this and checks whether the compiler version is compatable with the language version.
  2. The contract object is similar to objects and classes in other object oriented programming languages — for example you can use constructors, functions, inheritance — which we’ll get into later.
  3. Functions in Solidity are defined with a visibility — public, external, private, internal — to control access. We want our functions to be called externally and internally so we can use public visibility.
  4. The payable modifier allows a function or address to receive Ether — the native token on Ethereum — this is what we will deposit and withdraw from the contract.
  5. The argument in the withdraw function takes an argument of type uint, that can only hold positive values or zero. You can see why in this situation it is important to use uint.

Lets try compiling the contract at this stage:

solc --optimize --bin Faucet.sol

That translates the code into bytecode, ready to be deployed:

======= Faucet.sol:Faucet =======
Binary:
6080604052348015600e575f80fd5b5060be80601a5f395ff3fe6080604052600436106025575f3560e01c80632e1a7d4d146029578063d0e30db0146043575b5f80fd5b3480156033575f80fd5b506043603f3660046072565b6045565b005b604051339082156108fc029083905f818181858888f19350505050158015606e573d5f803e3d5ffd5b5050565b5f602082840312156081575f80fd5b503591905056fea264697066735822122013b1af624b94566b9838bc3d486424e498c49b05ae6ed7389cc7292c3aa8a47c64736f6c63430008190033

Error Handling

With our current implementation anyone can drain the entire balance of the faucet in one go, not ideal.

Let’s limit the withdrawal amount and add an error message if the amount is greater than the balance of the faucet. The require function tests the condition specified and throws an error if it’s not met.

contract Faucet {
function withdraw(uint withdraw_amount) public {
// error handling
require(withdraw_amount <= 0.1 ether, "Withdrawals are limited to 0.1 ether");
require(address(this).balance >= withdraw_amount, "Insufficient balance in faucet");

payable(msg.sender).transfer(withdraw_amount);
}

function deposit() public payable {}
}

Now when the withdraw function is called, the request will have to pass our two requirements in order for the transaction to go through.

Constructors and security

Let’s give the contract an owner. We can use a constructor function to assign the contract creator to an ‘owner’ variable of type address. The address type is used to store Ethereum addresses.

contract Faucet {
// constructor
address owner;
constructor() {
owner = msg.sender;
}

function withdraw(uint withdraw_amount) public {
require(withdraw_amount <= 0.1 ether, "Withdrawals are limited to 0.1 ether");
require(address(this).balance >= withdraw_amount, "Insufficient balance in faucet");

payable(msg.sender).transfer(withdraw_amount);
}

function deposit() public payable {}
}

Let’s add a layer of security. You can imagine in the case that an exploit is found in production, it may be necessary to pause withdrawals.

We can give the owner the ability to control access to functions. Let’s create a function that pauses withdrawals.

contract Faucet {
address owner;
bool public paused;
constructor() {
owner = msg.sender;
}
// pause function
function setPaused(bool _paused) public {
paused = _paused;
}

function withdraw(uint withdraw_amount) public {
// check that the function isn't paused
require(paused == false, "Function paused");
require(withdraw_amount <= 0.1 ether, "Withdrawals are limited to 0.1 ether");
require(address(this).balance >= withdraw_amount, "Insufficient balance in faucet");

payable(msg.sender).transfer(withdraw_amount);
}

function deposit() public payable {}
}

Function modifiers

At the moment anyone can use the pause function, let’s make it so only the owner can pause withdrawals.

We can make a function modifier — onlyOwner — that ensures the address that calls the function is the owner. Then we can add the modifier to the setPaused function.

contract Faucet {
address owner;
bool public paused;
constructor() {
owner = msg.sender;
}

// function modifiers
modifier onlyOwner {
require(msg.sender == owner, "Only contract owner can call this function");
_; // if require condition passes - execute code
}
// add the onlyOwner modifier to setPaused
function setPaused(bool _paused) public onlyOwner {
paused = _paused;
}

function withdraw(uint withdraw_amount) public {
require(paused == false, "Function paused");
require(withdraw_amount <= 0.1 ether, "Withdrawals are limited to 0.1 ether");
require(address(this).balance >= withdraw_amount, "Insufficient balance in faucet");

payable(msg.sender).transfer(withdraw_amount);
}

function deposit() public payable {}
}

Contract inheritance

Lets make our code readable and reusable by using contract inheritance. With contract inheritance the child contract inherits the functionality of the parent.

You can see in the code owned is the parent → functionality passed to pausable → passed to Faucet.

contract owned {
bool public paused;
address owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner {
require(msg.sender == owner, "Only contract owner can call this function");
_;
}
}

contract pausable is owned {
function setPaused(bool _paused) public onlyOwner {
paused = _paused;
}
}

contract Faucet is pausable {
function withdraw(uint withdraw_amount) public {
require(paused == false, "Function paused");
require(withdraw_amount <= 0.1 ether, "Withdrawals are limited to 0.1 ether");
require(address(this).balance >= withdraw_amount, "Insufficient balance in faucet");

payable(msg.sender).transfer(withdraw_amount);
}

function deposit() public payable {}
}

One more compile to check it’s working as expected:

solc --optimize --bin Faucet.sol

If you like this content, clap! 👏 If you don’t, comment 📝

Don’t worry I can take it.

Source

I’ll be writing lots more articles in the future including the remainder of this 3-part series, so don’t forget to follow! If you know someone who would enjoy this article, please share it with them!

Thank you for reading! 🙏

--

--