The Ethernaut #6 i #7 – delegation & force
Po krótkiej przerwie w rozwiązywaniu wyzwań Ethernaut, nadszedł czas, aby do nich powrócić. Dzisiaj skupimy się na dwóch kolejnych zadaniach. W rozwiązaniu pierwszego z nich przyda się wiedza zawarta w artykułach o tytule “Ponieważ kontekst ma znaczenie – call vs delegatecall“ oraz “Kiedy nieistniejące istnieje – funkcja fallback“, dlatego polecam zapoznać się z nimi przed rozpoczęciem. Jeśli już masz to za sobą, nie traćmy czasu i przejdźmy do wyzwań :).
Zadanie #6 – delegation
Standardowo rozpoczynamy od analizy otrzymanego kodu.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Delegate {
address public owner;
constructor(address _owner) {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
Wyzwanie składa się z dwóch kontraktów. Pierwszy z nich ma pole przechowujące wartość obecnego właściciela, funkcję do zmiany właściciela oraz prosty konstruktor. Nie ma w nim nic szczególnego. Drugi kontrakt również posiada pole mówiące o właścicielu oraz referencję do kontraktu “Delegate“. Oprócz tego zawiera konstruktor ustawiający obie wartości oraz funkcję “fallback“, o której mowa była w poprzednich wpisach. Funkcja “fallback” zostanie wywołana, jeśli spróbujemy uruchomić metodę na kontrakcie, która w nim nie istnieje. Implementacja, którą widzimy tutaj, jest bardzo prosta, a jej jedynym zadaniem jest wywołanie funkcji na kontrakcie “Delegate” przy użyciu “delegatecall“.
Naszym celem w tym zadaniu jest przejęcie kontroli nad kontraktem “Delegation“, Wykorzystamy do tego wiedzę na temat mechanizm “delegatecall“. Nie musimy tworzyć dodatkowego kodu, wystarczy odpowiednio wykorzystać już istniejący. W celu ułatwienia sobie zadania skorzystamy z Remixa.
Po skopiowaniu kontraktów przechodzimy do zakładki “Deploy“, wybieramy kontrakt “Delegate” i korzystamy z przycisku “At Address“, podając adres kontraktu “Delegation” pobrany z platformy Ethernaut. To pozwoli nam operować na kontrakcie “Delegation” przy użyciu interfejsu “Delegate“. Następnie musimy wywołać funkcję “pwn“, co de facto uruchomi funkcję “fallback“, która już zrobi swoje.
Prawdopodobnie po wykonaniu tej operacji właściciel się nie zmieni. Jest to spowodowane tym, że Metamask nieprawidłowo szacuje koszty tej transakcji. Wystarczy zwiększyć maksymalny limit gazu, który może zostać zużyty, przed zatwierdzeniem transakcji. Teraz wszystko powinno działać.
Po zakończonej transakcji musimy zatwierdzić kontrakt na platformie Ethernaut i voilà, wyzwanie zostaje pokonane. Możemy przejść do kolejnego.
Zadanie #6 – force
Celem tego wyzwania będzie umieszczenie dowolnych środków w postaci natywnych tokenów na podanym kontrakcie. W zależności od używanej sieci, mogą to być ether, matic lub inne monety. Jak wygląda wspomniany kontrakt?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
Na pierwszy rzut oka jest dość ubogi. Widzimy tutaj obrazek kota, który jest fajny, ale poza tym nie ma tam zbyt wiele. Jak możesz pamiętać z poprzednich wpisów, aby kontrakt mógł otrzymać środki, musi implementować funkcję “receive” lub inną funkcję typu “fallback“, oznaczoną jako “payable“. Ostatecznie mógłby też mieć metodę, która akceptuje przesłane środki. Niestety, w tym przypadku żadna z tych funkcji nie jest obecna i wydaje się, jakby twórcy wyzwania o czymś zapomnieli. Na szczęście wszystko jest zgodne z ich zamiarem. Istnieje bowiem sposób, który pozwala na przesłanie środków z jednego kontraktu na drugi, nawet jeśli ten drugi nie posiada żadnej z wymienionych funkcji. Aby to osiągnąć, należy skorzystać z mechanizmu “selfdestruct“. Zobaczymy może wyglądać kod, który z niego skorzysta.
contract Hacker {
function destroy(address payable _target) external payable {
selfdestruct(_target);
}
}
Stworzyliśmy dodatkowy kontrakt, który posiada jedną metodę o nazwie “destroy“. Funkcja ta przyjmuje jako parametr adres kontraktu “Force” i jest oznaczona jako “payable“, aby mogła przyjmować środki. Wewnątrz metody wywołujemy wbudowaną funkcję “selfdestruct“, która również przyjmuje wcześniej podany adres. Ta operacja spowoduje zniszczenie kontraktu “Hacker” i przesłanie wszystkich środków, które się w nim znajdowały, na podany adres. Nic nie jest w stanie temu przeszkodzić, niezależnie od tego, czy jest to adres portfela, czy kontraktu, który nawet nie implementuje funkcji “receive“. Środki trafią tam, gdzie zostało to wskazane.
Teraz wystarczy wywołać naszą funkcję “destroy“, przekazując do niej adres uzyskany z platformy Ethernaut. Pamiętaj, że podczas tej operacji należy dołączyć jakieś środki, może to być nawet 1 wei. Warto również zwiększyć limit gazu, ponieważ tutaj również może być problem z jego poprawną estymacją. Tym sposobem udało nam się zakończyć kolejne wyzwanie.
Warto zaznaczyć, że implementacja funkcji “selfdestruct” w kontraktach nie jest zalecana, ponieważ w przyszłości może zostać usunięta możliwość jej użycia. Wiele osób uważa, że narusza ona koncepcję blockchaina i należy się jej pozbyć. Trzeba również pamiętać, że jeśli użyjemy tej funkcji, to od tego momentu nie będziemy mogli korzystać z naszego kontraktu, ale jego historia zostanie zachowana w blockchainie.
Wykorzystanie metody “selfdestruct” może być frustrujące dla niedoświadczonych twórców zdecentralizowanych rozwiązań, którzy często opierają logikę swoich rozwiązań na saldzie swojego kontraktu. Niestety, ze względu na możliwość użycia metody “selfdestruct“, nie możemy opierać się wyłącznie na ilości przechowywanych środków.
Istnieje również drugi przypadek, w którym monety mogą trafić na kontrakt, mimo że my nie udostępniliśmy takiej możliwości. Jeśli ktoś wyśle środki na adres, pod którym jeszcze nie istnieje żaden kontrakt, a następnie zostanie stworzony kontrakt z takim właśnie adresem, to automatycznie jego saldo będzie większe niż zero. Chociaż jest to bardzo mało prawdopodobna sytuacja, to jednak możliwa.
Mam nadzieję, że te dwa wyzwania wzbogaciły Twoją wiedzę na temat blockchaina, pomimo niedużej ilości kodu, jaki się w nich znajdował. Na dzisiaj kończymy, ale niedługo wrócimy do kolejnych zadań. Do zobaczenia w kolejnym wpisie!