▦ 5 → SpaceX.sol
by: Shahruz Shaukat
Let's walk through a simple smart contract that can be deployed to Ethereum, and work towards implementing a basic version of the SpaceX protocol. This will require you being able to understand some standard programming syntax (mostly Javascript-like) but not very much.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SpaceX {
}
This is the basic boilerplate any contract would start off with.
-
// SPDX-License-Identifier: _____
is a way of specifying a license for this contract. You just need to set what license you want to use, or write UNLICENSED. -
pragma solidity ^0.8.0
specifies which version of the Solidity compiler you're planning to use. This line specifically is saying any version above 0.8.0 is fine. New versions don't ship all that often, and old versions stay online, so there's usually no reason to change this unless you have a good reason to. -
contract SpaceX { }
sets the name for our contract, and room in between the brackets for our code. If you're familiar with Classes in other programming languages, this "contract" syntax is mostly the same thing. Just name the thing you're creating after the word contract.
Now that we've got boilerplate, let's start filling it in.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SpaceX {
// Storage
// Functions
}
Before we write any real code, let's put some comments in to make sure our code stays relatively organized and sticks to existing conventions.
-
Most developers write all their storage declarations at the top of the inside of the contract. These lines look like writing
consts
andlets
andvars
in Javascript. One MAJOR distinction: these variables are stored automatically within the contract. There's no separate database your code interacts with, your variables are just managed and saved automatically. It can end up being more intuitive than writing traditional APIs after a bit of practice. -
After we set up our storage, we write our functions. These work just like functions in any programming language. You can have
if / else
style statements andfor
orwhile
loops, you can update variable values and compare them against other things, etc.
Let's start with something simple: storing a name and a number and reading them back.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SpaceX {
// Storage
string myName;
uint256 myNumber;
// Functions
function setMyName(string name) public {
myName = name;
}
function getMyName() public returns (string) {
return myName;
}
function setMyNumber(uint256 number) public {
myNumber = number;
}
function getMyNumber() public returns (uint256) {
return myNumber;
}
}
-
We've declared two simple variables: a string called
myName
and an integer calledmyNumber
. There's a few integer types in Solidity,uint256
is the most commonly used one as it allows you to use very large numbers when needed. -
We've written a setter and a getter function for both of our variables. Outside of some of the syntax in the function lines, this should hopefully look pretty intuitive.
When a function accepts a parameter (like in our setters), you have to include the type of that parameter and a name for it. Writing "public" after the name of the function but before the opening bracket tells the blockchain that this is a method that a user can call. In other words each public function is an automatically deployed public API. The alternative is "private" which is used for internal functions that can only be called by other functions within the contract.
If your function is returning a value, you need to write returns (typeOfResponseValue)
to let the compiler know what format to expect the response to be.
The way this contract is currently set up, anytime anyone calls any one of these setter functions, the value is changed for everyone.
Person A could run setMyName("Person A")
then when they run getMyName()
they'll get "Person A"
back.
Person B could then run setMyName("Person B")
and get "Person B"
when they run getMyName()
.
But Person A will also now get "Person B"
if they run getMyName()
again, as will anyone else that runs that function.
Let's change that so each person only can change the value for themselves.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SpaceX {
// Storage
mapping(address => string) myName;
mapping(address => uint256) myNumber;
// Functions
function setMyName(string name) public {
myName[msg.sender] = name;
}
function getMyName() public returns (string) {
return myName[msg.sender];
}
function setMyNumber(uint256 number) public {
myNumber[msg.sender] = number;
}
function getMyNumber() public returns (uint256) {
return myNumber;
}
}
- We've changed our storage here to include a new thing called a
mapping
. A mapping is a basic key value store (like an object in Javascript).
The syntax for a mapping works like this:
mapping(typeForKey => typeForValue) nameOfVariable;
To write something to a mapping you'd write something like nameOfVariable[key] = value;
, and to read something from a mapping you'd write nameOfVariable[key]
.
The address
type here is new too. Solidity natively understands addresses on the blockchain, which can reference user addresses or an address that a contract was deployed to (both exist in the same address space). When reading an address, it works like a string (for example, "0xBb167bCe93F2e1Db5aAe834702C8BDAEaB5e9831"
), but it has it's own type in Solidity to make it easier to work with, and avoid the possibility of accidentally using a string that's not an address.
- In our functions we have a new thing called
msg.sender
. This value is automatically filled with the address of the account running the function.
So now when Person A (who's address is 0xBb167bCe93F2e1Db5aAe834702C8BDAEaB5e9831
) runs setMyName("Person A")
, that value is stored internally at myName[0xBb167bCe93F2e1Db5aAe834702C8BDAEaB5e9831]
.
If Person A runs getMyName()
they'll get "Person A" back in response.
If Person B runs setMyName("Person B")
, it won't overwrite or interfere with Person A's storage, because it's stored in myName
at whatever Person B's address is.
Mappings default to empty values or 0's depending on the type. So if Person C were to come in and run getMyName()
without ever setting a name, they'd get back an empty string ""
in response. If they ran getMyNumber()
they'd get a 0.
We know almost everything we need now to make a basic SpaceX protcol contract, so let's remove the current storage and functions and rewrite them to follow the SpaceX protocol: "Every address gets space for exactly 10 IPFS links".
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SpaceX {
// Storage
mapping(address => string[10]) space;
// Functions
function updateSpace(uint256 index, string ipfsHash) public {
space[msg.sender][index] = ipfsHash;
}
function getSpace() public returns (string[10]) {
return space[msg.sender];
}
function getSpace(address spaceAddress) public returns (string[10]) {
return space[spaceAddress];
}
}
-
The contract's much shorter now but has our core functionality now. There's a new thing here in our mapping
string[10]
, which just means an array of 10 strings. So our mapping is now: for each address, there is an array of 10 strings. -
The
updateSpace()
function takes two parameters: an integerindex
and a stringipfsHash
. We'll use the index to decide which position in our array of 10 strings to place our ipfsHash in, so we expect a value from 0 - 9. (There's currently nothing checking to ensure that's the case, but we'll get to that later.)
Writing to our mapping variable looks a bit more complicated now too, but it's working the same way.
space[msg.sender]
just looks up the user's address in our space
mapping, which returns an array of 10 strings. If the address hasn't been used before, it'll be an array of 10 empty strings.
Then [index]
right after tells Solidity which position in that array to assign the value on the other side of the =
to, which is just the ipfsHash
that was sent.
- We also now have two getters. The first
getSpace()
works likegetMyName()
from earlier, it looks up the caller's address and returns their own space. The second getter accepts aspaceAddress
parameter and returns the information for that address. A contract can have multiple functions with the same name as long as they have unique parameters they accept.
This contract does a lot with very little code. There's other things you may want to implement:
-
Checking to make sure one post can't be in multiple blocks in the same space, by looping through the addresses' existing entries in the
updateSpace()
function before storing the ipfsHash. -
Allow for deleting a space, by writing a function that overwrites a space's array with empty strings.
-
Keep count the number of spaces an ipfsHash is currently included in by adding a
mapping(string => uint256) counter
, which you'd incremement when an ipfsHash is added and decrement when an ipfsHash is overwritten with something else. In this mapping thestring
type key is the ipfsHash, and theuint256
value is a count of how many spaces it's in, which defaults to 0 automatically for any key.
This contract is good enough to deploy locally or to a testnet and start interacting with, but not production ready. Because contracts are unchangeable after being deployed, it's considered essential to write tests for each function, testing every possible kind of input and making sure it works as expected.
Solidity also has require
statements, which let you define a condition to throw an error. For example if we wanted to ensure the index
passed to our updateSpace()
function is between 0 and 9, we could update it like this:
function updateSpace(uint256 index, string ipfsHash) public {
require(index >= 0, "Index must be greater or equal to 0.");
require(index <= 9, "Index must be less than or equal to 9.");
space[msg.sender][index] = ipfsHash;
}
If the first parameter of a require statement fails, an error is returned to the user containing the message in the second parameter.
These require statements get run before a transaction is submitted to the blockchain, meaning a user will recieve an error in their wallet app letting them know their index is too low or too high, without wasting anything on gas fees.
Professional Solidity auditing is another step you can take before shipping something to a mainnet. Auditors will poke through your code exploring all the ins and outs and use their knowledge of attack vectors to advise you on what to fix and how, or certify your contract is safe.
🕊 Where to next?
▦ 6 → Interplanetary Filesystems
✸ Tabs roll call