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
transferlubwithdrawprzekazują ś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.