Skip to content

Solidity — Nastavak

Interfejsi

Interfejs (interface) u Solidity-ju definiše skup funkcija bez implementacije — samo njihove potpise. Koriste se kada želimo da komuniciramo sa nekim spoljašnjim ugovorom, a ne znamo (ili nam nije potrebna) njegova unutrašnja logika.

Pravila interfejsa

  • Ne mogu imati konstruktore
  • Ne mogu imati state varijable
  • Sve funkcije moraju biti external
  • Ne mogu naslediti druge ugovore (mogu naslediti druge interfejse)
  • Ne mogu imati implementaciju (telo funkcije)

Primer

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Definišemo interfejs
interface IKalkulator {
    function saberi(uint a, uint b) external pure returns (uint);
    function oduzmi(uint a, uint b) external pure returns (uint);
}

// Implementacija interfejsa
contract Kalkulator is IKalkulator {
    function saberi(uint a, uint b) external pure returns (uint) {
        return a + b;
    }

    function oduzmi(uint a, uint b) external pure returns (uint) {
        return a - b;
    }
}

// Korišćenje interfejsa za interakciju
contract Korisnik {
    IKalkulator public kalkulator;

    constructor(address _adresa) {
        kalkulator = IKalkulator(_adresa);
    }

    function izracunaj() external view returns (uint) {
        return kalkulator.saberi(10, 5);
    }
}

Interfejsi su osnova interakcije između ugovora — standardizuju API i omogućavaju da se ugovori međusobno "razumeju" bez deljenja izvornog koda.


Interakcija sa drugim ugovorima na mreži

Na Ethereum mreži, svaki ugovor ima svoju adresu. Postoje tri načina da jedan ugovor pozove funkciju drugog:

Direktan poziv (najčešći način)

Ako znamo interfejs (ili ABI) ciljnog ugovora, koristimo kastovanje adrese na tip interfejsa:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface ICiljniUgovor {
    function getValue() external view returns (uint);
}

contract Pozivalac {
    function citajVrednost(address _ciljnaAdresa) external view returns (uint) {
        // Kastujemo adresu na interfejs i direktno pozivamo funkciju
        return ICiljniUgovor(_ciljnaAdresa).getValue();
    }
}

.call() — informativno

Kada ne znamo ABI ciljnog ugovora ili želimo veću kontrolu:

contract NiskoNivovskiPoziv {
    function posaljiPoziv(address _cilj, uint _iznos) external returns (bool, bytes memory) {
        // Enkodujemo potpis funkcije i argumente
        (bool uspeh, bytes memory podaci) = _cilj.call{value: _iznos}(
            abi.encodeWithSignature("deposit(uint256)", _iznos)
        );
        require(uspeh, "Poziv nije uspeo");
        return (uspeh, podaci);
    }
}

.delegatecall() — informativno

Izvršava kod ciljnog ugovora, ali u kontekstu pozivajućeg ugovora (koristi storage pozivajućeg ugovora).

contract ProxyUgovor {
    address public implementacija;

    function delegirajPoziv(bytes memory _podaci) external returns (bytes memory) {
        (bool uspeh, bytes memory rezultat) = implementacija.delegatecall(_podaci);
        require(uspeh, "Delegatecall neuspeo");
        return rezultat;
    }
}
Metoda Ko je msg.sender? Čiji storage se menja? Kada koristiti?
Direktan poziv Pozivajući ugovor Ciljnog ugovora Standardna interakcija
.call() Pozivajući ugovor Ciljnog ugovora Nepoznat ABI, slanje ETH
.delegatecall() Originalni pozivalac Pozivajućeg ugovora Proxy pattern, upgrades

Nasleđivanje

Solidity podržava višestruko nasleđivanje. Koristi se ključna reč is.

Osnovno nasleđivanje

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Zivotinja {
    string public ime;

    constructor(string memory _ime) {
        ime = _ime;
    }

    function jedi() public pure virtual returns (string memory) {
        return "Zivotinja jede";
    }
}

contract Macka is Zivotinja {
    constructor(string memory _ime) Zivotinja(_ime) {}

    // override menja ponašanje roditeljske funkcije
    function jedi() public pure override returns (string memory) {
        return "Macka jede hranu";
    }

    function zvuk() public pure returns (string memory) {
        return "mjau";
    }
}

Višestruko nasleđivanje i super -informativno

contract A {
    function poruka() public pure virtual returns (string memory) {
        return "A";
    }
}

contract B is A {
    function poruka() public pure virtual override returns (string memory) {
        return string(abi.encodePacked(super.poruka(), "B"));//vraca AB 
    }
}
/* abi je namespace za par funkcija, podrazumevano su dostupni
jedna od njih je:
abi.encodePacked() //gore iskoriscen za konkatenaciju dva stringa
*/
contract C is A {
    function poruka() public pure virtual override returns (string memory) {
        return string(abi.encodePacked(super.poruka(), "C"));// vraca AC
    }
}

// Redosled nasleđivanja je bitan! (C3 linearizacija)
// Poziv super.poruka() u D ide: D -> C -> B -> A
contract D is B, C {
    function poruka() public pure override(B, C) returns (string memory) {
        return string(abi.encodePacked(super.poruka(), "D")); //vraca ABCD
    }
}

Abstract ugovori - informativno

abstract ugovor ima barem jednu funkciju bez implementacije. Ne može se direktno deployovati.

abstract contract OsnovniToken {
    mapping(address => uint) public balanse;

    // Nema implementacije — naslednici moraju da je implementiraju
    function _mint(address primalac, uint iznos) internal virtual;

    function balanceOf(address racun) public view returns (uint) {
        return balanse[racun];
    }
}

contract MojToken is OsnovniToken {
    function _mint(address primalac, uint iznos) internal override {
        balanse[primalac] += iznos;
    }
}

Funkcije: internal / external

Solidity ima četiri modifikatora vidljivosti funkcija:

Modifikator Dostupno spolja? Dostupno iz naslednika? Dostupno unutar ugovora?
public
external ❌ (samo sa this.)
internal
private

internal funkcije

Dostupne samo unutar ugovora i njegovih naslednika.

contract Racunar {
    // Samo ovaj ugovor i naslednici mogu pozvati ovu funkciju
    function _izracunajKamatu(uint iznos, uint stopa) internal pure returns (uint) {
        return (iznos * stopa) / 100;
    }

    function izracunajUkupno(uint iznos) public pure returns (uint) {
        uint kamata = _izracunajKamatu(iznos, 5); // interno pozivanje
        return iznos + kamata;
    }
}

contract NapredniRacunar is Racunar {
    function izracunajSaStopom(uint iznos, uint stopa) public pure returns (uint) {
        // Nasleđeni ugovor može koristiti internal funkciju
        return iznos + _izracunajKamatu(iznos, stopa);
    }
}

external funkcije

Mogu se pozvati samo spolja (od strane korisnika ili drugih ugovora). Efikasnije su od public za funkcije koje prihvataju velike calldata argumente jer Solidity ne kopira podatke u memoriju.

contract TokenUgovor {
    mapping(address => uint) private balanse;

    // external — efikasnija za pozive spolja, calldata ne kopira u memoriju
    function transfer(address primalac, uint iznos) external returns (bool) {
        require(balanse[msg.sender] >= iznos, "Nedovoljno sredstava");
        balanse[msg.sender] -= iznos;
        balanse[primalac] += iznos;
        return true;
    }

    // internal — pomoćna funkcija, poziva je samo sam ugovor
    function _proveriBalans(address racun) internal view returns (bool) {
        return balanse[racun] > 0;
    }
}

ERC20 Standard

ERC20 je najrasprostranjeniji standard za fungibilne (zamenjive) tokene na Ethereum mreži. Definisao je uniformni interfejs koji svaki token mora da implementuje kako bi bio kompatibilan sa novčanicima, berzama i dApps-ovima.

Interfejs ERC20

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC20 {
    // Vraća ukupnu količinu tokena u opticaju
    function totalSupply() public view returns (uint);

    // Vraća balans tokena za datu adresu
    function balanceOf(address account) public view returns (uint);

    // Prenosi `amount` tokena na adresu `to` — poziva vlasnik tokena
    function transfer(address to, uint amount) public returns (bool);

    // Daje dozvolu (`spender`) da troši do `amount` tokena u ime vlasnika
    function approve(address spender, uint amount) public returns (bool);

    // Vraća preostalu količinu tokena koju `spender` sme potrošiti u ime `owner`-a
    function allowance(address owner, address spender) public view returns (uint);

    // Prenosi `amount` tokena sa `from` na `to` — poziva ovlašćeni spender
    function transferFrom(address from, address to, uint amount) public returns (bool);

    // Emituje se pri svakom transferu tokena
    event Transfer(address indexed from, address indexed to, uint value);

    // Emituje se kada vlasnik odobri spending
    event Approval(address indexed owner, address indexed spender, uint value);
}

Minimalna ERC20 implementacija

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MojToken {
    string public name = "Moj Token";
    string public symbol = "MTK";
    uint8 public decimals = 18;
    uint public totalSupply;

    mapping(address => uint) public balanceOf;
    mapping(address => mapping(address => uint)) public allowance;

    event Transfer(address indexed from, address indexed to, uint value);
    event Approval(address indexed owner, address indexed spender, uint value);

    constructor(uint _pocetnaKolicina) {
        totalSupply = _pocetnaKolicina * 10 ** decimals;
        balanceOf[msg.sender] = totalSupply;
        emit Transfer(address(0), msg.sender, totalSupply);
    }

    function transfer(address to, uint amount) public returns (bool) {
        require(balanceOf[msg.sender] >= amount, "Nedovoljno tokena");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }

    function approve(address spender, uint amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint amount) public returns (bool) {
        require(balanceOf[from] >= amount, "Nedovoljno tokena");
        require(allowance[from][msg.sender] >= amount, "Nedovoljno dozvole");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}

Fallback i Receive funkcije

Ove dve specijalne funkcije definišu šta se dešava kada ugovor primi ETH ili dobije poziv za nepostojeću funkciju.

receive()

Poziva se kada ugovor primi čist ETH transfer (bez calldata). Mora biti external payable.

contract PrimaocETH {
    event PrimljenETH(address posiljalac, uint iznos);

    // Poziva se kada se ETH pošalje direktno na ugovor (bez podataka)
    receive() external payable {
        emit PrimljenETH(msg.sender, msg.value);
    }
}

fallback()

Poziva se u dva slučaja: 1. Kada se pozove funkcija koja ne postoji u ugovoru 2. Kada se ETH pošalje sa podacima (msg.data nije prazno)

contract SvePrihvata {
    event FallbackPokrenut(address posiljalac, uint vrednost, bytes podaci);

    // Poziva se za nepostojeće funkcije ili ETH + calldata
    fallback() external payable {
        emit FallbackPokrenut(msg.sender, msg.value, msg.data);
    }
}

Zajedno: receive + fallback

contract KombinovaniPrimaoc {
    uint public ukupnoPrimljeno;
    mapping(address => uint) public uplate;

    // Čist ETH transfer
    receive() external payable {
        ukupnoPrimljeno += msg.value;
        uplate[msg.sender] += msg.value;
    }

    // Pogrešan potpis funkcije ili ETH sa podacima
    fallback() external payable {
        // Možemo logirati ili odbiti neprepoznate pozive
        ukupnoPrimljeno += msg.value;
    }
}

Re-Entrancy napad

Re-entrancy je jedan od najopasnijih napada u Solidity-ju. Napadač eksploatiše redosled operacija u funkciji — posebno kada ugovor šalje ETH pre nego što ažurira stanje.

Kako funkcioniše napad

  1. Napadač poziva withdraw() na žrtvinom ugovoru
  2. Žrtvin ugovor šalje ETH napadaču
  3. Pre nego što žrtvin ugovor ažurira balans — napadačev receive() se poziva
  4. receive() PONOVO poziva withdraw() na žrtvinom ugovoru
  5. Žrtvin ugovor misli da napadač još uvek ima balans (nije ažuriran)
  6. Ciklus se ponavlja dok žrtvin ugovor nema više ETH

Napomena: receive je ogranicen za potrosnju gasa, pa da napad ne bi probio gaslimit bolje je koristiti fallback funkciju.

primer reentrancy napada

Zaštita 1: Checks-Effects-Interactions (CEI) pattern - informativno

Uvek ažuriraj stanje pre eksternih poziva.

contract ZasticenUgovor_CEI {
    mapping(address => uint) public balanse;

    function deposit() public payable {
        balanse[msg.sender] += msg.value;
    }

    function withdraw() public {
        uint iznos = balanse[msg.sender];
        require(iznos > 0, "Nema sredstava");

        balanse[msg.sender] = 0;

        (bool uspeh, ) = msg.sender.call{value: iznos}("");
        require(uspeh, "Transfer neuspeo");
    }
}

Zaštita 2: ReentrancyGuard (mutex) - informativno

Koristi lock koji sprečava ponovni ulazak u funkciju dok se prethodna nije završila:

contract ReentrancyGuard {
    bool private zakljucan;

    modifier noReentrant() {
        require(!zakljucan, "ponovni ulaz");
        zakljucan = true;
        _;
        zakljucan = false;
    }
}

contract ZasticenUgovor_Guard is ReentrancyGuard {
    mapping(address => uint) public balanse;

    function deposit() public payable {
        balanse[msg.sender] += msg.value;
    }

    // Modifier sprečava bilo kakav ponovni ulazak
    function withdraw() public noReentrant {
        uint iznos = balanse[msg.sender];
        require(iznos > 0, "Nema sredstava");
        balanse[msg.sender] = 0;
        (bool uspeh, ) = msg.sender.call{value: iznos}("");
        require(uspeh, "Transfer neuspeo");
    }
}

Istorija: Re-entrancy napad iskorišćen je u čuvenom The DAO hack-u (2016), kada je ukradeno ~3.6 miliona ETH, što je dovelo do Ethereum hard fork-a i nastanka Ethereum Classic.


Oracles

Blockchain mreže su izolovane — pametni ugovori ne mogu direktno pristupiti podacima van lanca (cene, vremenska prognoza, ishodi sporta, itd.). Oracle je servis koji prenosi spoljne podatke na blockchain na kriptografski siguran način.

Problem: The Oracle Problem

Realni svet                    Blockchain
(off-chain)                    (on-chain)
-----------                    ----------
ETH/USD cena   ─── Oracle ───► Pametni ugovor
Vreme                         (koristi podatke)
Rezultati