Zabezpieczenie wdrożenia smart kontraktów przed błędami związanymi z limitami gazu

Wdrożenie smart kontraktów na blockchainie wiąże się z wyzwaniem optymalizacji użycia gazu – każda operacja w kontrakcie wymaga określonej ilości gazu, a błędy związane z przekroczeniem limitu mogą prowadzić do nieudanych transakcji i niepotrzebnych kosztów. Dobrze zaprojektowany kontrakt powinien być nie tylko funkcjonalny, ale również ekonomiczny pod względem wykorzystania gazu. Poniżej przedstawiamy najważniejsze kroki, które mogą pomóc zabezpieczyć kontrakty przed błędami związanymi z limitami gazu oraz zoptymalizować ich działanie.

1. Optymalizacja kodu – minimalizacja operacji wewnątrz funkcji

Złożone operacje i długie pętle to jedne z najczęstszych powodów wysokiego zużycia gazu. Dobrze napisany kod minimalizuje ilość operacji wewnątrz funkcji, np. poprzez przechowywanie wyników w zmiennych lub unikanie zbędnych operacji.

Przykład:

Zły kod – obliczanie sumy w pętli za każdym razem:

				
					uint256[] public values;

function getSum() public view returns (uint256) {
    uint256 total = 0;
    for (uint256 i = 0; i < values.length; i++) {
        total += values[i];
    }
    return total;
}

				
			

Dobry kod – przechowywanie sumy jako osobnej zmiennej:

				
					uint256[] public values;
uint256 public sum;

function addValue(uint256 _value) public {
    values.push(_value);
    sum += _value; // Aktualizacja sumy tylko przy dodaniu nowej wartości
}

function getSum() public view returns (uint256) {
    return sum;
}

				
			

W powyższym przykładzie obliczenie sumy odbywa się tylko raz, przy dodawaniu wartości, co znacząco obniża zużycie gazu.

2. Modularizacja funkcji – unikanie przetwarzania dużych zbiorów danych naraz

Duże funkcje, które przetwarzają całą listę danych, mogą przekraczać limit gazu. Rozbijanie operacji na mniejsze partie pozwala kontrolować zużycie gazu i uniknąć problemów z limitem.

Przykład:

Zły kod – przetwarzanie całej listy jednocześnie:

				
					function processPayments() public {
    for (uint256 i = 0; i < payments.length; i++) {
        payable(payments[i].recipient).transfer(payments[i].amount);
    }
}

				
			

Dobry kod – przetwarzanie płatności w partiach:

				
					function processBatchPayments(uint256 start, uint256 end) public {
    for (uint256 i = start; i < end && i < payments.length; i++) {
        payable(payments[i].recipient).transfer(payments[i].amount);
    }
}

				
			

Dzięki takiemu podejściu użytkownicy mogą wywoływać funkcję etapami, co zmniejsza ryzyko przekroczenia limitu gazu.

3. Używanie struktur danych oszczędzających gaz

Tablice są kosztowne w użyciu, zwłaszcza jeśli przechowują dużą ilość danych i wymagają iteracji. Mapowania (mapping) mogą być bardziej efektywne pod względem kosztów gazu, ponieważ pozwalają na bezpośredni dostęp do danych.

Przykład:

Zły kod – przechowywanie danych w tablicy:

				
					address[] public users;
uint256[] public balances;

function getBalance(address user) public view returns (uint256) {
    for (uint256 i = 0; i < users.length; i++) {
        if (users[i] == user) {
            return balances[i];
        }
    }
    return 0;
}

				
			

Dobry kod – użycie mapowania:

				
					mapping(address => uint256) public balances;

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

function getBalance(address user) public view returns (uint256) {
    return balances[user];
}

				
			

Dzięki mapowaniu możemy natychmiast uzyskać dostęp do salda użytkownika, co eliminuje potrzebę kosztownej pętli.

4. Unikanie złożonych obliczeń wewnątrz kontraktu

Skomplikowane operacje matematyczne mogą znacząco zwiększyć zużycie gazu. Wartość często wykorzystywanych wyników można zapisać jako stałe lub uprościć obliczenia.

Przykład:

Zły kod – wielokrotne wykonywanie złożonych obliczeń:

				
					function calculateInterest(uint256 principal, uint256 rate, uint256 time) public pure returns (uint256) {
    return principal * ((100 + rate) ** time) / (100 ** time);
}

				
			

Dobry kod – ustalanie stałych i uproszczenie obliczeń:

				
					uint256 constant INTEREST_RATE = 5; // Przykładowa stała stopa procentowa

function calculateInterest(uint256 principal, uint256 time) public pure returns (uint256) {
    return principal * ((100 + INTEREST_RATE) ** time) / (100 ** time);
}

				
			

Ustalona wartość stopy procentowej jako stała (const) zmniejsza zużycie gazu, ponieważ nie trzeba jej za każdym razem obliczać.

5. Zastosowanie optymalizacji pamięci

Pamięć (memory) w Ethereum jest tańsza niż przechowywanie danych w storage. Dlatego warto używać memory do przechowywania danych tymczasowych wewnątrz funkcji.

Przykład:

Zły kod – użycie storage do przechowywania zmiennych tymczasowych:

				
					uint256[] public numbers;

function calculateTotal() public view returns (uint256 total) {
    for (uint256 i = 0; i < numbers.length; i++) {
        total += numbers[i];
    }
}

				
			

Dobry kod – użycie memory dla zmiennej tymczasowej:

				
					function calculateTotal(uint256[] memory numbers) public pure returns (uint256 total) {
    for (uint256 i = 0; i < numbers.length; i++) {
        total += numbers[i];
    }
}

				
			

Przechowywanie danych w memory w tej sytuacji znacząco zmniejsza zużycie gazu, szczególnie przy długich listach danych.

6. Zabezpieczenie pętli limitami iteracji

Pętle oparte na danych wejściowych użytkownika mogą łatwo przekroczyć limit gazu. Dodanie limitów liczby iteracji w pętlach zapobiega tym problemom.

Przykład:

Zły kod – brak limitów na liczbę iteracji:

				
					function calculateBonus() public view returns (uint256 bonus) {
    for (uint256 i = 0; i < transactions.length; i++) {
        bonus += transactions[i].amount;
    }
}

				
			

Dobry kod – dodanie limitu na liczbę iteracji:

				
					function calculateBonus(uint256 maxTransactions) public view returns (uint256 bonus) {
    uint256 transactionsToProcess = maxTransactions < transactions.length ? maxTransactions : transactions.length;
    for (uint256 i = 0; i < transactionsToProcess; i++) {
        bonus += transactions[i].amount;
    }
}

				
			

Dzięki limitowi użytkownik sam może ustawić maksymalną liczbę iteracji, co daje mu kontrolę nad kosztem gazu.

7. Stały monitoring zużycia gazu i testowanie

Testowanie smart kontraktów pod kątem zużycia gazu w różnych scenariuszach pozwala na wczesne wykrycie miejsc wymagających optymalizacji. Narzędzia takie jak Truffle i Hardhat pozwalają symulować koszt gazu przed wdrożeniem kontraktu.

Przykład:

Zły kod – wdrażanie kontraktu bez sprawdzenia kosztów gazu:

				
					# Wdrożenie bez symulacji gazu
truffle migrate --network mainnet

				
			

Dobry kod – testowanie zużycia gazu w środowisku testowym:

				
					# Testowanie kosztów gazu przed wdrożeniem
truffle test --network testnet --gas

				
			

Podsumowanie

Optymalizacja smart kontraktów pod kątem limitów gazu to kluczowy element w zapewnianiu efektywności i ekonomiczności aplikacji blockchainowych. Unikanie złożonych operacji, modularne podejście, właściwy dobór struktur danych, optymalizacja pamięci i stałe testowanie to kroki, które pozwalają stworzyć bardziej ekonomiczny kod i zmniejszyć ryzyko błędów związanych z limitami gazu.