Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

227 migrate smart contract course from learnworlds #233

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add formating to text
Andyvargtz committed Jan 8, 2025
commit 9564aa2bcc16411acf50b7e9547c6adb53e4f8aa
Original file line number Diff line number Diff line change
@@ -6,66 +6,119 @@ authors: [martineckardt]
icon: BookOpen
---
Like with all programming languages, Solidity offers us a way to store multiple values in one location via built-in data structures. In this section, we will look at the following:
Mappings
Static Arrays
Dynamic Arrays
- Mappings
- Static Arrays
- Dynamic Arrays

Let's get started!
Mappings

## Mappings

Mappings in Solidity are similar to data structures like dictionaries found in other programming languages (e.g. Python). At its most basic level, mappings allow us to store key-value pairs. Unlike the types we explored earlier in this course, mappings can only be instantiated as state variables (i.e. we cannot create a new mapping data structure within the body of a function). Furthermore, declaring a mapping and defining key-values of a pair must be done separately. Below is the general syntax for a mapping:
```solidity
mapping(<key-type> => (value-type)) <optional-visibility> <mapping-name>;
```

Below shows an example of how to define a key-value pair within a mapping:

```solidity
mapping(address => uint) map;
map[0xc0ffee254729296a45a3885639AC7E10F9d54979 = 5;
Nested Mappings
```
## Nested Mappings

Below are some restrictions regarding mappings:
Keys can be any simple type (i.e. types associated with a singular value)
Values can be any type
- Keys can be any simple type (i.e. types associated with a singular value)
- Values can be any type
Since keys can be any type, and mappings are types by definition, this implies that we can map keys to mappings! Therefore, the following is legal syntax:

```solidity
mapping(uint => mapping(uint => address)) mapTwo;
```

To work with inner mappings, the following code snippet gives an idea as to how to access such values:

```solidity
address addr = mapTwo[5][6];
Structs
```

## Structs
The next type of data structure that we will examine are structs. For those who have worked with C++ or any other similar programming languages, structs in Solidity are the same concept. Structs, or structures, are data structures which allow us to group multiple values. We can think of structs as very basic classes, except that they lack any sort of behavior and are solely used to store information. Like mappings, we first must define the layout of a struct within the body of a smart contract; below is an example of how we would do so:

```solidity
struct Person {
uint256 age;
string name;
}
```
To create an instance of a struct as a state variable, we can use the following syntax:

```solidity
Person person = Person(5, "Rodrigo");
If you try using the syntax above in the body of a function, you will get an error regarding the location of the person variable. Since structs are a grouping of values, we must explictly state where these values are stored (either in memory or in storage). Therefore, we would want to use the following:
```

> If you try using the syntax above in the body of a function, you will get an error regarding the location of the person variable. Since structs are a grouping of values, we must explictly state where these values are stored (either in memory or in storage). Therefore, we would want to use the following:

```solidity
Person memory person = Person(5, "Rodrigo")
Static Arrays
```

## Static Arrays
The majority of programming languages provide some built-in data structures similar to lists. Solidity is no different in that it provides two types of list-like data structures:
Static arrays
Dynamic Arrays

- Static arrays
- Dynamic Arrays
Focusing first on static arrays, these are lists whose length is fixed at the time of initialization. This means that once a static array has been initialized, its length will never change. Below is the syntax to declare a static array:

```solidity
<array-type>[<array-size>] <array-name>;
```
Below is an example of declaring a static array:
```solidity
uint[5] arr;
```
With regards to declaring the value of a static array, we have two options:
Static Arrays in Memory
### Static Arrays in Memory
For a static array in memory, we first must declare it using the memory keyword:
```solidity
<array-type>[<array-size>] memory <array-name>;
```
Assuming this has been handled, we can declare individual values via indexing; an example of this can be found below:
```solidity
function test() public {
uint[] memory arr;
arr[7] = 4;
}
```
If we want to both declare and define a static array in memory, we can use the following:

```solidity
uint8[5] memory arr = [1, 2, 3, 4, 5];
Static Arrays in Storage
```

### Static Arrays in Storage

If we want to declare the value of a value in storage, we can again use array indexing. Likewise, as with static arrays declared in memory, we can also declare and define at the same time a static array in storage.
Dynamic Arrays

## Dynamic Arrays

Dynamic arrays differ from static arrays in that their size is not fixed. That is, once initialized, a dynamic array can vary in length throughout its lifetime. Due to the complexity of the underlying implementation of dynamic arrays, dynamic arrays can only be assigned to state variables. Below is an example of how we would declare a dynamic array in storage:

```solidity
<array-type>[] <array-name>;
```

By default, a dynamic array will be empty (in contrast with static arrays, which will be filled with the default value of the array type). Therefore, we can use the following associated methods to manipulate the state of a dynamic array:
push: pushes an element to the end of the dynamic array
pop: removes the last element of a dynamic array
- push: pushes an element to the end of the dynamic array
- pop: removes the last element of a dynamic array

Below is an example of the methods above in action:
```solidity
uint[] arr;
function test() public {
arr.push(1);
arr.pop();
}
}
```
Original file line number Diff line number Diff line change
@@ -6,17 +6,22 @@ authors: [martineckardt]
icon: BookOpen
---
As a segue to learning about the object-oriented programming aspect of Solidity, we will touch upon contract constructors. Similar to class constructors in other languages, constructors in Solidity allow us to define the state of a contract at the time of initialization.
Syntax
## Syntax
Below is the general syntax for a contract constructor:
constructor(<arguments>) {


```solidity
constructor(<arguments>) {
}
Example
```
## Example
Below is an example of a contract constructor in action:

```solidity
contract A {
uint num;

constructor(uint _num) {
num = _num;
}
}
}
```
Original file line number Diff line number Diff line change
@@ -6,30 +6,43 @@ authors: [martineckardt]
icon: BookOpen
---
Before discussing the concept of modifiers, we will first start by talking about the limitations of the visibilities provided by Solidity
Aside: Limitations of Visibility

## Aside: Limitations of Visibility

Recall that with regards to functions, we have the following four visibilities available:
Public
Private
Internal
External
- Public
- Private
- Internal
- External
Let's now consider the following contract:

```solidity
contract Safe {

function deposit() public {}

function withdraw() public {}
}
```
Above, we have the outline of a very basic safe contract which is meant to be accessed only by the deployer of the Safe contract. Currently, this Safe contract does not maintain the invariant previously mentioned as both deposit and withdraw are marked as public. However, even if we were to modify the visibility of the following functions, we would have the following:
The functions become inaccessible to all accounts except the contract itself
The functions still remain accessible to all accounts
- The functions become inaccessible to all accounts except the contract itself
- The functions still remain accessible to all accounts

The scenario above outlines the following problem: for functions whose access we want to restrict to only authorized users, we cannot rely on visibility.
Modifying the Behavior of Functions via Modifiers

## Modifying the Behavior of Functions via Modifiers

To combat the problem previously mentioned, we now look at modifiers. Modifiers behave similarly to functions and they modify the logic of the functions they are attached to. The syntax for defining a modifier is as follows:
```solidity
modifier <modifier-name>(<arguments>) {}
```
To attach a modifier to a function, we have the following:
```solidity
function func() public <modifier>(<arguments>){}
```
To introduce the concept of modifiers to our Safe contract, let's first modify it to the following:
```solidity
contract Safe {

address owner;
@@ -43,15 +56,20 @@ contract Safe {
function withdraw() public {}
}
```
With the above changes, we are setting the state variable owner equal to the contract deployer (i.e. msg.sender) and so we are now able to store the address of the contract deployer in our Safe contract. Let's now write a modifier to only allows for the owner of the Safe contract to call the function it is attached to:
```solidity
modifier onlyOwner() {
require(msg.sender == owner, "You are not the owner"!);
_;
}
```
Examining each line, we have:
Line 2: we are using a require statement; the require statement has the following syntax: require(boolean condition, error message). If the boolean condition is true, we move onto the next line of code. Otherwise, we raise an error with the error message provided in the statement. In this context, we are requiring that any person calling the function onlyOwner is attached to is the owner of the Safe contract.
Line 3: assuming that the owner of the Safe contract is calling the associated function, we now revert control back to the original function via the _; keyword.
- Line 2: we are using a require statement; the require statement has the following syntax: require(boolean condition, error message). If the boolean condition is true, we move onto the next line of code. Otherwise, we raise an error with the error message provided in the statement. In this context, we are requiring that any person calling the function onlyOwner is attached to is the owner of the Safe contract.
- Line 3: assuming that the owner of the Safe contract is calling the associated function, we now revert control back to the original function via the _; keyword.
Incorporating the modifier into our Safe contract we now have:

```solidity
contract Safe {

address owner;
@@ -70,7 +88,8 @@ contract Safe {
function withdraw() public onlyOwner() {}
}
```
For either deposit or withdraw, we now have the following execution flow:
An account first calls either deposit or withdraw
Since the onlyOwner modifier is attached to either function, onlyOwner is first executed
If the account is the contract owner, we return the execution flow back to the parent function (in this case, either deposit or withdraw)
- An account first calls either deposit or withdraw
- Since the onlyOwner modifier is attached to either function, onlyOwner is first executed
- If the account is the contract owner, we return the execution flow back to the parent function (in this case, either deposit or withdraw)
Original file line number Diff line number Diff line change
@@ -7,22 +7,43 @@ icon: BookOpen
---

Throughout this course, we have seen smart contract data stored in two places:
State Variables (i.e. in storage)
Function Bodies (i.e. in memory)
- State Variables (i.e. in storage)
- Function Bodies (i.e. in memory)

Let's now look at the advantages and disadvantages of each data location:
Storage: this data location is persistent between transactions and so we don't have to worry about state variables being lost. However, reading and writing to storage is computationally expensive and therefore, requires a substantial amount of gas to perform.
Memory: this data location is not persistent between transactions; therefore, values that you write in memory in one transaction will be lost after the transaction is finished executing. However, reading and writing to storage is computationally cheap and therefore, requires little gas.

-Storage: this data location is persistent between transactions and so we don't have to worry about state variables being lost. However, reading and writing to storage is computationally expensive and therefore, requires a substantial amount of gas to perform.
-Memory: this data location is not persistent between transactions; therefore, values that you write in memory in one transaction will be lost after the transaction is finished executing. However, reading and writing to storage is computationally cheap and therefore, requires little gas.

This brings up a good question: what if we wanted to permanently store data (and lets assume that this data is immutable) on the blockchain without having to use state variables? This leads us to the topic of this section: events.
Defining Events

## Defining Events

Events are data structures that we can then "emit." We first examine the syntax for defining events:
```solidity
event <event_name>(event_args)
```

The definition of an event goes in the body of a smart contract (but never within a function body). Below is a simple example of an event definition:

```solidity
event Transfer(address _from, address _to, uint256 _value);
Emitting Events
```

## Emitting Events

Now that we understand how to define events, we will now explore how to emit an instance of an event. Consider the following function:

```solidity
function emitEvent() public {}
```

To emit the transfer event we defined earlier, we implement the following:

```solidity
function emitEvent() public {
emit Transfer(address(0), address(0), 0);
}
```

where the arguments of the Transfer event are arbitrary.
Original file line number Diff line number Diff line change
@@ -5,17 +5,26 @@ updated: 2024-06-28
authors: [Andrea Vargas, Ash]
icon: Book
---

Perhaps the most famous contract standard, the ERC-20 interface allows for users to develop their own tokens that other users/contracts are able to interact with. To understand what the ERC-20 is from a high-level and why it's almost necessary, let's first consider the following scenario:
Aside: Lack of Information

## Aside: Lack of Information

The concept of a token contract, while intuitive at first, begins to become quite complex when we consider what the implementation of such a contract consists of. As an example, consider a car. We know that a car is a vehicle that takes us from point A to point B; furthermore, we can say that cars move via wheels and we can dictate the direction of a car via a steering wheel. However, consider the following:
How many seats does a car have?
Do all cars come with a retractable sunroof?
How is the car powered (i.e. via gasoline, electric, hydrogen, etc)?

- How many seats does a car have?
- Do all cars come with a retractable sunroof?
- How is the car powered (i.e. via gasoline, electric, hydrogen, etc)?

The questions above do not break down the concept of a car, but rather, they complicate any product meant to complement a car. Let's now focus on tokens. Abstractly, we can state the following:
All accounts have a balance of the token (even if its zero)
We can transfer tokens from one account to another
The token contract does not allow for double-spending and related forms of manipulation

- All accounts have a balance of the token (even if its zero)
- We can transfer tokens from one account to another
- The token contract does not allow for double-spending and related forms of manipulation

Let's now write a token smart contract which achieves the following:

```solidity
contract Token {
mapping(address => uint) balances;
@@ -26,7 +35,11 @@ contract Token {
balances[to] += amount;
}
}
```

Tying back to our car example, lets assume we have another Token contract with the following implementation:

```solidity
contract Token {
mapping(address => uint) balances;
@@ -37,12 +50,19 @@ contract Token {
balances[to] += amount;
}
}
```

The code block above, logically, does the exact same thing as the original Token contract before it. However, notice that the function name in this case changed - we went from transfer to transferTokens. The above demonstrates for a user to interact with the either Token contract, they would first need to find out the name of the function associated with transferring tokens.

But why stop there? We can also name the transfer function the following:

```solidity
function transferSomeTokens() public {}
function doTransfer() public {}
function sendTokens() public {}
// ...
```

As it might have become obvious, for any user that wants to interact with a particular Token contract, they would need to somehow find a way to get the correct function name or risk their transaction reverting. Furthermore, consider the case of a smart contract trying to call a Token contract. We would need to hardcode every single possible function name into our contract - something which is practically impossible.

This entire section leads us to the conclusion that for concepts like token contracts to work, there cannot be a lack of information regarding the behaviors (and their associated names). There needs to be consensus regarding a standard for token contracts which everyone can turn to. The ERC-20 contract, in essence, is this standard.
Loading