web3 by example

Creating a wallet from scratch

The first contract we're going to build here is a simple wallet that allows a contract to receive funds, and for the owner of the contract to withdraw them.

Source Layout

Solidity source files can contain a number of definitions for things such as open source license, version requirements, contract defitions, and many others. We're not going to cover all of them just now, but we will introduce a few for our first contract.

The Solidity docs recommend that every source file starts with a comment line (//) with a machine-readable SPDX license Identifier. This particular example is using The MIT License.

Next up is the "Version pragma" which will specify exactly what versions of the compiler this is compatible with. Use this to reduce the chance of a newer compiler version inadvertently introducing a breaking change. The ^0.8.10 here signifies that this will work up for any change that does not alter the left-most non-zero number. In this example the left-most non-zero number is 8, so a compiler version of 0.8.56 would work but 0.9.0 would fail. For more other examples of version specifies check the semver docs.

This is the Contract definition. A contract is ultimately little more than some state that is stored on the blockchain, and a collection of functions that can manipulate that state. The Solidity Style Guide recommends that contract names follow a CapWords naming convention.

Here we have an example of a variable definition. We'll dig into the specifics of this particular variable definition a little later. This is the ultimate where we'll define the "some state that is stored on the blockchain" part of what a contract represents.

And finally, as far as layout and structure is concerned, we have our function definitions. These are the "collection of functions that can manipulate that state" we mentioned previously.

The Constructor function

The constructor is a special, but optional, function. It is executed only once, when a contract is created. After the constructor has completed execution the final code of the contract is stored on the blockchain. The resulting contract however will not include the constructor function itself! It will include all public and external functions (we'll cover those later), as well as any code that is reachable via those functions. The constructor() function however will vanish, as will any code that was only reachable via the constructor.

This particular constructor is using the [special variable]https://docs.soliditylang.org/en/develop/units-and-global-variables.html#special-variables-and-functions msg.sender. msg.sender is a reference to the sender of the current message, which in the context of the constructor() function will always be the address (let's just say person) that is creating this contract. The datatype of this is actually of an address type, but we want to convert it to an address payable type (to signify it's an address we want to be able to send Ether to and not just a reference to another contract) and so we do an explict type conversion by calling payable(). Now that we have the reference to who is creating this contract as an address payable type we assign it to the variable owner.

Which is a perfect segue into...

Variables

This is a varible definition. It might be easiest to work from right to left for this example as we've already introduced owner in the constructor() function. This is the name of our variable and how we'll reference it to get the value in other functions later.

There's 3 types of variables in Solidity:

  • local: for use within a function, but not stored on the blockchain.
  • state: declared outside a function, stored on the blockchain.
  • global: information about the blockchain itself, or special variables like we saw before with msg.send.

We want to store the value of owner on the blockchain and so this is why we're making thie declaration outside of any function definitions.

Continuing to work from right to left on the variable declaration line we now have public. We need to emphasise that anything you store on the blockchain is visible to the whole world. All the use of public hear means is that the Solidity compiler will automatically creater getter functions to allow other contracts to fetch this value. The other options are internal and private which don't "hide" the value in any way. We've opted to make this value public so others can use it if required.

And finally we have address payable which is the data type we expect this value to be.

receive() function

This is our first introduction to one of the "special functions". The receive() function is declared using receive() external payable { ... } (note the lack of function preceeding evertying). It receives not arguments, it returns no value.

This is the function that will be called whenever someone tries to transfer Ether to this contract. If no receive() function exists then the contract can not receive Ether through regular transactions and will throw an exception (there are some exceptions to this generalisation we'll cover in a future lesson).

withdraw() function

Here's our function to enable withdrawing of funds. We start by defining the function with function withdrawal, and specifying that we expect a single argument of an unsigned integer (uint) that we'll name _amount. The last part of the method signature is to specify that it's externally callable (e.g., by other contracts) by adding the external modifier.

We don't want just anyone to be able to come along and withdraw funds though! So the first line of the function is a guard to ensure only the original creator can call this function. It does that by checking that the caller, provided as msg.sender within the execution of of withdraw(), matches the address we stored in the owner variable.

We pass the value of that equality test into require(). require() is a way to check for pre-conditions and raise an exception of expectations aren't met (assert and revert are the other options for this). This means if the msg.sender and owner are not equal we'll raise an exception and the next line will not execute.

If we make it to the next line however we'll initiate a transfer. We do that by taking the msg.sender address, convert it to an address payable by calling payable(), and then call the transfer() method on that converted object passing in the amount to withdraw.

Once that executes, the contract will have transferred the amount out of our contract and to the specified address!

getBalance() function

The last function definition in this contract is a convenient way to expose how much Ether is stored on this contract. To make it publicly available we add the external modifier. This time there's two additional values in there: view and returns.

view lets the compiler know that this function will not modify any state, which is important as it also signals that these functions have no gas cost (another concept we'll dive into in a future lesson. For now it's sufficient to know that storing or changing state will actually cost a small amount of Ether).

returns specifies that this function will return a value, in this case we declare that it will be an uint.

The final step is to return the actual value! We take the current contract via this, pass it into address, and then return the balance attribute on the result.

wallet.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract SimpleWallet {
address payable public owner;
constructor() {
owner = payable(msg.sender);
}
receive() external payable {}
function withdraw(uint _amount) external {
require(msg.sender == owner, "caller is not owner");
payable(msg.sender).transfer(_amount);
}
function getBalance() external view returns (uint) {
return address(this).balance;
}
}

And there we have it!