RPC Provider
- Infura RPC provider
- Alchemy RPC Provider
Contracts
- Openzeppllein
- Chainlink DevHub
- Chainlink VRFv2.5
- Chainlink automationv2.1
- Alchemy contracts
- Soulmate and brownie-smart-contracts
wallets and Frameworks
- Metamask
- Hardhat
- Foundry-forge
- Web3.js/ethers.js
- ThirdWeb
File storage
- IPFS (web/desktop app)
- Pinata
- svg to base64
- Sepolia Opensea
- Building
DApps
using (Hardhat/TypeScript/ReactJs/Web3.js) - Developing
ERC-20
token - Developing
ERC-721
NFTs collection
Natspec format
/// @title A simulator for trees
/// @author Larry A. Gardner
/// @notice You can use this contract for only the most basic simulation
/// @dev All function calls are currently implemented without side effects
/// @return Returning variable of function
/// @param A Parameters used in function
Layout of Contract:
// version
// imports
// interfaces, libraries, contracts
// errors
// Type declarations
// State variables
// Events
// Modifiers
// Functions
Layout of Functions:
// constructor
// receive function (if exists)
// fallback function (if exists)
// external
// public
// internal
// private
// internal & private view & pure functions
// external & public view & pure functions
- Here we will see the simple layout and structure to write the solidity smart contracts.
pragma solidity ^0.8.0;
contract MyFirstContract {
// functions
...
// variables
...
// arrays
}
- Variables in sol is similar to javascript. Except address.
uint8 , uint16, uint128, uint256;
string public str = "Hello world";
bool public correct = true/false;
-
Strings are actually arbitrary long bytes in UTF-8 representation. Strings do not have a length property.
-
When we are using 'string' as a parameter in our function define memory to allocate the memory to our string.
string public myString = "Hello world";
function getMyString(string memory _myString) public {
myString = _myString;
}
- Bytes have the length property.
bytes public myBytes = "Hello";
// myBytes = 0x72375375bc758734784c834874
- Here we will discuss the basic functions to write simple smart contracts.
uint256 result;
function addition(uint256 a, uint256 b) public returns(uint256){
return result=a+b;
}
- When we are just returning an variable inside an function we will use 'view' keyword.
function get() public view returns (uint256){
return result;
}
- Public : Can be used internally and externally.
- Our metamask wallet and other smart contracts can call this type of function.
function addition() public {
result+=10;
}
- Private : Can be used within contract.
uint256 result = 0;
function addition() private {
result+=10;
}
- Internal : Can be used within the contract and other inheriting contract.
- Our metamask cannot used the internal function.
- But, inheriting contracts can used this function.
contract Children {
uint256 public result = 0;
function addition(uint256 num) internal {
result+=num;
}
}
contract Parent is Children {
uint256 x;
// Here we have use the function from child contract.
// Inheriting.
function checkSum(uint256 x) internal {
add(x);
}
}
- External : Can only be accessed from external contracts or accounts.
- We cannot used this function internally inside an other function.
- Our metamask wallet can accessed this type of function.
function add(uint256 num) external {
result += num;
}
- The 'public' function uses more ethereum gas fees than 'external' function.
NOTE : The default behavior to error out if the maximum/minimum value is reached. But you can still enforce this behavior. With an 'unchecked' block. Let's see an example.
uint256 num; // by default it is 0
// Error
function Test1() public {
num--;
}
// 115758729374974893749972177129737
function Test2() public {
unchecked{
num--;
}
}
- It is a special function that is called only once during contract deployment.
- It is automatically called during Smart Contract deployment. And it can never be called again after that.
constructor(uint256 _myaddress) {
owner = _myaddress;
owner = msg.sender;
}
- The payable modifier tells solidity that the function is expecting eth to receive.
string public myMessage;
function getMessage(string memory _myMessage) public payable {
myMessage = _myMessage;
}
- The msg-object contains information about the current message with the smart contract.
- It's a global variable that can be accessed in every function.
string public myMessage = "Hello World";
function updateString(string memory myMessage) public payable {
if(msg.value == 1 ether) {
myMessage = myMessage;
}
else{
payable(msg.sender).transfer(msg.value);
}
}
- msg.sender is the address of the person who deployed the Smart Contract.
- msg.value is used to get the amount of ETH sent along with a transaction to a smart contract.
- Low-Level interaction
- This is the function that is executed on plain Ether transfers.
- The receive function is executed on a call to the contract with empty calldata.
- The receive function can only rely on 2300 gas being available.
uint public value;
string public message;
receive() external payable {
value = msg.value;
message = "recieved";
}
- Low-Level interaction
- When Call-data field is filled, then we can call fallback function.
- If a payable fallback function is also used in place of a receive function, it can only rely on 2300 gas being available.
- The fallback function always receives data, but in order to also receive Ether it must be marked payable.
- If Ether is sent to the contract without any data or with data that doesn't match any existing function signatures, the fallback function is triggered
uint public value;
string public message;
receive() external payable {
value = msg.value;
message = "recieved";
}
fallback() external payable {
value = msg.value;
message = "fallback";
}
- receive() is a function that gets priority over fallback() when a calldata is empty.
- But fallback gets precedence over receive when calldata does not fit a valid function signature.
- To send ETH on specific address we can use transfer function
// 1.
address payable toAddress;
toAddress.transfer(amountToSend);
// 2.
payable(address.transfer(amount));
- To receive an ETH on specific address, the address variable should also be an payable
-
Mapping types use the syntax mapping(KeyType => ValueType)
-
The KeyType can be any built-in value type, bytes, string, or any contract or enum type.
-
ValueType can be any type, including mappings, arrays and structs.
-
It is an key and value datatype that does not have lenght or storage property.
mapping (address => uint) public Balance;
function sendMoney() public payable {
Balance[msg.sender] += msg.value;
}
function withDrawMoney(address to, uint amount) public payable {
Balance[msg.sender] -= msg.value;
to.transfer(amount);
}
- Understand the mapping as key:value pair.
- In mapping the datatypes has its default value initially.
- mappings do not have a length or a concept of a key or value being set.
- Solidity provides a way to define new types in the form of structs.
- Solidity uses structs to define new datatypes and group several variables together.
- A struct is a way to generate a new DataType, by basically grouping several simple Data Types together.
// Defining struct
struct PaymentReceipt {
address from;
uint amount;
}
// THIS IS ONE METHOD TO ACCESS MAPS.
mapping (address => PaymentReceipt) public Balance;
function getPaymentHistory() public payable {
Balance[msg.sender].amount = msg.value;
}
// ANOTHER WAY TO USE STRUCT.
PaymentReceipt public payment;
payment.from = msg.sender;
payment.amount = msg.value;
- Now, we will nest an map in an struct, so that it will become more easy and powerful to use struct with mappings.
struct Transaction {
uint amount;
uint timestamp;
}
struct Payment {
uint totalBalance;
uint totalDeposit;
mapping(uint => Transaction) deposits;
}
mapping (address => Payment) public Balance;
function deposit() public payable {
Balance[msg.sender].totalBalance += msg.value;
Balance[msg.sender].deposits[Balance[msg.sender].totalDeposit].amount = msg.value;
Balance[msg.sender].deposits[Balance[msg.sender].totalDeposit].timestamp = block.timestamp;
Balance[msg.sender].totalDeposit++;
}
function checkBalance(address _address) public view returns (uint){
return Balance[msg.sender].totalBalance;
}
- By using struct inside an mappings, will be easier to handle the transaction.
- Just like an array in JS, arrays is solidity work exactly same.
contract SampleArray {
uint[] public dynamicArray;
string[] public StringArray;
function setValue(uint value) public {
dynamicArray.push(value);
}
function getValue(uint index) public view returns (uint){
return dynamicArray[index];
}
function deleteElement() public {
dynamicArray.pop();
}
function sizeOfArray() public view returns (uint){
return dynamicArray.length;
}
}
- An array can be of any data type (uint, string, struct, enum).
- An array can be of any data type (including struct) stored at specific index.
struct StudentReport{
string name;
uint mark1;
string HomeAdd;
}
StudentReport[] public ReportArray;
function fillReport(string _name, uint _mark1, string homeAddress) public {
StudentReport memory ReportCard = StudentReport({
name : _name,
mark1 : _mark1,
HomeAdd : homeAddress
});
ReportArray.push(ReportCard);
}
function getReportCard() public view returns(StudentReport[] memory){
return (ReportArray)
}
- require() Statements
- require(condition,"Error Occurred!!!")
- It read as if condition is false it will throw an error exception with log statement.
- And, if it is true it will execute the remaining code.
mapping (address => uint) public Balance;
function deposit() public payable {
Balance[msg.sender] += msg.value;
}
function withDrawMoney(address payable to, uint amount) public payable {
require(amount <= Balance[msg.sender], "Not enough funds!!!");
Balance[msg.sender] -= amount;
to.transfer(amount);
}
- require() statement works exactly opposit to if-else statements.
- assert() statements (DON'T KNOW WHEN TO USE???)
- Assert is used to check invariants
- Those are states our contract or variables should never reach, ever.
mapping (address => uint8) public Balance;
function deposit() public payable {
assert(msg.value == uint8(msg.value));
Balance[msg.sender] += uint8(msg.value);
assert(Balance[msg.sender] >= uint8(msg.value));
}
function withDrawMoney(address payable to, uint8 amount) public payable {
require(amount <= Balance[msg.sender], "Not enough funds!!!");
assert(Balance[msg.sender] >= Balance[msg.sender] - amount);
Balance[msg.sender] -= amount;
to.transfer(amount);
}
- try/catch statements
contract ExampleTryCatch {
// THIS FUNCTION WILL ALWAYS FAIL TO EXECUTE.
function tryCatch() public pure {
require(false,"Error Occurred!!!");
}
}
contract ErrorHandling {
event ErrorLogging(string reason);
event ErrorLoggingCode(uint errorcode);
function handlingError() public {
ExampleTryCatch test = new ExampleTryCatch();
try test.tryCatch(){
// do something
}
catch Error(string memory reason){
emit ErrorLogging(reason);
}
// Panic is for assert (optional)
catch Panic(uint errorcode){
emit ErrorLoggingCode(errorcode);
}
}
}
- enum can have multiple values
contract Enum {
enum Status{open, closed};
Status public s_status = Status.open;
function updateStatus() public {
s_status = Status.closed;
}
}
- Low-Level solidity call refers to recieve the funds from other smart contracts.
contract ContractOne {
mapping (address => uint) public Balance;
function deposit() public payable {
Balance[msg.sender] += msg.value;
}
function checkBalance() public view returns (uint){
return address(this).balance;
}
// 2. REQUIRES FALL-BACK FUNCTION TO RECIEVE THE FUNDS FROM DIFFERENT SMART CONTRACTS.
receive() external payable {
deposit();
}
}
contract ContractTwo {
receive() external payable { }
function DepositInContractOne(address to, uint amount) public {
// 1.
ContractOne one = ContractOne(to);
one.deposit{value:amount, gas:100000}();
// 2. REQUIRES FALL-BACK FUNCTION.
(bool send,) = to.call{value:amount, gas:100000}("");
require(send);
}
}
- Here, we have two methods to recieve funds from low-level calls.
- WITHOUT FALL-BACK FUNCTION
ContractOne one = ContractOne(to);
one.deposit{value:amount, gas:100000}();
- WITH FALL-BACK FUNCTION
(bool send,) = to.call{value:amount, gas:100000}("");
require(send);
- This provide logging facility of Ethereum.
- Events are a way to access this logging facility
// SAMPLE SMART CONTRACT TO UNDERSTAND THE EVENTS.
contract Events {
mapping(address => uint) public Balance;
events(address _from, address _to, uint _amount);
constructor(){
Balance[msg.sender] = 100;
}
function sendTokens(address _to, uint _amount) public payable{
require(Balance[msg.sender] >= _amount, "No Enough MOney");
Balance[msg.sender] -= _amount;
Balance[_to] += _amount;
emit(msg.sender, _to, _amount);
}
}
// THE LOGS WILL BE DISPLAYED ON THE 'logs' FIELD IN OUTPUT.
- Let's see a simple smart contract and how we can use the modifiers, inheritance and imports in our smart contracts.
// InheritedContract.sol
contract InheritedContract{
mapping (address => uint) public Account;
address owner;
constructor(){
owner = msg.sender;
Account[owner] = 100;
}
modifier isOwner(){
require(msg.sender == owner, "You are not allowed");
// placeholder input
_;
}
}
// InheritedContract.sol
import "InheritedContract.sol";
contract InheritedContract is InheritedContract {
event History(address _from, address _to, uint _amnt);
function createNewToken() public isOwner{
Account[owner]++;
}
function burnToken() public isOwner{
Account[owner]--;
}
function sendToken(address _to,uint _amount) public payable {
require(Account[msg.sender] >= _amount);
Account[msg.sender] -= _amount;
Account[_to] += _amount;
emit History(msg.sender, _to, _amount);
}
}
- Right now we have several similar require statements.
- To avoid code duplication and make it easier to change this from a single place, we can use modifiers
modifiers onlyOwner(){
require(msg.sender == owner, "You are not allowed");
_;
}
function sendTokens() public onlyOwner{
// some code...
}
- By inheritance we can make two or more smart contracts.
- And, we can use the function of second inherited smart contract.
contract Owned {
address owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "You are not allowed");
_;
}
}
contract Sample is Owned{
// sample code...
}
- Now, we export smart contract from one file to another using importing.
// Ownerable.sol
contract Owned {
address owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "You are not allowed");
_;
}
}
// SAMPLE SMART CONTRACT
import "./Owned.sol";
contract SampleSmartContract is Owned {
// some code...
}
- We can inherit the smart contract from one file to another file.
- Sometimes we are required different smart contract to handle some conditions necessary to our DApps.
- Just like in (ReactJs -> Componenets) we have smart contract to handle conditions.
// Test1.sol
contract ContractA {
uint data;
function setData(uint _data) public {
data = _data;
}
function getData() public view returns(uint){
return data;
}
}
// Test2.sol
interface IContractA {
function setData(uint _data) external;
function getData() external view returns (uint);
}
contract ContractB {
IContractA public contractA;
// sc address wil be of the interfaced contract address
constructor(address _smartcontractAddress) {
contractA = IContractA(_smartcontractAddress);
}
function setDataInContractA(uint _data) external {
contractA.setData(_data);
}
}
- After minting an nft's the price will be stored in smart contract and not in owners balance.
- To withdraw money,
function withDrawMoney(address _address) external payable {
uint balance = address(this).balance;
payable(_address).transfer(balance);
}
- address(this) will return the current smart contract address.
- Web3.js is a JavaScript-library that lets us interact with a blockchain node via its RPC interface or Websockets.
- Here, there are JavaScript functions to interact with a blockchain node.
Providers
are services that are responsible for enabling Web3.js connectivity with the Ethereum network.Providers
helps us to querying data, sending transactions, and interacting with smart contracts.
import { Web3 } from 'web3';
// new web3 instance
const web3 = new Web3(/* PROVIDER*/);
await web3.eth.getBlockNumber();
Providers types
- HTTP Provider: a request-response protocol and does not support persistent connection(not suitable)
- WebSocket Provider: a persistent connection between a client and a server(suitable)
- IPC Provider: This offer high-performance local communication and provide a faster alternative to HTTP providers.(suitable)
Provider Origins
-
Remote Provider:
- Services like
Alchemy,Infura,QuickNode
offer Ethereum node services that can be accessed via HTTP or Websocket.
import { Web3 } from 'web3'; const web3 = new Web3('https://eth-mainnet.alchemyapi.io/v2/your-api-key');
- Services like
-
Injected Provider:
- Web3.js supports any injected provider that is compliant with
EIP-1193
- most often a wallet or web browser
import { Web3 } from 'web3'; const {ethereum} = window; const web3 = new Web3(ethereum);
- Web3.js supports any injected provider that is compliant with
- We mostly have used the
injected and remote providers
- It is used to interact with smart contracts.
- ABI ARRAY provides web3js, what functions are present in smart contract.
- The ABI Array contains all functions, inputs, as well as all variables and their types from a smart contract
// THIS IS THE SIMPLE EXAMPLE OF ABI ARRAY.
let abiArray = [
{
"inputs": [],
// name : variables, functions,
"name": "setData",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
]
- We can use Web3 libraries to interact with our smart contract.
- web3.eth provide us different methods/functions to interact with smart contract
// TO GET THE ACCOUNTS
(async () => {
const accounts = await web3.eth.getAccounts();
console.log(accounts);
console.log(accounts.lenght);
})()
- To interact with smart contract we have,
// SAMPLE SMART CONTRACT
contract setData {
uint public data = 100;
function setData(uint _data) public {
data = _data;
}
function getData() public view returns(uint){
return (data);
}
}
// JS CODE TO INTERACT WITH SMART CONTRACT
(async ()=>{
// TO GET ALL ACCOUNTS
let accounts = await web3.eth.getAccounts();
let SmartContractAddress = "";
let abiArray = [
{
"inputs": [],
"name": "myInt",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
];
let contract = new web3.eth.Contract(abiArray,contractAddress);
// TO UPDATE ANY STATE VARIABLE
async function setData(newData){
await contract.methods.setData(newData).send({from : accounts[0]});
}
// TO DISPLAY/READ DATA
let result = await contract.methods.getData().call();
console.log(result);
})();
- From above code we can interact with smart contracts.
-
new web3.eth.Contract() -> await myContract.methods.myMethod().call() To call an variable
-
new web3.eth.Contract() -> await myContract.methods.myMethod({from:account[0]}).send() To update variable or function with params.
- Fungible tokens(cryptocurrencies) : These tokens are identical and can be exchanged for one another with equal value. Think of fungible tokens like currency
- interchangeable, identical in value (e.g., money).
Fungible =
Link,Dollar
- Non-fungible tokens(NFTs) : These tokens are unique and cannot be exchanged on a one-to-one basis for something of equal value because each one has its distinct attributes.
- NFTs are often used to represent digital art, collectibles, or unique assets
- like a serial number or unique content
- unique, distinct in value (e.g., a rare collectible).
Non-fungible = A pokemon card with diff. stats
- public mint function
- mint multiple nfts
- add uri function
- view nfts on opensea
- withdraw function
- allowlist minting function
- minting window to enable and disable
- MAX LIMIT PER WALLET
- Refund functionality and Refund time period
- Testing smart contract on Testnet
- a formal suggestion for changes or updates to the Ethereum network.
- According to
EIP
the token functionality is improved or added. - Tokens follows EIP pattern.
- Token is a representation of something in the digital or physical world that resides on the Ethereum blockchain
- Managed by a smart contract, which is a program on Ethereum, a token can represent just about anything.
- It can be fungible(cryptocurrency) and non-fungible(NFT)
-
ERC20
is a smart contract that follows the ERC20 standard created byEIP
. -
And,
keeps track of balances using storage variables in the contract.
-
ERC-20 tokens are simple smart contract.
-
Ex:Chainlink
-
ERC20 is a standard for fungible tokens, which are all identical and interchangeable.
-
For tokens where every unit is the same (like currency).
-
ERC20 is exactly the same as any other ERC20, just like how one dollar is the same as another dollar.
EXAMPLE
// ERC-20 TOKEN SAMPLE CONTRACT FROM OZ
contract Web3 is ERC20, Ownable {
constructor()
ERC20("Web3", "ERC20")
Ownable(msg.sender)
{
_mint(msg.sender, 10000 * 10 ** decimals()); // 10000 ether
}
}
-
For ERC721 you have one address that has many tokens and all unique. The SC also works same.
-
ERC721 is a standard for non-fungible tokens, meaning each token is unique and cannot be exchanged one-for-one with another.
-
Each NFT has its own value based on its uniqueness.
-
For unique, one-of-a-kind items (like art or collectibles).
EXAMPLE
:
- Think of an NFT for a unique piece of digital artwork. If you have a token representing DigitalArt#1, it's unique and can't just be swapped for DigitalArt#2
- ERC1155 is a flexible standard that allows both fungible and non-fungible tokens within the same smart contract.
- This means you can create tokens that are all the same, all unique, or even some of each!
- A mix, allowing both identical and unique items in one contract
EXAMPLE
- Imagine a game where you have coins(fungible) and weapons(non-fungible).
- With ERC1155, you can have GameCoins(all identical) and unique items like rare swords or shields, all in one place.
- PUBLIC MINT FUNCTION
function publicMint() public payable {
require(true | false, "Minting is closed!!!");
require(msg.value == 0.1 ether, "Not enough funds!!!");
require(totalSupply() < 5, "We sold out!!!");
uint256 tokenId = _nextTokenId++;
_safeMint(msg.sender, tokenId);
}
- ALLOWLIST MINTING
function allowListMint() public payable {
require(allowListAddress[msg.sender], "You are not in allow list!!!");
require(true | false, "Minting is closed!!!");
require(msg.value == 0.01 ether, "Not enough funds!!!");
require(totalSupply() < 5, "We sold out!!!");
uint256 tokenId = _nextTokenId++;
_safeMint(msg.sender, tokenId);
}
- WITHDRAW FUNCTION
function withDrawMoney(address _address) external payable {
uint balance = address(this).balance;
payable(_address).transfer(balance);
}
- SET ALLOWLIST ADDRESS
mapping(address => bool) public allowListAddress;
function setAllowList(address[] memory addresses) external onlyOwner{
for(uint i=0;i<addresses.length;i++){
allowListAddress[addresses[i]] = true;
}
}
- PUBLIC MINTING (ERC-1155)
- THE amount REFERS TO TOTAL NO. OF NFTs TO BE MINTED.
uint constant public maxPurchasePerWallet = 3;
function publicMint(uint256 id, uint256 amount) public payable {
require(true | false, "Minting is closed!!!");
require(purchasePerWallet[msg.sender] + amount <= maxPurchasePerWallet, "MAX LIMIT PER WALLET REACHED");
// by this require we can mint multiple nfts
require(msg.value == (amount * 2 ether), "NOT ENOUGH MONEY SENT!!!");
require(totalSupply(id) + amount <= maxSupply, "SORRY! WE MINTED OUT.");
require(id < 5, "YOU ARE TRYING TO MINT WRONG NFTs");
_mint(msg.sender, id, amount, "");
}
- AllowListMint function is same just add mapping of allowlist addresses.
- ADD URI FUNCTION (ERC1155)
- This will return us the link for our NFT Metadata through IPFS
- We have added the IPFS Link this function will append NFT id and .json extension
function uri(uint256 _id) public view virtual override returns (string memory){
require(exists(_id), "URI : NOT EXIST");
return
string(
abi.encodePacked(super.uri(_id), Strings.toString(_id), ".json")
);
}
- PAYMENT SPLITTER (ERC-1155)
- IT SPLITS BETWEEN THE ADDRESS(_payees) FOR SHARES(_shares).
- WE WILL ADD address_array and shares_array DURING DEPLOYMENT.
constructor(address[] memory _payees, uint[] memory _shares)
ERC1155("ipfs://Qmaa6TuP2s9pSKczHF4rwWhTKUdygrrDs8RmYYqCjP3Hye/")
Ownable(msg.sender)
PaymentSplitter(_payees,_shares)
{
}
- This can be integrated in our DApps.
- PUBLIC MINT (ERC-721A)
function publicMint(uint256 amount) public payable {
require(msg.value == (publicPrice * amount),"NOT ENOUGH MONEY!!!");
// returns the number of tokens minted by 'owner'
require(_numberMinted(msg.sender) + amount < maxMintPerUser, "SORRY LIMIT REACHED FOR MINTING NFTs!!!");
// returns the total amount of tokens minted in the contract
require(_totalMinted() <= maxNFTMinting, "WE SOLD OUT!!!");
_safeMint(msg.sender, amount);
refundEndTimeStamp = block.timestamp + refundPeriod;
for(uint i = _currentIndex - amount; i<_currentIndex; i++){
refundEndTimeStamps[i] = refundEndTimeStamp;
}
}
- REFUND FUNCTIONALITY
// refund functionality
function refund(uint tokenId) external payable {
// check token expiry time stamp
require(getTokenTimeStamp(tokenId) > block.timestamp, "REFUND PERIOD EXPIRED!!!");
// you should be the owner of tokenid
require(msg.sender == ownerOf(tokenId), "YOU ARE NOT THE OWNER OF THIS TOKEN");
// mark the refund
hasRefunded[tokenId] = true;
// Transfer Ownership of nft
_transfer(msg.sender, refundAddress, tokenId);
// refund the price
uint refundAmount = getRefundAmount(tokenId);
payable(msg.sender).transfer(refundAmount);
}
// GET REFUND AMOUNT
function getRefundAmount(uint tokenId) public view returns (uint){
if(hasRefunded[tokenId]){
return 0;
}
return publicPrice;
}
- A liquidity pool on the Ethereum blockchain is like a big money jar filled with two types of tokens (ETH AND ERC-20).
- Here, people can trade directly with this jar, making it much faster and easier.
- Anyone who wants to trade ETH for DAI (or DAI for ETH) can use this jar.
- Creating Pool(Uniswap) : A liquidity pool is created by someone who puts both ETH and ERC into a digital jar (smart contract) on platform like Uniswap.
- Trading : Now, anyone who wants to trade ETH for DAI (or DAI for ETH) can use this smart contract(jar)
- Swapping : When someone swaps ETH for DAI, the pool takes in ETH and gives out DAI.
- Pricing : The prices adjust automatically based on how much ETH and DAI are in the jar.
In, NFT staking sc we will use both ERC20 and ERC721 token
- An NFT staking sc is a type of program on the blockchain that allows NFT holders(ERC721) to stake(or lock up) their NFTs in the contract for a certain period of time.
- In exchange, they earn rewards, usually in the form of tokens(ERC20) or other benefits.
- It's a way for NFT owners to gain extra value from their NFTs without selling them.
- To understand the process flow we will consider some basic tokens and contract :
Staking SmartContract(App) : A contract with functions for staking, unstaking, and getting rewards (main function). ERC-721(NFT) : The NFT that users lock up for a set period when staking. ERC-20(token) : The reward token users earn when they unstake their NFTs.
- STAKING : NFT holders connect their wallets to the staking platform and select an NFT they want to stake.
- Lock Period : The NFT is locked and temporarily transferred to the smart contract for a specific time
- Rewards : For keeping their NFT staked, users earn rewards like tokens, points. depending on the terms of the smart contract and company for which we are building
- Unstaking : When the lock period ends, users can withdraw their NFT and collect any rewards they earned.
- Layer1 (Main Blockchain Layer) -> (Bitcoin, Ethereum, and Solana)
-
the foundational blockchain layer, the core network where transactions are processed directly.
-
They each have their own rules, consensus mechanisms, and security.
-
Problem Layer 1 blockchains can get slow and expensive when there's high demand of tsx
- LAYER2 (Secondary Layer on Top of Layer1) -> (Polygon, Arbitrum, and Optimism)
-
Layer2 solutions are built on top of Layer1 blockchains.
-
handle transactions faster and more cheaply.
-
They offload some transactions from the main chain and then submit them in batches to Layer 1.
-
BENEFITS : Layer 2 can offer faster processing and lower fees,
- Rollups (Special Type of Layer 2 Solution) -> Arbitrum, zkSync
-
Rollups bundle many transactions together, process them on Layer 2, and then send a single, compact summary back to Layer 1 for finalization.
-
This reduces the number of transactions Layer 1 has to handle directly.
-
BENEFITS : Rollups improve scalability even more than regular Layer2s by minimizing amount of data sent to the main chain.
-
The Oracle problems refers to the connectivity issue of smart contract with the off-chain data(such as market-data, api_calls, api-data) with on-chain data.
-
Blockchain Oracles is any device that interacts with the off-chain world to provide external data or computation on on-chain.
-
Blockchain oracle is a secure piece of middleware that facilitates communication between blockchains and any off-chain system, including data providers, web APIs, enterprise backends, cloud providers, IoT devices, e-signatures, payment systems.
-
We will not use centralized computation or centralized oracle/node for our extrenal data.
-
Chainlink is a decentralized Oracle Network
-
When a smart contract combines on-chain and off-chain data, can be defined as hybrid smart contract
Now, how this blockchain oracles work:
-
With the help of decentralized oracle network
-
On off-chain, chainlink nodes will store external data from data-providers.
-
On on-chain, the chainlink node will transfer the data to Reference contract so that other contract can used this data.
-
Here, we will use the chainlink functions to get the external data for our smart contract
-
And, this chainlink function will be the future of DeFi apps.
- CHAINLINK FUNCTIONS
- CHAINLINK VRF
- CHAINLINK AUTOMATION
- END-TO-END RELIABILITY(TAKE INPUT, RETURN OUTPUT)
- Chainlink DATA Feeds
- Chainlink VRF (Verifiable Random Function) is a provably fair and verifiable random number generator (RNG)
- that enables smart contracts to access random values without compromising security or usability.
- Use Chainlink VRF to build reliable smart contracts for any applications that rely on unpredictable outcomes
Two methods to request randomness
-
Subscription Method
(Programmaticaly || Manually)- Create Subscription
- Get Subscription ID
- Fund the Subscription
- Add contract address as consumer
- Use the random number
-
Direct funding
- Add consumer
- allow contract to pay TNX fee to request random number
- To get the a random word, we will follow the following structure :
- requestRandomWords
- fulfillRandomWords
- Getting the random word
-
Import the libiraries
import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol"; import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol"; contract MyContract is VRFConsumerBaseV2Plus{}
-
Contract Variables
uint256 s_subscriptionId; address vrfCoordinator; bytes32 s_keyHash; uint32 callbackGasLimit; uint16 requestConfirmations; uint32 numWords = 1;
-
Initializing the contract
constructor( uint256 subscriptionId, address vrfCoordinator, ) VRFConsumerBaseV2Plus(vrfCoordinator) { s_subscriptionId = subscriptionId; }
-
requestRandomWords function
function requestRandomWords() public view returns(uint256 requestId){ // This will call the oracles network for requestId. VRFV2PlusClient.RandomWordsRequest memory request = VRFV2PlusClient.RandomWordsRequest({ keyHash: i_keyHash, subId: s_subscriptionId, requestConfirmations: requestConfirmations, callbackGasLimit: callbackGasLimit, numWords: numWords, extraArgs: VRFV2PlusClient._argsToBytes( VRFV2PlusClient.ExtraArgsV1({nativePayment: false}) ) }); requestId = s_vrfCoordinator.requestRandomWords(request); console.log(requestId); }
-
fulfillRandomWords function
// The coordinator sends the result of our generated randomWords back to fulfillRandomWords function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) public { uint randomNum = (randomWords[0]%arrayLength); <!-- Now, implement the functionality you want to use!!! --> }
- To automate some functions in our smart contract,
checkUpkeep()
checks if enough time has passed to call main logicperformUpkeep()
main logic which has to be automated!!!!
-
Import the libraries
import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/automation/AutomationCompatible.sol";
-
Initializing state variables
constructor(uint256 _interval) { interval = _interval; lastTimeStamp = block.timestamp; counter = 0; }
-
checkUpkeep() and performUpkeep()
contract Counter is AutomationCompatibleInterface{ // constructor // this function will check if we need to call our logic to call again function checkUpkeep( bytes calldata /* checkData */ ) external view override returns (bool upkeepNeeded, bytes memory /* performData */) { upkeepNeeded = (block.timestamp - lastTimeStamp) > interval; } // Our main logic of function function performUpkeep(bytes calldata /* performData */) external override { (bool upkeepNeeded,) = checkUpkeep(""); if(!upkeepNeeded){ revert(); } // some condition n logic to increase counter counter = counter + 1; } }
- To get the real time data for assets,currency coversion. We can use chainlink data feeds.
Getting the latest price of ETH in USD
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
contract DataConsumerV3 {
AggregatorV3Interface internal dataFeed;
/**
* Network: Sepolia
* Aggregator: BTC/USD
* Address: 0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43
*/
constructor() {
dataFeed = AggregatorV3Interface(
0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43
);
}
function getChainlinkDataFeedLatestAnswer() public view returns (int) {
(,int price,,) = dataFeed.latestRoundData();
return answer;
}
function getValueInUsd() public view returns(uint256 usdValue){
int price = getChainlinkDataFeedLatestAnswer();
uint256 private constant PRICE_FEED_SCALE_FACTOR = 1e10;
uint256 private constant TOKEN_DECIMAL_STANDARD = 1e18;
uint256 public amount = 1;
// (price * 1e10 * 1)/1e18 == price/1e8 == $3425.781919...
usdValue = (uint256(price) * PRICE_FEED_SCALE_FACTOR * amount) / TOKEN_DECIMAL_STANDARD;
}
}
NOTE : In solidity, multiply has more preference than division.
- When a functionality can be commonly used, we can create a library to efficiently manage repeated parts of codes.
- Use keyword library instead of contract.
// PriceConverter.sol
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
library PriceConverter {
function getPrice() internal view returns (uint256) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306);
(int256 answer) = priceFeed.latestRoundData();
return uint256(answer * 10000000000);
}
function getConversionRate(uint256 ethAmount) internal view returns (uint256) {
uint256 ethPrice = getPrice();
uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
return ethAmountInUsd;
}
}
- Now, to use the library in our solidity file, we have:
- Here, msg.value is first-parameter and value in parenthesis is second-parameter.
import {PriceConverter} from "./PriceConverter.sol";
using PriceConverter for uint256;
require(msg.value.getConversionRate(123) >= minimumUsd, "didn't send enough ETH");
- Using keyword like constant and immutable can optimize the gas cost.
NOTE : constant and immutable keyword are used for gas optimization in smart contract
- Modulo operator returns the remainder when a number is divided by a specific modulo.
// modulo of 10
function mod10(uint num) public pure returns (uint){
return (num%10);
}
// modulo of 2
function mod2(uint num) public pure returns (uint){
return (num%2);
}
- Send the balance of smart contract to specific address
function sendETH() public payable{
address userAddress = "0x001";
// sending ETH from smart contract to user
(bool success,)= userAddress.call{value: address(this).balance}("");
if(!success){
revert();
}
}
1 Ether = 1e18 wei;
1 Ether = 1e9 wei;
uint256 public constant SEND_VALUE = 1e18;
uint256 public constant SEND_VALUE = 1_000_000_000_000_000_000;
uint256 public constant SEND_VALUE = 1000000000000000000;
- npm install -g truffle (globally)
- mkdir project_name
- truffle init
- npm init -y
- echo "node_modules" > .gitignore
- npm i --save @openzeppelin/contracts
- This initialize the ganache rpc server
npm i --global ganache
ganache
- add a network called "ganache" to the truffle-config.js file
// ganache-cli opens an RPC listener on Port 8545
module.exports = {
networks: {
ganache: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},
},
};
- In truffle, we will use migration to deploy our sc.
// migrations/01-web3-deployment.js
const Web3 = artifacts.require("Web3");
module.exports = function (deployer, network, accounts) {
deployer.deploy(Web3, { from: accounts[0] });
};
- artifacts.require function will scan the contents of the build/contracts folder from json and extract all relevant function from json.
- Truffle has an integrated and interactive JavaScript console using ganache.
// deploy our sc on ganache
truffle migrate --network ganache
// inititalize console
truffle console --network ganache
- Now, inside the console we can interact with our smart contract.
- Run "migrate" to redeploy our sc inside console
// Create a new contract instance:
let token = await Web3.deployed();
// Truffle will scan the complete build artifacts directory and inject all contracts with their ABI and addresses directly in the console.
token.name();
// This way you can also mint an NFT
let accounts = await web3.eth.getAccounts();
await token.publicMint(accounts[0], 0.1 ether);
- Here contract instance will make a call (which you can see in Ganache) and return the output stored in the contract. Note : Truffle will scan the complete build artifacts directory and inject all contracts with their ABI and addresses directly in the console
// TO TEST OUR SC
truffle console --network ganache
migrate
test
- Truffle can do tests in JavaScript and solidity
// test/Web3.test.js
const Web3 = artifacts.require("Web3");
contract("Web3", (accounts) => {
// first test check
it("compare the two addresses for transaction", async () => {
let token = await Web3.deployed();
assert.equal(accounts[0], accounts[1], "Alert!!! Transaction fraud");
});
// second test check
it("Testing sample smart contract", async () => {
let token = await Web3.deployed();
let tokenName = await token.name();
console.log(tokenName);
});
});
-
Here each it(...) function represents new test, which expects a function as second params and msg as first params
-
Here, contract function gets all accounts which are injected by Truffle by doing a web3.eth.getAccounts() before starting the test.
-
truffle uses Mocha, here instead of 'describe' we will use 'contract'.
-
Truffle will automatically redeploy the contracts based on the migrations files to offer so called "clean room testing"
- You can't choose the account you're sending the TX from in Solidity.
- You can't modify anything on the chain.
- You can't listen to events.
-
This is the most standard way to deploy sc which is also followed by hardhat and foundary also.
-
Here, We start with Infura to deploy the Smart Contract
-
We sign up with a service(Infura) that hosts these blockchain nodes and get access.
-
The first thing we need to do is instruct Truffle to sign a transaction before sending it (using HDWALLET-PROVIDER)
npm install @truffle/hdwallet-provider
touch .secret
echo ".secret" >> .gitignore
touch .infura
echo ".infura" >> .gitignore
-
.secret : Add the secretPhrase from our metamask.
-
.infura : Add the infura_project_id from infura dashboard
-
.etherscan : To verify our sc on etherscan
-
Here, we will not use '.env' because it is not good practice in terms of security.
-
We will use 'fileSystem' to store our key.
-
Use the HDWalletProvider in your network configuration.
// tailwind-config.js
const HDWalletProvider = require("@truffle/hdwallet-provider");
const fs = require("fs");
const mnemonicPhrase = fs.readFileSync(".secret").toString().trim();
const infuraProjectID = fs.readFileSync(".infura").toString().trim();
const ETHERSCAN_ID = fs.readFileSync(".etherscan_id").toString().trim();
module.exports = {
// ...
networks: {
sepolia: {
provider: () =>
new HDWalletProvider(
mnemonicPhrase,
`https://sepolia.infura.io/v3/${infuraId}`
),
network_id: 11155111, // Sepolia's id
confirmations: 2, // # of confirmations to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true, // Skip dry run before migrations? (default: false for public nets)
},
},
};
- Deploy our sc on sepolia using migrate
// inside truffle-console
truffle migrate --network sepolia
truffle console --network sepolia
-
We will use MetaMask to do the actual interaction with a blockchain. It's in there for a while, but not used very widely.
-
Start with,
// This will open an RPC tunnel to a website where MetaMask can connect to
truffle dashboard
truffle migrate --network dashboard
- in this method we are not required to define the network in tailwind-config.js file
- login to your etherscan account and copy the API key.
- Save API key in .env or in fileSystem.
npm install -D truffle-plugin-verify
// tailwind-config.js
module.exports = {
/* ... rest of truffle-config */
plugins: ['truffle-plugin-verify'],
api_keys:{
etherScanId:process.env.ETHERSCAN_ID,
},
}
- Simply, run the following command in terminal
// FOR INFURA
truffle run verify Spacebear --network sepolia
// FOR DASHBOARD AND METAMASK
truffle run verify Spacebear --network dashboard
- Reduces the gas needed for contract deployment as well as for external calls made to the contract.
module.exports = {
//...
compilers: {
solc: {
version: "0.8.16", // Fetch exact version from solc-bin (default: truffle's version)
// docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
settings: { // See the solidity docs for advice about optimization and evmVersion
optimizer: {
enabled: false,
runs: 200
},
// evmVersion: "byzantium"
// }
}
},
}
- Most of the time the tailwind-config.js will look like:
// tailwind-config.js
const HDWalletProvider = require("@truffle/hdwallet-provider");
const fs = require("fs");
const mnemonicPhrase = fs.readFileSync(".secret").toString().trim();
const infuraProjectID = fs.readFileSync(".infura").toString().trim();
module.exports = {
plugins: ["truffle-plugin-verify"],
api_keys: {
etherscan: fs.readFileSync(".etherscan").toString().trim(),
},
networks: {
// GANACHE
ganache: {
host: "127.0.0.1",
port: 8545,
network_id: "*",
},
// SEPOLIA
sepolia: {
provider: () =>
new HDWalletProvider(
mnemonicPhrase,
`https://sepolia.infura.io/v3/${infuraId}`
),
network_id: 11155111, // Sepolia's id
confirmations: 2, // # of confirmations to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true, // Skip dry run before migrations? (default: false for public nets)
},
},
// Set default mocha options here, use special reporters, etc.
mocha: {
// timeout: 100000
},
// Configure your compilers
compilers: {
solc: {
version: "0.8.16", // Fetch exact version from solc-bin (default: truffle's version)
// docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
settings: {
// See the solidity docs for advice about optimization and evmVersion
optimizer: {
enabled: false,
runs: 200,
},
// evmVersion: "byzantium"
},
},
},
};
npm install @ganache/console.log
// import
import "@ganache/console.log/console.sol";
- Start using the 'console.log' in your smart contract for debugging.
ganache
truffle console --network ganache
migrate
- Try to mint an NFTs and you will see your console.log on the ganache server
-
There it is, your contract, on Mainnet. But suddenly users are reporting a bug. Something isn't right.
-
because your users funds are at stake, after all...
-
Now,
-
Ganache can fork the mainchain
-
Truffle can debug smart contracts without even having the source code locally
// Pick any recent transaction from Etherscan and check it is verified smart contract.
ganache --fork.network sepolia
// Then you can use truffle to connect to Ganache, which will happily serve the requests.
truffle debug <TXHASH> --network ganache --fetch-external
- You can get the TXHASH from Web3.json file in networks object.
-
can fork mainchain/ testnet
-
ganache downloads the code
-
ganache offers debug-trace transaction rpc call (normal blockchain node do not offers this)
-
Hit return a few times to see the actual execution of the code.
-
Hit h for help and v for the current variables
- IPFS is a decentralized P2P distributed file storing protocol.
- Storing data on blockchain is way expensive. So, the company store their data on
centralized server and cloud providers
- In IPFS, files and other data are stored in a network of nodes.
When a file is added to IPFS, it is split into smaller blocks, coverted to hash using hash algorithm (SHA-256).
- This
hash
is called asCID(Content Identifier)
- Everytime re-uploading file a new CID is generated.
- To retrieve data, a user requests it using the hash.
- IPFS locates the nodes storing the corresponding blocks and downloads them.
-
Traditional web uses location-based addressing, where content is accessed by its location on a server (URL).
-
IPFS uses content-based addressing, where content is accessed by a hash of its content. This ensures that as long as the content remains the same, its address does not change.
- If file is not in used in node using garbage collection process, file will be deleted.
- To prevent from garbage collection process we will use pinning service.
-
On-chain storage
:- refers to the practice of storing data directly on the blockchain, leveraging its inherent security features but at the cost of speed and expense.
Base64 encoding
is best example
-
off-chain decentralized storage
- involves storing data across a network of decentralized nodes or servers.
IPFS,arweave,filecoin
-
Off-chain private storage solutions
- encompass traditional
cloud-based and legacy data storage
options designed for secure and controlled access. AWS,azure,GCS
- encompass traditional
-
NFT metadata -> TokenURI
-
Hosted NFT image -> ImageURI
-
NFT metadata is the sum of all data that describes an NFT, typically including its name, traits, trait rarity, link to the hosted image, total supply, transaction history, and other essential data.
-
NFT MetaData contains all description for our NFT including image, image_uri, description, attributes.
// NFT-MetaData-Template
{
"name": "Cryptodunks #101",
"description": "",
"image": "ipfs://QmXtHPbZoUNkUwGcZTcqD8TLRtozdxpjReMroioMPEvkSC/0.png",
"attributes": [
{
"trait_type": "language",
"value": "JavaScript"
},
{
"trait_type": "OS",
"value": "Windows"
},
{
"trait_type": "Token",
"value": "ERC-721"
}
]
}
- We can see our minted and deployed NFT on
opensea (Testnet)
through Etherscan and also on ourMetamask portfolio.
Off-chain URI Representation ipfs://CID/?filename=NFT.json https://ipfs.io/ipfs/CID/?filename=NFT.json
on-chain URI Representation data:application/json;base64,hash
-
off-chain decentralized storage
:-
IPFS, FileCoin and Arweave
are some off-chain storage solutions. -
They do the heavy lifting with respect to decentralized NFT data storage by leveraging a ‘community’ of nodes around the world!!!
-
NFTMetadata -> Host on IPFS -> Hash/CID -> TokenURI
-
We can access the tokenUri in browser by:
- https://ipfs.io/ipfs/hash/?filename=NFT.json
- ipfs://hash/?filename=NFT.json
-
-
On-chain storage
:- We can store the files on-chain and it will be more secure but
storage and expensive
- The steps to store files on-chain is:
- Encode it in
Base64
format - Concatenate
baseUri + encodedFile
- Encode it in
- We can store the files on-chain and it will be more secure but
-
BaseURI and Base64 conversion
:-
Convert directly in terminal base64 -i image.svg > image.txt
-
Use the online converter and store it in txt file
-
Use
Base64.sol && abi.encodePacked(args)
this will encode our raw file to Base64
string memory SVG = vm.readFile('img/img.svg'); string memory SVG_TO_BASE64 = Base64.encode(bytes(string(abi.encodePacked(SVG))));
-
-
Use above any method to encode raw file to base64 format
-
Our toktenURI and ImageURI will look like this after storing them on-chain!!! data:image/svg+xml;base64,vhsdhvyfYFUYFYUFdcg4758....73248YUDTRTRXGHCyucxjc==
BaseURI
# This is the format of base uri.
data:<media-type>;base64,
<media-types> : application/json , image/svg+xml , text/plain , video/mp4
data:application/json;base64,
data:image/svg+xml;base64,
- artifacts folder in our root directory contains our smart contract abi array.
- Hardhat comes built-in with Hardhat Network, a local Ethereum network designed for development.
- It allows you to deploy your contracts, run your tests and debug your code, all within the confines of your local machine
npm init
npm install --save-dev hardhat
npx hardhat init
npm i --save @openzeppelin/contracts
npm install --save-dev @nomicfoundation/hardhat-toolbox
- Start by creating a new directory called contracts and create a file inside the directory called Web3.sol
npx hardhat compile
Note : Hardhat will run every *.js file in test/
- Start the testing using following command :
npx hardhat test
- We will use Ether.js and Mocha-Chai for our testing.
Mocha-Chai is popular JavaScript assertion library
// installing ether.js and mocha
npm install ethers
npm install --save-dev mocha
- Create a new directory called test inside our project root directory and create a new file in there called web3test.js
- To test our contract, we are going to use Hardhat Network, a local Ethereum network designed for development.
// test/web3test.cjs
const { expect } = require("chai");
const { ethers } = require("hardhat");
const assert = require("assert");
describe("Token contract", function () {
// first test check
it("Deployment should assign the total supply of tokens to the owner", async function () {
// RETURNS THE ETHEREUM ACCOUNT.
// And, this account will execute the deployment
const [owner, addr1, addr2] = await ethers.getSigners();
// start the deployment of our token contract
// return a Promise that resolves to a Contract
// This is the contract intance that has a method for each of your smart contract functions.
const token = await ethers.deployContract("Web3");
// Once contract is deployed, we can call contract methods using contract instance(token).
// the account in the owner variable executed the deployment,
const ownerBalance = await token.balanceOf(owner.address);
// Here, the token's supply amount and we're checking that it's equal to ownerBalance, as it should be.
// We have used expect(...) function from Mocha-chai to compare
expect(await token.totalSupply()).to.equal(ownerBalance);
// either we can use expect(...) method or assert.equal(...) method
assert.equal(await toke.totalSupply(), ownerBalance);
});
});
- Above, we can use either use expect(...) or assert.equal(...) method
-
This setup could involve multiple deployments and other transactions.
-
Doing that in every test means a lot of code duplication.
-
Plus, executing many transactions at the beginning of each test can make the test suite much slower.
-
To can avoid code duplication and improve the performance of your test suite we can use fixtures
-
A fixture is a setup function that is run only the first time it's invoked.
-
On every invocationshardhat will reset the state of networks.
const {
loadFixture,
} = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { expect } = require("chai");
const assert = require("assert");
const { ethers } = require("hardhat");
describe("Token contract", function () {
// deploy token fixtures
async function deployTokenFixture() {
const [owner, addr1, addr2] = await ethers.getSigners();
const token = await ethers.deployContract("Token");
await token.waitForDeployment();
// Fixtures can return anything you consider useful for your tests
return { token, owner, addr1, addr2 };
}
// first test check
it("Deployment should assign the total supply of tokens to the owner", async function () {
// for every test now we can use fixture instead of redepolying contract.
const { token, owner } = await loadFixture(deployTokenFixture);
const ownerBalance = await token.balanceOf(owner.address);
expect(await token.totalSupply()).to.equal(ownerBalance);
assert.equal(await token.name(), "ERC721");
});
// second test check
it("Test function", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
// ...rest of code
});
});
Note : To know more testing methods read hardhat testing docs (you know!!!)
- For debugging we will use console.log() in soilidity similar to JS.
- you can print logging messages and contract variables from your Solidity code.
// contract/Web3.sol
import "hardhat/console.sol";
contract TestContract {
// some code ...
function publicMint() public payable{
console.log("Amount transfer :", msg.value);
// rest of code...
}
}
- Lastly, run npx hardhat test to seee the result of debugging in terminal.
- To run deploy our smart contract use following code:
// THIS INITITALIZE AN RPC SERVER
npx hardhat node
// DEPLOYING ON LOCALHOST
npx hardhat ignition deploy ./ignition/modules/deploy.cjs --network localhost
// DEPLOYING ON TESTNET
npx hardhat ignition deploy ./ignition/modules/deploy.cjs --network sepolia
- In Hardhat, deployments are defined through Ignition Modules.
- Ignition modules are written inside ./ignition/modules directory.
// ./ignition/modules/deploy.js
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
const TokenModule = buildModule("TokenModule", (m) => {
// constructor params
const entranceFee = 1_000_000_000n; // 0.01 ETH
const interval = 30; // 30 seconds
const initialOwner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
const token = m.contract("Web3",[entranceFee,interval,initialOwner]);
return { token };
});
module.exports = TokenModule;
- Now to deploy our smart contract we will initialize an RPC server locally in our terminal(port:8545)
- We need to modify our hardhat.config.js file.
hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
const { vars } = require("hardhat/config");
// SET YOUR API KEY IN VARS AND NOT IN .ENV
// npx hardhat vars set API_KEY_NAME
const INFURA_API_KEY = vars.get("INFURA_API_KEY");
const PRIVATE_KEY = vars.get("PRIVATE_KEY");
module.exports = {
solidity: "0.8.27",
networks: {
sepolia: {
url: `https://sepolia.infura.io/v3/${INFURA_API_KEY}`,
accounts: [PRIVATE_KEY],
},
},
etherscan: {
apiKey: vars.get("ETHERSCAN_ID"),
},
sourcify: {
enabled: true,
},
};
- Here, we are using Infura blockchain node provider
- We will store our API_KEY, private_key and seed-phrase in var and not in .env/.env.local
// RUN THE FOLLOWING COMMAND TO STORE THE KEY IN VAR
npx hardhat vars set API_KEY_NAME
// hardhat.config.cjs
const { vars } = require("hardhat/config");
const INFURA_API_KEY = vars.get("INFURA_API_KEY");
const PRIVATE_KEY = vars.get("PRIVATE_KEY");
// 1.
npx hardhat verify --network sepolia DEPLOYED_CONTRACT_ADDRESS
// 2.
npx hardhat ignition verify sepolia-deployment
- On frontend,
if we need to send ether to contract
:
await lottery.methods.sendSomeEthToContract().send({
from:account,
value:web3.utils.toWei('0.001','ether'),
gas:"300000",
gasPrice:undefined
});
- Always, the balance is returned in wei(10^18).
To convert in ETH
:
// this will convert wei -> eth
import Web3 from "web3";
const web3 = new Web3();
const weiToEth = web3.utils.toWei('0.01','ether');
- Foundry totally written in solidity.
Note : dependencies are added as git-submodules and not as npm or nodejs modules
-
src folder : All our main smart contracts
-
test folder : All the test are written here.
-
scripts folder : To interact with smart contract we will write scripting file in soilidity
-
Project is configured using the foundry.toml file
-
lib folder : Dependencies are stored as git-submodules in lib/
-
After compiling/deploying the smart contract abi array will be in out/ folder in contract name file
// only once
curl -L https://foundry.paradigm.xyz | bash
source ~/.bashrc
foundryup
// to initialize project
forge init --no-commit
forge install openzeppelin/openzeppelin-contracts --no-commit
forge : the build, test, debug, deploy smart contracts anvil : the foundry equivalent of Ganache cast : low level access to smart contracts (a bit of a truffle console equivalent)
forge build
// .env
SEPOLIA_RPC_URL=
PRIVATE_KEY=
ETHERSCAN_API_KEY=
// foundry.toml
[rpc_endpoints]
sepolia = "${SEPOLIA_RPC_URL}"
[etherscan]
sepolia = { key = "${ETHERSCAN_API_KEY}"}
// This loads in the private key from our .env file
uint256 privateKey = vm.envUint("ANVIL_PRIVATE_KEY");
-
Written in solidity
-
they are run on the fast Foundry EVM backend, which provides dry-run capabilities.
-
By default, scripts are executed by calling the function named
run
, our entrypoint. -
Pass all the constructor params in contract instance.
-
We will use
HelperConfig.s.sol and Intraction.s.sol
file in ourDeploy.s.sol
// Scripting with Arguments(Passing params from command line) OPTIONAL
forge script --chain sepolia script/Deploy.s.sol:MyScript "NFT tutorial" TUT baseUri --sig 'run(string,string,string)' --rpc-url $SEPOLIA_RPC_URL --broadcast --verify -vvvv
// using anvil
anvil
forge script script/Deploy.s.sol:MyScript --fork-url http://localhost:8545 --broadcast
forge script script/Deploy.s.sol:MyScript --fork-url http://localhost:8545 --account <account_name> --sender <address> --broadcast
// on testnet sepolia
forge script script/Deploy.s.sol:MyScript --rpc-url $SEPOLIA_RPC_URL --broadcast --verify -vvvv
forge script script/Deploy.s.sol:MyScript --rpc-url $SEPOLIA_RPC_URL --account <account_name> --sender <address> --broadcast --verify -vvvv
forge script script/Deploy.s.sol:MyScript --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY --broadcast --verify -vvvv
- Here, we will not store our private key in dotenv file. Rather, we will store it in KeyStore provided by foundry.
- Once we have stored it in keystore we can used it in any project. Note : This is useful when we need to submit our private key in an terminal.
cast wallet import privateKey --interactive
cast wallet list
- deploy our Smart Contract using Foundry scripts.
- We will write the deploy code in the script folder in solidity.
By default, scripts are executed by calling the function named run, our entrypoint.
script/Deploy.s.sol
import {Script} from "forge-std/Script.sol";
import {TestContract} from "../src/Web3.sol";
contract MyScript is Script{
// BY DEFAULT forge script EXECUTES THE 'run' FUNCTION DURING DEPLOYMENT
function setUp() external returns(TestContract){
// This loads in the private key from our .env file
uint256 privateKey = vm.envUint("ANVIL_PRIVATE_KEY");
// contract creations made by our main script contract.
// private key is passed to instruct to use that key for signing the transactions.
vm.startBroadcast(privateKey);
// If we have constructor then passed the value in the function as params.
// CREATED A NEW CONTRACT INSTANCE.
TestContract token = new TestContract("Token Name","ETH", "base_URL");
vm.stopBroadcast();
return token;
}
function run() external returns(TestContract){
return setUp();
}
}
- In
HelperConfig.s.sol
file we will declare all theparams, function and variables
we need to pass in constructor during deployment.
HelperConfig.s.sol
contract HelperConfig is Script{
// ERROR
error HelperConfig__InvalidChainId();
// TYPES (pass all the constructor params here)
struct NetworkConfig {
uint priceFeed;
}
// STATE VARIABLES
// Local network state variables
NetworkConfig public localNetworkConfig;
mapping(uint256 chainId => NetworkConfig) public networkConfigs;
// FUNCTIONS
constructor(){
networkConfigs[ETH_SEPOLIA_CHAIN_ID] = getSepoliaETHConfig();
networkConfigs[ZKSYNC_SEPOLIA_CHAIN_ID] = getL2ChainConfig();
networkConfigs[LOCAL_CHAIN_ID] = getAnvilETHConfig();
}
function getConfig() public view returns(NetworkConfig memory){
return getConfigByChainId(block.chainid);
}
function getConfigByChainId(uint256 chainId) public view returns(NetworkConfig memory){
if(networkConfigs[chainId].VRFCoordinator != address(0)){
return networkConfigs[chainId];
} else if(chainId == LOCAL_CHAIN_ID){
return networkConfigs[chainId];
}else{
revert HelperConfig__InvalidChainId();
}
}
// CONFIGS FOR SEPOLIA AND L2 CHAINS
function getSepoliaETHConfig() public pure returns(NetworkConfig memory){
return NetworkConfig({priceFeed:200});
}
function getL2ChainConfig() public view returns(NetworkConfig memory){
return NetworkConfig({priceFeed:200});
}
// LOCAL CONFIG (Local testing using a Mock contract)
// Here, we will write the mock script smart contract on local network
function getAnvilETHConfig() public returns(NetworkConfig memory){
// Check to see if we set an active network config
if(localNetworkConfig.VRFCoordinator != address(0)){
return localNetworkConfig;
}
// DEPLOY MOCK SMART CONTRACT
vm.startBroadcast();
VRFCoordinatorV2_5Mock mockVRFcontract = new VRFCoordinatorV2_5Mock(MOCK_BASEPRICE);
vm.stopBroadcast();
localNetworkConfig = NetworkConfig({priceFeed:200});
return localNetworkConfig;
}
}
- In
Interaction.s.sol
we will create functions from which ouron-chain data interacts with off-chain data
- Example : chainlink VRF, chainlink automation, Data feeds and chainlink functions.
Interaction.s.sol
import {Lottery} from "src/Lottery.sol";
import {HelperConfig, CodeConstants} from "./HelperConfig.s.sol";
contract FundSubscription is Script{
function fundSubscriptionWithConfig() public {
HelperConfig helperConfig = new HelperConfig();
uint subId = helperConfig.getConfig().subscriptionId;
fundSubscription(subId);
}
function fundSubscription(uint256 subId) public {
uint amount = 0.01 ether;
vm.startBroadcast();
MockContract(contractAddress).topUpSubscription(amount);
vm.stopBroadcast();
}
function run() public {
fundSubscriptionWithConfig();
}
}
contract AddConsumer is Script{
function addConsumerWithConfig() public {
HelperConfig helperConfig = new HelperConfig();
addConsumer();
}
function addConsumer() public {
vm.startBroadcast();
MockContract(contractAddress).addConsumers(address(0));
vm.stopBroadcast();
}
function run() public {
addConsumerWithConfig();
}
}
- This is the basic structure of writing HelperConfig and Interaction file.
By default, scripts are executed by calling the function named run, our entrypoint.
- This is the
pattern and best practice
we should followed!!!
Deploy.s.sol
import {Contract} from "../src/Contract.sol";
import {HelperConfig} from "./HelperConfig.s.sol";
import {FundSubscription, AddConsumer} from "./Interaction.s.sol";
contract MyScript is Script {
function setUp() public returns (Contract, HelperConfig){
// CREATED NEW HELPERNETWORK CONFIG INSTANCE
HelperConfig helperConfig = new HelperConfig();
HelperConfig.NetworkConfig memory config = helperConfig.getConfig();
// If for some valid condition we need to call the interaction.s.sol
if(condition){
// funding subscription
FundSubscription fundSubscription = new FundSubscription();
// add consumer after deployment
AddConsumer addConsumer = new AddConsumer();
}
vm.startBroadcast();
// pass all the constructor params here...
Contract token = new Contract(
config.priceFedd,
config.DataFeed,
);
vm.stopBroadcast();
return {token,helperConfig};
}
// BY DEFAULT forge script EXECUTES THE 'run' FUNCTION DURING DEPLOYMENT
function run() external returns(Contract,HelperConfig) {
return setUp();
}
}
change the .env and foundry.toml file
.env
# SEPOLIA TESTNET
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/{INFURA_KEY}
ETHERSCAN_API_KEY=
PRIVATE_KEY=
# ANVIL LOCALLY
LOCALLY_RPC_URL=http://localhost:8545
ANVIL_PRIVATE_KEY=
foundry.toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = [
'@chainlink/contracts@1.2.0/=lib/chainlink-brownie-contracts/contracts',
'forge-std/=lib/forge-std/src/',
'@solmate/=lib/solmate/src'
]
solc = "0.8.26"
via_ir = true
fs_permissions = [
{ access = "read", path = "./broadcast" },
{ access = "read", path = "./reports" },
]
[fuzz]
runs=256
- After deploying sc we can interact (send/call) the functions using cast
cast send <address> "setName(string)" "anurag" --rpc-url <rpc_url> --private-key <private_key>
cast call <address> "getName()"
cast to-base 0x7717 dec
# returns function selector/functionID/methodID
cast sig "transfer(address,uint256)"
# To check params with HEX data before TNX
cast --calldata-decode "transfer(address,uint256)" HEXData
// to use vanilla-foundry
foundryup
// to use L2/ROLLUPS
foundry-zksync
- For L2 and rollups you can refer there docs for more clearance
- --zksync refers that we are running on L2/rollups blockchain
-
The tests in Foundry are written in Solidity.
-
If the test function reverts, the test fails, otherwise it passes.
-
We will use VM Cheatcodes.
-
contract name starting with test are considered as a good practice in foundry
-
Forge Standard Library -> forge-std
-
UNIT TESTING - TESTING A SPECIFIC PARTs OF OUR CODE.
-
INTEGRATION TEST - TESTING THE INTERACTIONS PART OF OUR SMART CONTRACT
-
FORKED TEST - TESTING OUR CODE ON A SIMULATED REAL ENVIRONMENT(Sepolia or Rollups)
-
STAGING TEST - TESTING OUR CODE IN TESTNET/MAINNET. EX:- SEPOLIA, ANVIL LOCAL TESTING
-
FUZZ TESTING - identify vulnerabilities in a smart contract by systematically inputting random data values
- Stateful fuzz
- stateless fuzz
- formal verification
-
Forge supports testing in a forked environment
-
To run all tests in a forked environment, such as a forked Ethereum mainnet, pass an RPC URL via the --fork-url flag
-
Sometimes we need to run test from scratch. Before running test again remove the cache directory/forge clean
// TO LOAD THE .env CONTENT
source .env
echo $RPC_URL
// TESTING SC
forge test -vvv
forge test --fork-url $RPC_URL -vvvv
// TO RUN THE SINGLE TEST
forge test --mt testFunctionName
forge test --mt testBalance -vvv --fork-url $RPC_URL
// DEBUGGING SC
forge debug --debug src/Web3.sol:TestContract --sig "function(argu)" "arguValue"
// Verifiying smart contract on etherscan
forge test --fork-url <your_rpc_url> --etherscan-api-key <your_etherscan_api_key>
- Vm.sol: Up-to-date cheatcodes interface
- console.sol and console2.sol: Hardhat-style logging functionality
- Script.sol: Basic utilities for Solidity scripting
- Test.sol: A superset of DSTest containing standard libraries, a cheatcodes instance (vm), and Hardhat console
-
vm.prank(address(1))
- simulate a TNX to be sent from given specific address.
-
vm.deal(address(this), 1 ether)
- Used to give the test contract Ether to work with.
- We also can send Ether to specific address for test!!!
-
vm.expectRevert()
- Agar mera call/send function revert ho gaya, Toh mera test pass ho jayega.
- Else, test fail ho jayega.
-
vm.expectRevert(Contract.CustomError.selector)
- import the error from contract with 'selector'
-
vm.expectRevert(abi.enocodeSelector(Contract.CustomError.selector, params1, params2))
-
test_FunctionName
- Functions prefixed with 'test' are run as a test case by forge.
-
For, testFail
- A good practice is to use the pattern test_Revert[If|When]_Condition in combination with the expectRevert cheatcode
function test_RevertCannotSubtract43() public { vm.expectRevert(stdError.arithmeticError); testNumber -= 43; }
-
Test functions must have either external or public visibility.
-
type aliases(enum, struct, array,errors,events) can be call using main contract(Lottery) only.
function test_GetRaffleState() public view { assert(lottery.getLotteryStatus() == Lottery.LotteryStatus.Open); }
-
functions(call/send) can be called by our instance(lottery)
solidity function test_CheckEntranceFee() public view { assertEq(lottery.getEntryFeeAmount(), 0.01 ether); }
-
To Transfer some value during calling or Transact eth to SC
```solidity
function test_LotteryCheckIfUserIsAdded() external {
vm.prank(USER);
// by this method we pass some eth to our user.
lottery.enterLottery{value:_entranceFee}();
}
```
-
vm.expectEmit()
:- a specific log is emitted during the next call.
function test_LotteryEntranceFeeEvents() external{ vm.prank(USER); // for indexed params we will set it true vm.expectEmit(true, false, false,false , address(lottery)); emit EnteredUser(USER); lottery.enterLottery{value:_entranceFee}(); }
-
vm.warp() || vm.roll()
- Sets block.timestamp.
- Sets block.timestamp.
function test_UserNotAllowedToEnterLotteryWhenClosed() external { vm.prank(USER); lottery.enterLottery{value:_entranceFee}(); vm.warp(block.timestamp + _interval + 1); vm.roll(block.timestamp + 1); }
-
vm.recordLogs() || vm.getRecordedLogs()
- Tells the VM to start recording all the emitted events.
- To access them, use
getRecordedLogs
function test_GetEventsLogs() public { vm.recordLogs(); lottery.performUpkeep(""); Vm.Log[] memory logs = vm.getRecordedLogs(); bytes32 value = logs[1].topics[1]; assert(uint256(value) > 0); }
-
vm.readFile(path)
- This cheatcode is used for
filesystem manipulation operations
. - We can read different file(svg,json,text).
foundry.toml
fs_permissions = [{ access = "read", path = "./"}]
Use vm.readFile(path)
string memory SVG = vm.readFile('img/monkey.svg');
- This cheatcode is used for
-
nonReentrant() modifier
:- often used to make sure that a function cannot be called again before the current call completes.
import {ReentrancyGuard } from "lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; function randomFunction() external nonReentrant{ // code here... }
-
transfer() and transferFrom()
:transfer()
we use this when we send transaction to ourself.transferFrom()
we use this function when we want to send transaction to another account.
-
During testing with foundry, keep some point for best practices:
- Never make a variable public which contain imp. keys.
- Write
getterFunctions
- Only main contract can call
errors,events,structs,enums,types aliases
- Contract instance can call/send
getter n write functions
- continue...
- Testing the constructor params:
- To test whether the constructor params are passed correctly or not:
function test_ConstructorParams() public { // create new contract instance vm.expectRevert(); new Contract(params1,params2,params3); }
- For, advance testing we will use
HelperConfig, MainContract and Deploy
file. - Follow,
Best practices and vm cheatcodes above for advance and better testing
.
Contract.t.sol
import {Contract} from "src/Contract.sol";
import {ContractScript} from "script/Deploy.s.sol";
import {HelperConfig,CodeConstants} from "script/HelperConfig.s.sol";
contract ContractTest is Test {
Contract contracts;
HelperConfig helperConfig;
// all constructor params and used variables
uint params1;
uint params2;
uint params3;
// events : Copy all events from contract to be used
/**
* here we will use our deploy script contract instance
* our deploy script setUp() returns 'Main contract' and 'HelperConfig contract'
* provide some eth to user for testing
*/
function setUp() public {
ContractScript contractScript = new ContractScript();
(contracts,helperConfig) = contractScript.setUp();
HelperConfig.NetworkConfig memory config = helperConfig.getConfig();
_param1 = config.param1;
_param2 = config.param2;
_param3 = config.param3;
// provide some eth to user for testing
vm.deal(address(0),1e18);
}
function test_GetContractStatus() public {
assert(contracts.getStatus() == Open);
}
function test_SomeChecks() external {
assert(contracts.getSomeVar() == 1 ether);
}
}
- Before running the forge remapping command we need to store the path in toml
- Forge can remap dependencies to make them easier to import. Forge will automatically try to deduce some remappings for you:
remapping = ['@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts']
- @chainlink/contracts now is equal to the actual path of contract
forge remappings
- Displays which parts of your code are covered by tests.
// View summarized coverage:
forge coverage
// Create lcov file with coverage data:
forge coverage --report lcov
// This will create a .txt file that will give us the parts of our contracts cover:
forge coverage --report debug > coverage.txt
- The EVM basically represents all the instructions a computer needs to be able to read.
- Any language that can compile down to bytecode with these opcodes is considered
EVM compatible
-
Data in Transactions:
- When we send a transaction, it is
compiled
down to bytecode - The EVM processes this data to determine which function to call and what inputs to provide.
- When we send a transaction, it is
-
Bytecode:
- When a contract is deployed, it is compiled into
bytecode understood by the EVM.
- This
bytecode
represents exactly thelow level instructions
to make our contract happen. - This bytecode consist of opcodes
- When a contract is deployed, it is compiled into
-
Opcodes:
- Each opcode is a 2-character hexadecimal, represents some special instruction
- This opcode reader is sometimes abstractly called the
EVM
-
Encoding Data:
- Now,
ABI encoding will convert data into bytes
- abi.encodePacked() || abi.encode()
- Now,
-
Decoding data:
- Decoding is the process of taking the raw bytes and reconstructing the original data
- abi.decode()
-
Low-Level call and staticcall:
-
call
- How we call functions to change the state variable of the blockchain
function stateChange(address _address) public { s_state[_address] = 12; }
-
staticcall
- This is how (at a low level) we do our "view" or "pure" function calls
function getState() public view returns(uint256){ return s_state; }
-
Converting SVG string to Base64 using EVM
string memory SVG = vm.readFile('img/img.svg');
string memory SVG_TO_BASE64 = Base64.encode(bytes(string(abi.encodePacked(SVG))));
-
In order to call a function using only the data field of call, we need to encode:
- function name
- parameters we want to add
-
Now each contract assigns each function a
function ID/Method ID
:Function selector
is the first 4 bytes of the function signatureFunction signature
a string that defines the function name & parameterstransfer(address,uint256)
send TNX by calling a function by populating the data field!!!:
- Lets assume, we need to call
transfer(address,uint256)
but by filling the data field!!!
-
getFunctionSelector
:// By this method we will get function selector of the calling function. function getSelector() public pure returns (bytes4 selector) { selector = bytes4(keccak256(bytes("transfer(address,uint256)"))); }
-
call Transfer Function with selector
:- Will use
abi.encodeWithSelector(bytes4 selector, args1, args2)
// Here, using EVM cheatcodes we can directly call the transfer() function using data-field function callTransferData(address _address, uint256 _amount) public returns (bytes4 ,bool) { (bool success, bytes memory data) = address(this).call( abi.encodeWithSelector(getSelector(), _address,_amount) ); return (bytes4(data), success); }
- Will use
-
call Transfer Function with signature
:- Will use
abi.encodeWithSignature(string functSignature, args1, args2)
// Here, we will not use function selector. Rather, we will use function signature to call our transfer() function by populating data field. function callTransferDataSig(address _address, uint256 _amount) public returns (bytes4 ,bool) { (bool success, bytes memory data) = address(this).call( abi.encodeWithSignature("transfer(address,uint256)", _address,_amount) ); return (bytes4(data), success); }
- Will use
- The calls we discuss previously all our
low-level calls
that our used to omptimize our code.
-
Check address
-
Check function selector(MethodID/FunctionID):
cast sig "transfer(address,uint256)"
- This will return the function selector
-
Decode the call data to check params:
cast --calldata-decode "transfer(address,uint256)" HexData
- This will return the params passed, before TNX
- HexData will be available in metamask TNX pop-up!!!
A repo to get the most recent deployment from a given environment in foundry. This way, you can do scripting off previous deployments in solidity.
It will look through your broadcast
folder at your most recent deployment.
- Get the most recent deployment of a contract in foundry
- Checking if you're on a zkSync based chain
- Update forge-std to use newer FS cheatcodes
forge install Cyfrin/foundry-devops --no-commit
forge install foundry-rs/forge-std@v1.8.2 --no-commit
1. Update your foundry.toml
to have read permissions on the broadcast
folder.
fs_permissions = [
{ access = "read", path = "./broadcast" },
{ access = "read", path = "./reports" },
]
- Import the package, and call
DevOpsTools.get_most_recent_deployment("MyContract", chainid);
ie:
import {DevOpsTools} from "lib/foundry-devops/src/DevOpsTools.sol";
import {MyContract} from "my-contract/MyContract.sol";
.
.
.
function interactWithPreviouslyDeployedContracts() public {
address contractAddress = DevOpsTools.get_most_recent_deployment("MyContract", block.chainid);
MyContract myContract = MyContract(contractAddress);
myContract.doSomething();
}
- foundry-zksync
- You'll know you did it right if you can run
foundryup-zksync --help
and you see a response like:
- You'll know you did it right if you can run
The installer for Foundry-zksync.
Update or revert to a specific Foundry-zksync version with ease.
.
.
.
In your contract, you can import and inherit the abstract contract ZkSyncChainChecker
to check if you are on a zkSync based chain. And add the skipZkSync
modifier to any function you want to skip if you are on a zkSync based chain.
It will check both the precompiles or the chainid
to determine if you are on a zkSync based chain.
import {ZkSyncChainChecker} from "lib/foundry-devops/src/ZkSyncChainChecker.sol";
contract MyContract is ZkSyncChainChecker {
function doStuff() skipZkSync {
skipZkSync
: Skips the function if you are on a zkSync based chain.onlyZkSync
: Only allows the function if you are on a zkSync based chain.
isZkSyncChain()
: Returns true if you are on a zkSync based chain.isOnZkSyncPrecompiles()
: Returns true if you are on a zkSync based chain using the precompiles.isOnZkSyncChainId()
: Returns true if you are on a zkSync based chain using the chainid.
In your contract, you can import and inherit the abstract contract FoundryZkSyncChecker
to check if you are on the foundry-zksync
fork of foundry
.
!Important: Functions and modifiers in
FoundryZkSyncChecker
are only available if you runfoundry-zksync
with the--zksync
flag.
import {FoundryZkSyncChecker} from "lib/foundry-devops/src/FoundryZkSyncChecker.sol";
contract MyContract is FoundryZkSyncChecker {
function doStuff() onlyFoundryZkSync {
You must also add ffi = true
to your foundry.toml
to use this feature.
onlyFoundryZkSync
: Only allows the function if you are onfoundry-zksync
onlyVanillaFoundry
: Only allows the function if you are onfoundry
is_foundry_zksync
: Returns true if you are onfoundry-zksync
- It contains some
code snippets for frontend
that are used repeatedly in frontend!!!
'use client';
import React from 'react'
import { ConnectWalletBtn } from '../components/ConnectWalletBtn';
import ToDoList from '../components/ToDoList';
import { Web3 } from 'web3';
import address from "../config";
import abi from "../abi";
const Home = () => {
const [correctNet, setCorrectNet] = React.useState(false);
const [loggedIn, setLoggedIn] = React.useState(false);
const [currentAcc, setCurrentAcc] = React.useState('');
const [input, setInput] = React.useState('');
const [task,setTasks] = React.useState([]);
const web3 = new Web3('https://sepolia.infura.io/v3/6e2aaaa2ff0c4e00995a96624cca8e7a');
// INITIALIZE THE SMART CONTRACT
const token = new web3.eth.Contract(abi.abi, address.contractAddress);
// CONNECT WALLET BTN FUNCTION
const ConnectWallet = async ()=>{
try {
const {ethereum} = window;
if(!ethereum){
console.log("MetaMask not detected!!!");
return;
}
const chainId = await ethereum.request({method:"eth_chainId"});
console.log("connected to chain id : ", chainId);
const sepoliaChainId = "0xaa36a7";
if(chainId != sepoliaChainId){
alert("You are not connected to sepolia test network!!!")
setCorrectNet(false);
return;
}
setCorrectNet(true);
const accounts = await ethereum.request({method:"eth_requestAccounts"});
console.log("account address detected : ", accounts[0]);
setLoggedIn(true);
setCurrentAcc(accounts[0]);
} catch (error) {
console.log(error);
}
}
// ADD TASK FUNCTION
const AddTask = async ()=>{
try {
const {ethereum} = window;
if(ethereum){
await token.methods.addTask(input).send({from:currentAcc})
.then(res=>{
setTasks(...task,task);
console.log("Task added");
})
.catch(error =>{
console.log(error);
})
} else{
console.log("Eth does not exist");
}
} catch (error) {
console.log(error);
}
}
return (
<div>
<div>
<h1 className='btn btn-primary m-4'>{currentAcc}</h1>
{
loggedIn ? <ToDoList setInput={setInput} AddTask={AddTask}></ToDoList> : <ConnectWalletBtn ConnectWallet={ConnectWallet} />
}
</div>
</div>
)
}
export default Home;
DeFi is an finance system that provides permissionless and transparent financial services
using blockchain network and smart contracts.
- Decentralized finance, or DeFi, is an emerging P2P finance system attempting to remove third parties and centralized institutions from financial transactions.
- It is built on cryptocurrencies, blockchain technology, and software that provides financial services like lending, borrowing, trading, and investing without needing a middleman.
-
Lending and Borrowing:
Aave and Compound
allow users to lend their crypto and earn interest.
-
Decentralized Exchanges (DEXs):
Uniswap, SushiSwap
Users can trade tokens without a centralized exchange or order book.
-
Stablecoins:
DAI,USDC
this bridge the gap between traditional finance and cryptocurrencies by offering the stability of fiat with the efficiency of blockchain!
-
Yield Farming:
- Users earn rewards by providing liquidity or staking tokens in DeFi protocols.
-
Insurance:
- Platforms like
Nexus Mutual
offer decentralized insurance for smart contract risks.
- Platforms like
-
Derivatives and Prediction Markets:
Synthetix
Prediction markets let users bet on outcomes of events.
- A
DeFi protocol
is a set of rules or smart contracts that enable decentralized financial services like lending, borrowing, and trading without the need for banks or intermediaries. - These protocols run on blockchains like Ethereum.
Examples
Uniswap: A DeFi protocol for swapping cryptocurrencies.
MakerDAO/DAI: A protocol that manages the DAI stablecoin.
Stablecoins is a cryptocurrencies whose buying power remains stable!!!
Stablecoins
are usually tied to the value of something stable, like a national currency (US Dollar) or commodity(gold).- For ex, Bitcoin or Ethereum, which are not Stablecoins, whose prices can change drastically.
EX: (1 USDC == 1 USD)
-
Relatively Stabled(Pegged/Floating Stablecoin)
:- Coins which depends on other assets(cryptocurrencies).
Pegged StableCoin
have their value tied to another assetsFloating stablecoin
use maths and mechanism to maintain a constant buying power.
-
Stability method(Governced/Algorithmic Stablecoin)
:- This is a mechanism that makes coin stable.
Governced Stablecoin
this mint and burn token via human intervationAlgorithmic Stablecoin
it uses a transparent math equation or set of codes to mint and burn tokens.
-
Collateral Types
:-
This include
endogenouslly and exogenouslly Collateralized Stablecoins
-
Exogenous: Backed by external assets (USD, ETH). Generally more stable. (
USDT/DAI/RAI/USDC/FRAX
) -
Endogenous: Backed by internal assets (project's own token). More risky due to dependency on the token's value.(
UST-Lunna/Terra
)
-
-
Fiat-Collateralized Stablecoins:
- These are backed by real-world money, like dollars, stored in a bank.
- Example: USDT (Tether), USDC
-
Crypto-Collateralized Stablecoins:
- These are backed by other cryptocurrencies as collateral.
- This are often
over-collateralized
- Example: DAI,RAI,FRAX
-
Algorithmic Stablecoins:
- This uses algorithms and smart contracts to manage the coin's supply and keep its value stable.
- Example: UST(Terra/Luna)
-
Reduce Volatility:
- Unlike Bitcoin or Ethereum, their value doesn't swing wildly, making them safer for
financial transactions
.
- Unlike Bitcoin or Ethereum, their value doesn't swing wildly, making them safer for
-
Ease of Use:
- They provide a familiar, dollar-like currency for DeFi users.
-
Seamless Trading:
- Stablecoins are used as a base currency in trading pairs, allowing people to trade crypto assets without dealing with volatility.
-
Lending and Borrowing:
- Users can lend and borrow stablecoins on DeFi platforms, avoiding the risks of crypto price changes.
-
Savings:
- Stablecoins allow users to earn interest without worrying about their savings losing value.
-
Tether(USDT): Backed by fiat currency reserves.
-
USD Coin(USDC): Similar to Tether but focuses on transparency.
-
DAI/RAI/FRAX: Decentralized and backed by other crypto assets.