Laborator 5
Tematica laboratorului: Retele de test si Design Patterns
Retele publice de test
Extensia de browser MetaMask permite conectarea la o retea Ethereum, atat din cele publice cat si una simulata local de exemplu folosind Ganache, si initierea de tranzactii prin intermediul browserului, sau a altor aplicatii ce ruleaza in browser (tipic prin intermediul unei biblioteci JavaScript precum web3).
Un contract scris in Remix poate fi de exemplu instantiat si accesat pe o retea publica Ethereum prin intermediul browserului, selectand "Injected Provider" ca "Environment" in pluginul pentru deployment, si confirmand tranzactia si plata aferenta prin MetaMask.
Retelele de test publice Ethereum disponibile in prezent sunt urmatoarele:
Pentru a obtine fonduri pe retelele de test publice Ethereum, se pot utiliza asa numitele site-uri de tip "faucet", ce distribuie ETH de test gratuit in anumite conditii (de exemplu unele necesita o cerere facuta prin intermediul unei retele sociale).
O indexare a diverse alternative de faucets este disponibila la aceasta adresa.
Tranzactiile de pe retelele de test pot fi monitorizate pe adresele Etherscan respective: Sepolia, Holesky.
Oracle
Acest tip de pattern este legat de necesitatea de a obtine in cadrul unui contract informatii externe contextului retelei blockchain, prin intermediul unui contract tert specializat - asa zis "oracol".
Contractul respectiv primeste cereri pentru furnizarea de astfel de informatii externe pe care le acceseaza printr-o comunicare externa si le trimite de obicei ca raspuns asincron contractului solicitant (serviciul e asigurat de regula contra cost). Printre furnizorii independenti de astfel de servicii de tip "oracol" se numara: Chainlink, Chronicle, si Witnet.
In varianta cea mai generala de tip request-response, pentru utilizarea unui oracol este necesara implementarea intr-un contract a doua functii:
- O functie cu rolul de a initia si trimite cererea catre oracol in cadrul unei tranzactii, ce depinde de API-ul pus la dispozitie de implementarea oracolului. Functia poate include diversi parametri, transfera costul necesar cererii si retine eventual un identificator al cererii primit ca retur.
- O functie callback ce va fi apelata asincron de oracol pentru a furniza raspunsul la cererea formulata cand acest raspuns este disponibil. Functia respectiva poate stoca raspunsul sau initia alte operatii in cadrul contractului dupa caz. Avand in vedere ca astfel de operatii initiate la primirea raspunsului pot induce alte costuri, un element necesar in functia callback este de regula verificarea ca apelantul functiei este strict oracolul, iar apelul este un raspuns la cererea trimisa (in functie de identitatea contractului respectiv, ID cerere, etc.).
Mai jos se regaseste un contract ce foloseste un oracol creat utilizand API-ul pus la dispozitie de Chainlink, ce e o replica aproape identica a exemplului disponibil la aceasta adresa, cu diferenta ca informatia obtinuta e cursul de schimb ETH/USD in loc de volumul tranzactionat. Contractele importate ce asigura interactiunea cu serviciile de oracol ale Chainlink au sursele disponibile public la aceasta adresa, si pot fi instalate local folosind npm (in Remix sunt descarcate automat la intalnirea referintei din codul sursa de mai jos).
Pentru utilizarea contractului exemplu de mai jos este necesara obtinerea de Ether de test pe reteaua Sepolia, precum si de LINK, un token folosit in particular de serviciile de oracol oferite de Chainlink. Obtinerea acestor fonduri de test, in special LINK, poate fi posibila prin intermediul faucet-urilor Chainlink pentru Sepolia (necesita conectarea cu un cont Github), si specificarea unui cont disponibil in MetaMask unde sa fie primite fondurile. Accesul la cantiatea de tokens LINK obtinuta in Metamask se face prin functionalitatea "import tokens" si adaugarea adresei contractului de token pentru LINK. Pe reteaua Sepolia aceasta este: 0x779877A7B0D9E8603169DdbD7836e478b4624789. Dupa acest pas ar trebui sa fie vizibila in MetaMask si cantitatea de LINK pe langa Ether, ambele pe reteaua Sepolia.
Dupa deployment, contractul de mai jos va necesita LINK pentru a trimite cereri catre serviciul de oracol. Alimentarea contractului cu LINK se va face prin transfer din MetaMask catre adresa acestuia (o descriere in detaliu este disponibila aici). Serviciile oracol puse la dispozitie de Chainlink sunt grupate in jobs, formate din tasks, cele standard fiind descrise la aceasta adresa. Pe retelele de test, precum Sepolia, fiecare cerere de job este tarifata cu 0.1 LINK, deci contractul ar trebui sa fie alimentat corespunzator cu un numar de LINK tokens (atentie, costul de executie a tranzactiilor necesita in continuare si plata de Ether pe Sepolia).
Functia care trimite cererea in contractul de mai jos este requestPriceData. Aceasta necesita compunerea parametrilor jobului conform descrierii de la adresa mentionata mai sus, si a plasarii taxei corespunzatoare. Functia de callback este fulfill.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import {Chainlink, ChainlinkClient} from "@chainlink/contracts/src/v0.8/ChainlinkClient.sol";
import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol";
/**
* Request testnet LINK and ETH here: https://faucets.chain.link/
* Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/
*/
/**
* THIS IS AN EXAMPLE CONTRACT WHICH USES HARDCODED VALUES FOR CLARITY.
* THIS EXAMPLE USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
contract APIConsumer is ChainlinkClient, ConfirmedOwner {
using Chainlink for Chainlink.Request;
// The "using for" directive above indicates that for the Chainlink.Request variables, the functions taking
// this type as first parameter in the imported Chainlink library can be called directly on the variable
// e.g., ChainlinkRequestVar.function(... where function is defined in Chainlink source code
uint256 public price;
bytes32 private jobId;
uint256 private fee;
event RequestVolume(bytes32 indexed requestId, uint256 volume);
/**
* @notice Initialize the link token and target oracle
*
* Sepolia Testnet details:
* Link Token: 0x779877A7B0D9E8603169DdbD7836e478b4624789
* Oracle: 0x6090149792dAAeE9D1D568c9f9a6F6B46AA29eFD (Chainlink DevRel)
* jobId: ca98366cc7314957b8c012c72f05aeeb
*
*/
constructor() ConfirmedOwner(msg.sender) {
// The constructor definition passes msg.sender to base contract constructor
_setChainlinkToken(0x779877A7B0D9E8603169DdbD7836e478b4624789);
_setChainlinkOracle(0x6090149792dAAeE9D1D568c9f9a6F6B46AA29eFD);
jobId = "ca98366cc7314957b8c012c72f05aeeb";
fee = (1 * LINK_DIVISIBILITY) / 10; // 0,1 * 10**18 (Varies by network and job)
}
/**
* Create a Chainlink request to retrieve API response, find the target
* data, then multiply by 1000000000000000000 (to remove decimal places from data).
*/
function requestPriceData() public returns (bytes32 requestId) {
Chainlink.Request memory req = _buildChainlinkRequest(
jobId,
address(this),
this.fulfill.selector
// The "selector" qualifier identifies the corresponding function signature in this contract
);
// Set the URL to perform the GET request on
req._add(
"get",
"https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD"
);
// Set the path to find the desired data in the API response, where the response format is:
// {"RAW":
// {"ETH":
// {"USD":
// {
// "PRICE": xxx.xxx,
// }
// }
// }
// }
// request.add("path", "RAW.ETH.USD.PRICE"); // Chainlink nodes prior to 1.0.0 support this format
req._add("path", "RAW,ETH,USD,PRICE"); // Chainlink nodes 1.0.0 and later support this format
// Multiply the result by 1000000000000000000 to remove decimals
int256 timesAmount = 10 ** 18;
req._addInt("times", timesAmount);
// Sends the request
return _sendChainlinkRequest(req, fee);
}
/**
* Receive the response in the form of uint256
*/
function fulfill(
bytes32 _requestId,
uint256 _price
) public recordChainlinkFulfillment(_requestId) {
// The "recordChainlinkFulfillment" is a modifier checking if the caller of this
// this callback function corresponds to the address used for the submitted request Id
emit RequestVolume(_requestId, _price);
price = _price;
}
/**
* Allow withdraw of Link tokens from the contract
*/
function withdrawLink() public onlyOwner {
LinkTokenInterface link = LinkTokenInterface(_chainlinkTokenAddress());
require(
link.transfer(msg.sender, link.balanceOf(address(this))),
"Unable to transfer"
);
}
}
Proxy delegate
Principala utilitate a acestui pattern este necesitatea de a modifica anumite contracte. Fiindca un contract care este "deployed" este in esenta nemodificabil, si pentru a nu crea probleme privind posibilele dependente, modalitatea uzuala de a permite un asa zis "update" la un contract (efectiv in practica un nou deployment) este de a referi contractul existent pentru acces prin intermediul unui "proxy". Patternul poate implica in esenta trei nivele de contract:
- contractul care apeleaza o functie dintr-un alt contract: caller contract
- contractul intermediar care asigura transmiterea apelului spre ultima versiune a contractului apelat: proxy contract
- contractul la versiunea din care se face efectiv apelul: delegate contract
In esenta contractul proxy nu se modifica, dar se pot modifica valorile din variabilele sale de stare ce pot include adrese de contract delegat in vederea efectuarii unui update a versiunii acestuia.
Apelurile catre contractul delegat se fac prin intermediul proxy-ului prin delegare catre versiunea de contract activa, ce include codul actualizat al functiilor.
Rezultatul este returnat catre proxy, care la randul sau il va returna catre apelant.
Noile versiuni de contract catre care se face delegarea apelurilor, pot avea deci modificari de functii, dar nu ar trebui sa aiba modificari structurale si in ce priveste variabilele de stare (ar trebui sa pastreze aceeasi configuratie a variabilelor existente in proxy). Motivul este ca apelurile catre versiunea de contract delegat se realizeaza din proxy prin intermediul metodei delegatecall. Aceasta va executa codul din contractul delegat dar in contextul contractului proxy, ceea ce implica operarea pe variabilele de stare ale contractului proxy, si nu pe ale contractului delegat. Eventuale modificari structurale ale variabilelor de stare ar putea cauza deci probleme.
Apelul functiilor prin delegare se vor face efectiv in functia fallback din cadrul contractului proxy. Aceasta va fi apelata implicit la orice apel al unei functii ce nu exista in contractul proxy, si ar trebui sa includa un apel delegatecall care sa preia signatura functiei apelate si sa o invoce din contractul delegat. Avand in vedere ca implementarea respectiva ar trebui sa fie independenta - pliabila pe orice functie - se prefera ca in functia fallback sa fie folosita o secventa "standard" de cod assembly care sa poata prelua orice apel de functie si sa poata returna orice retur de functie. Un exemplu in acest sens se poate regasi la aceasta adresa care propune un contract proxy abstract ce poate fi derivat pentru o implementare proprie a patternului.
Tools: Tenderly Debugger
O varianta disponibila posibil utila pentru debugging in ce priveste executia contractelor este oferita de platforma Tenderly (necesita inregistrare, dar ofera si un plan de utilizare gratuit). Pentru observarea executiei contractelor in Tenderly este necesara instantierea acestora pe o retea publica de test, si furnizarea catre Tenderly a sursei contractului (sau a ABI-ului corespondent obtinut in urma compilarii), precum si a versiunii de compilator folosita. Platforma Tenderly ofera diverse functionalitati ce pot fi utile intr-o sesiune de debugging: trace-ul pas cu pas pe executia tranzactiilor cu monitorizarea gas-ului consumat, inspectarea unei valori sau expresii din contextul de executie a unei functii, re-simularea unei tranzactii vechi, posibilitatea stabilirii indexului de bloc din blockchain la care se face simularea, etc.
La aceasta adresa se regaseste exemplul de contract pentru utilizarea oracolului discutat mai sus incarcat public in Tenderly, in urma instantierii pe reteaua Sepolia, fiind disponibil pentru simularea si debugging-ul tranzactiilor executate (poate necesita eventual includerea intr-un proiect personal de lucru creat in platforma).