Home Blog Games About
Jun 06, 2021

▦ 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.

  1. // 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.

  2. 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.

  3. 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.

  1. Most developers write all their storage declarations at the top of the inside of the contract. These lines look like writing consts and lets and vars 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.

  2. 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 and for or while 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;
  }
}
  1. We've declared two simple variables: a string called myName and an integer called myNumber. 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.

  2. 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;
  }
}
  1. 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.

  1. 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];
  }

}
  1. 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.

  2. The updateSpace() function takes two parameters: an integer index and a string ipfsHash. 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.

  1. We also now have two getters. The first getSpace() works like getMyName() from earlier, it looks up the caller's address and returns their own space. The second getter accepts a spaceAddress 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:


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

1: Solidity docs