Learn Solidity in 20 Mins by Alysia Tech

Learn Solidity in 20 Mins (Crash Course)

Follow along by reading this article or watching the youtube video, but either way, you’re learning and writing Solidity code today!

--

Why Solidity?

Solidity is the most popular programming language for creating apps on Blockchains. These apps are called smart contracts and these blockchains are decentralized networks that make it easy to manage and transact digital assets over the internet.

Some blockchains allow anyone to create apps (usually called DApps, short for decentralized apps) that can be used to create and automate the movement of assets based on criteria the programmer specifies. These blockchains are called smart contract platforms.

Level One — Getting Started

We will write Solidity code in an online developer environment which has great features for testing and deploying the code in a simulated blockchain environment or on a real blockchain network.

We will deploy to a real blockchain network at the end of the video. Like and subscribe if you’re enjoying the video so far.

Our first mission is to say hello to this world.

Go to

Click contracts and new file called LevelOne.sol.

Here’s the Github repo so that you can have access to the files.

In the file, enter the license identifier by typing a comment. The license identifier says whether or not the code is available to be open sourced etc. We are going to use the MIT license.

// SPDX-License-Identifier: MIT

After the license, specify the version of Solidity by typing the following.

// SPDX-License-Identifier: MIT

pragma solidity 0.8.18;
  • pragma specifies the compiler version of Solidity.

We are going to encounter a lot of keywords that helps our World, aka, compiler understand what want to do in the game.

Now for our contract, write the keyword contractfollowed by the name of the contract.

// SPDX-License-Identifier: MIT

pragma solidity 0.8.18;

contract LevelOne{
}

Within the contract, create a variable, which we use to hold important values in our contract. Add the line string public greeting='Hello World'

// SPDX-License-Identifier: MIT

pragma solidity 0.8.18;

contract LevelOne{
string public greeting="Hello World";
}
  • Now we compile the contract to make sure that we’re speaking the Solidity language correctly. Click on the third icon on the left, make sure that contract is selected in the contract dropdown and then hit the compile button.

We can now deploy this contract on the simulation of an Ethereum blockchain which we call the Remix VM by clicking on the fourth icon down on the left.

It will look something like this once deployed. Remix helpfully shows us the public variable that we just created and if you click the varible name we will see the value the variable contains.

If you wanted to change the greeting to something that you can remain to for example, “Hola”, “Waz di Scene” if you’re Trinidad or “Wa gwan my G” if you’re from London.

Then you can update the code to reflect that

compile, and redeploy.

Variable Visibility

We can see this variable because of the visibility that we, the Solidity programmer, has written. The public declaration allows any contract or account can access the variable but three other options exist. private only allows the variable to be visible inside the function. internal variables can only be seen by its own contract and contracts that inherit them. and external is like the opposite of internal where only the inheriting contract and other accounts can access it but not its own contract. The external modifier can only be used for functions.

Enjoying this but prefer a video? Watch the video coding tutorial for learning Solidity in 20 minutes here.

Learn Solidity in 20 Mins Video Tutorial featuring Alysia Tech

Level Two — Functions, Data Locations

Let’s move to level two. So what if we wanted to allow anyone to change the greeting so that they can change the greeting of the smart contract. So we’ll keep the greeting private but create a function to modify the variable. Function is small units of work that you app can perform. It’s especially great to encapsulate repeatable work. So a function may or may not receive some input, does some work (which may or may not update state) and then there may or may not have some input. Let’s head to the code.

create a new file, LevelTwo.sol

// SPDX-License-Identifier: MIT

pragma solidity 0.8.18;

contract LevelTwo{
string public greeting="Hello World";

function updateGreeting(string calldata newGreeting) public{
greeting = newGreeting;
}

function showGreeting() public view returns (string memory){
return greeting;
}
}
  • The contract has two functions: updateGreeting and showGreeting.
  • The updateGreeting function is marked as public, which means it can be called by anyone. It takes a newGreeting string as an input and updates the greeting variable with the new value.
  • The showGreeting function is also marked as public. It is a view function, which means it only reads data from the contract without modifying it. This function returns the current value of the greeting variable.

Let’s introduce a new concept, Data Locations. In the function signatures, you may notice the use of the calldata and memory keywords. These indicate where the function arguments and return values are stored. Variables are declared as either storage, memory or calldata to explicitly specify the location of the data.

  • storage - variable is a state variable (store on blockchain)
  • memory - variable is stored in temporary memory and it exists while a function is being called
  • calldata - special data location that contains function arguments, it specifiies that the input is read-only

We see that Remix says that the code can cost infinite gas.

Every operation performed on the Ethereum blockchain requires a certain amount of computational and network resources. Gas is a unit of measurement that represents the amount of these resources consumed by a particular operation.

The infinite gas issue is likely due to the fact that you are returning a string from the showGreeting function. In Solidity, returning dynamic-sized data types like string or bytes can cause gas estimation problems because the gas cost depends on the size of the returned data.

Let’s compile, deploy and test.

Currently the contract returns this.

But we can update the greeting to ‘gm’ and we see that it’s updated in the smart contract state.

Level Three — Mapping

Let’s move onto LevelThree, create a contract called LevelThree.sol.

Let’s say we wanted to store all the greetings and how many times they entered in via the update. Then we’d need a structure that allowed to store the greeting and it’s entry count. A mapping is a good data structure to use in this instance.

// SPDX-License-Identifier: MIT

pragma solidity 0.8.18;

contract LevelThree{
string public greeting="Hello World";
mapping(string => uint256) private greetingCounts;

function updateGreeting(string calldata newGreeting) public{
greeting = newGreeting;

if(greetingCounts[newGreeting]==0){
greetingCounts[newGreeting]=1;
}else{
greetingCounts[newGreeting]++;
}
}

function showGreeting() public view returns (string memory){
return greeting;
}

function getGreetingCount(string calldata _greeting) public view returns (uint256){
return greetingCounts[_greeting];
}
}

Maps are created with the syntax mapping(keyType => valueType).

The keyType can be any built-in value type, bytes, string, or any contract.

valueType can be any type including another mapping or an array.

Mappings are not iterable.

We define our mapping like this:

mapping(string => uint) public greetingCounts;

In the updated code, a mapping called greetingCounts is added to store the greetings and their respective entry counts. Whenever the updateGreeting function is called, it increments the entry count for the given greeting in the greetingCounts mapping.

Additionally, a new function getGreetingCount is provided to retrieve the entry count for a specific greeting. This allows you to keep track of how many times each greeting has been entered.

Level Four — Searching

In Level Four, we’ll create a file called LevelFour.sol to iterate through the mapping to find the greeting with the highest votes.

In Solidity, mappings are unordered data structures, which means there is no inherent way to retrieve the “first” entry of a mapping. The order of entries within a mapping is not preserved.

If you need to access a specific entry from a mapping, you will typically need to know the corresponding key associated with that entry. With the key, you can directly access the value stored in the mapping.

However, if you want to access an arbitrary entry from the mapping, you can use an external data structure such as an array or a separate mapping to store the keys of the original mapping. This additional data structure can be used to keep track of the order in which the entries were added, allowing you to retrieve the “first” entry based on that order.

Here’s an example of how you can achieve this by maintaining an array of keys:

Add this variable at the start of the contract code string[] private greetingKeys;

// SPDX-License-Identifier: MIT

pragma solidity 0.8.18;

contract LevelFour {
string private greeting = "Hello World";
mapping(string => uint256) private greetingCounts;
string[] private greetingKeys;


function updateGreeting(string calldata newGreeting) public {
greeting = newGreeting;

if(greetingCounts[newGreeting]==0){
greetingCounts[newGreeting]=1;
}else{
greetingCounts[newGreeting]++;
}
greetingKeys.push(greeting);
}

function showGreeting() public view returns (string memory) {
return greeting;
}

function getGreetingCount(string calldata _greeting) public view returns (uint256) {
return greetingCounts[_greeting];
}

function getGreetingWithMostCounts() public view returns (string memory, uint256) {
require(greetingKeys.length > 0, "No greetings available.");

string memory mostPopularGreeting = greetingKeys[0];
uint256 highestCount = greetingCounts[mostPopularGreeting];

for (uint256 i = 1; i < greetingKeys.length; i++){
string memory _greeting = greetingKeys[i];
uint256 count = greetingCounts[_greeting];

if(count>highestCount){
highestCount = count;
mostPopularGreeting = _greeting;
}
}

return (mostPopularGreeting, highestCount);
}
}

In this line of code, we are declaring a variable named greetingKeys. It is of type string[], which means it is an array that can store multiple strings. The private keyword indicates that this variable is only accessible within the contract and cannot be accessed or modified by external parties.

In this updated code, a new function getGreetingWithMostCounts is introduced. It iterates over the greetingKeys array and compares the count of each greeting with the highest count seen so far. It updates the mostPopularGreeting variable accordingly.

After the loop, the function returns the greeting with the highest count along with its count as a tuple.

By calling the getGreetingWithMostCounts function, you can retrieve the greeting with the most counts from the greetingCounts mapping.

Level Five — Efficiency

We can also optimize this code in LevelFive.sol

To make the code more efficient, we can optimize the implementation by eliminating the need for an array to store the keys while still providing access to the most popular greeting. Instead of using an array, we can modify the code to track the most popular greeting dynamically using a single variable. Here’s an improved implementation:

// SPDX-License-Identifier: MIT

pragma solidity 0.8.18;

contract LevelFour {
string private greeting = "Hello World";
mapping(string => uint256) private greetingCounts;
string[] private greetingKeys;


function updateGreeting(string calldata newGreeting) public {
greeting = newGreeting;

if(greetingCounts[newGreeting]==0){
greetingCounts[newGreeting]=1;
}else{
greetingCounts[newGreeting]++;
}
greetingKeys.push(greeting);
}

function showGreeting() public view returns (string memory) {
return greeting;
}

function getGreetingCount(string calldata _greeting) public view returns (uint256) {
return greetingCounts[_greeting];
}

function getGreetingWithMostCounts() public view returns (string memory, uint256) {
require(greetingKeys.length > 0, "No greetings available.");

string memory mostPopularGreeting = greetingKeys[0];
uint256 highestCount = greetingCounts[mostPopularGreeting];

for (uint256 i = 1; i < greetingKeys.length; i++){
string memory _greeting = greetingKeys[i];
uint256 count = greetingCounts[_greeting];

if(count>highestCount){
highestCount = count;
mostPopularGreeting = _greeting;
}
}

return (mostPopularGreeting, highestCount);
}
}

In this updated implementation, we only store the most popular greeting and its count, eliminating the need for an array to track keys. Here’s how the optimization works:

  • In the updateGreeting function, we increment the count of the newGreeting directly in the greetingCounts mapping.
  • If the updated count is higher than the current highestCount, we update both the highestCount and mostPopularGreeting to reflect the new most popular greeting.
  • Additionally, if there is a tie between greetings with the same count, we select the lexicographically smaller greeting as the most popular.

By avoiding the need for an array, this implementation saves storage costs and reduces gas consumption during updates. It maintains efficiency while still providing access to the most popular greeting and its count.

Level Six — Payments, Events

Things seem to be getting a bit crazy with everyone being able to update the greeting so let’s have people pay ETH to the smart contract to be able to update the greeting. In LevelSix.sol, enter the following code

// SPDX-License-Identifier: MIT

pragma solidity 0.8.18;

contract LevelSix {
mapping(string => uint256) private greetingCounts;
string private mostPopularGreeting;
uint256 private highestCount;

uint256 public constant UPDATE_GREETING_FEE = 0.001 ether;

event GreetingUpdated(address indexed sender, string newGreeting, uint256 amountPaid);

function updateGreeting(string calldata newGreeting) public payable {
require(msg.value >= UPDATE_GREETING_FEE, "Insufficient payment to update greeting.");

uint256 count = greetingCounts[newGreeting] + 1;
greetingCounts[newGreeting] = count;

if (count > highestCount) {
highestCount = count;
mostPopularGreeting = newGreeting;
} else if (count == highestCount && bytes(newGreeting).length < bytes(mostPopularGreeting).length) {
mostPopularGreeting = newGreeting;
}

emit GreetingUpdated(msg.sender, newGreeting, UPDATE_GREETING_FEE);
}

function getGreetingCount(string calldata greeting) public view returns (uint256) {
return greetingCounts[greeting];
}

function getGreetingWithMostCounts() public view returns (string memory, uint256) {
require(highestCount > 0, "No greetings available.");

return (mostPopularGreeting, highestCount);
}
}contract HelloWorld {
mapping(string => uint256) private greetingCounts;
string private mostPopularGreeting;
uint256 private highestCount;

In this updated implementation, the updateGreeting function requires a payment (ETH) to update the greeting. The msg.value field represents the amount of ETH sent with the transaction. The require statement ensures that a payment greater than zero is made before allowing the greeting update.

Additionally, an event named GreetingUpdated is emitted, which logs the sender's address, the new greeting, and the amount of ETH paid. This event can be used to track and monitor the updates made by different senders. Events allow logging to the Ethereum blockchain. Some use cases for events are:

  • Listening for events and updating user interface

Level Seven — Withdrawals, Modifiers

We’ll let the owner of the contract withdraw funds in the file LevelSeven.sol

// SPDX-License-Identifier: MIT

pragma solidity 0.8.18;

contract LevelSeven {
mapping(string => uint256) private greetingCounts;
string private mostPopularGreeting;
uint256 private highestCount;
address private owner;

uint256 private constant UPDATE_GREETING_FEE = 0.001 ether;

event GreetingUpdated(address indexed sender, string newGreeting, uint256 amountPaid);
event FundsWithdrawn(address indexed owner, uint256 amount);

constructor() {
owner = msg.sender;
}

modifier onlyOwner() {
require(msg.sender == owner, "Only the contract owner can call this function.");
_;
}

function updateGreeting(string calldata newGreeting) external payable {
require(msg.value >= UPDATE_GREETING_FEE, "Insufficient payment to update greeting.");

uint256 count = greetingCounts[newGreeting] + 1;
greetingCounts[newGreeting] = count;

if (count > highestCount) {
highestCount = count;
mostPopularGreeting = newGreeting;
} else if (count == highestCount && bytes(newGreeting).length < bytes(mostPopularGreeting).length) {
mostPopularGreeting = newGreeting;
}

emit GreetingUpdated(msg.sender, newGreeting, UPDATE_GREETING_FEE);
}

function getGreetingCount(string calldata greeting) external view returns (uint256) {
return greetingCounts[greeting];
}

function getGreetingWithMostCounts() external view returns (string memory, uint256) {
require(highestCount > 0, "No greetings available.");

return (mostPopularGreeting, highestCount);
}

function withdrawFunds(uint256 amount) external onlyOwner {
require(amount <= address(this).balance, "Insufficient contract balance.");

payable(owner).transfer(amount);
emit FundsWithdrawn(owner, amount);
}
}
  1. The onlyOwner modifier is implemented to restrict access to certain functions, allowing only the contract owner to call them.
  2. The withdrawFunds function is added, which allows the contract owner to withdraw a specified amount of funds from the contract. The function performs a balance check to ensure that the contract has sufficient funds to withdraw and then transfers the amount to the owner address. An event FundsWithdrawn is emitted to log the withdrawal.

By adding the withdrawFunds function and associated modifications, the contract owner can withdraw funds from the contract as needed, providing an additional mechanism for managing the contract's finances.

Deploying the Smart Contract

Watch this video to learn how to deploy the smart contract and your next steps.

Want to continue learning? Here are some resources:

--

--