Laborator 4
Tematica laboratorului:
- Instantierea unui nou contract si accesul la acesta din cadrul unui contract existent
- Mostenirea contractelor
- Contracte abstracte si interfete
- TEMA 1
Instantierea unui nou contract si accesul la acesta din cadrul unui contract existent
Un tip de contract poate fi instantiat in cadrul unei functii a altui contract folosind cuvantul cheie "new". Noua instanta va fi automat plasata ("deployed") pe reteaua Ethereum in care a fost rulata functia ce a initiat instantierea, si va primi o adresa asociata noului contract.
Un contract deja existent poate fi accesat in codul Solidity (fara a crea un nou "deployment"), prin intermediul adresei sale, folosind un cast la tipul contractului respectiv.
In listingul de mai jos se pot observa cele doua situatii: instantierea unui nou contract si utilizarea sa, respectiv accesul la un contract existent si utilizarea sa.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <=0.8.21;
contract MyContract {
uint private someValue;
event fallbackCall(string);
event receivedFunds(address, uint);
function getValue() public view returns (uint) {
return someValue;
}
function setValue(uint _value) payable public {
someValue = _value;
}
function someFunction(uint someParam) view public returns (uint) {
return someValue + someParam;
}
//Functie receive
receive () payable external {
emit receivedFunds(msg.sender, msg.value);
}
//Este apelata implicit in situatia in care
//contractul primeste un apel fara date precum un transfer monetar.
//Costul executiei este limitat implicit la 2300 gas.
//Nu se recomanda includerea de operatii complexe.
//Detalii: https://docs.soliditylang.org/en/v0.8.21/contracts.html#receive-ether-function
//Functie fallback
fallback () external {
emit fallbackCall("Falback Called!");
}
//Este apelata implicit in situatiile:
//a) nu exista in contract o functie ce se potriveste cu apelul invocat
//b) se transfera fonduri catre contract fara un apel de functie si nu exista o functie receive
//(pentru situatia b functia trebuie sa fie marcata ca payable si are limita de 2300 gas)
//Nu se recomanda includerea de operatii complexe.
//Detalii: https://docs.soliditylang.org/en/v0.8.21/contracts.html#fallback-function
function getBalance() view public returns (uint) {
return address(this).balance;
}
}
contract ClientContract {
address payable public myContractAddress;
//Functie ce instantiaza un nou contract.
function useNewKeyword() public returns (uint) {
MyContract myObj = new MyContract();
myObj.setValue(10);
//In cazul in care contractul nu contine o functie receive sau fallback payable nu va fi posibila conversia la o adresa payable.
myContractAddress = payable(myObj);
return myObj.getValue();
}
//Dupa instantiere se poate consulta adresa contractului ce este retinuta in variabila contractAddress.
//Functie ce acceseaza contractul de la adresa retinuta mai sus.
function useExistingAddress() public returns (uint, address) {
MyContract myObj = MyContract(myContractAddress);
myObj.setValue(7);
return (myObj.getValue(), address(myObj));
}
//In returul functiei se observa ca instanta utilizata nu creeaza un nou contract, adresa fiind aceeasi.
//Functie de transfer balanta - alimenteaza balanta contractului curent si transfera 1 wei catre celalalt contract.
function forwardMoney() payable public {
if (address(this).balance > 1) {
//Transferul va cauza invocarea functiei receive
myContractAddress.transfer(1);
}
}
//Trimite fonduri catre contractul curent din a carui balanta 1 Wei va fi trimis mai departe.
//Functie care exemplifica transferul de balanta intre contracte prin apel de functie payable.
//(in forma curenta va avea succes doar dupa ce balanta contractului e alimentata suficient prin functia anterioara)
function examplePayByFunction() public {
MyContract myObj = MyContract(myContractAddress);
if (address(this).balance > 10) {
//Functia payable setValue din MyContract se poate apela precizand optional intre acolade si o valoare monetara
//(value - 10 wei in exemplu) ce va fi transferata din balanta contractului curent in cea a instantei MyContract
myObj.setValue{value:10}(7);
}
}
receive() payable external {}
function getBalance() view public returns (uint) {
return address(this).balance;
}
}
Mostenirea contractelor
Solidity permite mostenirea intre contracte, inclusiv mostenire multipla. Mostenirea se realizeaza prin folosirea cuvantului cheie "is" la definirea tipului contractului urmat de o lista a tipurilor de baza. Tipul efectiv al unei noi instante de contract va fi cel precizat la instantiere, chiar daca la declarare e utilizat tipul de baza, similar cu majoritatea limbajelor orientate obiect.
Functiile din cadrul unui contract pot fi supradefinite in contractele care il mostenesc doar daca sunt marcate cu "virtual". In aceasta situatie supradefinirea functiei trebuie sa fie marcata cu "override".
Similar cu functiile virtuale in C++ si a implementarii in alte limbaje orientate obiect, in cazul existentei mai multor functii cu aceeasi signatura in contractele derivate, functia executata la apel va fi "cea mai derivata/specifica", in functie de ierarhia de mostenire.
Posibilitatea de mostenire multipla in Solidity (mai multe contracte de baza), creeaza contextul posibil pentru o situatie generica in OOP cunoscuta drept "dreadful diamond derivation". Mai precis aceasta implica existenta unei functii in contractul de baza, re-implementate sub aceeasi signatura in mai multe contracte derivate, ce sunt mostenite la randul lor de un contract pe un al doilea nivel ierarhic. S-ar pune deci problema care dintre implementarile cele mai specifice mostenite ale functiei corespunde acestui ultim contract?
Ca solutie la ambiguitati de acest gen, in ultimele versiuni ale Solidity a fost introdusa obligativitatea supradefinirii explicite a unei functii intr-un contract, in cazul in care contractul mosteneste mai multe alte contracte ce includ o functie cu signatura identica. In aceasta situatie, marcarea "override" va fi urmata in paranteze de lista tuturor contractelor pentru care se realizeaza supradefinirea. Daca nu se doreste o implementare specifica a functiei in acest ultim contract mostenitor, se poate apela o functie dintr-un contract de nivel superior specificand explicit numele acestuia sau calificativul super (va fi ales ultimul contract din lista celor mostenite).
In listingul de mai jos sunt evidentiate mostenirea multipla si comportamentul diferit ce se poate observa la apelul functiilor in functie de tipul contractului.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <=0.8.21;
contract SumContract {
function Sum(uint a, uint b) pure public virtual returns (uint) {
return a + b;
}
function Multiply(uint a, uint b) pure public virtual returns (uint) {
return (a*b)/(a*b);
}
}
contract MultiContract is SumContract {
function Multiply(uint a, uint b) pure public override virtual returns (uint) {
return a * b;
}
function Square(uint a) pure public returns (uint) {
return a*a;
}
}
contract DivideContract is SumContract {
function Multiply(uint a, uint b) pure public override virtual returns (uint) {
return a / b;
}
}
contract SubContract is MultiContract, DivideContract {
function Sum(uint a, uint b) pure public override returns (uint) {
return a - b;
}
//Este obligatorie definirea functiei Multiply pentru ca ambele contracte parinte o implementeaza.
function Multiply(uint a, uint b) pure public override(DivideContract, MultiContract) returns (uint) {
//Echivalent cu DivideContract.Multiply(a,b) pe baza listei de mosteniri;
return super.Multiply(a, b);
}
function Multi(uint a, uint b) pure public returns (uint) {
return MultiContract.Multiply(a, b);
}
}
// SumContract
// / \
// MultiContract DivideContract
// \ /
// SubContract
//DivideContract este mai specific decat MultiContract ca tip de baza datorita ordinii in lista de mosteniri.
contract ClientContract {
function inheritanceEffect() public returns (uint, uint, uint, uint, uint, uint, uint) {
uint a = 30;
uint b = 10;
SubContract subtype = new SubContract();
//Tipul efectiv al sumtype va fi tot SubContract acesta fiind folosit la instantiere
SumContract sumtype = new SubContract();
return (subtype.Sum(a,b),
//Se poate observa ca apelul Multiply se realizeaza din DivideContract
subtype.Multiply(a,b),
//In ciuda castului, functia este aleasa tot din tipul mai specific DivideContract
MultiContract(subtype).Multiply(a,b),
//In apelul Multi din definitia contractului se apeleaza explicit functia din MultiContract
subtype.Multi(a,b),
//O functie ce nu este redefinita in DivideContract va fi apelata din MultiContract
subtype.Square(a),
//Se observa ca functiile sunt apelate conform cu tipul efectiv folosit la instantiere
sumtype.Sum(a,b),
sumtype.Multiply(a,b));
}
}
Contracte abstracte si interfete
Contractele abstracte sunt similare claselor abstracte din programarea orientata obiect. Acestea sunt contracte care au cel putin o functie neimplementata (functia are doar un prototip, fara corp, finalizat prin caracterul ";"). Respectivele contracte nu se pot instantia, si pot fi utilizate doar ca si contracte de baza pentru alte contracte.
Interfetele sunt similare contractelor abstracte, cu diferenta majora ca acestea contin strict doar functii neimplementate (fara constructor sau variabile de stare), si nu pot mosteni alte contracte sau interfete. Interfetele se definesc folosind cuvantul cheie "interface" in loc de "contract". Toate functiile definite in interfete sunt considerate implicit virtuale.
Mai multe detalii despre mostenire, contracte abstracte si interfete pot fi consultate in sectiunile respective din documentatia Solidity.
Exercitiu
Scrieti un contract derivat din contractul ClientContract (primul exemplu) care sa supradefineasca functia forwardMoney(), astfel incat fondurile respective sa fie transferate catre o alta adresa setata ca parametru in constructorul contractului derivat. Adaugati o functie prin care sa verificati daca balanta contractului derivat a scazut doar cu 1 Wei dupa apelul functiei supradefinite.
TEMA 1
Contextul implementarii este cel de trasabilitate al unor produse intr-un scenariu de tip supply-chain. Rolul contractelor propuse este de a oferi o garantie de incredere in ce priveste informatiile din tranzactii ce nu pot fi modificate.
Se cere implementarea a trei contracte, ProductIdentification, ProductDeposit si ProductStore. Fiecare dintre cele trei tipuri de contracte va avea un proprietar, initializat la crearea contractului. Contractele vor include functionalitatile urmatoare.
Contractul ProductIdentification va permite:
- Setarea de catre proprietar a unei taxe publice de inregistrare producator.
- Inregistrarea unui producator, ce va memora adresa acestuia in starea contractului, contra taxei respective.
- Inregistrarea unui produs ce va putea fi facuta doar de catre unul dintre producatorii inregistrati. Un producator poate inregistra mai multe produse, ce vor fi retinute pe baza unui id unic per produs si vor include ca informatie adresa producatorului, denumirea produsului si o valoare volumetrica a produsului (pentru simplificare se vor folosi doar unitati intregi de volum).
- Posibilitatea verificarii pe baza adresei unui producator daca acesta este inregistrat.
- Posibilitatea verificarii pe baza id-ului unui produs daca acesta este inregistrat, si aflarea informatiilor despre acesta.
Contractul ProductDeposit va asigura:
- Setarea de catre proprietar a unei taxe publice unica de depozitare pe unitate de volum, si a unui volum maxim al depozitului.
- Inregistrarea depozitarii unui produs ce va putea fi realizata de un producator autorizat pentru produsul respectiv. Depozitarea poate fi inregistrata cu o anumita cantitate (mai multe unitati din produsul respectiv) ce necesita plata cu valoarea corespunzatoare volumului total al cantitatii respective.
- Inregistrarea de catre un producator a unui magazin autorizat (adresa unui contract ProductStore) pentru vanzarea produselor proprii.
- Inregistrarea retragerii din depozit a unei cantitati dintr-un produs de catre producatorul ce a facut o depunere sau de catre un magazin autorizat.
- Mentinerea volumului disponibil actualizat in urma depozitarilor si retragerilor.
Contractul ProductStore va permite:
- Setarea de catre proprietar a adresei depozitului si a adresei contractului de identificare a produselor (contractele ProductDeposit si ProductIdentification).
- Adaugarea de catre proprietar a unei cantitati dintr-un produs din depozit in propriul magazin, conditionata de autorizarea depozitului pentru retragere. Contractul curent va retine informatiile minim necesare pentru un produs.
- Setarea de catre proprietar a unui pret pe unitatea de produs.
- Posibilitatea de a verifica de catre un client disponibilitatea unui produs in magazin si a autenticitatii acestuia pe baza id-ului.
- Inregistrarea unei tranzactii de achizitionare a unui produs. Efectul acesteia va fi scaderea numarului de unitati de produs disponibile si transferul a jumatate din pretul platit catre producatorul produsului.
Pentru toate tranzactiile ce implica plata, acestea vor esua daca plata nu este suficienta si vor returna apelantului restul in cazul in care plata este mai mare decat cea necesara. Se va lua in calcul la notare eficienta implementarii din punct de vedere a costurilor tranzactiilor: evitarea unde este posibil a redundantelor in stocare si a iteratiilor.
Detalii organizare si termene
Tema poate fi realizata in echipe de pana la 3 studenti. Predarea temei se va face prin e-mail - arhiva zip cu sursele Solidity sau link la un repository cu drepturi de acces, subiect: [Blockchain] Tema 1 ; seriile MSD, MISS la adresa emanuel.onica@uaic.ro ; seriile MSI si MIAO la adresa andrei.arusoaie@uaic.ro - nu se accepta echipe mixte intre serii care nu urmeaza laboratorul cu acelasi profesor. Data limita a predarii este 9 noiembrie inclusiv, fiind urmata de finalizarea evaluarii in cadrul laboratoarelor urmatoare (15-16 noiembrie), aceasta urmand a se desfasura online. O programare a echipelor pe sloturi orare si alte detalii vor fi anuntate in data de 13 noiembrie.
Hints - Trimitere de fonduri
// Modalitatea preferata de transfer inainte de EIP-1884 (discutabil dupa)
// Esecul arunca exceptie urmata de revert
function withTransfer(address payable destination) public payable {
destination.transfer(msg.value);
}
// Modalitatea propusa initial in Solidity pentru plati - de regula nerecomandata
// Esecul returneaza false
function withSend(address payable destination) public payable {
bool sent = destination.send(msg.value);
require(sent, "Failure!");
}
// Modalitatea fara limita de gas (uneori recomandata dupa EIP-1884)
// Poate crea anumite probleme de securitate
function withCall(address payable destination) public payable {
(bool sent, bytes memory data) = destination.call{value: msg.value}("");
require(sent, "Failure!");
}