Zmniejszanie kontraktów w walce z limitem rozmiaru kontraktu
Dlaczego istnieje limit?
22 listopada 2016 r. (opens in a new tab) hard fork Spurious Dragon wprowadził EIP-170 (opens in a new tab), który dodał limit rozmiaru inteligentnego kontraktu wynoszący 24,576 kb. Dla Ciebie, jako dewelopera Solidity, oznacza to, że gdy dodajesz coraz więcej funkcjonalności do swojego kontraktu, w pewnym momencie osiągniesz limit i podczas wdrażania zobaczysz błąd:
Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on Mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries.
Limit ten został wprowadzony, aby zapobiegać atakom typu „odmowa usługi” (Denial-of-Service, DOS). Każde wywołanie kontraktu jest stosunkowo tanie pod względem zużycia gazu. Jednak wpływ wywołania kontraktu na węzły Ethereum wzrasta nieproporcjonalnie w zależności od rozmiaru kodu wywoływanego kontraktu (odczytywanie kodu z dysku, wstępne przetwarzanie kodu, dodawanie danych do dowodu Merkle). Zawsze, gdy mamy do czynienia z sytuacją, w której atakujący potrzebuje niewielu zasobów, aby spowodować dużo pracy dla innych, pojawia się potencjalne zagrożenie atakami DOS.
Początkowo był to mniejszy problem, ponieważ jednym z naturalnych ograniczeń rozmiaru kontraktu jest limit gazu w bloku. Oczywiście kontrakt musi zostać wdrożony w ramach transakcji, która zawiera cały kod bajtowy kontraktu. Jeśli w bloku umieścisz tylko tę jedną transakcję, możesz zużyć cały ten gaz, ale nie jest on nieskończony. Od czasu aktualizacji London limit gazu w bloku może wahać się między 15 a 30 milionami jednostek w zależności od zapotrzebowania w sieci.
W dalszej części przyjrzymy się kilku metodom uporządkowanym według ich potencjalnego wpływu. Pomyśl o tym w kategoriach utraty wagi. Najlepszą strategią na osiągnięcie docelowej wagi (w naszym przypadku 24 kb) jest skupienie się w pierwszej kolejności na metodach o dużym wpływie. W większości przypadków wystarczy zmiana diety, aby osiągnąć cel, ale czasami potrzeba czegoś więcej. Wtedy możesz dodać trochę ćwiczeń (średni wpływ), a nawet suplementy (mały wpływ).
Duży wpływ
Oddziel swoje kontrakty
To powinno być zawsze Twoje pierwsze podejście. Jak możesz podzielić kontrakt na kilka mniejszych? Zazwyczaj zmusza to do opracowania dobrej architektury dla swoich kontraktów. Mniejsze kontrakty są zawsze preferowane z perspektywy czytelności kodu. Aby podzielić kontrakty, zadaj sobie pytanie:
- Które funkcje pasują do siebie? Każdy zestaw funkcji może najlepiej pasować do osobnego kontraktu.
- Które funkcje nie wymagają odczytywania stanu kontraktu lub tylko określonego podzbioru stanu?
- Czy możesz rozdzielić przechowywanie i funkcjonalność?
Biblioteki
Jednym z prostych sposobów na przeniesienie kodu funkcjonalności z dala od miejsca przechowywania danych jest użycie biblioteki (opens in a new tab). Nie deklaruj funkcji biblioteki jako wewnętrznych (internal), ponieważ zostaną one dodane do kontraktu (opens in a new tab) bezpośrednio podczas kompilacji. Ale jeśli używasz funkcji publicznych (public), to w rzeczywistości znajdą się one w osobnym kontrakcie bibliotecznym. Rozważ użycie using for (opens in a new tab), aby uczynić korzystanie z bibliotek wygodniejszym.
Proxy
Bardziej zaawansowaną strategią jest system proxy. Biblioteki używają w tle DELEGATECALL, który po prostu wykonuje funkcję innego kontraktu w kontekście stanu kontraktu wywołującego. Zapoznaj się z tym wpisem na blogu (opens in a new tab), aby dowiedzieć się więcej o systemach proxy. Dają one więcej funkcjonalności, np. umożliwiają aktualizację, ale dodają też sporo złożoności. Nie dodawałbym ich tylko po to, aby zmniejszyć rozmiar kontraktu, chyba że z jakiegoś powodu jest to jedyna dostępna opcja.
Średni wpływ
Usuń funkcje
To powinno być oczywiste. Funkcje znacznie zwiększają rozmiar kontraktu.
- Zewnętrzne (External): Często dla wygody dodajemy wiele funkcji typu view. Jest to w porządku, dopóki nie osiągniesz limitu rozmiaru. Wtedy warto poważnie zastanowić się nad usunięciem wszystkich funkcji oprócz tych absolutnie niezbędnych.
- Wewnętrzne (Internal): Możesz także usunąć funkcje wewnętrzne/prywatne i po prostu wstawić kod w miejscu wywołania, o ile funkcja jest wywoływana tylko raz.
Unikaj dodatkowych zmiennych
1function get(uint id) returns (address,address) {2 MyStruct memory myStruct = myStructs[id];3 return (myStruct.addr1, myStruct.addr2);4}1function get(uint id) returns (address,address) {2 return (myStructs[id].addr1, myStructs[id].addr2);3}Prosta zmiana, taka jak ta, robi różnicę 0,28 kb. Istnieje szansa, że znajdziesz wiele podobnych sytuacji w swoich kontraktach, a te mogą zsumować się do znacznych oszczędności.
Skróć komunikaty o błędach
Długie komunikaty revert i w szczególności wiele różnych komunikatów revert może niepotrzebnie zwiększać rozmiar kontraktu. Zamiast tego użyj krótkich kodów błędów i dekoduj je w swojej aplikacji. Długi komunikat może stać się znacznie krótszy:
1require(msg.sender == owner, "Tylko właściciel tego kontraktu może wywołać tę funkcję");1require(msg.sender == owner, "OW1");Używaj niestandardowych błędów zamiast komunikatów o błędach
Niestandardowe błędy zostały wprowadzone w Solidity 0.8.4 (opens in a new tab). Są świetnym sposobem na zmniejszenie rozmiaru Twoich kontraktów, ponieważ są kodowane w ABI jako selektory (tak jak funkcje).
1error Unauthorized();23if (msg.sender != owner) {4 revert Unauthorized();5}Rozważ niską wartość „runs” w optymalizatorze
Możesz również zmienić ustawienia optymalizatora. Domyślna wartość 200 oznacza, że próbuje on zoptymalizować kod bajtowy tak, jakby funkcja była wywoływana 200 razy. Jeśli zmienisz tę wartość na 1, w zasadzie mówisz optymalizatorowi, aby optymalizował pod kątem jednokrotnego uruchomienia każdej funkcji. Funkcja zoptymalizowana pod kątem jednokrotnego uruchomienia oznacza, że jest zoptymalizowana pod kątem samego wdrożenia. Pamiętaj, że zwiększa to koszty gazu za uruchamianie funkcji, więc możesz nie chcieć tego robić.
Mały wpływ
Unikaj przekazywania struktur do funkcji
Jeśli używasz ABIEncoderV2 (opens in a new tab), pomocne może być nieprzekazywanie struktur do funkcji. Zamiast przekazywać parametr jako strukturę, przekaż wymagane parametry bezpośrednio. W tym przykładzie zaoszczędziliśmy kolejne 0,1 kb.
1function get(uint id) returns (address,address) {2 return _get(myStruct);3}45function _get(MyStruct memory myStruct) private view returns(address,address) {6 return (myStruct.addr1, myStruct.addr2);7}1function get(uint id) returns(address,address) {2 return _get(myStructs[id].addr1, myStructs[id].addr2);3}45function _get(address addr1, address addr2) private view returns(address,address) {6 return (addr1, addr2);7}Deklaruj poprawną widoczność dla funkcji i zmiennych
- Funkcje lub zmienne, które są wywoływane tylko z zewnątrz? Deklaruj je jako
externalzamiastpublic. - Funkcje lub zmienne wywoływane tylko z wnętrza kontraktu? Deklaruj je jako
privatelubinternalzamiastpublic.
Usuń modyfikatory
Modyfikatory, zwłaszcza gdy są intensywnie używane, mogą mieć znaczący wpływ na rozmiar kontraktu. Rozważ ich usunięcie i zamiast tego użyj funkcji.
1modifier checkStuff() {}23function doSomething() checkStuff {}1function checkStuff() private {}23function doSomething() { checkStuff(); }Te wskazówki powinny pomóc znacznie zmniejszyć rozmiar kontraktu. Jeszcze raz podkreślam, zawsze skupiaj się na dzieleniu kontraktów, jeśli to możliwe, aby uzyskać największy efekt.
Strona ostatnio zaktualizowana: 25 lutego 2026