Laborator 4
Tematica laboratorului:
- Instantierea unui nou contract si accesul la acesta din cadrul unui contract existent
- Mostenirea contractelor
- Contracte abstracte si interfete
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.28;
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.28/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.28/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.28;
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.
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!");
}