Table of contents
- FundMe.sol smart contract
- Chainlink & Oracles
- Basic Solidity arrays & structs
- Libraries
- SafeMath, Overflow Checking, and the "unchecked" keyword
- Basic Solidity For Loop
- Basic solidity constructor
- advanced solidity: immutable & constant
- advanced solidity custom errors
- advanced solidity receive & fallback
- Latest version of our contracts
These are my notes for Freecodecamp's we3/blockchain course Lesson 4: Remix FundMe. Again, all the credit goes to Patrick Collins and Freecodecamp's wonderful community. I'm just sharing my notes.
You can find my notes for the Lesson 3: Storage Factory here => muratcanyuksel.hashnode.dev/creating-a-stor..
You can find the original video tutorial here=> youtube.com/watch?v=gyMwXuJrbJQ&ab_chan..
You can find the source code to this tutorial in Patrick's GitHub => https://github.com/PatrickAlphaC/fund-me-fcc
In this lesson, we're going to work with two contracts primarily: FundMe.sol
and PriceConverter.sol
. The idea is to create a smart contract that can be sent money in the form of native blockchain token (whether it be ethereum, avalanche, polygon, phantom or whatever else), that which can hold them until the owner of the contract withdraws them. Also, we want to keep track of who sent how much money. While doing that, we want to work with US dollars, but there's a catch in here: Blockchains are deterministic systems, closed boxes if I understood correctly, and they cannot get real-time value of ETH in USD. So, we need to use a third party service to convert the USD to ETH: Oracles- Chainlink in our case. Down the road, we'll be covering liraries, some weird Solidity math, and some advanced concepts to make our contracts more professional and more gas efficient.
I know it sounds a lot, and probably it is, Patrick even suggests that some parts of this tutorial might be difficult to grasp, but we'll not dishearten ourselves, because we know that even if we don't get every single detail in this tutorial, we'll eventually understand them more and more as we continue writing smart contracts. So, buckle up and let's get started with our FundMe.sol
contract!
FundMe.sol smart contract
Now, we want this contract to do a couple of things. It should
- Get funds from users
- Withdraw funds
- Set a minimum funding value in USD
In this contract, we have two main functions: fund
and withdraw
. Along the way, we'll create helper functions that support these two functions.
Let's start with fund
function. Now, we want people or entities to send some money to this function, but we also want to set requirement for the minimum amount of money sent. So, first of all, this function should be public
so it can be interacted outside of this contract, and it also should be payable
so that it can receive funds. Check the snippet out:
function fund() public payable{
require(msg.value>1e18, "Didn't send enough!");
}
Now, what is going on inside of this function? First off, we're using the require
syntax which means that "execute what follows in this function only if the following conditions are met, if not, revert
and give an error message" (which is Didn't send enough!
in this case). The msg.value
is the amount of money that was sent to this function. The 1e18
is the smallest amount of money that can be sent to this function.
msg.value
is the amount of money sent by the person or entity who called the fund function. We want this msg.value
to be greater than 1e18
, which means 1 ETH. It actually means 1*10**18
that is 1000000000000000000
wei and that is 1 ETH. No worries, we're going to deal with more manageable numbers soon.
If the amount paid is less than 1 ETH, it will "revert" the transaction and give an error message. But what that revert thing means? Well, it means that "undo any action before, and send remaining gas back". So it DOES spend some gas. For instance, if there's an action before the require statement, it will do that action, spend the necessary amount of gas for that action, and when it faces the require statement and if it couldn't pass the requirement, it will send what remains from the initial gas cost (probably estimated, or agreed IDK). So it will spend gas for any computation before the require statement and can't return them, but the cost of the ones after the require statement will be returned.
But we don't want to deal with these kind of things all the time. We would maybe like our users to send us some amount of ether in terms of usd? In order to do that, we're going to use oracles, or in our case, Chainlink's Price Feeds.
Chainlink & Oracles
A blockchain oracle is any device that interacts with the off-chain world to provide external data or computation to smart contracts.
Now if you go to the following link => https://docs.chain.link/docs/get-the-latest-price
you'll see an example smart contract that makes use of Chainlink's AggregatorV3Interface
aggregator contract. We're going to use that contract.
If we wanted to inspect this AggregatorV3Interface
aggregator contract, we could go to the following link and check it out => https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol
As you can see, it has some functions like version
, getRoundData
, latestRoundData
and so on. The thing is, we can import this aggrgator contract in our smart contract and actually use those functions in our smart contract.
In order to interact with an outside contract, we need the contract ABI (application binary interface) and the address. And we need the address for goerli
network because the others are deprecated now.
We need two public functions, getPrice
and getConversionRate
.
The ABI here is the aggregator contract AggregatorV3Interface
, but we also need the address
. To get the address that we'll make use of in our call, we'll go to the following link => https://docs.chain.link/docs/ethereum-addresses/
, find the section for Goerli Testnet
and copy the address related to ETH/USD
because that's what we want to do: We want to convert between ether and usd. The address we're looking for is the following => 0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e
Now, let's chill a bit and write some code, things will be clear by the end of this section.
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract FundMe{
function fund() public payable{
require(msg.value>1e18, "Didn't send enough!");
}
function getPrice () public{
//the function that we take the price in terms of USD
//ABI
//Adress
AggregatorV3Interface priceFeed= AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e);
(,int256 price,,,)= priceFeed.latestRoundData();
}
function getConversionRate () public{
//
}
// function withdraw{}
}
As you can see, after the usual License identifier and indicating the Solidity compiler, we're importing something with the line import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
. That's our aggregator contract AggregatorV3Interface
, now since we've imported it, we can use its functions inside our contract! And that's what we're doing in our getPrice
function.
We are creating a variable named priceFeed
with the type of AggregatorV3Interface
we've just imported, and set it to the AggregatorV3Interface
contract called with göerli ETH/USD address. But, what about the (,int256 price,,,)= priceFeed.latestRoundData();
part? It has too many commas, right? That's not a mistake.
Now, if we examine the snippet using AggregatorV3Interface in this page => https://docs.chain.link/docs/get-the-latest-price/
we notice that their priceFeed
variable returns the following when it calls latestRoundData()
function:
/*uint80 roundID*/,
int price,
/*uint startedAt*/,
/*uint timeStamp*/,
/*uint80 answeredInRound*/
But we do not need all of them. We don't need roundID
, startedAt
, timeStamp
and answeredInRound
, we only need int price
. But if we omitted them, the Solidity compiler would give an error. It's like we're saying that okay okay we know, we realize and notice that you return those things too, but since we're not going to use them, we're going to pass them as blanks.
Nb! In the documentation, it's int price
, we changed it to int256 price
to make it flexible.
Now, this part is confusing a bit. When returning the price, we first need to convert it from int256
to uint256
(a practice that's called typecasting
). Now, for some reason, Solidity doesn't work well with decimals, so we need to convert them. Eth in terms of Wei has 18 decimals. But, the USD price returned has 8 decimals (in the tutorial it's 3000.00000000
). So, to convert them we write as such: return uint256(price * 1e10)
, 1e10
meaning 1**10
. This is the latest version of our getPrice
function.
function getPrice () public view returns(uint256){
//the function that we take the price in terms of USD
//ABI
//Adress
AggregatorV3Interface priceFeed= AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e);
(,int256 price,,,)= priceFeed.latestRoundData();
//ETH in terms of USD
//3000.00000000
return uint256(price * 1e10)
}
Please be aware that things have changed since the tutorial was recorded. Now ETH worths less than that, but I'm not changing anything as this part was quite mind boggling in my opinion (not a CS major you see).
Now let's check the getConversionRate
function.
getConversionRate function
Check out this snippet:
function getConversionRate (uint256 ethAmount) public view returns (uint256){
//this is the function that takes the ethAmount and returns the amount in terms of USD
uint ethPrice= getPrice();
uint256 ethAmountInUSD= (ethPrice * ethAmount) / 1e18;
return ethAmountInUSD;
}
This function takes some value in eth form, and spits it out in usd form.
uint ethPrice
calls the previous getPrice
function, and then we multiply it by the ethAmount. Then we divide it to 1e18
to get rid of the decimals so that it'll be converted to usd form. We return ethAmountInUSD
.
Now, since we can convert from eth to usd, we can go all the way back to the top and our first function, fund()
and change it accordingly so that it would be a function that really checks for the amount of usd sent:
contract fundMe{
uint256 public minimumUsd= 50 * 1e18;
function fund() public payable{
require(getConversionRate(msg.value)>= minimumUsd, "Didn't send enough!");
}
//...
//...
}
Now, you realize that minimumUsd
variable equals to 50 * 1e18
. The reason for it that, getConversionRate returns the number with 18 zeroes after the decimal point, so we upgrade the minimumUsd
to 50 * 1e18
.
Basic Solidity arrays & structs
Now we want to keep track of all the people who sent us money. To do that, we'll reate a dynamic array of addresses called funders
like so => address[] public funders;
And in our fund
function we'll push the msg.sender
, i.e. the address of the person or entity who's calling the function, to the funders
array of addresses like so=> funders.push(msg.sender);
Now, the latest version of our fund
function is as follows:
function fund() public payable{
require(getConversionRate(msg.value)>= minimumUsd, "Didn't send enough!");
funders.push(msg.sender);
}
To reiterate:
msg.value
stands for how much ethereum or how much native blockchain currency is sent
msg.sender
is the addresss of whoever calls the fund
function
Now that we have our funders, we might want to check how much money they're sending by using mapping
s.
First off, we create this: mapping(address => uint256) public addressToAmountFunded;
. Then, we add addressToAmountFunded[msg.sender] = msg.value;
to our fund
function to assign the amount of money sent to the address who sent it in our addressToAmountFunded
mapping, so now our fund
function alongside with the variables coming before it looks like this :
uint256 public minimumUsd= 50 * 1e18;
address[] public funders;
mapping(address => uint256) public addressToAmountFunded;
function fund() public payable{
require(getConversionRate(msg.value)>= minimumUsd, "Didn't send enough!");
funders.push(msg.sender);
addressToAmountFunded[msg.sender] = msg.value;
}
Now, to the next part of the lesson.
Libraries
So far we've created some cool stuff, but it seems like our FundMe.sol
contract is gtting a bit cluttered. We did indeed say we'd have two main functions and some helpers around them. Those two function we had in mind were fund()
and withdraw()
. So maybe we'd like to move all the other helper functions to somewhere else that we can then import into our FundMe.sol
smart contract and use as we wish. In order to do that, we'll create what's called a library
in Solidity.
Now, libraries are like contracts. But they enable us to give functions to uint256, like msg.value. So that we can call, say getConversionRate on msg.value for instance, like so => msg.value.getConversionRate();
Also, libraries can't have state variables, can't send ether, and all the functions in the library are going to be internal
.
To start, we create a new solidity file named PriceConverter.sol
.
Now we cut the following functions from FundMe.sol
and paste them into PriceConverter.sol
: getPrice
, getVersion
, and getConversionRate
. We're going to do the same for our AggregatorV3Interface
import since we're not using it anymore in FundMe.sol
. So we remove/add this => import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
Lastly, we'll make all the functions inside our library internal
.
Now, as I've mentioned above, we're going to make this library's functions usable for uint256
. In order to do that, we'll import this library into our contract, and then add using PriceConverter for uint256;
inside our FundMe
contract. Now, we can change the require statement from this require(getConversionRate(msg.value)>= minimumUsd, "Didn't send enough!");
to this => require(msg.value.getConversionRate() >= minimumUsd, "Didn't send enough!");
How this works is, after creating our library, the functions which normally expect a variable passed into them (such as function getConversionRate (uint256 ethAmount) internal view returns (uint256)
is expecting an uint256 ethAmount
) when used in a smart contract (such as our FundMe.sol
) will regard msg.value
as their first variable. "So, msg.value
will be considered as the first parameter for any of these library functions."
If we wanted to pass a 2nd variable to that function (getConversionRate
in our case), like, if was like this => function getConversionRate (uint256 ethAmount, uint256 somethingElse) internal view returns (uint256)
that uint256 somethingElse
would be passed like a normal function call. Like so: msg.value.getConversionRate(123);
where 123
is the 2nd parameter.
But we're not gonna do that.
Now, this is our PriceConverter.sol
file so far:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
library PriceConverter{
function getPrice () internal view returns(uint256){
//the function that we take the price in terms of USD
//ABI
//Adress
AggregatorV3Interface priceFeed= AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e);
(,int256 price,,,)= priceFeed.latestRoundData();
//ETH in terms of USD
//3000.00000000
return uint256(price * 1e10);
}
function getConversionRate (uint256 ethAmount) internal view returns (uint256){
//this is the function that takes the ethAmount and returns the amount in terms of USD
uint ethPrice= getPrice();
uint256 ethAmountInUSD= (ethPrice * ethAmount) / 1e18;
return ethAmountInUSD;
}
function getVersion() internal view returns (uint256){
// an AggregatorV3Interface variable called priceFeed
AggregatorV3Interface priceFeed= AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e);
return priceFeed.version();
}
}
And this is our FundMe.sol
file:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./PriceConverter.sol";
contract FundMe{
//importing the PriceConverter library
using PriceConverter for uint256;
uint256 public minimumUsd= 50 * 1e18;
address[] public funders;
mapping(address => uint256) public addressToAmountFunded;
function fund() public payable{
//getConversionRate function became available for type uin256 (msg.value is type uint256)
require(msg.value.getConversionRate() >= minimumUsd, "Didn't send enough!");
funders.push(msg.sender);
addressToAmountFunded[msg.sender] = msg.value;
}
// function withdraw{}
}
SafeMath, Overflow Checking, and the "unchecked" keyword
This section is not directly related to the project at hand, but a handy piece of knowledge that Patrick presents to us so that we won't be surprised when we see such a thing.
Now, OpenZeppelin's SafeMath contract was paramount before the solidity version 0.8, and now it's almost nonexistent. So, in order to test it, we create a SafeMathTester.sol
file and use any version BELOW 0.8.0.
Now, before solidity version 0.8.0, numbers were "unchecked". What does that mean? Consider the following code:
//SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract SafeMathTester{
uint8 public bigNumber = 255; // unchecked
function add() public{
bigNumber= bigNumber + 1;
}
}
Now, first off, uint8 public bigNumber = 255;
because 256
is the biggest number an uint8
variable can contain. So, if we deployed this contract using a compiler between 0.6 and 0.8, and tried to add 1
to it, it would overflow, i.e. revert back to 0
instead of 256
. SafeMath seems to be dealing with this unexpected problem. After 0.8.0, numbers are checked, so this problem doesn't occur. Although, we can use the unchecked
keyword to make solidity follow the same behavior. Check this out:
//SPDX-License-Identifier: MIT
// upgraded the compiler version
pragma solidity ^0.8.0;
contract SafeMathTester{
uint8 public bigNumber = 255; // unchecked
function add() public{
//unchecked keyword makes the number go to 0 if it's bigger than 255 now
unchecked{bigNumber= bigNumber + 1;}
}
}
The two snippets above do the very same thing, in different compiler versions.
So what's the use of this unchecked
keyword? Well, it turns out it is more gas efficient. So, if you sure that your math won't round and cause problems, you can use this technique to make your contract more gas efficient.
Basic Solidity For Loop
Now we'll write the withdraw
function. This function will loop through the funders
array and set the amount of money sent by an individual number to 0 in addressToAmountFunded
mapping. It is no different from the for loop
in Javascript.
This is the withdraw
function:
function withdraw() public{
for (uint256 funderIndex= 0; funderIndex < funders.length; funderIndex++){
//create a funder address variable that's equal to the current index in the funders array
address funder= funders[funderIndex];
//set the amount of money sent by the funder found in addressToAmountFunded mapping to 0
addressToAmountFunded[funder]= 0;
}
}
Now we need to do 2 more things: Reset the funders
array, and actually withdraw the funds.
Resetting an array
We reset the funders
array by writing this piece of code => funders= new address[](0);
We reset the array funders
in the withdraw
function because we were keeping track of who sent us money, and now since we've withdrawn the money, we don't need to keep track of them. And to think about it, when after withdrawing, if someone else sends us funds, we'd like to keep track of them and distinguish them from the ones who sent us money before we've withdrew the funds.
Now, withdraw
function with the above line added to it:
function withdraw() public{
for (uint256 funderIndex= 0; funderIndex < funders.length; funderIndex++){
address funder= funders[funderIndex];
addressToAmountFunded[funder]= 0;
}
//reset the array
funders= new address[](0);
//actually withdraw the funds
}
Our next step is to actually withdraw the funds, i.e. send all the money in this contract to an address. But, how to send money from a contract?
Sending ETH from a contract
There are 3 different ways: transfer
, send
, and call
.
For reference, this link is quite useful => https://solidity-by-example.org/sending-ether/
transfer
We can write the following line => `payable(msg.sender).transfer(address(this).balance)
Normally msg.sender
is of type address
. What we're doing when we add payable
in front of it here is we're typecasting
like we did with ints and uints earlier. We are doing this because in Solidity native tokens can only be sent to payable
addresses. With this little tweak, our address can be sent money.
this
keyword here refers to the whole contract, i.e. FundMe.sol
.
The thing with transfer
method is that its gas cost is capped at 2300 gas
, if it requires more gas cost, then it reverts the function and throws an error.
send
send
method is also capped at 2300 gas limit, but instead of an error, it returns a boolean. So, we write it as follows to revert it in case of an error (because it gives a boolean, it wouldn't revert by itself)
bool sendSuccess= payable(msg.sender).send(address(this).balance);
require(sendSuccess, "Couldn't send the funds");
call
Call is one of the lower level commands in Solidity. It's really powerful. We can use it to virtually call any function in Ethereum- without even having to have the ABI.
Check this snippet out:
(bool callSuccess, bytes memory dataReturned)= payable(msg.sender).call{value: address(this).balance}("");
Now, let's explain that. the parenthesis after .balance
is empty. Normally, when we call a function, we put any function information or any information about the function we want to call in some other contract. We actually don't want to call a function, so we're going to leave this one blank. And to tell Solidity that we leave it blank, we write is as such ("")
.
The line call.{value: address(this).balance}
works as such: You know there's this Value
part in Remix IDE where we enter the amount we want to pay, it works like that. So it calls, or say, enters the amount of money in the address of this
contract.
This call function actually returns 2 variables. And when a function returns two variables, we can show that by placing them into parenthesis on the left-hand side. The first variable that's returned is bool callSuccess
and the second is bytes dataReturned
. The second one is the data returned if we were calling a function. And bytes
objects are arrays, that's why we wrote them in memory
in the beginning like so bytes memory dataReturned
.
But, since we're not calling any function with our call
method, we do not need this second variable. We used this syntax before in getPrice
function. We just delete the 2nd parameter but leave the comma intact to tell Solidity "yea we know there's a second variable but we don't need it". We also add a require statement. So we write it like this in the end:
//notice that the comma is there
(bool callSuccess,)= payable(msg.sender).call{value: address(this).balance}("");
require(callSuccess, "Call failed");
For the most part, call
is recommended. Most, I say.
Here's the last version of withdraw
function :
function withdraw() public{
for (uint256 funderIndex= 0; funderIndex < funders.length; funderIndex++){
address funder= funders[funderIndex];
addressToAmountFunded[funder]= 0;
}
//reset the array
funders= new address[](0);
//actually withdraw the funds
(bool callSuccess,)= payable(msg.sender).call{value: address(this).balance}("");
require(callSuccess, "Call failed");
}
Basic solidity constructor
There's a BIG problem with our contract right now. That is, with the latest withdraw
function we wrote, ANYBODY can withdraw the funds. We do not want that. To make sure only the person who deployed the contract can withdraw the funds we're going to make use of constructor
s.
So we want to make sure that whomever deploys this contract will be the owner of the contract, and only the owner can withdraw funds.
Constructors get called immediately with the contract being deployed.
Now, in order to make sure only the owner can call certain functions, such as the withdraw
function, we'll create an address
variable of owner
and set it to msg.sender
in our constructor
. Check these out=>
address public owner;
constructor(){
owner= msg.sender;
}
But, in order for these to work, we need require statements in our functions, or better, a modifier. Before writing the modifier, let's see how we'd write the require statement: require(msg.sender == owner, "You are not the owner!");
. This line goes inside the withdraw
function, in the 1st line of that function. And Patrick here points out the difference between =
and ==
. So, a single equal sign (=
) means it is setting
something to something. Whereas a double (==
) means it is checking
something against something.
Now let's write our modifier
named onlyOwner
=>
modifier onlyOwner{
require(msg.sender == owner, "You are not the owner!");
_;
}
The underscore
means that "execute the rest of the code in the function that this modifier was attached to ". Now we need to attach this modifier into the function(s) we need to have this modifier. In our case, it is the withdraw
function and we do it like this:
function withdraw() public onlyOwner{
//...
//...
}
advanced solidity: immutable & constant
In this section, we're going to make this contract a bit more professional. More gas efficient for instance. We'll start by 2 keywords: constant
and immutable
.
Now, our uint256 public minimumUsd= 50 * 1e18;
is fired once the contract is deployed, like the constructor, and never changes. We can make this variable more gas efficient by adding the constant
keyword like so:
uint256 public constant minimumUsd= 50 * 1e18;
With constant
keyword added, this minimumUsd
variable does not take a storage spot, and is much easier to read.
Now, constant
have a naming convention. Instead of writing the variable name with camelCase, we generally write them in pascal_case and with majuscule letters. So, minimumUsd
becomes MINIMUM_USD
We can use the immutable
keyword for variables we set for one time, but outside the same line they're declared, opposed to our MINIMUM_USD
for instace, we set the owner
and use it in the constructor
, so it's sued in 2 lines. The declaration convention with them is they start with i_
, like, for our owner
variable, it becomes i_owner
like so => address public immutable i_owner
advanced solidity custom errors
As of 0.8.4 version of solidity ,we can write custom errors for our reverts. This saves us lots of gas too. To work with them, we first define our errors OUTSIDE of the contract like so =>
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./PriceConverter.sol";
//realize that this error thingy is outside of the FundMe contract
error NotOwner();
contract FundMe{
//...
And in our contract, wherever we wish to use instead of the require
statement, say, we want to use it in one of the modifiers, we do it such=>
modifier onlyOwner{
//comment this old way out
// require(msg.sender == i_owner, "You are not the owner!");
if(msg.sender != i_owner){ revert NotOwner(); }
_;
}
advanced solidity receive & fallback
Okay, problem: What if people send money to our contract without calling the fund
function? Yes, they can do that, and we won't be able to track who sent us what in that case. Solidity has 2 special functions
for situations like that: receive
and fallback
.
This link is useful => https://docs.soliditylang.org/en/latest/contracts.html?highlight=special#special-functions
Now, say that we create a separate file called FallbackExample.sol
and populate it with the following code:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract FallbackExample{
uint256 public result;
receive() external payable{
result= 1;
}
}
NB! Notice that special functions such as the receive function do not have the "function" keyword in front of them. Similar examples are: fallback, constructor...
Now, in Remix IDE, to send this contract money, all we need to do is to deploy it (we don't need to use Injected for this one, we can just use Javascript EVM for it) and after deployment, at the bottom we'll see Low level interactions
that has some CALLDATA
input field and then a Transact
button. We can define the amount of money we want to send like usual with the Value
field above, and when we hit transact, as the receive
function suggests, the value of result
should turn into 1
from 0
.
What if we don't leave the CALLDATA
section blank and add some data into it? Like say we entered 0x00
into the CALLDATA
field. We'd get the following error "Fallback" function is not defined
What happens here is that Solidity says "oh, since you're sending some data you're not looking for the receive
, you're looking for some function, so let me find that function for you. Mmm, I don't see any function that matches to 0x00
so I'm gonna look for your fallback
function." Of course, since we don't have it, we get the above error.
Now, let's add the fallback
function like so:
fallback() external payable{
result= 2;
}
After adding the code above, if we'd done what we did above, i.e. send some transaction with data in it, so say, entering 0x00
into the CALLDATA
field and hitting the Transact
button, instead of getting an error, the transaction would pass successfully. What happens here, since our contract is being called without a VALID function, Solidity realizes that we're doing this, i.e. calling the contract without a valid function, a function that it cannot find mostly because it doesn't exist, it returns and fires the fallback
function, which turns the result
into 2
.
Here's a nice chart explaiing what to expect from Solidity in such cases:
// Explainer from: https://solidity-by-example.org/fallback/
// Ether is sent to contract
// is msg.data empty?
// / \
// yes no
// / \
// receive()? fallback()
// / \
// yes no
// / \
//receive() fallback()
To finish it up, let's add those receive()
and fallback()
functions to our contract, so that if there's something wrong going on, these functions can in turn call the fund
function so that our contract can function as we wished.
fallback() external payable {
fund();
}
receive() external payable {
fund();
}
Now, if somebody accidentally sends us money without calling our fund
function, they'll get routed to the fund
function automatically.
This costs a bit more gas though.
Latest version of our contracts
FundMe.sol
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./PriceConverter.sol";
//realize that this error thingy is outside of the FundMe contract
error NotOwner();
contract FundMe{
using PriceConverter for uint256;
uint256 public constant MINIMUM_USD= 50 * 1e18;
address[] public funders;
mapping(address => uint256) public addressToAmountFunded;
address public immutable i_owner;
constructor(){
i_owner= msg.sender;
}
function fund() public payable{
require(msg.value.getConversionRate() >= MINIMUM_USD, "Didn't send enough!");
funders.push(msg.sender);
addressToAmountFunded[msg.sender] = msg.value;
}
function withdraw() public onlyOwner{
for (uint256 funderIndex=0; funderIndex < funders.length; funderIndex++){
address funder = funders[funderIndex];
addressToAmountFunded[funder] = 0;
}
//reset the array
funders= new address[](0);
//actually withdraw the funds
(bool callSuccess,)= payable(msg.sender).call{value: address(this).balance}("");
require(callSuccess, "Call failed");
}
modifier onlyOwner{
// require(msg.sender == i_owner, "You are not the owner!");
if(msg.sender != i_owner){ revert NotOwner(); }
_;
}
fallback() external payable {
fund();
}
receive() external payable {
fund();
}
}
PriceConverter.sol
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
library PriceConverter{
function getPrice () internal view returns(uint256){
//the function that we take the price in terms of USD
//ABI
//Adress
AggregatorV3Interface priceFeed= AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e);
(,int256 price,,,)= priceFeed.latestRoundData();
//ETH in terms of USD
//3000.00000000
return uint256(price * 1e10);
}
function getConversionRate (uint256 ethAmount) internal view returns (uint256){
//this is the function that takes the ethAmount and returns the amount in terms of USD
uint ethPrice= getPrice();
uint256 ethAmountInUSD= (ethPrice * ethAmount) / 1e18;
return ethAmountInUSD;
}
function getVersion() internal view returns (uint256){
// an AggregatorV3Interface variable called priceFeed
AggregatorV3Interface priceFeed= AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e);
return priceFeed.version();
}
}