Funkcje wymagające szczególnego podejścia do testów smart contract w Solidity

Istnieje kilka rodzajów funkcji w smart contractach w Solidity, które wymagają szczególnego podejścia do testowania. Te funkcje często mają kluczowe znaczenie dla bezpieczeństwa i poprawności działania kontraktu, a ich testowanie wymaga precyzyjnego podejścia. Oto przegląd najważniejszych z nich:

1. Funkcje Modyfikujące Stan (np. payable, withdraw, transfer)

Funkcje, które modyfikują stan kontraktu (np. przechowują dane, zmieniają saldo, przesyłają tokeny), wymagają dokładnego testowania, ponieważ często są związane z zarządzaniem środkami i ich potencjalnym ryzykiem utraty lub błędów.

  • Testy związane z gazem: Sprawdź, czy funkcja nie zużywa zbyt dużo gazu. Przykładowo, jeśli funkcja operuje na dużych strukturach danych, może doprowadzić do wyczerpania limitu gazu.
  • Sprawdzanie poprawności transferów: Testuj scenariusze, w których funkcje transfer lub withdraw przekazują środki na poprawny adres oraz czy kwoty są zgodne z oczekiwaniami.
  • Warunki graniczne: Testuj, czy funkcja poprawnie działa przy różnych kwotach, w tym przy minimalnych i maksymalnych wartościach.

Przykład testu dla funkcji withdraw:

				
					const MyContract = artifacts.require("MyContract");

contract("MyContract", accounts => {
    it("should allow owner to withdraw funds", async () => {
        const instance = await MyContract.deployed();
        const initialBalance = await web3.eth.getBalance(accounts[0]);
        
        await instance.deposit({ from: accounts[1], value: web3.utils.toWei("1", "ether") });
        await instance.withdraw(web3.utils.toWei("1", "ether"), { from: accounts[0] });

        const finalBalance = await web3.eth.getBalance(accounts[0]);
        assert.isAbove(Number(finalBalance), Number(initialBalance), "Funds were not withdrawn correctly.");
    });
});

				
			

2. Funkcje Payable

Funkcje payable umożliwiają kontraktowi przyjmowanie Etheru, co wymaga dodatkowego testowania, ponieważ:

  • Testy przyjmowania środków: Sprawdź, czy funkcja poprawnie akceptuje Ether i aktualizuje stan kontraktu (np. saldo).
  • Testy nieprawidłowych wartości: Sprawdź, co się dzieje, gdy użytkownik wysyła za mało lub za dużo Etheru.
  • Warunki akceptacji płatności: Upewnij się, że funkcja odrzuca Ether od niewłaściwych kont, jeśli jest to wymagane.

Przykład testu dla funkcji payable:

				
					const MyContract = artifacts.require("MyContract");

contract("MyContract", accounts => {
    it("should accept payment and increase contract balance", async () => {
        const instance = await MyContract.deployed();
        
        const initialBalance = await web3.eth.getBalance(instance.address);
        await instance.sendTransaction({ from: accounts[1], value: web3.utils.toWei("1", "ether") });
        
        const finalBalance = await web3.eth.getBalance(instance.address);
        assert.equal(finalBalance, Number(initialBalance) + Number(web3.utils.toWei("1", "ether")), "Contract did not accept payment.");
    });
});

				
			

3. Funkcje Zarządzania Dostępem (np. onlyOwner, require na konkretne role)

Funkcje te często zawierają modyfikatory ograniczające dostęp tylko do określonych kont (np. właściciela kontraktu lub kont z konkretnymi uprawnieniami). Kluczowe jest, aby testować, czy ograniczenia działają poprawnie.

  • Testy dostępu: Upewnij się, że funkcje są wywoływane tylko przez uprawnione konta.
  • Testy błędów: Sprawdź, czy próby dostępu przez nieuprawnione konta powodują błąd.

Przykład testu dla funkcji z onlyOwner:

				
					const MyContract = artifacts.require("MyContract");

contract("MyContract", accounts => {
    it("should allow only owner to execute function", async () => {
        const instance = await MyContract.deployed();
        
        try {
            await instance.ownerOnlyFunction({ from: accounts[1] });
            assert.fail("Function should not be accessible by non-owner");
        } catch (error) {
            assert.include(error.message, "revert", "Expected revert for unauthorized access");
        }
        
        // Sprawdzenie działania dla właściciela
        const result = await instance.ownerOnlyFunction({ from: accounts[0] });
        assert.isTrue(result, "Function did not execute for owner.");
    });
});

				
			

4. Funkcje z Operacjami na Liczbach Całkowitych (Przepełnienie i Niedomiar)

W Solidity, w szczególności w starszych wersjach, przepełnienie liczb całkowitych może prowadzić do błędów. Funkcje wykonujące operacje matematyczne wymagają użycia SafeMath i dokładnego testowania granicznych wartości.

  • Testy przepełnienia: Sprawdź, czy liczby przekraczające maksymalne wartości typu powodują błąd.
  • Testy małych wartości i ujemnych wyników: Upewnij się, że liczby całkowite nie mogą spaść poniżej zera w nieoczekiwany sposób.

Przykład testu z operacjami arytmetycznymi:

				
					const SafeMathContract = artifacts.require("SafeMathContract");

contract("SafeMathContract", accounts => {
    it("should prevent overflow when adding large numbers", async () => {
        const instance = await SafeMathContract.deployed();
        
        try {
            await instance.addLargeNumbers(2**256 - 1, 1); // Zbyt duża wartość
            assert.fail("Overflow did not cause error");
        } catch (error) {
            assert.include(error.message, "revert", "Expected revert for overflow");
        }
    });
});

				
			

5. Funkcje Wywołujące Zewnętrzne Kontrakty

Funkcje, które wywołują inne kontrakty, są podatne na ataki typu re-entrancy oraz na problemy związane z zaufaniem do zewnętrznych kontraktów.

  • Testy bezpieczeństwa re-entrancy: Przetestuj zabezpieczenia przed atakiem re-entrancy.
  • Testy awaryjne: Sprawdź, jak funkcja reaguje na błędy zewnętrznego kontraktu (np. co się stanie, gdy zewnętrzny kontrakt nie zwróci oczekiwanej wartości).

Przykład testu na wypadek ataku re-entrancy:

				
					const VulnerableContract = artifacts.require("VulnerableContract");
const AttackingContract = artifacts.require("AttackingContract");

contract("VulnerableContract", accounts => {
    it("should prevent re-entrancy attack", async () => {
        const vulnerableInstance = await VulnerableContract.deployed();
        const attacker = await AttackingContract.new(vulnerableInstance.address);
        
        await vulnerableInstance.deposit({ from: accounts[1], value: web3.utils.toWei("1", "ether") });
        
        try {
            await attacker.attack({ from: accounts[1], value: web3.utils.toWei("0.5", "ether") });
            assert.fail("Re-entrancy attack was successful");
        } catch (error) {
            assert.include(error.message, "revert", "Expected revert for re-entrancy protection");
        }
    });
});

				
			

6. Funkcje losowe (np. oparte na block.timestamp lub block.number)

Losowość w Solidity jest trudna do osiągnięcia i często wymaga użycia zmiennych globalnych takich jak block.timestamp lub block.number, które są przewidywalne i łatwe do zmanipulowania.

  • Testy przewidywalności: Upewnij się, że funkcje, które powinny być losowe, nie mogą być przewidziane przez złośliwego użytkownika.
  • Testy manipulacji: Sprawdź, czy zmienne globalne są używane w sposób bezpieczny.

Przykład testu dla funkcji pseudo-losowej:

				
					const LotteryContract = artifacts.require("LotteryContract");

contract("LotteryContract", accounts => {
    it("should not allow predictable outcomes", async () => {
        const instance = await LotteryContract.deployed();
        
        const outcome1 = await instance.generateRandomNumber();
        const outcome2 = await instance.generateRandomNumber();
        
        assert.notEqual(outcome1, outcome2, "Outcomes should not be predictable");
    });
});

				
			

Każda z powyższych funkcji w Solidity wymaga szczególnego podejścia do testów, ponieważ jest narażona na różne rodzaje zagrożeń i problemów. Dobrze zaprojektowane testy jednostkowe i integracyjne mogą pomóc w wykryciu i zapobieganiu potencjalnym błędom i zwiększyć bezpieczeństwo oraz niezawodność kontraktów.