diff --git a/content/academy/meta.json b/content/academy/meta.json index 0277e8657e7..eacd7481a08 100644 --- a/content/academy/meta.json +++ b/content/academy/meta.json @@ -1,3 +1,3 @@ { - "pages": ["blockchain-fundamentals", "avalanche-fundamentals", "multi-chain-architecture", "interchain-messaging", "interchain-token-transfer", "customizing-evm", "avacloudapis", "icm-chainlink", "l1-tokenomics"] + "pages": ["blockchain-fundamentals", "avalanche-fundamentals", "multi-chain-architecture", "interchain-messaging", "interchain-token-transfer", "customizing-evm", "avacloudapis", "solidity-foundry", "icm-chainlink", "l1-tokenomics"] } \ No newline at end of file diff --git a/content/academy/solidity-foundry/02-avalanche-starter-kit/01-avalanche-starter-kit.mdx b/content/academy/solidity-foundry/02-avalanche-starter-kit/01-avalanche-starter-kit.mdx new file mode 100644 index 00000000000..76fa9b20a14 --- /dev/null +++ b/content/academy/solidity-foundry/02-avalanche-starter-kit/01-avalanche-starter-kit.mdx @@ -0,0 +1,19 @@ +--- +title: Avalanche Starter Kit +description: Environment Setup +updated: 2024-05-31 +authors: [martineckardt] +icon: Book +--- + +To make easier your journey through this course, we have prepared a Starter Kit repo consisting of everything you will need to start developing your own dApps on Avalanche. This repo will provide a self-contained environment with Avalanche-CLI, and Foundry so you can follow the course without the need of installing anything else other than launching the environment. + +## What You Will Learn + +In this section, you will go through the following topics: + +- How to launch your own Codespace +- Create your own Interchain Messaging Enabled Custom Blockchain +- Foundry configuration + +At the end of this section, you will have your environment ready to follow with the Cross-Chain dApp development with Interchain Messaging \ No newline at end of file diff --git a/content/academy/solidity-foundry/02-avalanche-starter-kit/02-set-up.mdx b/content/academy/solidity-foundry/02-avalanche-starter-kit/02-set-up.mdx new file mode 100644 index 00000000000..df333867c16 --- /dev/null +++ b/content/academy/solidity-foundry/02-avalanche-starter-kit/02-set-up.mdx @@ -0,0 +1,11 @@ +--- +title: Set Up Avalanche Starter Kit +description: Environment Setup +updated: 2024-05-31 +authors: [martineckardt] +icon: Terminal +--- + +import SetUp from "@/content/common/avalanche-starter-kit/set-up.mdx"; + + \ No newline at end of file diff --git a/content/academy/solidity-foundry/02-avalanche-starter-kit/03-close-and-reopen-codespace.mdx b/content/academy/solidity-foundry/02-avalanche-starter-kit/03-close-and-reopen-codespace.mdx new file mode 100644 index 00000000000..ffee101ed8d --- /dev/null +++ b/content/academy/solidity-foundry/02-avalanche-starter-kit/03-close-and-reopen-codespace.mdx @@ -0,0 +1,11 @@ +--- +title: Close and Reopen Codespace +description: Environment Setup +updated: 2024-05-31 +authors: [martineckardt] +icon: Terminal +--- + +import CloseAndReopen from "@/content/common/codespaces/close-and-reopen-codespace.mdx"; + + \ No newline at end of file diff --git a/content/academy/solidity-foundry/02-avalanche-starter-kit/03-create-blockchain.mdx b/content/academy/solidity-foundry/02-avalanche-starter-kit/03-create-blockchain.mdx new file mode 100644 index 00000000000..6e251d21265 --- /dev/null +++ b/content/academy/solidity-foundry/02-avalanche-starter-kit/03-create-blockchain.mdx @@ -0,0 +1,13 @@ +--- +title: Create a Blockchain +description: Environment Setup +updated: 2024-05-31 +authors: [martineckardt] +icon: Terminal +--- + +import CreateDefaultBlockchain from "@/content/common/avalanche-starter-kit/create-default-blockchain.mdx"; + +In order to send messages between two chains, we need to create a blockchain. In this guide, we will spin up a local Avalanche network that has it's own Primary Network (C-, P- and X-Chain) using the Avalanche CLI. Furthermore, we will create a blockchain in that Avalanche network, so we can send messages between the C-Chain and the newly created blockchain in the next chapters. + + \ No newline at end of file diff --git a/content/academy/solidity-foundry/02-avalanche-starter-kit/04-networks.mdx b/content/academy/solidity-foundry/02-avalanche-starter-kit/04-networks.mdx new file mode 100644 index 00000000000..b7f96423a1f --- /dev/null +++ b/content/academy/solidity-foundry/02-avalanche-starter-kit/04-networks.mdx @@ -0,0 +1,13 @@ +--- +title: Networks +description: Environment Setup +updated: 2024-05-31 +authors: [martineckardt] +icon: BookOpen +--- + +import Networks from "@/content/common/avalanche-starter-kit/networks.mdx"; + +You just deployed and interacted with your first local network. But what does that mean? You can interact with your Avalanche L1, but you will notice that no one else can. Therefore, we have to understand what different Networks are in Avalanche. + + \ No newline at end of file diff --git a/content/academy/solidity-foundry/02-avalanche-starter-kit/05-pause-and-resume.mdx b/content/academy/solidity-foundry/02-avalanche-starter-kit/05-pause-and-resume.mdx new file mode 100644 index 00000000000..c734a71817c --- /dev/null +++ b/content/academy/solidity-foundry/02-avalanche-starter-kit/05-pause-and-resume.mdx @@ -0,0 +1,11 @@ +--- +title: Pause and Resume +description: If you've deployed an Avalanche L1, you can preserve and restore the state of your deployed Avalanche L1s. +updated: 2024-10-07 +authors: [0xstt] +icon: BookOpen +--- + +import PauseAndResume from "@/content/common/avalanche-starter-kit/pause-and-resume.mdx"; + + diff --git a/content/academy/solidity-foundry/03-smart-contracts/01-building-programs-on-blockchain.mdx b/content/academy/solidity-foundry/03-smart-contracts/01-building-programs-on-blockchain.mdx new file mode 100644 index 00000000000..346c434840f --- /dev/null +++ b/content/academy/solidity-foundry/03-smart-contracts/01-building-programs-on-blockchain.mdx @@ -0,0 +1,28 @@ +--- +title: Building Programs on Blockchain +description: Learn what a Smart Contract is +updated: 2024-06-28 +authors: [Andrea Vargas, Ash] +icon: Book +--- + +First off, thank you for enrolling in this course! We assume that you are taking this course in order to learn how to write smart contracts. This begs the question - what are smart contracts? + +From a high-level, you may have heard that smart contracts are just code that automate a process. Want to create a decentralized insurance protocol? You can build a smart contract for that. Want to create and distribute NFTs? You can also build a smart contract for that as well. So while we might understand the capabilities of a smart contract, this course will be focused on defining the actual code (and therefore, business logic) required for such intents. + +## The EVM Model +In this course, we will learn Solidity, a high-level programming language that we will use to design smart contracts. Before we delve into learning Solidity, it is important to understand how our smart contracts operate in the grand scheme of things. + +Smart contracts are defined by the following: their behaviors and their state. Focusing first on the behaviors of a smart contract, this is simply the functions that one can call on the smart contracts. Examples of such behaviors can be found below: + +- __Tokens__: behaviors include creating tokens, transferring them, getting the balances of token holders +- __Decentralized Exchange__: behaviors include swapping tokens, adding liquidity to a pool, getting the adresses of both tokens of a liquidity pools +- __DAO__: behaviors include submitting a proposal, voting on a proposal, checking if someone is a governance member + +We now focus on the state of a contract. Abstractly, we can define the state of a contract as being the values and stateful data structures that are associated with said contract. Examples of the state of a smart contract are listed below: + +- __Tokens__: the name of the token, the symbol of the token, the balances of the users +- __Decentralized Exchange__: the number of tokens of a liquidity pool, the amount of tokens a liquidity provider is entitled to +- __DAO__: a list of all governance members, a list of all outstanding proposals + +Understanding what a smart contract under the EVM model consists of, we are now ready to understand the relationship between Solidity and the underlying blockchain. \ No newline at end of file diff --git a/content/academy/solidity-foundry/03-smart-contracts/02-what-is-solidity.mdx b/content/academy/solidity-foundry/03-smart-contracts/02-what-is-solidity.mdx new file mode 100644 index 00000000000..7b4aaccd776 --- /dev/null +++ b/content/academy/solidity-foundry/03-smart-contracts/02-what-is-solidity.mdx @@ -0,0 +1,28 @@ +--- +title: What is Solidity +description: Learn what a Smart Contract is +updated: 2024-06-28 +authors: [Andrea Vargas, Ash] +icon: Book +--- +Our objective throughout this course is to learn how to define the code required to implement a smart contract that matches our intent. The previous section, furthermore, told us that all smart contracts are defined by their state and their behaviors. Therefore, we claim the following: + +> Solidity is a high-level programming language which allows us to define both the state and behaviors of a smart contract. + +Although these behaviors can be almost anything, the majority of the time, such behaviors manipulate the state of the contract. + +## Solidity v.s. Other Programming Languages + +The first question one might have when learning a new language is the following: what does the language even look like? To start, we consider the following code snippet: + +```solidity +contract A { + function getOne() public pure returns(uint) { + return 1; + } +} +``` + +In the above, we have a contract named A. Furthermore, we see that A contains a function getOne() that, when called, returns 1. Even though you might not understand everything that is going on in the code snippet, web developers may realize that the code snippet looks similar to JavaScript. The Solidity syntax, as a matter of fact, is actually inspired by JavaScript, and is a testament to the fact that Solidity itself is a high-level programming language. + +Now that we have an idea of what both Solidity allows us to do and what the actually syntax of Solidity looks like, its time to learn the actual language itself. \ No newline at end of file diff --git a/content/academy/solidity-foundry/03-smart-contracts/03-foundry-quickstart.mdx b/content/academy/solidity-foundry/03-smart-contracts/03-foundry-quickstart.mdx new file mode 100644 index 00000000000..86f6fcfebb2 --- /dev/null +++ b/content/academy/solidity-foundry/03-smart-contracts/03-foundry-quickstart.mdx @@ -0,0 +1,13 @@ +--- +title: Foundry Quickstart +description: Environment Setup +updated: 2024-05-31 +authors: [martineckardt] +icon: BookOpen +--- + +import FoundryQuickstart from "@/content/common/avalanche-starter-kit/foundry-quickstart.mdx"; + +In this chapter, we will look into the Foundry that will be used to deploy and interact with smart contracts on the Avalanche network in our Avalanche Starter kit. + + \ No newline at end of file diff --git a/content/academy/solidity-foundry/03-smart-contracts/04-create-new-smart-contract.mdx b/content/academy/solidity-foundry/03-smart-contracts/04-create-new-smart-contract.mdx new file mode 100644 index 00000000000..19074b5d780 --- /dev/null +++ b/content/academy/solidity-foundry/03-smart-contracts/04-create-new-smart-contract.mdx @@ -0,0 +1,46 @@ +--- +title: Create a new Smart Contract +description: Learn what a Smart Contract is +updated: 2024-06-28 +authors: [Andrea Vargas, Ash] +icon: Book +--- + +Alright, let's create our first contract. Head to the Avalanche Starter Kit Codespace and create a new folder called my-contracts in the _src_ folder. + +Now in there create a new file called HelloWorld.sol and paste the following contents: + +```solidity +contract HelloWorld { + function sayHello() public pure returns (string memory) { + return "Hello World"; + } +} +``` + +Now let's deploy the contract: + +```bash +forge create --rpc-url local-c --private-key $PK src/my-contracts/HelloWorld.sol:HelloWorld +``` + +If deployed successfully we should see something like this: + +```bash +[⠒] +Deployer: 0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC +Deployed to: 0x52C84043CD9c865236f11d9Fc9F56aa003c1f922 +Transaction hash: 0x28247e1292e9489c3b51456e2b848eeb6b82ccbcda18836a638f5d81605ac508 +``` + +This means we have deployed our contract to the address 0x52C84043CD9c865236f11d9Fc9F56aa003c1f922. Using this address we can now call our contract: + +```bash +cast call --rpc-url local-c 0x52C84043CD9c865236f11d9Fc9F56aa003c1f922 "sayHello()(string)" +``` + +Don't forget to replace the contract address with the one your contract was deployed to. The result should look like this: + +```solidity +"Hello World" +``` \ No newline at end of file diff --git a/content/academy/solidity-foundry/04-hello-world-part-1/01-intro.mdx b/content/academy/solidity-foundry/04-hello-world-part-1/01-intro.mdx new file mode 100644 index 00000000000..9e53a048e23 --- /dev/null +++ b/content/academy/solidity-foundry/04-hello-world-part-1/01-intro.mdx @@ -0,0 +1,13 @@ +--- +title: Hello World! Part 1 +description: Learn about Solidity +updated: 2024-06-28 +authors: [Andrea Vargas, Ash] +icon: Book +--- +In this chapter, our main objective will be to learn about the necessary components require to compile and deploy a smart contract. In particular, we want to be able to: + +- Properly create a new Solidity file +- Understand how to organize the business logic of our contract + +After learning the two main points above, you will be tasked with building your first smart contract! With the above in mind, let's get started. \ No newline at end of file diff --git a/content/academy/solidity-foundry/04-hello-world-part-1/02-primitive-value-and-types.mdx b/content/academy/solidity-foundry/04-hello-world-part-1/02-primitive-value-and-types.mdx new file mode 100644 index 00000000000..744f5592b4d --- /dev/null +++ b/content/academy/solidity-foundry/04-hello-world-part-1/02-primitive-value-and-types.mdx @@ -0,0 +1,54 @@ +--- +title: Primitive Values and Types +description: Learn about Solidity +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +icon: Book +--- + +All code, regardless of the language it is written in, can be described (in very very simple terms) as the manipulation of values. Therefore, if we want to learn a new language, we should start at the type of values we will be manipulating; this is where we will start our Solidity journey. + +## Declaring Variable + +We first note that Solidity is a statically-typed language; this means that for any variable that we declare, we must declare its type at the time of initialization. Therefore, the general syntax for declaring a variable is as follows: + +```solidity + ; +``` + +The following are examples of us declaring variables in Solidity: + +```solidity +address addr; +uint256 num; +bool b; +``` + +For right now, we won't focus on what the types mentioned above actually mean. However, the biggest takeaway is that for any variable that we initialize, we must declare its type. + +## Defining Variables + +Now that we know how to declare a variable in Solidity, the other half of the puzzle that we want to solve for is actually assigning a value to these variables. The following code snippet is an example of how we would assign values to variables: + +```solidity +addr = 0x7f610402ccc4CC1BEbcE9699819200f5f28ED6e3; +num = 0; +b = false; +``` + +Rather than having to take the two separate steps of declaring a variable and then defining said variable, we can do these two steps in just line; the following code shows how we would do this: + +```solidity +address addr = 0x7f610402ccc4CC1BEbcE9699819200f5f28ED6e3; +uint256 num = 0; +bool b = false; +``` + +## Types + +Now that we've discussed the process of both declaring and defining a variable, let's return to the topic of types. In Solidity, the following is a (non-exhaustive) list of elementary types that are available: + +- Unsigned Integers: any value which is an unsigned integer is a nonnegative integer. Furthermore, the unsigned integer type is not a singular type, but rather a group of unsigned integers types differentiated by their bit size. Any unsigned integer type has a bit size that is a multiple of 8. So the following types are valid unsigned integer types: uint8, uint16, uint24, uint32, ..., uint256 (uint256 is the maximum unsigned integer bit size). Furthermore, for any unsigned integer uintx (where x is a multiple of 8 and between 0 and 256), we have that the range of uintx is from 0 up to (but not including) 2^(x). +- Signed Integers: any value which is a signed integer is a integer that can be either negative or positive. Like the unsigned integer type, the signed integer type is a group of signed integer types differentiated by their bit size. Any signed integer type has a bit size that is a multipe of 8. Examples of signed integer types are the following: int8, int16, int24, ..., int256 (int256 is the maximum signed integer size). Furthermore, for any signed integer intx (where x is a multiple of 8 and between 0 and 256), we have that the range of intx is from -2^(x - 1) up to (but not including) 2^(x - 1) +- Addresses: the address type consists of a 20-byte value. This type, as its name might suggest, represents the address of an EVM account. +- Booleans: can be either true or false. Treating booleans as integers, a true value is equal to 1 while a false value is equal to 0. diff --git a/content/academy/solidity-foundry/04-hello-world-part-1/03-functions.mdx b/content/academy/solidity-foundry/04-hello-world-part-1/03-functions.mdx new file mode 100644 index 00000000000..cfc5e501ea4 --- /dev/null +++ b/content/academy/solidity-foundry/04-hello-world-part-1/03-functions.mdx @@ -0,0 +1,42 @@ +--- +title: Functions +description: Learn about Solidity +updated: 2024-05-31 +authors: [martineckardt] +icon: BookOpen +--- + +In this section, we will look at functions. Recall from the beginning of this course that all smart contracts consist of a state and their behaviors. With regards to the latter, functions allow us to define the behavior of a smart contract. +## Structure of a Function +Similar to other programming languages, all functions consist of two sections: their header and their body. An example of the following can be found below: + +```solidity +function getOne() public pure returns(uint) { + return 1; +} +``` +In the above, we have the code function getOne() public pure returns(uint) as the header of the function, while the code return 1; is the body of the function (the body of a function is always encapsulated by curly brackets). + +## Function Header +Focusing first on the header of a function, below is the required syntax that all function must have: + +```solidity +function () +``` + +In the parentheses, we list the parameters of the function. In addition to having a name, each parameter of a function must also be marked with its type. The only concept that might be new for most developers is the visibility of a function. In Solidity, we can mark a function with the following visibility traits: + +- Public +- Private +- Internal +- External + +## Function Body +As of right now, the only thing we know what to do inside the body of a function is declaring/defining local variables. Inside the bodies of functions, we can also use regular mathematical operations like in other programming languages; the following code demonstrates this: + +```solidity +function getSquare(uint num) public returns(uint) { + uint square = num ** 2; + return square; +} +``` \ No newline at end of file diff --git a/content/academy/solidity-foundry/04-hello-world-part-1/04-contracts.mdx b/content/academy/solidity-foundry/04-hello-world-part-1/04-contracts.mdx new file mode 100644 index 00000000000..d6e631f5eb8 --- /dev/null +++ b/content/academy/solidity-foundry/04-hello-world-part-1/04-contracts.mdx @@ -0,0 +1,86 @@ +--- +title: Contracts +description: Learn about Solidity +updated: 2024-05-31 +authors: [martineckardt] +icon: BookOpen +--- + +As the halfway mark between knowing absolutely nothing about programming smart contracts and being able to deploy smart contracts, we arrive at the concept of defining contracts themselves. Recall that smart contracts consist of the following: + +- A state +- A set of behaviors + +We will first start by defining the general structure of a smart contract, before diving into defining each individual component that makes a smart contract what it is. + +## General Contract Structure +Below is the general syntax for a smart contract in Solidity: + +```solidity +contract { + + // State Variables go here + + // Contract functions go here + +} +``` + +We declare a contract with the contract keyword, followed by the name of the smart contract (which usually follows CamelCase notation). Afterwards, we use curly brackets to encapsulate the body of the smart contract. + +## State Variables + +The same workflow and rules regarding the declaration and definition of variables apply to variables associated with the state of a smart contract. State variables, as they're called, are persistent between transactions and can only be modified in the following two scenarios: +- During the initialization of the smart contract itself +- By a function of the smart contract itself + +No other account can modify the variables of a smart contract directly; only the contract itself can. An example of state variables with a smart contract is as follows: + +```solidity +contract A { + + uint256 num = 5; + +} +``` + +## State Variable Visibility + +- Solidity is an object-oriented programming language (where contracts act as classes) and so as we will see soon, contracts are able to inherit other contracts. However, we still want to define the inheritance properties of the state variables of a contract. Therefore, we introduce state variable visibility. The following are the three possible type of state variable visibilities: +- Public: the state variable is found in the parent contract and all children contract. Furthermore, any state variable defined as public will contain a getter function of the same name +- Internal: this is the same as public, except a getter function is not created. Any state variable not annotated with a visibility is, by default, set as internal +- Private: the state variable is found only in the parent contract - children contract do not inherit the state variable + +> In this context, the private visibility does not mean that the value of the associated variable is accessible only to the contract. Anyone with access to the blockchain can find the value of a private variable. + +Below are examples of explicitly declaring the visibility of state variable: + +```solidity +contract A { +​ + address private addr; + uint internal num; + int public numTwo; + +} +``` + +## Contract Functions + +Having gone over the state of a contract, we now discuss about contract functions, the second property of smart contracts and the logic that allows us to modify state variables. The same manner in which we declared and defined functions earlier also apply here. The only new thing here, however, is that our contract functions can modify the state variables of the associated contract. As an example, below is a contract with a state variable, and associated functions to get/set the values of the variable: + +```solidity +contract A { + + uint num; + + function setNum(uint _num) public { + num = _num; + } + + function getNum() public view returns(uint) { + return num; + } + +} +``` \ No newline at end of file diff --git a/content/academy/solidity-foundry/04-hello-world-part-1/05-solidity-file-structure.mdx b/content/academy/solidity-foundry/04-hello-world-part-1/05-solidity-file-structure.mdx new file mode 100644 index 00000000000..7d0487e4655 --- /dev/null +++ b/content/academy/solidity-foundry/04-hello-world-part-1/05-solidity-file-structure.mdx @@ -0,0 +1,58 @@ +--- +title: Solidity File Structure +description: Learn about Solidity +updated: 2024-05-31 +authors: [martineckardt] +icon: BookOpen +--- + +Now that we understand how to write (very basic) smart contracts in their entirety, we are certainly ready to deploy smart contracts onto the blockchain... right? + +Unfortunately, we are not yet ready to do so. To understand why, consider the EVM, the virtual machine which will be executing the logic of your smart contract. The EVM does not execute Solidity code - rather, it executes opcodes in the form of bytes. Therefore, for any Solidity smart contract that we develop, we need a way to convert it into EVM bytecode that the EVM can then parse. + +Although the above sounds like a complex task, this is actually not the case! All we need to do is put our smart contract logic into a .sol file, which a Solidity compiler can then convert into EVM bytecode! + +## Compiler and License +At the top of every Solidity file goes the SPDX-License-Identifier. This identifier specifies how you wish for the source code of your file to be used by other people. The most popular SPDX-License-Identifier is the MIT identifier. Afterwards, we will want to specify the version of Solidity that we want to use to compile our source code. This is usually in the format of the following: + +```solidity +pragma solidity ; +``` + +For more information regarding which version of Solidity you should use, head over to the official Solidity [documentation](https://docs.soliditylang.org/en/v0.8.28/) where you can find more information about the details of each release version. Generally speaking though, the following can be observed about the age of release versions: + +- Older release versions are less optimized and lack many features of newer Solidity versions, but they have been battle-tested over the years and are thus more secure +- Newer release versions bring much-desired features and are more optimized, but have had less time to be tested for security purposes + +Sometimes, you might see the following inside a Solidity file: + +```solidity +pragma solidity ^; +``` + +The caret specifies that the source file is meant to be compiled with any version of Solidity that is compatible with version. As an example, consider the line: +pragma solidity ^0.6.4; + +Then the source file can be compiled with any version of Solidity that has the prefix 0.6. However, any compiler whose prefix does not match that of 0.6 cannot compile the source file above. + +## Tying Everything Together +The below is an example of a Solidity source file that can be compiled into bytecode: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +​ +contract A { + + uint256 num; + + function getNum() public view returns(uint) { + return num; + } + + function setNum(uint _num) public { + num = _num; + } + +} +``` \ No newline at end of file diff --git a/content/academy/solidity-foundry/04-hello-world-part-1/06-build-basic-smart-contract.mdx b/content/academy/solidity-foundry/04-hello-world-part-1/06-build-basic-smart-contract.mdx new file mode 100644 index 00000000000..b1487917737 --- /dev/null +++ b/content/academy/solidity-foundry/04-hello-world-part-1/06-build-basic-smart-contract.mdx @@ -0,0 +1,82 @@ +--- +title: Build Basic Smart Contract +description: Learn about Solidity +updated: 2024-05-31 +authors: [martineckardt] +icon: BookOpen +--- + +Let's create and interact with the basic smart contract. Create a new solidity file called NumberStorage.sol and fill it with the contract below: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +​ +contract NumberStorage { + + uint256 num; + + function getNum() public view returns(uint) { + return num; + } + + function setNum(uint _num) public { + num = _num; + } + +} +``` + +Now let's deploy it: + +```bash +forge create --rpc-url local-c --private-key $PK src/my-contracts/NumberStorage.sol:NumberStorage +``` + +The result should look something like this: +```bash +[⠒] Compiling... +[⠢] Compiling 1 files with 0.8.18 +[⠆] Solc 0.8.18 finished in 14.67ms +Compiler run successful! +Deployer: 0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC +Deployed to: 0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25 +Transaction hash: 0x5717e501e12f40d644c60030bc0ab9569ddb9f4cba968546ab597fb516eae09b +``` + +Next, let's store a number: + +```bash +cast send --rpc-url local-c --private-key $PK 0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25 "setNum(uint)" 42 +``` + +Since we are now writing to and not just reading from the blockchain we will see the transaction details: +```bash +blockHash 0x02eb13d317a43976ea8ba21a76e5deb6d02d257a0b98c1a84734e0609b8a6fec +blockNumber 3 +contractAddress +cumulativeGasUsed 43516 +effectiveGasPrice 28000000000 +from 0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC +gasUsed 43516 +logs [] +logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +root +status 1 +transactionHash 0xd382101a4955f05f8a96e4ab4b62457700697930dc6ed84246a346e51d41d3cb +transactionIndex 0 +type 2 +to 0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25 +``` + +Next, let's get the number from the storage: + +```bash +cast call --rpc-url local-c 0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25 "getNum()(uint)" +``` +As a result we should get the number we have stored earlier: + +```bash +@martineckardt ➜ /workspaces/avalanche-starter-kit (main) $ cast call --rpc-url local-c 0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25 "getNum()(uint)" +42 +``` \ No newline at end of file diff --git a/content/academy/solidity-foundry/05-hello-world-part-2/01-intro.mdx b/content/academy/solidity-foundry/05-hello-world-part-2/01-intro.mdx new file mode 100644 index 00000000000..4e6659771b0 --- /dev/null +++ b/content/academy/solidity-foundry/05-hello-world-part-2/01-intro.mdx @@ -0,0 +1,17 @@ +--- +title: Hello World Part. 2 +description: More about Solidity +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +icon: Book +--- + +If you've made it this far into the course, congratulations! At this point so far, you are now able to write very simple smart contracts which can be compiled down into EVM bytecode and deployed onto the blockchain! In this chapter, we will consider the following questions: + +- How can we utilize Solidity's built-in data structures? +- How can we use control flow in our functions? +- How can we store data on the blockchain in a cheap manner? +- How can we utilize contract constructors? +- What does the Solidity inheritance model look like? + +In this chapter, we will tackle the questions above. By looking into these questions, we will learn the skills necessary for taking our smart contracts from being simple programs to complex ones. \ No newline at end of file diff --git a/content/academy/solidity-foundry/05-hello-world-part-2/02-control-flow.mdx b/content/academy/solidity-foundry/05-hello-world-part-2/02-control-flow.mdx new file mode 100644 index 00000000000..2cf889fe2b8 --- /dev/null +++ b/content/academy/solidity-foundry/05-hello-world-part-2/02-control-flow.mdx @@ -0,0 +1,63 @@ +--- +title: Control Flow +description: More about Solidity +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +icon: Book +--- + +When first learning about how to write smart contract functions, we first focus on making sure that our syntax is correct before everything else. Therefore, if you have looked deeply into your starter code, you will notice that your code is purely sequential. That is, every single line of code inside of your functions are executed in a linear manner. While easy to write and understand, writing purely sequential code means that our contracts will behave the exact same way, regardless of the arguments passed in or the state of the contract. If we want our functions to behave differently based on the arguments passed in and the state of the contract, we will want to embrace control flow in our code +## If/Else Statements +The first type of control flow that we will examine is the if/else statement; below is an example of an if/else statement in Solidity: +```solidity +if () { + +} else { + +} +``` +If we wanted to add multiple branches to our if/else statement, we would do the following: + +```solidity +if () { + +} else if () { + +} else { + +} +``` + +## For Loops +The next type of control flow that we will examine are for-loops; for-loops allow us to iterate over a range of integers. Below is the general syntax for for-loops: + +```solidity +for (; ; ) { + +} +``` + +Below is an implementation of the factorial algorithm, which uses for-loops: + +```solidity +function factorial(uint256 num) public pure returns(uint256) { + if (num == 0) { + return 1; + } else if (num == 1) { + return 1; + } + uint256 result = num; + for (uint i = 2; i < num; i++) { + result = result * i; + } + return result; +} +``` +Although not necessary, we usual set the type of the loop variable to type uint256 (uint) as this will become useful when indexing into arrays. +## While Loops +The last type of control flow that we will examine are while loops. Below is the general syntax of while loops in Solidity: +```solidity +while () { + +} +``` \ No newline at end of file diff --git a/content/academy/solidity-foundry/05-hello-world-part-2/03-data-structures.mdx b/content/academy/solidity-foundry/05-hello-world-part-2/03-data-structures.mdx new file mode 100644 index 00000000000..22ed50cfa34 --- /dev/null +++ b/content/academy/solidity-foundry/05-hello-world-part-2/03-data-structures.mdx @@ -0,0 +1,124 @@ +--- +title: Data Structures +description: More about Solidity +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, 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 + +Let's get started! + +## 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( => (value-type)) ; +``` + +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 + +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 +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 +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: + +```solidity +Person memory person = Person(5, "Rodrigo") +``` + +## 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 +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 +[] ; +``` +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 +For a static array in memory, we first must declare it using the memory keyword: +```solidity +[] memory ; +``` +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 + +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 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 +[] ; +``` + +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 + +Below is an example of the methods above in action: +```solidity +uint[] arr; +​ +function test() public { + arr.push(1); + arr.pop(); +} +``` \ No newline at end of file diff --git a/content/academy/solidity-foundry/05-hello-world-part-2/04-contract-constructor.mdx b/content/academy/solidity-foundry/05-hello-world-part-2/04-contract-constructor.mdx new file mode 100644 index 00000000000..a9c19a753b4 --- /dev/null +++ b/content/academy/solidity-foundry/05-hello-world-part-2/04-contract-constructor.mdx @@ -0,0 +1,27 @@ +--- +title: Contract Constructor +description: More about Solidity +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, 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 +Below is the general syntax for a contract constructor: + +```solidity +constructor() { +} +``` +## Example +Below is an example of a contract constructor in action: + +```solidity +contract A { + uint num; + + constructor(uint _num) { + num = _num; + } +} +``` \ No newline at end of file diff --git a/content/academy/solidity-foundry/05-hello-world-part-2/05-modifiers.mdx b/content/academy/solidity-foundry/05-hello-world-part-2/05-modifiers.mdx new file mode 100644 index 00000000000..fbaf05578b3 --- /dev/null +++ b/content/academy/solidity-foundry/05-hello-world-part-2/05-modifiers.mdx @@ -0,0 +1,95 @@ +--- +title: Modifiers +description: More about Solidity +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, 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 + +Recall that with regards to functions, we have the following four visibilities available: +- 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 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 + +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 () {} +``` +To attach a modifier to a function, we have the following: +```solidity +function func() public (){} +``` +To introduce the concept of modifiers to our Safe contract, let's first modify it to the following: +```solidity +contract Safe { + + address owner; + + constructor() { + owner = msg.sender; + } + + function deposit() public {} + + 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. +Incorporating the modifier into our Safe contract we now have: + +```solidity +contract Safe { + + address owner; + + modifier onlyOwner() { + require(msg.sender == owner, "You are not the owner"!); + _; + } + + constructor() { + owner = msg.sender; + } + + function deposit() public onlyOwner() {} + + 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) \ No newline at end of file diff --git a/content/academy/solidity-foundry/05-hello-world-part-2/06-events.mdx b/content/academy/solidity-foundry/05-hello-world-part-2/06-events.mdx new file mode 100644 index 00000000000..cb3c8c2b3b1 --- /dev/null +++ b/content/academy/solidity-foundry/05-hello-world-part-2/06-events.mdx @@ -0,0 +1,49 @@ +--- +title: Events +description: More about Solidity +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +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) + +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. + +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 + +Events are data structures that we can then "emit." We first examine the syntax for defining events: +```solidity +event (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 + +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. \ No newline at end of file diff --git a/content/academy/solidity-foundry/06-contract-standarization/01-contract-standarization.mdx b/content/academy/solidity-foundry/06-contract-standarization/01-contract-standarization.mdx new file mode 100644 index 00000000000..5ea9165277f --- /dev/null +++ b/content/academy/solidity-foundry/06-contract-standarization/01-contract-standarization.mdx @@ -0,0 +1,68 @@ +--- +title: Contract Standarization +description: Learn how to reuse standard code +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +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 + +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)? + +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 + +Let's now write a token smart contract which achieves the following: + +```solidity +contract Token { +​ + mapping(address => uint) balances; +​ + function transfer(address to, uint amount) public { + require(balances[msg.sender] >= amount); + balances[msg.sender] -= amount; + 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; +​ + function transferTokens(address to, uint amount) public { + require(balances[msg.sender] >= amount); + balances[msg.sender] -= amount; + 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. \ No newline at end of file diff --git a/content/academy/solidity-foundry/06-contract-standarization/02-inheritance.mdx b/content/academy/solidity-foundry/06-contract-standarization/02-inheritance.mdx new file mode 100644 index 00000000000..909b563d5e4 --- /dev/null +++ b/content/academy/solidity-foundry/06-contract-standarization/02-inheritance.mdx @@ -0,0 +1,93 @@ +--- +title: Inheritance +description: Learn how to reuse standard code +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +icon: Book +--- +In the previous section, we foreshadowed inheritance, a concept that Solidity shares with other object-oriented programming languages. In this section, we will go over how to inherit other smart contracts and introduce inheritance in the concept of contract constructors. + +## B is A + +At the most fundamental level, contract inheritance works as follows: + +```solidity +contract A {} +​ +contract B is A {} +``` + +In the code snippet above, we have an arbitrary contract A and a contract B which inherits A. Although simple, this doesn't really explain the full concept of inheritance. Let's consider a more sophisticated example: + +```solidity +contract A { + uint num; + + function square(uint base) public pure returns(uint) { + return base ** 2; + } +} +​ +contract B is A {} +``` + +In the code above, we have two contract A and B. The definition of contract A is nothing new, but what about contract B? Well, since B is inheriting A, this implies that B has the following properties: + +- B has the state variable num +- B has the function square + +To really drive home the idea of inheritance, let's consider this final code snippet: + +```solidity +contract A { + + uint private num1; + string internal name; + + function getOne() private pure returns(uint) { + return 1; + } + + function getTwo() public pure returns(uint) { + return 2; + } +} +​ +contract B is A {} +``` + +The code snippet above is more sophisticated in that we are explicitly introducing visibility in the context of inheritance. Again, the definition of A should be trivial. Focusing on B, we have the following: + +- B does not have the state variable num1 and the function getOne, since these are marked as private and therefore, belong only to A +- B has the state variable name since it is marked as internal and therefore, able to be derived by B +- B has the function getTwo since it is marked as public + +## Constructors and Inheritance + +Just like we can inherit functions from parent functions, we can also inherit constructors from parent contracts. The syntax for inheriting parent functions is as follows: + +```solidity +constructor() () {} + +``` + +An example of constructor inheritance can be found below: + +```solidity + +contract A { + uint numl; + constructor() { + num1 = 5; + } +} +​ +contract B is A { + uint num2; + constructor() A() { + num2 = 7; + } +} +``` + +For contract A, we are setting num1 equal to 5 at the time of initialization. For contract B, we first call the constructor of A (which sets num1 in B to 5) and then sets num2 to 7. \ No newline at end of file diff --git a/content/academy/solidity-foundry/06-contract-standarization/03-interfaces.mdx b/content/academy/solidity-foundry/06-contract-standarization/03-interfaces.mdx new file mode 100644 index 00000000000..51bb311df2c --- /dev/null +++ b/content/academy/solidity-foundry/06-contract-standarization/03-interfaces.mdx @@ -0,0 +1,113 @@ +--- +title: Interfaces +description: Understanding Smart Contract Interfaces and Their Role in Standardization +updated: 2025-01-24 +authors: [Andrea Vargas, Ash, martineckardt] +icon: Book +--- + +# Interfaces in Smart Contracts + +## Introduction + +In smart contract development, **interfaces** play a crucial role in ensuring standardization and interoperability between different contracts. By defining a consistent structure that contracts must adhere to, interfaces enable seamless integration across various decentralized applications (dApps), wallets, and protocols. + +Some contracts such as **ERC-20, ERC-721, and ERC-1155** that we will cover later, leverage interfaces to create widely accepted token standards. These standards help different smart contracts interact efficiently without needing to understand each contract's internal implementation. + +In this lesson, we will explore how interfaces work in Solidity, why they are important, and how they contribute to the broader ecosystem of dApps. + +--- + +## What Is an Interface? + +In Solidity, an **interface** is a contract type that defines function signatures without implementing them. This means that an interface only specifies which functions must exist in a contract but does not provide their actual logic. + +Here’s an example of a simple Solidity interface: + +```solidity +interface IExample { + function getValue() external view returns (uint256); + function setValue(uint256 _value) external; +} +``` + +Any contract that implements `IExample` must provide concrete implementations for the `getValue` and `setValue` functions. + +--- + +## Why Are Interfaces Important? + +Interfaces provide several benefits in smart contract development: + +### 1. **Standardization** + - Interfaces ensure that different contracts follow a common structure. + - Examples include **ERC-20** for fungible tokens and **ERC-721** for NFTs, allowing various dApps and protocols to interact with them effortlessly. + +### 2. **Interoperability** + - Contracts that adhere to a shared interface can seamlessly interact without knowing each other’s internal implementation. + - This is essential for decentralized finance (DeFi) protocols, where lending, staking, and trading contracts often need to communicate. + +### 3. **Security and Modularity** + - Developers can create separate implementations for different use cases while ensuring compatibility with existing protocols. + - Security audits become easier since standardized interfaces limit unexpected behaviors. + +--- + +## Implementing an Interface + +A contract that implements an interface **must** include all the functions defined in the interface. Here’s an example implementation: + +```solidity +// Define the interface +interface IExample { + function getValue() external view returns (uint256); + function setValue(uint256 _value) external; +} + +// Implement the interface in a contract +contract ExampleContract is IExample { + uint256 private value; + + function getValue() external view override returns (uint256) { + return value; + } + + function setValue(uint256 _value) external override { + value = _value; + } +} +``` + +### Key Points: +- The `ExampleContract` explicitly states that it implements `IExample` using the `is` keyword. +- The `override` keyword is used to ensure the function implementations match the interface definitions. +- The contract **must** implement all functions declared in `IExample`; otherwise, it will fail to compile. + +--- + +## Abstract Contracts vs. Interfaces + +In Solidity, **abstract contracts** and **interfaces** serve similar purposes but have key differences: + +| Feature | Interface | Abstract Contract | +|-------------------|-----------|------------------| +| Function Implementation | No (only function signatures) | Yes (can have implementations) | +| State Variables | No | Yes | +| Constructor | No | Yes | +| Multiple Inheritance | Yes | No (only single inheritance) | + +### When to Use Which? +- **Use an interface** when you only need to define a contract's required structure. +- **Use an abstract contract** when you need some reusable logic while leaving specific implementations for derived contracts. + +--- + +## Conclusion + +Interfaces are a foundational concept in Solidity that enable **standardization, interoperability, and security** in smart contract development. By enforcing a predefined structure, interfaces allow different contracts to communicate efficiently, fostering the seamless integration of protocols in Ethereum and beyond. + +In the next section, we will explore **abstract contracts** and how they compare to interfaces when designing modular and reusable smart contract architectures. + +--- + +By understanding and utilizing interfaces correctly, developers can ensure their smart contracts remain flexible, compatible, and scalable across the decentralized ecosystem. diff --git a/content/academy/solidity-foundry/06-contract-standarization/04-abstract.mdx b/content/academy/solidity-foundry/06-contract-standarization/04-abstract.mdx new file mode 100644 index 00000000000..9a293496012 --- /dev/null +++ b/content/academy/solidity-foundry/06-contract-standarization/04-abstract.mdx @@ -0,0 +1,170 @@ +--- +title: Abstract Contracts +description: Understanding Abstract Contracts and Their Role in Smart Contract Inheritance +updated: 2025-01-24 +authors: [Andrea Vargas, Ash, martineckardt] +icon: Book +--- + +# Abstract Contracts in Smart Contracts + +## Introduction + +When developing smart contracts on the EVM, developers often need to create reusable and modular contract structures. **Abstract contracts** provide a way to define base contract logic that other contracts can inherit while leaving certain details for implementation in derived contracts. + +Abstract contracts are particularly useful when building **standardized contract templates**, **frameworks for protocols**, and **secure upgradeable contract architectures**. Unlike interfaces, abstract contracts can contain both **implemented and unimplemented (abstract) functions**, making them more flexible for **inheritance-based development**. + +In this lesson, we will explore what abstract contracts are, how they work, and their role in designing scalable and maintainable smart contract architectures. + +--- + +## What Is an Abstract Contract? + +An **abstract contract** is a contract that **cannot be deployed on its own** because it contains at least one function without an implementation. Instead, it serves as a **base contract** that other contracts can inherit and extend. + +Here's an example of an abstract contract: + +```solidity +// Abstract contract defining a base structure +abstract contract BaseContract { + function getValue() public view virtual returns (uint256); +} +``` + +Since `getValue()` is not implemented, `BaseContract` cannot be deployed directly. Any contract inheriting `BaseContract` **must provide an implementation** for `getValue()`. + +--- + +## Why Are Abstract Contracts Important? + +Abstract contracts offer several benefits for **EVM smart contract development**: + +### 1. **Code Reusability** + - Developers can define **common logic** in an abstract contract and extend it in multiple derived contracts. + - This reduces code duplication and improves maintainability. + +### 2. **Flexibility in Design** + - Abstract contracts allow developers to create **modular architectures** where child contracts can implement logic differently based on specific needs. + +### 3. **Secure Standardization** + - By enforcing the implementation of required functions in child contracts, abstract contracts help prevent **inconsistent implementations**. + - This is useful for **token standards, access control mechanisms, and governance contracts**. + +--- + +## Implementing an Abstract Contract + +To use an abstract contract, a child contract must **inherit** from it and implement its unimplemented functions. Here’s an example: + +```solidity +// Abstract contract defining a required function +abstract contract BaseContract { + function getValue() public view virtual returns (uint256); +} + +// Concrete contract inheriting from BaseContract +contract DerivedContract is BaseContract { + uint256 private value = 42; + + function getValue() public view override returns (uint256) { + return value; + } +} +``` + +### Key Points: +- `BaseContract` defines `getValue()` as a **virtual** function, meaning it must be overridden in a derived contract. +- `DerivedContract` **inherits** `BaseContract` and provides an implementation for `getValue()`, making it deployable. + +--- + +## Abstract Contracts vs. Interfaces + +Both **abstract contracts** and **interfaces** define required function structures, but they have key differences: + +| Feature | Abstract Contract | Interface | +|-------------------|------------------|-----------| +| Function Implementation | Yes (can have implementations) | No (only function signatures) | +| State Variables | Yes | No | +| Constructor | Yes | No | +| Multiple Inheritance | No (only single inheritance) | Yes | + +### When to Use Which? +- **Use an abstract contract** when: + - You need to provide **some** reusable logic alongside function definitions. + - You want to define **shared state variables** that child contracts can inherit. + - You are building **modular contract frameworks** with base functionality. + +- **Use an interface** when: + - You only need to **define function signatures** without implementation. + - You want to ensure compatibility with **multiple unrelated contracts**. + +--- + +## Abstract Contracts in Token Standards + +Abstract contracts are frequently used in **EVM token standards** to provide **reusable logic**. For example, OpenZeppelin’s implementation of ERC-20 and ERC-721 uses abstract contracts to define **common functionality**: + +```solidity +abstract contract ERC20 { + function transfer(address recipient, uint256 amount) public virtual returns (bool); +} + +contract MyToken is ERC20 { + function transfer(address recipient, uint256 amount) public override returns (bool) { + // Token transfer logic + return true; + } +} +``` + +By extending the abstract `ERC20` contract, `MyToken` **inherits** its structure while allowing specific implementations for token transfers. + +--- + +## Using Abstract Contracts for Access Control + +Another common use case for abstract contracts is **access control**. Developers can create a base contract that **enforces role-based permissions**, allowing child contracts to inherit this functionality. + +Example of an **abstract access control contract**: + +```solidity +abstract contract AccessControl { + address public owner; + + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Not the owner"); + _; + } + + function restrictedFunction() public view virtual onlyOwner returns (string memory); +} + +contract SecureContract is AccessControl { + function restrictedFunction() public view override onlyOwner returns (string memory) { + return "Access granted!"; + } +} +``` + +### Key Takeaways: +- `AccessControl` defines an **owner** variable and an `onlyOwner` modifier to restrict access. +- The function `restrictedFunction()` is declared **virtual**, meaning any child contract **must implement it**. +- `SecureContract` inherits from `AccessControl` and provides an implementation for `restrictedFunction()`. + +--- + +## Conclusion + +Abstract contracts are a **powerful tool** for **modular, reusable, and secure** smart contract development on the **EVM**. They allow developers to define **base contract logic** while enforcing **required implementations** in child contracts. + +By leveraging **abstract contracts**, developers can: +✅ Reduce code duplication +✅ Improve security through **standardized implementations** +✅ Design flexible and scalable smart contract architectures + +In the next section, we will explore one of the most common contract standars used for **Fungible Tokens**, diving deeper into this standard, and how it extends and interact with one another efficiently in **EVM-based smart contract development**. diff --git a/content/academy/solidity-foundry/07-erc20-smart-contracts/01-erc20-intro.mdx b/content/academy/solidity-foundry/07-erc20-smart-contracts/01-erc20-intro.mdx new file mode 100644 index 00000000000..9af6a1d3611 --- /dev/null +++ b/content/academy/solidity-foundry/07-erc20-smart-contracts/01-erc20-intro.mdx @@ -0,0 +1,63 @@ +--- +title: Intro ERC-20 Tokens +description: Fungible Token Standard +updated: 2025-01-24 +authors: [Andrea Vargas, Ash, martineckardt] +icon: Book +--- + +# Intro ERC-20 Tokens + +## Fungible Token Standard + +Previously, we explored the concept of smart contracts and how they can be used to execute predefined rules on a blockchain. One of the most impactful use cases of smart contracts is the implementation of token standards. The **ERC-20** standard was introduced to create a universal and interoperable framework for tokens on the EVM. + +The ERC-20 standard defines a set of functions and events that a token contract must implement, making it easier for wallets, exchanges, and applications to interact with these tokens. As a result, ERC-20 tokens have become the foundation for countless decentralized finance (DeFi) protocols, governance systems, and utility applications. + +To better understand why ERC-20 is such a critical part of EVM’s ecosystem, let's first explore the concept of fungibility. + +--- + +## Fungibility + +Fungibility refers to an asset’s ability to be exchanged for another of the same type without any difference in value. Let’s use currency as an example: + +- A $10 bill is considered fungible because it holds the same value as another $10 bill. If you exchange one $10 bill for another, their value remains the same. +- Similarly, you can break a $10 bill into smaller denominations like two $5 bills or ten $1 bills, without changing the total value. + +ERC-20 tokens are fungible by design. For instance: + +- 1 unit of an ERC-20 token (e.g., USDC or USDT) is indistinguishable and interchangeable with any other unit of the same token. +- The fungibility of ERC-20 tokens makes them ideal for use cases such as stablecoins, governance tokens, and liquidity pool assets. + +However, fungibility also introduces a limitation: if a use case requires unique attributes for individual tokens, the ERC-20 standard is insufficient. For such scenarios, the ERC-721 standard is used instead, as we will discusse in next chapter. + +--- + +## ERC-20: The Standard + +The ERC-20 token standard was designed to simplify the implementation of fungible tokens and ensure compatibility across various EVM-based applications. At its core, an ERC-20 token contract allows the following operations: + +- Tracking balances of token holders. +- Transferring tokens between accounts. +- Approving other accounts to spend tokens on behalf of the owner. +- Querying details about the token, such as its name, symbol, and total supply. + +Below is the interface for ERC-20 tokens as defined in the original proposal: + +```solidity +interface ERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address recipient, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} diff --git a/content/academy/solidity-foundry/07-erc20-smart-contracts/02-technical-walkthrough.mdx b/content/academy/solidity-foundry/07-erc20-smart-contracts/02-technical-walkthrough.mdx new file mode 100644 index 00000000000..251d6b0508b --- /dev/null +++ b/content/academy/solidity-foundry/07-erc20-smart-contracts/02-technical-walkthrough.mdx @@ -0,0 +1,90 @@ +--- +title: ERC20 Technical Walkthrough +description: Fungible Token Standard +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +icon: Book +--- +In this section, we will introduce the ERC-20 interface that all token contracts should aim to implement. Furthermore, we will go through each function that the ERC-20 contract mentions and explain what it does in-depth. +## ERC-20 Interface + +Below is the ERC-20 interface from EIP-20: + +```solidity +interface IERC20 { +​ + function name() external view returns (string memory); +​ + function symbol() external view returns (string memory); +​ + function decimals() external view returns (uint8); +​ + function totalSupply() external view returns (uint256); +​ + function balanceOf(address _owner) external view returns (uint256 balance); +​ + function transfer(address _to, uint256 _value) external returns (bool success); +​ + function transferFrom(address _from, address _to, uint256 _value) external returns (bool success); +​ + function approve(address _spender, uint256 _value) external returns (bool success); +​ + function allowance(address _owner, address _spender) external view returns (uint256 remaining); +​ +} +``` +The above is derived from eips.ethereum.org/EIPS/eip-20, the official proposal page for the ERC-20 standard. We will now examine each function: + +```solidity +name() +``` + +As the name might suggest, this function returns the name of the token. As an example, for the Wrapped AVAX contract, this would return "Wrapped AVAX". + +```solidity +symbol() +``` + +This function returns the ticker representation of the token. As an example, the United States Dollar usually has the ticker symbol "USD". For Wrapped Avax, we have "WAVAX". + +```solidity +decimals() +``` + +Perhaps the first unfamiliar topic here, decimals refers to the precision of the token. In Solidity, there is no support for floating point numbers (i.e. decimals) and therefore, any quantity must be represented as a whole number. + +To get around this intricacy, we can represent floating-point numbers by multiplying them with a large number to convert them into a whole number: Consider the following examples: + +$$0.05 * 100 = 5$$ +$$0.54367 * 100000 = 54367$$ +$$0.3 * 10 = 3$$ + +Multiplying by a power of 10 is equivalent to shifting a number one place to the right. Therefore, for a quantity of token x (which could be a floating point number), we represent this value on-chain as x * 10^n, where n is a number greater than 0. Therefore, n is what decimals() would return. +For the rest of this section, we will refer to the following terms: +Numerical Representation: a quantity of the token that has been converted from a floating-point number to a whole number (via multiplying by 10^n) +User-Representation: a quantity of the token represented in its true form (i.e. floating-point representation) +totalSupply() +This function returns the total number of tokens in circulation. The number returned is in terms of the token numerical representation. +balanceOf(address _owner) +This function returns the balance of the address passed in as the argument. The number returned is in terms of the token numerical representation. +transfer(address _to, uint256 _value) +When called, this function transfer _value amount of tokens (in terms of the token numerical representation) from the function caller to _to. This function is success if the balance of the caller is at least equal to _value. Furthermore, a successful call of this function should emit the following event: +event Transfer(address indexed _from, address indexed _to, uint256 _value) +transferFrom(address _from, address _to, uint256 _value) +Before discussing the functionality of transferFrom(), we will first discuss the concept of allowances. + +As we saw with the transfer() function, holders of tokens are able to transfer their tokens to other accounts. While this is function is sufficient for a large amount of use cases, consider protocols such as decentralized exchanges which allow users to swap tokens. Generally, the logic of swaps are as follows: +Take x of token A from the user +Give y of token B to the user +The limitations of the transfer() function is with regards to the first step of the logic above. With our current understanding of ERC-20, the swapper would first need to give the decentralized exchange the tokens it wants to exchange. Therefore, this would result in two transactions in order for the whole swap to be executed, something which is highly undesirable. + +ERC-20, therefore, introduces the concept of allowances. By giving another account permission to take a prespecified amount of your tokens, protocols such as decentralized exchanges are able to conduct behaviors like swaps in an atomic manner. + +Focusing back on transferFrom(), this function transfers _value amount of tokens (in terms of the token numerical representation) from _from to _to. For this function to be successful, _from must have previously allocated the function caller at least _value amount of tokens to spend on their behalf. + +This function also emits the Transfer event seen previously. Furthermore, this function updates the amount of tokens that the caller is able to spend on behalf of _from. +approve(address _spender, uint256 _value) +This function gives permission to _spender to spend at most _value tokens (in terms of the token numerical representation) from the balance of the function caller. If successful, this function emits the following event: +event Approval(address indexed _owner, address indexed _spender, uint256 _value) +allowance(address _owner, address _spender) +This function returns the amount of tokens (in terms of the token numerical representation) that _spender is allowed to spend on behalf of _owner. \ No newline at end of file diff --git a/content/academy/solidity-foundry/07-erc20-smart-contracts/03-interacting-with-erc20-tokens.mdx b/content/academy/solidity-foundry/07-erc20-smart-contracts/03-interacting-with-erc20-tokens.mdx new file mode 100644 index 00000000000..d1670ba49c8 --- /dev/null +++ b/content/academy/solidity-foundry/07-erc20-smart-contracts/03-interacting-with-erc20-tokens.mdx @@ -0,0 +1,128 @@ +--- +title: Interacting with ERC20 Tokens +description: Fungible Token Standard +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +icon: Book +--- +In this section, we will go through an example of how to interact with an ERC-20 from another contract. +## Read Operations +Before calling any operations of an ERC-20 contract, we will need to have the following available: + +- The ERC-20 interface +- The address of the ERC-20 token we want to interact with + +Our starting Solidity file, therefore, will the following code: + +```solidity + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +​ +interface IERC20 { +​ +​ + function name() external view returns (string memory); +​ + function symbol() external view returns (string memory); +​ + function decimals() external view returns (uint8); +​ + function totalSupply() external view returns (uint256); +​ + function balanceOf(address _owner) external view returns (uint256 balance); +​ + function transfer(address _to, uint256 _value) external returns (bool success); +​ + function transferFrom(address _from, address _to, uint256 _value) external returns (bool success); +​ + function approve(address _spender, uint256 _value) external returns (bool success); +​ + function allowance(address _owner, address _spender) external view returns (uint256 remaining); +​ +} +``` + +```solidity​ +contract Test { +​ + address tokenAddress; +​ +} +``` + +As a starter, let's write a function that gets the name of the token: + +```solidity +contract Test { +​ + address tokenAddress; + + function getName() public view returns(string memory) { + return IERC20(tokenAddress).name(); + } +​ +} +``` + +Perhaps the only unfamiliar piece of syntax is in line 6. In Solidity, note the following: + +- Contracts and interfaces are inherently types. +- We can wrap an address type into a contract/interface type. If the contract associated with the address + +With the above in mind, the logic of getName is as follows: + +- We call the function name at address token by wrapping the address type with the IERC20 interface +- name returns us the name of the token contract +- getName returns the name of the token contract + +Just like that, we were able to call a read function of an ERC-20 contract. But what if we wanted to modify the state of an ERC-20 contract? + +## Write Operations + +In this example, we will look at calling the transferFrom function of an ERC-20 token contract. Recall that, if authorized, transferFrom allows us to transfer tokens from one account to another. As a starter, we have the following code: + +```solidity +contract Test { +​ + address tokenAddress; + address from; + address to; + uint amt; + + function getName() public view returns(string memory) { + return IERC20(tokenAddress).name(); + } + + function doTransferFrom() public returns(bool) { + return IERC20(tokenAddress).transferFrom(from, to, amt); + } +​ +} +``` + +In addition to adding additional state variables for context, we have also added the function doTransferFrom which calls the transferFrom function of the ERC-20 token contract. In particular, doTransferFrom returns the boolean returned by transferFrom, which represents whether if the function was successful or not. While the code above works, we can make it better by first checking if we are have been allocated enough tokens such that transferFrom(from, to, amt) will return true. To do this, we can make a call to the allowance function of the ERC-20 token contract. Our updated code is as follows: + +```solidity +contract Test { +​ + address token; + address from; + address to; + uint amt; + + function getName() public view returns(string memory) { + return IERC20(token).name(); + } + + function doTransferFrom() public returns(bool) { + if (IERC20(token).allowance(from, address(this) < amt) { + return false; + } + return IERC20(token).transferFrom(from, to, amt); + } +​ +} +``` + +In line 13, we are passing in both the address of the spender and the address of our own smart contract to the allowance function, which returns the amount of allocated tokens which we compare to our amt variable. If we are have not been allocated enough tokens, we return false and therefore, we do not spend any additional computation resources calling transferFrom, which we know would have been unsuccessful. \ No newline at end of file diff --git a/content/academy/solidity-foundry/07-erc20-smart-contracts/04-deploying-your-erc20-token.mdx b/content/academy/solidity-foundry/07-erc20-smart-contracts/04-deploying-your-erc20-token.mdx new file mode 100644 index 00000000000..7b56a37431a --- /dev/null +++ b/content/academy/solidity-foundry/07-erc20-smart-contracts/04-deploying-your-erc20-token.mdx @@ -0,0 +1,36 @@ +--- +title: Deploying your own ERC20 Token +description: Fungible Token Standard +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +icon: BookOpen +--- + +At this point of this chapter, you have become familiar with the design and implementation of the ERC-20 token standard. Still, you might have the following question: how does one actually go about deploying ERC-20 tokens? In this section, we will cover the following deployment methods: + +- Custom Deployment +- OpenZeppelin Wizard Contract Deployer + +## Custom Deployment + +By custom deployment, we mean writing your own ERC-20 contract by hand. Right off the bat, this option offers the most customization as you are fully in control over the implementation details of your ERC-20 token. As long as you adhere to the ERC-20 token interface, your token is able to be used by any ERC-20 compatible protocol. However, consider the following downsides of writing your own ERC-20 token by-hand: + +- Security: by writing your own custom code, you are putting your ERC-20 token at risk of being hacked by malicious actors. Even the best of programmers can fall victim to security vulnerabilities related to ERC-20 contracts! +- Gas Inefficiencies: it could be that while your contract is "correct," it may be expensive for users to interact with your ERC-20 contract. + +The two reasons above, in additions to other small nuances, make it evident that writing your own ERC-20 token from scratch is not the best idea. This leads us to the other option: + +## OpenZeppelin Wizard + +OpenZeppelin's Wizard Contract Creator is a useful tool which allows for developers to deploy contracts on the fly. By this, we mean that by just filling a couple of fields and selecting a few options, we can autogenerate the code necessary to deploy a ERC-20 token to our liking! To get started, head over to [wizard.openzeppelin.com/#erc20](wizard.openzeppelin.com/#erc20) where you will see the following: + +![](/course-images/smartcontract-foundry/erc20-OpenZeppelin.png) + + +Front and center, you will see some Solidity code. At first, it seem as if this ERC20 contract OpenZeppelin has generated for us contains basically nothing. However, if you look closely, you will see that the MyToken contract that OpenZeppelin has generated for us inherits the ERC20, ERC20Permit contracts, which are defined and contain all the logic for our ERC-20 contract. This is the great part about the OpenZeppelin Wizard Contract Creator - we can easily modify our ERC-20 contract by inheriting the contracts whose functionalities we want. Therefore, as an example, let's create the following contract: + +![](/course-images/smartcontract-foundry/erc20-OpenZeppelin2.png) + +In the above, we've created a new ERC-20 contract named BigRedCoin. Furthermore, this ERC-20 token allocates to the deployer 20000 tokens on initialization and gives the owner full rights over the behavior of the token. Now that we have our ERC-20 token contract customized, how do we actually deploy it? + +If you look at the righthand side of the Wizard page, you will see the button Copy to Clipboard. As the name might suggest, this allows us to copy our contract and paste it to a file in our Avalanche Starter Kit, which we can then compile and deploy onto the blockchain! \ No newline at end of file diff --git a/content/academy/solidity-foundry/08-erc721-smart-contracts/01-erc721-intro.mdx b/content/academy/solidity-foundry/08-erc721-smart-contracts/01-erc721-intro.mdx new file mode 100644 index 00000000000..0576fe5702c --- /dev/null +++ b/content/academy/solidity-foundry/08-erc721-smart-contracts/01-erc721-intro.mdx @@ -0,0 +1,133 @@ +--- +title: Intro ERC-721 Tokens +description: Non-Fungible Token Standard +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +icon: Book +--- +Previously, we explored the ERC-20 interface, a contract standard which allowed for the implementation of a token contract which the majority of smart contract users are able to easily call. As a result of the ERC-20 standard, many protocols were able flourish without having to worry about incompatible token interfaces. However, there is one thing that the ERC-20 standard did not fix... +To understand the biggest shortcoming of the ERC-20 standard, we have discussed the concept of fungibility. Its design enables crucial fucntionality. However, this also implies that for a use case that requires for 1 unit of some ERC-20 A to be different in value than 1 unit of the same ERC-20 token A, the ERC-20 token standard is not sufficient for such a use case. + +## ERC-721 + +To make clear a particular that ERC-20 cannot support, imagine we wanted to represent an art collection within a smart contract. The smart contract would contain the following functionality in a similar fashion to an ERC-20 token: +- The ability to query the "balance" (i.e. the art holdings) of a particular user +- The ability to transfer art pieces from one account to another +- The ability to get information about the art collection +- The biggest different between an arbitrary ERC-20 token contract and an art collection is the fungibility of individual items - ERC-20 tokens are inherently fungible with items in the art collections are not (since the items each vary in value). + +To account for uses cases like an art collection, the ERC-721 standard was introduced. Formally, ERC-721 is a standard for non-fungible tokens. Below is the interface of the ERC-721 token from its original proposal : + +```solidity +interface ERC721 { + /// @dev This emits when ownership of any NFT changes by any mechanism. + /// This event emits when NFTs are created (`from` == 0) and destroyed + /// (`to` == 0). Exception: during contract creation, any number of NFTs + /// may be created and assigned without emitting Transfer. At the time of + /// any transfer, the approved address for that NFT (if any) is reset to none. + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); +​ + /// @dev This emits when the approved address for an NFT is changed or + /// reaffirmed. The zero address indicates there is no approved address. + /// When a Transfer event emits, this also indicates that the approved + /// address for that NFT (if any) is reset to none. + event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); +​ + /// @dev This emits when an operator is enabled or disabled for an owner. + /// The operator can manage all NFTs of the owner. + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); +​ + /// @notice Count all NFTs assigned to an owner + /// @dev NFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param _owner An address for whom to query the balance + /// @return The number of NFTs owned by `_owner`, possibly zero + function balanceOf(address _owner) external view returns (uint256); +​ + /// @notice Find the owner of an NFT + /// @dev NFTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// @param _tokenId The identifier for an NFT + /// @return The address of the owner of the NFT + function ownerOf(uint256 _tokenId) external view returns (address); +​ + /// @notice Transfers the ownership of an NFT from one address to another address + /// @dev Throws unless `msg.sender` is the current owner, an authorized + /// operator, or the approved address for this NFT. Throws if `_from` is + /// not the current owner. Throws if `_to` is the zero address. Throws if + /// `_tokenId` is not a valid NFT. When transfer is complete, this function + /// checks if `_to` is a smart contract (code size > 0). If so, it calls + /// `onERC721Received` on `_to` and throws if the return value is not + /// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + /// @param data Additional data with no specified format, sent in call to `_to` + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; +​ + /// @notice Transfers the ownership of an NFT from one address to another address + /// @dev This works identically to the other function with an extra data parameter, + /// except this function just sets data to "". + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; +​ + /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + /// TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE + /// THEY MAY BE PERMANENTLY LOST + /// @dev Throws unless `msg.sender` is the current owner, an authorized + /// operator, or the approved address for this NFT. Throws if `_from` is + /// not the current owner. Throws if `_to` is the zero address. Throws if + /// `_tokenId` is not a valid NFT. + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function transferFrom(address _from, address _to, uint256 _tokenId) external payable; +​ + /// @notice Change or reaffirm the approved address for an NFT + /// @dev The zero address indicates there is no approved address. + /// Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param _approved The new approved NFT controller + /// @param _tokenId The NFT to approve + function approve(address _approved, uint256 _tokenId) external payable; +​ + /// @notice Enable or disable approval for a third party ("operator") to manage + /// all of `msg.sender`'s assets + /// @dev Emits the ApprovalForAll event. The contract MUST allow + /// multiple operators per owner. + /// @param _operator Address to add to the set of authorized operators + /// @param _approved True if the operator is approved, false to revoke approval + function setApprovalForAll(address _operator, bool _approved) external; +​ + /// @notice Get the approved address for a single NFT + /// @dev Throws if `_tokenId` is not a valid NFT. + /// @param _tokenId The NFT to find the approved address for + /// @return The approved address for this NFT, or the zero address if there is none + function getApproved(uint256 _tokenId) external view returns (address); +​ + /// @notice Query if an address is an authorized operator for another address + /// @param _owner The address that owns the NFTs + /// @param _operator The address that acts on behalf of the owner + /// @return True if `_operator` is an approved operator for `_owner`, false otherwise + function isApprovedForAll(address _owner, address _operator) external view returns (bool); +} +``` + +## Implementation Design + +Notice that for an ERC-20 token, we can store the balance of all token holders with the following data structure: + +```solidity +mapping(address => uint) balances; +``` + +In the case of the ERC-721 token standard, this is not enough, since using just this data structure would imply that all tokens of an ERC-721 contract are fungible. Therefore, we want to use the following data structures: + +```solidity +mapping(address => uint) balances; +mapping(uint => address) holders; +``` + +The balances data structure will map accounts to the number of non-fungible tokens they hold. The data structure holders, meanwhile, will map the ID of each non-fungible token to the address which holds the token. Therefore, by adding just another mapping, we've just allowed our token contract to incorporate non-fungible tokens! \ No newline at end of file diff --git a/content/academy/solidity-foundry/08-erc721-smart-contracts/02-technical-walkthrough.mdx b/content/academy/solidity-foundry/08-erc721-smart-contracts/02-technical-walkthrough.mdx new file mode 100644 index 00000000000..80016103ff7 --- /dev/null +++ b/content/academy/solidity-foundry/08-erc721-smart-contracts/02-technical-walkthrough.mdx @@ -0,0 +1,130 @@ +--- +title: ERC721 Technical Walkthrough +description: Non-Fungible Token Standard +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +icon: Book +--- +Just like with the ERC-20 chapter, in this section we will examine each of the functions that make up the ERC-721 interface. Below is the ERC-721 interface seen from the last section: + +```solidity +pragma solidity ^0.4.20; +​ +/// @title ERC-721 Non-Fungible Token Standard +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// Note: the ERC-165 identifier for this interface is 0x80ac58cd. +interface ERC721 { + /// @dev This emits when ownership of any NFT changes by any mechanism. + /// This event emits when NFTs are created (`from` == 0) and destroyed + /// (`to` == 0). Exception: during contract creation, any number of NFTs + /// may be created and assigned without emitting Transfer. At the time of + /// any transfer, the approved address for that NFT (if any) is reset to none. + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); +​ + /// @dev This emits when the approved address for an NFT is changed or + /// reaffirmed. The zero address indicates there is no approved address. + /// When a Transfer event emits, this also indicates that the approved + /// address for that NFT (if any) is reset to none. + event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); +​ + /// @dev This emits when an operator is enabled or disabled for an owner. + /// The operator can manage all NFTs of the owner. + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); +​ + /// @notice Count all NFTs assigned to an owner + /// @dev NFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param _owner An address for whom to query the balance + /// @return The number of NFTs owned by `_owner`, possibly zero + function balanceOf(address _owner) external view returns (uint256); +​ + /// @notice Find the owner of an NFT + /// @dev NFTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// @param _tokenId The identifier for an NFT + /// @return The address of the owner of the NFT + function ownerOf(uint256 _tokenId) external view returns (address); +​ + /// @notice Transfers the ownership of an NFT from one address to another address + /// @dev Throws unless `msg.sender` is the current owner, an authorized + /// operator, or the approved address for this NFT. Throws if `_from` is + /// not the current owner. Throws if `_to` is the zero address. Throws if + /// `_tokenId` is not a valid NFT. When transfer is complete, this function + /// checks if `_to` is a smart contract (code size > 0). If so, it calls + /// `onERC721Received` on `_to` and throws if the return value is not + /// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + /// @param data Additional data with no specified format, sent in call to `_to` + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; +​ + /// @notice Transfers the ownership of an NFT from one address to another address + /// @dev This works identically to the other function with an extra data parameter, + /// except this function just sets data to "". + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; +​ + /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + /// TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE + /// THEY MAY BE PERMANENTLY LOST + /// @dev Throws unless `msg.sender` is the current owner, an authorized + /// operator, or the approved address for this NFT. Throws if `_from` is + /// not the current owner. Throws if `_to` is the zero address. Throws if + /// `_tokenId` is not a valid NFT. + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function transferFrom(address _from, address _to, uint256 _tokenId) external payable; +​ + /// @notice Change or reaffirm the approved address for an NFT + /// @dev The zero address indicates there is no approved address. + /// Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param _approved The new approved NFT controller + /// @param _tokenId The NFT to approve + function approve(address _approved, uint256 _tokenId) external payable; +​ + /// @notice Enable or disable approval for a third party ("operator") to manage + /// all of `msg.sender`'s assets + /// @dev Emits the ApprovalForAll event. The contract MUST allow + /// multiple operators per owner. + /// @param _operator Address to add to the set of authorized operators + /// @param _approved True if the operator is approved, false to revoke approval + function setApprovalForAll(address _operator, bool _approved) external; +​ + /// @notice Get the approved address for a single NFT + /// @dev Throws if `_tokenId` is not a valid NFT. + /// @param _tokenId The NFT to find the approved address for + /// @return The approved address for this NFT, or the zero address if there is none + function getApproved(uint256 _tokenId) external view returns (address); +​ + /// @notice Query if an address is an authorized operator for another address + /// @param _owner The address that owns the NFTs + /// @param _operator The address that acts on behalf of the owner + /// @return True if `_operator` is an approved operator for `_owner`, false otherwise + function isApprovedForAll(address _owner, address _operator) external view returns (bool); +} +``` + +__balanceOf(address _owner)__ +Similar to the function of the same name in the ERC20 interface, balanceOf returns the number of non-fungible tokens that _owner has. + +__ownerOf(uint256 _tokenId)__ +This function returns the address that owns the non-fungible token with id _tokenId +__safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data)__ +This function is very similar to the transferFrom function in the ERC-20 specification. safeTransferFrom transfer a NFT of id _tokenId from _from to _to . However, after transferring the token, safeTransferFrom checks if the account is capable of handling the NFT (via passing in the arguments _from, _to, _tokenId, and data. If _to is not capable of handling the NFT, safeTransferFrom reverts. +__safeTransferFrom(address _from, address _to, uint256 _tokenId)__ +This function behaves exactly like the function above, except that the argument data is set to the empty string. +__transferFrom(address _from, address _to, uint256 _tokenId)__ +This function behaves exactly like safeTransferFrom, except that we do not check if _to is capable of receiving the NFT with id _tokenId. +__approve(address _approved, uint256 _tokenId)__ +When called, this function gives _approved transfer rights to the NFT with id _tokenId. This function fails if an account without transfer rights to the NFT is the caller. +__setApprovalForAll(address _operator, bool _approved)__ +When called, this function gives _operator either transfer rights to all NFTs of the function caller (if _approved is true) or revokes transfer rights to all NFTs of the function caller (if _approved is false). +__getApproved(uint256 _tokenId)__ +This function returns the address of the account with transfer rights to the NFT with id _tokenId. +__isApprovedForAll(address _owner, address _operator)__ +This function returns true if _operator has transfer rights to all of the NFTs that _owner holds, and false otherwise. \ No newline at end of file diff --git a/content/academy/solidity-foundry/08-erc721-smart-contracts/03-interacting-with-erc721-tokens.mdx b/content/academy/solidity-foundry/08-erc721-smart-contracts/03-interacting-with-erc721-tokens.mdx new file mode 100644 index 00000000000..ade9895cceb --- /dev/null +++ b/content/academy/solidity-foundry/08-erc721-smart-contracts/03-interacting-with-erc721-tokens.mdx @@ -0,0 +1,283 @@ +--- +title: Interacting with ERC721 Tokens +description: Non-Fungible Token Standard +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +icon: Book +--- +Just like with the ERC-20 chapter, in this section, we will take a look at interacting with the functions that make up a ERC-721 token contract: + +```solidity +/// @title ERC-721 Non-Fungible Token Standard +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// Note: the ERC-165 identifier for this interface is 0x80ac58cd. +interface ERC721 { + /// @dev This emits when ownership of any NFT changes by any mechanism. + /// This event emits when NFTs are created (`from` == 0) and destroyed + /// (`to` == 0). Exception: during contract creation, any number of NFTs + /// may be created and assigned without emitting Transfer. At the time of + /// any transfer, the approved address for that NFT (if any) is reset to none. + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); +​ + /// @dev This emits when the approved address for an NFT is changed or + /// reaffirmed. The zero address indicates there is no approved address. + /// When a Transfer event emits, this also indicates that the approved + /// address for that NFT (if any) is reset to none. + event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); +​ + /// @dev This emits when an operator is enabled or disabled for an owner. + /// The operator can manage all NFTs of the owner. + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); +​ + /// @notice Count all NFTs assigned to an owner + /// @dev NFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param _owner An address for whom to query the balance + /// @return The number of NFTs owned by `_owner`, possibly zero + function balanceOf(address _owner) external view returns (uint256); +​ + /// @notice Find the owner of an NFT + /// @dev NFTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// @param _tokenId The identifier for an NFT + /// @return The address of the owner of the NFT + function ownerOf(uint256 _tokenId) external view returns (address); +​ + /// @notice Transfers the ownership of an NFT from one address to another address + /// @dev Throws unless `msg.sender` is the current owner, an authorized + /// operator, or the approved address for this NFT. Throws if `_from` is + /// not the current owner. Throws if `_to` is the zero address. Throws if + /// `_tokenId` is not a valid NFT. When transfer is complete, this function + /// checks if `_to` is a smart contract (code size > 0). If so, it calls + /// `onERC721Received` on `_to` and throws if the return value is not + /// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + /// @param data Additional data with no specified format, sent in call to `_to` + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) external payable; +​ + /// @notice Transfers the ownership of an NFT from one address to another address + /// @dev This works identically to the other function with an extra data parameter, + /// except this function just sets data to "". + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; +​ + /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + /// TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE + /// THEY MAY BE PERMANENTLY LOST + /// @dev Throws unless `msg.sender` is the current owner, an authorized + /// operator, or the approved address for this NFT. Throws if `_from` is + /// not the current owner. Throws if `_to` is the zero address. Throws if + /// `_tokenId` is not a valid NFT. + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function transferFrom(address _from, address _to, uint256 _tokenId) external payable; +​ + /// @notice Change or reaffirm the approved address for an NFT + /// @dev The zero address indicates there is no approved address. + /// Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param _approved The new approved NFT controller + /// @param _tokenId The NFT to approve + function approve(address _approved, uint256 _tokenId) external payable; +​ + /// @notice Enable or disable approval for a third party ("operator") to manage + /// all of `msg.sender`'s assets + /// @dev Emits the ApprovalForAll event. The contract MUST allow + /// multiple operators per owner. + /// @param _operator Address to add to the set of authorized operators + /// @param _approved True if the operator is approved, false to revoke approval + function setApprovalForAll(address _operator, bool _approved) external; +​ + /// @notice Get the approved address for a single NFT + /// @dev Throws if `_tokenId` is not a valid NFT. + /// @param _tokenId The NFT to find the approved address for + /// @return The approved address for this NFT, or the zero address if there is none + function getApproved(uint256 _tokenId) external view returns (address); +​ + /// @notice Query if an address is an authorized operator for another address + /// @param _owner The address that owns the NFTs + /// @param _operator The address that acts on behalf of the owner + /// @return True if `_operator` is an approved operator for `_owner`, false otherwise + function isApprovedForAll(address _owner, address _operator) external view returns (bool); +} +``` + +## Example +To start, let's define a Solidity file with the following: + +- The ERC721 token standard interface +- A caller contract which stores the address of the ERC-721 contract we want to interact with and an arbitrary NFT id + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +​ +/// @title ERC-721 Non-Fungible Token Standard +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// Note: the ERC-165 identifier for this interface is 0x80ac58cd. +interface ERC721 { + /// @dev This emits when ownership of any NFT changes by any mechanism. + /// This event emits when NFTs are created (`from` == 0) and destroyed + /// (`to` == 0). Exception: during contract creation, any number of NFTs + /// may be created and assigned without emitting Transfer. At the time of + /// any transfer, the approved address for that NFT (if any) is reset to none. + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); +​ + /// @dev This emits when the approved address for an NFT is changed or + /// reaffirmed. The zero address indicates there is no approved address. + /// When a Transfer event emits, this also indicates that the approved + /// address for that NFT (if any) is reset to none. + event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); +​ + /// @dev This emits when an operator is enabled or disabled for an owner. + /// The operator can manage all NFTs of the owner. + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); +​ + /// @notice Count all NFTs assigned to an owner + /// @dev NFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param _owner An address for whom to query the balance + /// @return The number of NFTs owned by `_owner`, possibly zero + function balanceOf(address _owner) external view returns (uint256); +​ + /// @notice Find the owner of an NFT + /// @dev NFTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// @param _tokenId The identifier for an NFT + /// @return The address of the owner of the NFT + function ownerOf(uint256 _tokenId) external view returns (address); +​ + /// @notice Transfers the ownership of an NFT from one address to another address + /// @dev Throws unless `msg.sender` is the current owner, an authorized + /// operator, or the approved address for this NFT. Throws if `_from` is + /// not the current owner. Throws if `_to` is the zero address. Throws if + /// `_tokenId` is not a valid NFT. When transfer is complete, this function + /// checks if `_to` is a smart contract (code size > 0). If so, it calls + /// `onERC721Received` on `_to` and throws if the return value is not + /// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + /// @param data Additional data with no specified format, sent in call to `_to` + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) external payable; +​ + /// @notice Transfers the ownership of an NFT from one address to another address + /// @dev This works identically to the other function with an extra data parameter, + /// except this function just sets data to "". + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; +​ + /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + /// TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE + /// THEY MAY BE PERMANENTLY LOST + /// @dev Throws unless `msg.sender` is the current owner, an authorized + /// operator, or the approved address for this NFT. Throws if `_from` is + /// not the current owner. Throws if `_to` is the zero address. Throws if + /// `_tokenId` is not a valid NFT. + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function transferFrom(address _from, address _to, uint256 _tokenId) external payable; +​ + /// @notice Change or reaffirm the approved address for an NFT + /// @dev The zero address indicates there is no approved address. + /// Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param _approved The new approved NFT controller + /// @param _tokenId The NFT to approve + function approve(address _approved, uint256 _tokenId) external payable; +​ + /// @notice Enable or disable approval for a third party ("operator") to manage + /// all of `msg.sender`'s assets + /// @dev Emits the ApprovalForAll event. The contract MUST allow + /// multiple operators per owner. + /// @param _operator Address to add to the set of authorized operators + /// @param _approved True if the operator is approved, false to revoke approval + function setApprovalForAll(address _operator, bool _approved) external; +​ + /// @notice Get the approved address for a single NFT + /// @dev Throws if `_tokenId` is not a valid NFT. + /// @param _tokenId The NFT to find the approved address for + /// @return The approved address for this NFT, or the zero address if there is none + function getApproved(uint256 _tokenId) external view returns (address); +​ + /// @notice Query if an address is an authorized operator for another address + /// @param _owner The address that owns the NFTs + /// @param _operator The address that acts on behalf of the owner + /// @return True if `_operator` is an approved operator for `_owner`, false otherwise + function isApprovedForAll(address _owner, address _operator) external view returns (bool); +} +​ +contract Caller { +​ + address tokenAddress; + + uint tokenId; +​ +} +``` + +As a starter, let's write a function that will query the owner of a NFT with id tokenId: + +```solidity +contract Caller { +​ + address tokenAddress; + + uint tokenId; +​ + function getNFTOwner() public view returns(address) { + return ERC721(tokenAddress).ownerOf(tokenId); + } +​ +} +``` + +Next, let's write a function that, when called by some account A, transfers the NFT with id tokenId to the caller (i.e. we give up our own NFT; you definitely do not want to do this in the real world!): + +```solidity +contract Caller { +​ + address tokenAddress; + uint tokenId; +​ + function getNFTOwner() public view returns(address) { + return ERC721(token).ownerOf(tokenId); + } +​ + function transferNFT() public { + ERC721(tokenAddress).safeTransferFrom(address(this), msg.sender, tokenId); + } +​ +} +``` + +Finally, let's write a function that, when called, gives the caller transfer rights to all of our NFTs (again, don't do this in the real world!): + +```solidity +contract Caller { +​ + address tokenAddress; + uint tokenId; +​ + function getNFTOwner() public view returns(address) { + return ERC721(tokenAddress).ownerOf(tokenId); + } +​ + function transferNFT() public { + ERC721(tokenAddress).safeTransferFrom(address(this), msg.sender, tokenId); + } +​ + function giveAllTransferRights() public { + ERC721(tokenAddress).setApprovalForAll(msg.sender, true); + } +​ +} +``` \ No newline at end of file diff --git a/content/academy/solidity-foundry/08-erc721-smart-contracts/04-deploying-your-erc721-token.mdx b/content/academy/solidity-foundry/08-erc721-smart-contracts/04-deploying-your-erc721-token.mdx new file mode 100644 index 00000000000..8ab6bc2c4f1 --- /dev/null +++ b/content/academy/solidity-foundry/08-erc721-smart-contracts/04-deploying-your-erc721-token.mdx @@ -0,0 +1,20 @@ +--- +title: Deploying your own ERC20 Token +description: Non-Fungible Token Standard +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +icon: BookOpen +--- + +We again come across the question of how to deploy a ERC-721 token on the blockchain. Since we have access to the ERC-721 contract interface, there is nothing stopping us from writing our own implementation of an ERC-721 token. However, if you recall the deployment section of the ERC-20 chapter, then you know that we were able to use OpenZeppelin's Contract Wizard to autogenerate our own ERC-20 contract. Likewise, OpenZeppelin's Contract Wizard also allows to create ERC-721 tokens on the fly. For the rest of this section, we will look at just that! + +## Autogenerating a ERC-721 Token +To start, head over to [wizard.openzeppelin.com/#erc721](wizard.openzeppelin.com/#erc721) where you should see the following: + +![](/course-images/smartcontract-foundry/erc721-OpenZeppelin.png) + +As with the ERC-20 section, the base contract MyToken is deriving the ERC721 contract, which contains the logic necessary for a functional ERC-721 token contract. MyToken, in essence, allows us to define the initial values of our ERC-721 token. Just like the ERC-20 contract, we can also inherit optional features for our ERC-721 token as well. Below is an example of us customizing our ERC-721 token: + +![](/course-images/smartcontract-foundry/erc721-OpenZeppelin2.png) + +To end off the ERC-721 chapter, we will look at one particular feature that makes ERC-721 token special: the URI Storage feature. \ No newline at end of file diff --git a/content/academy/solidity-foundry/08-erc721-smart-contracts/05-uris.mdx b/content/academy/solidity-foundry/08-erc721-smart-contracts/05-uris.mdx new file mode 100644 index 00000000000..e365c9d836e --- /dev/null +++ b/content/academy/solidity-foundry/08-erc721-smart-contracts/05-uris.mdx @@ -0,0 +1,48 @@ +--- +title: URIs +description: Non-Fungible Token Standard +updated: 2024-06-28 +authors: [Andrea Vargas, Ash, martineckardt] +icon: BookOpen +--- +Ignoring the technical side of ERC-721 tokens, perhaps the most famous use case of such tokens is to track digital images. Whenever most people hear the term NFT, they mostly think of JPEGs that can be tracked on the blockchain. + +With our current understanding of ERC-721 tokens, it does not really make sense how we can use such a token standard to track pictures, considering NFTs are just mappings from numbers to addresses. In this section, we will look at how we can extend the functionality of ERC-721 tokens to be able to track digital assets such as JPEGs. + +## URIs + +URIs, or uniform resource indicators, can be thought of as a particular format of URLs which allow us to easily build a particular URL using just a base URL and a specific URL. As an example, consider the following base URL: rodrigo.xyz/images . As the URL might suggest, this URL is the base string for all webpages which store images. Some sample specific URLs would be /dog.jpeg, /cat.jpeg, ..., etc. By organizing our URLs in this manner, it makes it easy to organize our collection of images. + +NFTs, likewise, use the same principle. In the case of JPEGs, NFTs simply keep track of the URL of the jpeg via the URI format. + +## Another Mapping + +Tying everything together, to track digital assets such as JPEGs which use URIs, we can add the following mapping to our ERC-721 function: + +```solidity +mapping(uint256 tokenId => string) private _tokenURIs; +``` + +The mapping above stores the specific token URIs, while the base URI is stored as a separate state variable. To be able to query the full URI of a particular token, we can use the following function (from OpenZeppelin's ERC721URIStorage contract): + +```solidity +function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + _requireOwned(tokenId); +​ + string memory _tokenURI = _tokenURIs[tokenId]; + string memory base = _baseURI(); +​ + // If there is no base URI, return the token URI. + if (bytes(base).length == 0) { + return _tokenURI; + } + // If both are set, concatenate the baseURI and tokenURI (via string.concat). + if (bytes(_tokenURI).length > 0) { + return string.concat(base, _tokenURI); + } +​ + return super.tokenURI(tokenId); +} +``` + +Just like that, our NFTs are now able to track digital assets like JPEGs! \ No newline at end of file diff --git a/content/academy/solidity-foundry/index.mdx b/content/academy/solidity-foundry/index.mdx new file mode 100644 index 00000000000..076c87ae1d3 --- /dev/null +++ b/content/academy/solidity-foundry/index.mdx @@ -0,0 +1,52 @@ +--- +title: Welcome to the course +description: Learn the basics about programming Smart Contracts in Solidity for an EVM Blockchain +updated: 2024-05-31 +authors: [Andrea Vargas] +icon: Smile +--- + +In this course, you will learn how to build Solidity dApps on Avalanche. + +## Why Take This Course? + +A significant innovation in blockchain is the development of multi-chain systems, like Avalanche, which provide a significant improvement in scalability, interoperability, and flexibility. While a blockchain on Avalanche can be run with any VM, the most prominent choice currently is the Ethereum Virtual Machine (EVM). Users can deploy their own logic in the form of smart contracts to the EVM. + +These smart contracts can be written in Solidity. Learning Solidity can enable you to leverage the features of blockchain for your dApp. + +## Course Content + +### Smart Contracts +In the first section, we will look at what smart contracts are, their basic structure and how they work. + +### Hello World Part I +In this section we will look at the primitive types (strings, integers, bool, ...) and function as well as the Solidity file structure. + +### Hello World Part II +We will look at control flows (if & else), data structures and constructors. Furthermore, we learn about inheritance from other contracts and modifiers and events. + +### Contract Standardization +You will learn how contracts can be standardized and how inheritance and interfaces can help us to do so. + +## Prerequisites + +### Blockchain / Web3 +This course is meant for people with a some experience when it comes to web3. You should be familiar with these concepts: +- Wallet: What they are and how to create one +- dApp: What a decentralized application is and how to interact with one + +If any of this is unclear, we strongly recommend taking the Avalanche Fundamentals and Subnet Architecture courses first, that give a soft introduction into these topics from a user stand point. + +### Software Development +You will need a general understanding of Software Development. Therefore, we recommend: +- Programming: Familiarity with most basic concepts of programming, such as variables, control flows (if, else) and loops. All exercises will consist of writing small contracts in Solidity. +- IDE: It will help if you're generally familiar with the concept of an integrated developer environment. We will be leveraging Remix. + +## Learning Outcomes + +By the end of this course, students will: + +- Interact and Deploy contracts using Foundry +- Get familiar with ERC20 and ERC721 token standards +- Understand important concepts such as inheritance, modifiers, and events. +- Apply their knowledge by building their own smart contracts. diff --git a/content/academy/solidity-foundry/meta.json b/content/academy/solidity-foundry/meta.json new file mode 100644 index 00000000000..961174b6643 --- /dev/null +++ b/content/academy/solidity-foundry/meta.json @@ -0,0 +1,22 @@ +{ + "title": "Intro to Solidity with Foundry", + "root": true, + "pages": [ + "index", + "---Avalanche Starter Kit---", + "...02-avalanche-starter-kit", + "---Smart Contracts---", + "...03-smart-contracts", + "---Hello World Part 1---", + "...04-hello-world-part-1", + "---Hello World Part 2---", + "...05-hello-world-part-2", + "---Contract Standarization---", + "...06-contract-standarization", + "---ERC-20 Smart Contracts---", + "...07-erc20-smart-contracts", + "---ERC-721 Smart Contracts---", + "...08-erc721-smart-contracts" + + ] +} diff --git a/content/courses.tsx b/content/courses.tsx index f76ac4e8ad5..8989ab748dd 100644 --- a/content/courses.tsx +++ b/content/courses.tsx @@ -102,6 +102,17 @@ const officialCourses: Course[] = [ languages: ["Typescript"], instructors: ["Owen Wahlgren"] }, + { + name:"Solidity Programming with Foundry", + description:"Learn the basics on how to code in Solidity with Foundry", + slug:"solidity-foundry", + icon: , + duration: "1 hour", + status: "featured", + tools: ["Starter-Kit", "Foundry"], + languages: ["Solidity"], + instructors: ["Andrea Vargas"] + }, { name:"HyperSDK", description:"Learn how to build a high-performance blockchain using HyperSDK", diff --git a/public/course-banner/solidity-fundamentals.jpg b/public/course-banner/solidity-foundry.jpg similarity index 100% rename from public/course-banner/solidity-fundamentals.jpg rename to public/course-banner/solidity-foundry.jpg diff --git a/public/course-images/smartcontract-foundry/erc20-OpenZeppelin.png b/public/course-images/smartcontract-foundry/erc20-OpenZeppelin.png new file mode 100644 index 00000000000..1b7fc702db5 Binary files /dev/null and b/public/course-images/smartcontract-foundry/erc20-OpenZeppelin.png differ diff --git a/public/course-images/smartcontract-foundry/erc20-OpenZeppelin2.png b/public/course-images/smartcontract-foundry/erc20-OpenZeppelin2.png new file mode 100644 index 00000000000..94180564294 Binary files /dev/null and b/public/course-images/smartcontract-foundry/erc20-OpenZeppelin2.png differ diff --git a/public/course-images/smartcontract-foundry/erc721-OpenZeppelin.png b/public/course-images/smartcontract-foundry/erc721-OpenZeppelin.png new file mode 100644 index 00000000000..3b77163ea01 Binary files /dev/null and b/public/course-images/smartcontract-foundry/erc721-OpenZeppelin.png differ diff --git a/public/course-images/smartcontract-foundry/erc721-OpenZeppelin2.png b/public/course-images/smartcontract-foundry/erc721-OpenZeppelin2.png new file mode 100644 index 00000000000..f28618baade Binary files /dev/null and b/public/course-images/smartcontract-foundry/erc721-OpenZeppelin2.png differ