ProCat Solutions
Web3 kitérő: amit a javíthatatlan smart contractok tanítottak
Token presale Solidityben és OpenZeppelinnel, Merkle-fás whitelist, referral, gas Polygonon és Arbitrumon, Ethers.js backend, audit, és ami ebből megmaradt.
Egy telepített smart contract nem javítható. Ha hiba van benne, a hiba ott marad, és a felhasználók pénzével fizetnek érte. Az elmúlt két évben több Web3-projektben dolgoztunk: token presale rendszereken, whitelist- és referral-logikán, kriptofizetés-integráción EVM-kompatibilis hálózatokon. Egy olyan csapatnak, amely egyébként backend-rendszereket épít, ez a mondat minden döntést átírt. Ez a cikk arról szól, mit jelentett a gyakorlatban smart contractot fejleszteni és üzemeltetni, és mit hoztunk vissza belőle a megszokott munkánkba.

Ne írj sajátot
A Solidity-fejlesztés első szabálya, amit megtanultunk: a szabványos komponenseket magunk nem írjuk meg. Az ERC-20, az ERC-721, a hozzáférés-vezérlés és az újrahívás elleni védelem auditált, széles körben használt formában megvan az OpenZeppelin könyvtárban. Minden saját implementáció csak kockázatot ad hozzá.
Saját kód ott kezdődik, ahol az üzleti logika: a presale-szerződésben, amely eldönti, ki, mikor, mennyiért és mennyit vásárolhat. Ebben négy elvet követünk:
- a szerződés állapota minimális; ami nem kell a láncon belüli döntéshez, az láncon kívül marad,
- minden módosító függvény explicit jogosultsághoz kötött (
OwnablevagyAccessControl), - a szerződés szüneteltethető (
Pausable), mert hiba esetén ez az egyetlen vészfék, - a pénzmozgás a “checks-effects-interactions” mintát követi, és
ReentrancyGuardvédi.
A javíthatatlanság miatt a hibakezelés, a tesztelés és a jogosultságok itt szigorúbbak, mint bármelyik backendnél, amit korábban írtunk.
Merkle-fa a címlista helyett
Egy tipikus presale-rendszer három részből áll. A szerződés kezeli a vásárlást, a backend tartja a whitelistet és a referral-kódokat, a frontenden pedig a felhasználó tárcát csatlakoztat.
A whitelist naiv változata, a címek listája a szerződésben, drága: minden új cím egy tranzakció. Mi Merkle-fát használunk. A backend kiszámolja a jogosult címek fájának gyökerét, csak ez kerül a szerződésbe, a felhasználó pedig vásárláskor bizonyítékot (Merkle proof) küld, amelyet a szerződés ellenőriz. A whitelist frissítése így egyetlen tranzakció.
A referral logikát szándékosan megosztjuk a lánc és a backend között. A szerződés eseményként csak annyit rögzít, melyik cím vásárolt és milyen referral-azonosítóval. A jutalékot, a jogosultság ellenőrzését és a kifizetések ütemezését a backend intézi. A szabályok így a szerződés újratelepítése nélkül módosíthatók, és a láncra nem kerül olyan adat, amelynek nem kellene nyilvánosnak lennie.

Polygon, Arbitrum, gas
Az Ethereum főhálózat tranzakciós díjai a legtöbb presale-nek vállalhatatlanok, ezért a projektek Polygonon vagy Arbitrumon futnak. Mindkettő EVM-kompatibilis, a szerződéskód változtatás nélkül telepíthető. Az üzemeltetés viszont eltér:
- más a gas ára és a blokkidő, ezért a tranzakció-megerősítésre váró logikát hálózatonként állítjuk be,
- az Arbitrum L2-es díjába az L1-es adatköltség is beleszámít, ami a tranzakció méretétől függ; a nagy calldata, például egy hosszú Merkle proof, itt drágább, mint elsőre gondolnánk,
- a Polygonon időnként torlódás és átszervezett blokkok (reorg) fordulnak elő, ezért az eseményfeldolgozás csak több megerősítés után tekint véglegesnek egy tranzakciót.
A szerződéskódban a gas-optimalizálás valódi pénzt jelent: tárolási változók összecsomagolása, felesleges írások elhagyása, események használata tárolás helyett. A Hardhat gas-riportja minden tesztfutásnál jelzi, ha valami drágult.
A websocket csendben szakad meg
A backendünk Node.js-ben, Ethers.js-szel beszél a lánccal. Két dolga van: tranzakciókat küld (whitelist-gyökér frissítése, kifizetések) és eseményeket figyel (vásárlások).
Az üzemeltetési gondok többségét az eseményfigyelés okozta. A websocket-alapú figyelés megbízhatatlan, a kapcsolat szó nélkül megszakad. Áttértünk lekérdezésre (polling): a backend nyilvántartja az utoljára feldolgozott blokkot, és rendszeresen lekéri az azóta keletkezett eseményeket. A reorgok miatt mindig néhány blokkal a láncfej mögött marad. A feldolgozás idempotens, a kulcs a tranzakció-hash és a log-index.
A küldő oldalon a nonce-kezelés és a beragadt tranzakciók pótlása külön logikát kíván. Egy alacsony gas-árral küldött tranzakció a hálózat drágulásakor órákig függhet, és amíg ott van, a következő nonce sem megy át.
Az audit a leíratlan feltevéseket találja meg
A smart contractokat sokkal alaposabban teszteljük, mint a backend-kódot. Hardhat alatt minden ágra van egységteszt, fork-tesztek futnak valós hálózati állapoton, a számítási függvényeket fuzz-tesztekkel ellenőrizzük. Telepítés előtt független auditot kérünk. Ez kötelező, mert a hiba ára a felhasználók pénze.
Az audit többnyire nem kritikus hibát talál. Azokat a feltételezéseket hozza felszínre, amelyeket a fejlesztő soha nem írt le: mi van, ha elvész a tulajdonosi kulcs, mi lesz a presale lejárta után a szerződésben maradt összeggel, ki hívhatja meg a vészleállítót.
Ami megmaradt
A Web3 nálunk kitérő maradt, fő irány nem lett belőle. Néhány dolog viszont velünk maradt:
- az “ez nem javítható” gondolkodás: a migrációkat, a fizetési folyamatokat és a külső integrációkat úgy tervezzük, mintha nem lehetne visszavonni őket,
- az idempotens, újrajátszható eseményfeldolgozás lett az alapminta minden webhooknál és üzenetsornál,
- a jogosultsági modellt minden módosító műveletre explicit leírjuk és teszteljük,
- a kritikus kódrészeket független szem is átnézi, akkor is, ha nincs formális audit.
Ezek a minták a következő projektjeinkben, a hangalapú rendszereknél is beváltak. Ott a hívásesemények feldolgozása pontosan ugyanezeket a problémákat veti fel.