Błędy w kontraktach

6 lat temu
Jakie mogą być skutki błędów w kontraktach? Jednym z nich jest utrata środków, o czym niektórzy użytkownicy portfela *Parity* mieli okazję boleśnie przekonać się w lipcu tego roku. *Blockchain* nie zapomina, to co w nim zaszło nie może być odwołane. Jest to jego niewątpliwa zaleta, która w pewnych sytuacjach staje się wadą. Kontrakt rządzi się regułami takimi jakie zostały w nim zaimplementowane. Tylko je możemy wykorzystać, aby uratować nasze środki, jeżeli zachodzi podejrzenie, iż w kontrakcie nie są one już bezpieczne. choćby jeżeli zasady są błędne to przez cały czas w kontrakcie są zasadami.

## Dziura w Parity

[Parity](https://github.com/paritytech/parity) to jedna z implementacji klienta wraz z kontraktem portfela dla Ethereum. W poprzednim wpisie omówiłem inną implementację portfela jaką jest *Ethereum Wallet*.

W uproszczeniu implementacja portfela *Parity* składa się z dwóch kontraktów. Jeden z nich spełnia rolę biblioteki `WalletLibrary`, która dostarcza implementacji funkcji wykorzystywanych przez kontrakt `Wallet`. Technikę taką stosuje się, aby nie ponosić wysokich kosztów przy tworzeniu instancji kontraktów, jeżeli część z ich kodu może być wspólna. Bibliotekę tworzymy w *blockchain* wyłącznie raz, instancji portfela będzie wiele.

Biblioteka `WalletLibrary` dostarcza między innymi funkcję `initWallet`, która była wywoływana z konstruktora portfela oraz funkcję `changeOwner`, która służy do zmiany właściciela, a wołana była z metody `changeOwner`. Poniżej przedstawiłem uproszczoną implementację tej części kontraktu *Parity*.

```solidity
contract WalletLibrary {
address public owner;

// funkcja wołana przy tworzeniu portfela
function initWallet(address _owner) {
owner = _owner;
// ...
}

// zmiana właściciela portfela
function changeOwner(address _new_owner) external {
// sprawdzamy czy funkcję wywołuje obecny właściciel
if (msg.sender == owner) {
owner = _new_owner;
}
}
}

contract Wallet {
address public _walletLibrary;
address public owner;

// konstruktor portfela, woła initWallet
function Wallet(address _owner) {
// ...
_walletLibrary.delegatecall(bytes4(sha3("initWallet(address)")), _owner);
}

// zmiana właściciela portfela
function changeOwner(address _new_owner) {
_walletLibrary.delegatecall(bytes4(sha3("changeOwner(address)")), _new_owner);
}

// funkcja awaryjna, wołana w razie braku możliwości dopasowania innej funkcji
function () payable {
_walletLibrary.delegatecall(msg.data);
}
}
```

W tym miejscu należy wytłumaczyć jak działa `delegatecall`. Wywołanie z poziomu kontraktu funkcji innego kontraktu dzięki `delegatecall` powoduje wykonanie kodu na rzecz wołającego kontraktu. Oznacza to, iż `delegatecall` do funkcji `changeOwner` w rzeczywistości zmodyfikuje pole `owner` konkretnej instancji kontraktu `Wallet` a nie pole `owner` w `WalletLibrary`. Jest to sensowne zachowanie, wszak wszystkie portfele nie mają wspólnego właściciela.

Powyższa implementacja wykonuje `delegatecall` z konstruktora do funkcji `initWallet` oraz z `changeOwner` do odpowiedniej funkcji w bibliotece.
Zauważmy, iż funkcja `initWallet` zmienia właściciela kontraktu bezwarunkowo. Wydaje się to poprawne, w konstruktorze dopiero tworzymy kontrakt, więc będzie to pierwszy właściciel. Konstruktor może być wywołany tylko raz. Funkcja `changeOwner` weryfikuje czy to aktualny właściciel ją wywołuje i tylko wtedy pozwala na ustawienie nowego adresu.

Wewnątrz *Ethereum Virtual Machine* wywołanie funkcji kontraktu z innego kontraktu polega na wyliczeniu jej sygnatury oraz przekazaniu (doklejeniu do sygnatury) wartości parametrów. Sygnatura funkcji to cztery bajty ze skrótu jej nazwy wraz z typami parametrów. Wyliczana jest ona przez kontrakt z ciągu znaków. W powyższym przykładzie dla `initWallet(address)` będzie to `9da8be21`. Zatem taką sygnaturę można wyliczyć samodzielnie poza kontraktem. Czy w przedstawionym kontrakcie można w jakiś sposób wywołać funkcję `initWallet` gdy kontrakt jest już utworzony? Przyjrzyjmy się implementacji funkcji *bez nazwy*, która jest tak zwaną funkcją awaryjną (ang. *fallback*) wołaną w razie braku możliwości dopasowania innej funkcji. Wykonuje ona `delegatecall` podając jako parametr dane przekazane w transakcji. Bingo! Wystarczy zatem doprowadzić do wywołania funkcji awaryjnej z sygnaturą `initWallet(address)` podając jako parametr wybrany przez nas adres nowego właściciela. To działało, ponieważ kontrakt `Wallet` nie miał implementacji funkcji o sygnaturze `initWallet(address)`, więc wywołanie trafiało do funkcji awaryjnej i było przekazywane do biblioteki `WalletLibrary`, po czym wykonywało się bez przeszkód na rzecz kontraktu wołającego.

Błąd w kontrakcie *Parity* polegał na tym, iż możliwość wywołania funkcji `initWallet` nie była ograniczona tylko do przypadku procesu tworzenia nowego portfela. Został on wykorzystany przez atakujących do wyprowadzenia Etherów z istniejących kontraktów. Straty osiągnęły kwotę kilkudziesięciu milionów dolarów, a byłyby jeszcze większe gdyby w porę nie zareagowali *dobrzy hakerzy*, którzy zabezpieczyli środki z błędnych kontraktów poprzez wykorzystanie tego samego ataku. Różnica polegała na tym, iż zwrócili potem wyprowadzone środki prawowitym właścicielom kontraktów.

Poprawka w kontrakcie *Parity* dodatkowo objęła funkcje, które bezwarunkowo modyfikują adres właściciela. Powinny być funkcjami `internal`, co oznacza, iż nie ma możliwości wywołania ich spoza kontraktu. Trzeba pamiętać, iż w języku Solidity domyślnie funkcje mają status `public`.

Łatkę w portfelu *Parity*, która eliminowała te błędy można zobaczyć [pod tym
adresem](https://github.com/paritytech/parity/commit/b640df8fbb964da7538eef268dffc125b081a82f#diff-8ea4aa7c2ba715c683bc764337f51585).
Cytując niektóre z komentarzy: `internal` wart 30 milionów dolarów.

## Podsumowanie

Powyższy przykład pokazuje jak dużą ostrożność trzeba zachować implementując kontrakty w Solidity i wykorzystując przygotowany kod jako ich użytkownik. Niuanse języka Solidity oraz maszyny wirtualnej EVM mogą mieć fatalne skutki dla kontraktów i ich właścicieli. Rozwijanych jest wiele narzędzi, które pomagają w analizie kodu kontraktów. Stosowane są też różne techniki, jak na przykład dzienny limit wypłat, które pozwalają zminimalizować skutki wykorzystania potencjalnych dziur. Wiele sprytnych ataków jeszcze się pojawi. Na pewno jednym ze sposobów na ich uniknięcie jest nauka poprzez implementację różnych kontraktów i ich analiza we własnej sieci Ethereum.
Idź do oryginalnego materiału