Umowy proxy są ważnym narzędziem dla twórców inteligentnych kontraktów. Obecnie w systemie kontraktowym istnieje wiele modeli agencyjnych i odpowiadających im zasad użytkowania. Wcześniej przedstawiliśmy najlepsze praktyki bezpieczeństwa w przypadku umów proxy z możliwością aktualizacji.

W tym artykule przedstawimy inny model proxy, który jest popularny w społeczności programistów, model diamentowego proxy.

Diamentowy kontrakt agencyjny, znany również jako „Diament”, to wzorzec projektowy dla inteligentnych kontraktów Ethereum wprowadzony przez Ethereum Improvement Proposal (EIP) 2535.

Model Diamentu pozwala na nieograniczoną funkcjonalność umowy poprzez podzielenie jej funkcjonalności na mniejsze umowy (zwane także w przenośni „plasterkami”). Diamenty pełnią rolę proxy, kierując wywołania funkcji do odpowiednich aspektów.

Tryb diamentowy ma na celu rozwiązanie problemu maksymalnego limitu wielkości kontraktu sieci Ethereum. Dzieląc duży kontrakt na mniejsze aspekty, wzór rombu umożliwia programistom tworzenie bardziej złożonych i bogatych w funkcje inteligentnych kontraktów bez podlegania ograniczeniom wielkości.

Diamentowi Agenci oferują ogromną elastyczność w porównaniu z tradycyjnymi umowami z możliwością aktualizacji. Umożliwiają aktualizację części umowy, dodawanie, zastępowanie lub usuwanie wybranych części funkcji bez dotykania innych części.

Artykuł ten zawiera przegląd protokołu EIP-2535, w tym porównania z powszechnie używanym trybem przezroczystego proxy i trybem proxy UUPS, a także względy bezpieczeństwa dla społeczności programistów.

W kontekście EIP-2535 „Diament” jest umową zastępczą, której funkcjonalną realizację zapewniają różne kontrakty logiczne, zwane „aspektami”.

Wyobraź sobie, że prawdziwe diamenty mają różne strony, zwane fasetami, wówczas odpowiedni kontrakt diamentowy Ethereum również ma różne fasety. Każda umowa o funkcję pożyczania diamentów ma różne aspekty lub aspekty.

Standard Diamond rozszerza możliwości Diamond Cut przez analogię o dodawanie, zastępowanie lub usuwanie faset i funkcji.

Ponadto Diamond Standard udostępnia funkcję zwaną „Diamentową Lupą”, która zwraca informacje o fasetach i obecności diamentów.

W porównaniu z tradycyjnym modelem agencyjnym, „diament” jest równoznaczny z umową agencyjną, natomiast różne „aspekty” odpowiadają umowie wdrożeniowej. Różne aspekty Agenta Diamentowego mogą współużytkować funkcje wewnętrzne, biblioteki i zmienne stanu. Kluczowe składniki diamentu są następujące:

Kontrakt centralny, który działa jako serwer proxy i kieruje wywołania funkcji do odpowiednich aspektów. Zawiera mapowanie selektorów funkcji na adresy „aspektów”.

Pojedynczy kontrakt realizujący określoną funkcję. Każdy aspekt zawiera zestaw funkcji, które można wywołać za pomocą rombu.

to zestaw standardowych funkcji zdefiniowanych w EIP-2535, który dostarcza informacji o fasetach i selektorach funkcji używanych w diamentach. Diamond Magnifier umożliwia programistom i użytkownikom sprawdzanie i zrozumienie struktury diamentów.

Funkcje dodawania, zastępowania lub usuwania faset i odpowiadających im selektorów cech w diamencie. Cięcie diamentów mogą wykonywać wyłącznie autoryzowane adresy (np. właściciel diamentu lub umowa z wieloma podpisami).

Podobnie jak w przypadku tradycyjnych agentów, gdy na agencie diamentowym zostanie wywołane funkcja, zostanie uruchomiona funkcja rezerwowa agenta (funkcja awaryjna). Główna różnica w stosunku do diamentowego proxy polega na tym, że w funkcji rezerwowej istnieje mapowanie selektorToFacet, które przechowuje i określa, który adres kontraktu logicznego ma implementację wywoływanej funkcji. Następnie używa delegatall do wykonania funkcji, podobnie jak tradycyjny delegat.

Wszystkie serwery proxy używają funkcji fallback() do delegowania wywołań funkcji na adresy zewnętrzne. Poniżej znajduje się implementacja agenta diamentowego i implementacja tradycyjnego agenta.

Warto zauważyć, że ich bloki kodu asemblera są bardzo podobne, więc jedyną różnicą jest adres aspektu w wywołaniu delegata diamentowego proxy i adres impl w tradycyjnym wywołaniu delegata proxy.

Główna różnica polega na tym, że w diamentowym proxy adres aspektu jest określany przez hashmapę msg.sig wywołującego (selektor funkcji) na adres aspektu, podczas gdy w tradycyjnym proxy adres impl nie zależy od wchodzi dzwoniący.

Funkcja rezerwowa agenta diamentowego

Tradycyjna funkcja zastępcza proxy

Mapowanie SelectorToFacet określa, który kontrakt zawiera implementację każdego selektora funkcji. Personel projektu często musi dodawać, zastępować lub usuwać to mapowanie selektorów funkcji do umów wdrożeniowych. EIP-2535 stanowi: Aby osiągnąć ten cel, musi istnieć funkcja DiamondCut(). Poniżej znajduje się przykładowy interfejs.

Każda struktura FacetCut zawiera adres aspektu i czterobajtową tablicę selektorów funkcji, które mają być aktualizowane w umowie agenta diamentowego. FaceCutAction umożliwia dodawanie, zastępowanie i usuwanie selektorów funkcji. Implementacja funkcji DiamondCut() powinna obejmować odpowiednią kontrolę dostępu, aby zapobiec kolizjom gniazd pamięci, odzyskać dane w przypadku awarii itp.

Aby sprawdzić jakie funkcje ma środek diamentowy i jakie wykorzystuje fasety, używamy „Diamentowej lupy”. „Diamentowe szkło powiększające” to specjalny aspekt, który implementuje następujący interfejs zdefiniowany w EIP-2535:

Funkcja facets() powinna zwracać adresy wszystkich aspektów i ich czterobajtowych selektorów funkcji. Funkcja facetFunctionSelectors() powinna zwracać wszystkie selektory funkcji obsługiwane przez określony aspekt. Funkcja facetAddresses() powinna zwracać adresy wszystkich faset używanych przez diament.

Funkcja facetAddress() powinna zwrócić aspekt obsługujący dany selektor lub adres (0), jeśli nie zostanie znaleziony. Należy pamiętać, że nie powinno być więcej niż jednego adresu aspektu z tym samym selektorem funkcji.

Biorąc pod uwagę, że Diamentowy Agent deleguje różne wywołania funkcji do różnych umów wdrożeniowych, kluczowe znaczenie ma odpowiednie zarządzanie miejscami magazynowymi, aby zapobiec konfliktom. EIP-2535 wspomina o kilku metodach zarządzania gniazdami pamięci.

Ten aspekt może deklarować zmienne stanu w strukturze. W tym aspekcie można wykorzystać dowolną liczbę struktur, każda z innym miejscem przechowywania. Każda konstrukcja ma określone miejsce w magazynie kontraktowym. Aspekty mogą deklarować własne zmienne stanu, ale nie mogą one kolidować z miejscem przechowywania zmiennych stanu zadeklarowanych przez inne aspekty. EIP-2535 zapewnia przykładową bibliotekę i umowę na przechowywanie diamentów, jak pokazano na poniższym rysunku:

App Storage to bardziej wyspecjalizowana wersja Diamond Storage. Ten wzorzec służy do wygodniejszego i łatwiejszego udostępniania zmiennych stanu aspektu. Struktura magazynu aplikacji jest zdefiniowana tak, aby zawierała dowolną liczbę i typ zmiennych stanu wymaganych przez aplikację. Aspekt zawsze deklaruje strukturę AppStorage jako pierwszą i jedyną zmienną stanu, umieszczoną w gnieździe 0. Różne aspekty mogą następnie uzyskać dostęp do zmiennych z tej struktury.

Istnieją również inne strategie zarządzania miejscami do przechowywania, w tym połączenie Diamond Storage i AppStorage. Na przykład niektóre struktury są wspólne dla różnych aspektów, a niektóre są unikalne dla określonych aspektów. We wszystkich przypadkach ważne jest, aby zapobiegać przypadkowym kolizjom zbiorników magazynujących.

Porównanie z przezroczystymi proxy i proxy UUPS

Dwa główne tryby proxy używane obecnie przez społeczność programistów Web3 to przezroczysty tryb proxy i tryb proxy UUPS. W tej sekcji pokrótce porównujemy wzorzec proxy Diamond ze wzorcami proxy Transparent proxy i proxy UUPS.

1.EPI-2535:https://eips.ethereum.org/EIPS/eip-2535 #Facets,%20 State%20 Variables%20 and%20 Diamond%20 Storage

2.EPI-1967:https://eips.ethereum.org/EIPS/eip-1967    

3. Implementacja referencyjna proxy Diamond: https://github.com/mudgen/Diamond

4. Implementacja OpenZeppelin: https://github.com/OpenZeppelin/openzeppelin-contracts/tree/v4.7.0/contracts/proxy

Agenci i skalowalne rozwiązania to bardziej złożone systemy, a OpenZeppelin zapewnia biblioteki kodów i obszerną dokumentację dla agentów UUPS, transparentnych i skalowalnych Beacon. Jednakże w przypadku wzorca diamentowego proxy, chociaż OpenZeppelin potwierdził jego zalety, nadal zdecydował się nie włączać implementacji diamentu EIP-2535 do swojej biblioteki.

Dlatego programiści korzystający z istniejących bibliotek stron trzecich lub sami wdrażający rozwiązanie muszą zachować szczególną ostrożność podczas jego wdrażania. W tym miejscu przygotowaliśmy listę najlepszych praktyk w zakresie bezpieczeństwa, które powinna wziąć pod uwagę społeczność programistów.

Dzieląc logikę kontraktu na mniejsze, łatwiejsze w zarządzaniu moduły, programiści mogą łatwiej testować i kontrolować swój kod.

Dodatkowo takie podejście pozwala programistom skoncentrować się na budowaniu i utrzymywaniu określonych aspektów kontraktu, zamiast na zarządzaniu złożoną, monolityczną bazą kodu. Efektem końcowym jest bardziej elastyczna i modułowa baza kodu, którą można łatwo aktualizować i modyfikować bez wpływu na inne części umowy.

Źródło: Github Aavegotchi

Po wdrożeniu kontraktu diamentowego proxy musi on dodać adres kontraktu DiamondCutFacet do kontraktu diamentowego proxy i wdrożyć funkcję DiamondCut(). Funkcja DiamondCut() służy do dodawania, usuwania lub zastępowania aspektów i funkcji. Bez funkcji DiamondCutFacet i DiamondCut() agent diamentów nie może działać poprawnie.

Źródło: kask Diamond-3-Hardhat firmy Mugen

Dodając nową zmienną stanu do struktury pamięci w inteligentnym kontrakcie, należy ją dodać na końcu struktury. Dodanie nowej zmiennej stanu na początku lub w środku struktury spowoduje, że nowa zmienna stanu nadpisze istniejące dane zmiennej stanu, a każda zmienna stanu znajdująca się po nowej zmiennej stanu może odwoływać się do niewłaściwej lokalizacji przechowywania.

Wzorzec AppStorage wymaga zadeklarowania jednej i tylko jednej struktury dla agenta diamentowego, a struktura ta jest wspólna dla wszystkich aspektów. Jeżeli wymaganych jest wiele struktur, należy zastosować wzorzec DiamondStorage.

Nie umieszczaj struktury bezpośrednio wewnątrz innej struktury, chyba że masz pewność, że nie planujesz dodawać więcej zmiennych stanu do wewnętrznej struktury. Podczas aktualizacji nie można dodać nowych zmiennych stanu do struktury wewnętrznej bez nadpisywania miejsc przechowywania zmiennych zadeklarowanych po strukturze.

Rozwiązaniem jest dodanie nowej zmiennej stanu do struktury mapy pamięci zamiast umieszczania „struktury” bezpośrednio w „strukturze”. Zmienne miejsca do przechowywania na mapie są obliczane w różny sposób i nie sąsiadują ze sobą w magazynie.

Na wielkość tablicy będzie miał wpływ rozmiar struktury. Dodanie nowej zmiennej stanu do struktury powoduje zmianę rozmiaru i układu struktury.

Może to powodować problemy, jeśli struktura jest używana jako element tablicy. Jeśli zmieni się rozmiar i układ struktury, zmieni się również rozmiar i układ tablicy, co może powodować problemy z indeksowaniem lub innymi operacjami, które opierają się na stałym rozmiarze i układzie struktury.

Podobnie jak w przypadku innych wzorców proxy, każda zmienna powinna mieć unikalne miejsce do przechowywania. W przeciwnym razie dwie różne struktury w tym samym miejscu zastąpiłyby się nawzajem.

Funkcja inicjalizacji() jest zwykle używana do ustawiania ważnych zmiennych, takich jak adres uprzywilejowanej roli. Jeśli kontrakt nie zostanie zainicjowany podczas wdrażania, złośliwy aktor może wywołać kontrakt i przejąć nad nim kontrolę.

Zaleca się dodanie odpowiedniej kontroli dostępu do funkcji inicjowania/ustawiania lub zapewnienie, że funkcja zostanie wywołana po wdrożeniu kontraktu i nie będzie można jej wywołać ponownie.

Jeśli jakikolwiek aspekt umowy może wywołać funkcję selfdestruct(), może to zniszczyć całą umowę, powodując utratę środków lub danych. Jest to niezwykle niebezpieczne w modelu diamentowego proxy, ponieważ wiele aspektów może uzyskać dostęp do pamięci i danych umowy proxy.

Obecnie coraz więcej projektów wykorzystuje w swoich inteligentnych kontraktach model agencji diamentowej. Oferuje elastyczność i inne zalety w porównaniu z tradycyjnymi agentami.

Jednak dodatkowa elastyczność może również oznaczać zapewnienie atakującym szerszej powierzchni ataku. Mamy nadzieję, że ten artykuł będzie pomocny społeczności programistów w zrozumieniu mechaniki wzorca diamentowego proxy i jego względów bezpieczeństwa.

Jednocześnie zespół projektowy powinien przeprowadzić rygorystyczne testy i audyty zewnętrzne, aby zmniejszyć ryzyko wystąpienia podatności związanych z realizacją umowy agencyjnej diamentowej.

CertiK będzie nadal publikować takie artykuły techniczne, aby pomóc większej liczbie programistów w bezpiecznym rozwoju. Śledź nas, aby uzyskać więcej podobnych informacji i informacji!