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.
And there we have it!