Kapatmak için ESC'ye basın

ERC Standartları Açıkları: Akıllı Sözleşme Mimarisi

Selam dostum! Yolun bu yazıya düştüyse, ya akıllı sözleşme güvenliği konusuna fena halde kafayı takmışsın ya da şu sıralar yeni bir DeFi protokolünün mimarisi üzerinde çalışıyorsun ve "Ulan yarın saçma sapan bir açık yüzünden her şey patlar mı?" diye için için tırsıyorsun.

Bak kardeşim, ben bu sektörün eskisiyim. Pizza ve enerji içeceği eşliğinde, dizimizin üstünde hackathonlarda exploit kastığımız günlerden kripto borsası CTO'luğuna kadar uzanan bir yoldan geçtim. Ve sana ne diyeceğim biliyor musun? Bugüne kadar incelediğim, araştırdığım (ve bir kısmını daha denetim aşamasındayken engellediğim) o can yakan büyük hack olaylarının çoğu kriptografinin patlamasından falan kaynaklanmadı. Solidity derleyicisinin delirmesinden de olmadı. Hepsi, ERC standartlarının birbiriyle kesiştiği noktalarda nasıl davranacağını temelden kavrayamamaktan ötürü yaşandı.

Biz genelde standartları mutlak bir kanun ve güvenlik teminatı gibi görmeye alışmışız. Ama şeytan, entegrasyon detaylarında ve gizli yan etkilerde gizlidir. Gel şimdi mimarların en çok nerede baltayı taşa vurduğunu ve geceleri kafayı yastığa rahat koymak için bir sistemi nasıl tasarlaman gerektiğini masaya yatıralım.

1. ERC-20'nin Gizli Tehlikesi: Ölümcül Klasik

Sorsan ERC-20’yi yutmamış adam yoktur, her şeyiyle ezberlenmiştir. Ne ters gidebilir ki? Eğer dışarıdan gelen tokenları sıkı bir kontrolden geçirmeden kendi protokolüne entegre ediyorsan, her şey ama her şey ters gidebilir.

Dönen Değerin Olmaması Problemi (The No-Return Dilemma)

Spesifikasyona göre transfer ve transferFrom fonksiyonlarının geriye bir bool değer döndürmesi şarttır. Ama piyasada sözü geçen bir sürü eski ve köklü token (merhaba eski kontratlardaki USDT ve BNB) gerçekte bunu yapmıyor. İşlem başarılı olsa bile geriye hiçbir şey döndürmüyorlar.

Eğer senin kontratın, standart arayüz üzerinden şöyle bir bool beklentisine giriyorsa:

// Rastgele tokenlarla çalışıyorsan bunu sakın yapma!
IERC20(token).transferFrom(msg.sender, address(this), amount);

USDT ile etkileşime girdiğin an işlem çat diye patlar (revert olur). Çünkü EVM yığında (stack) dönecek bir değer arayacak ama bulamayacaktır. Ya da daha kötüsü, eğer dönen sonucu hiç kontrol etmiyorsan (yani require(token.transfer(...)) yerine düz token.transfer(...) yazdıysan), bazı tokenlar hata durumunda revert etmek yerine geriye false döndürür ve senin kontrat hiçbir şey olmamış gibi çalışmaya devam eder. Sonuç? Kullanıcı havadan kendine bakiye basmış olur.

Çözüm: Doğrudan transfer ve transferFrom çağırmayı tamamen unut. OpenZeppelin'in SafeERC20 kütüphanesini ve onun safeTransfer / safeTransferFrom metotlarını kullan. Bu kütüphane arka planda düşük seviyeli (low-level) dönüş verilerini kontrol eder ve standarda uymayan sakat kontratları bile düzgünce amorte eder.

Weird ERC-20 Tokens: Standart Patates Olduğunda

İşte sana ders kitaplarında yazanlara hiç uymayan, garip davranan tokenlar için küçük bir rehber tablo. Bir mimar olarak bunları likidite havuzlarının mantığına mutlaka yedirmek zorundasın.

Token Tipi (Weird ERC-20)Olayı Ne?Mimari İçin Riski Ne?
Deflationary / Fee-on-Transfer (örn. STA, PAXG)Transfer esnasında doğrudan kesinti/komisyon alırlar.Sen kontrata 100 token girdi sanırsın ama hesaba 99 tane geçmiştir. İç likidite muhaseben altüst olur, havuzda açık oluşur.
Upgradable Proxies (örn. USDC, USDT)Tokenın mantığı adminler tarafından her an değiştirilebilir.Kara liste (Blacklist) mevzusu. Eğer senin kontrat adresini banlarlarsa, içerideki tüm likidite sonsuza kadar kilitli kalır.
Rebasing Tokens (örn. AMPL)Cüzdanlardaki bakiye dinamik olarak değişir (fiyatı sabitlemek için arz sürekli oynar).Kontratın bakiyesi, hiçbir transfer fonksiyonu çağrılmadığı halde kendi kendine azalıp artabilir.

2. ERC-721 ve ERC-1155: onERC721Received ve Reentrancy Tuzağı

Geldik en sevdiğim mevzuya. Sırf bu "güvenli transfer" fonksiyonları yüzünden kaç tane NFT pazar yeri ve lending protokolü dımdızlak ortada kaldı, haddi hesabı yok.

ERC-721 veya ERC-1155 standartlarında safeTransferFrom çağırdığında, token kontratı alıcının bir akıllı sözleşme olup olmadığını kontrol eder. Eğer sözleşmeyse, alıcı kontrattaki onERC721Received veya onERC1155Received hook'unu (tetikleyicisini) tetikler.

Amaç ne? Alıcı kontratın NFT'lerle çalışmayı bilip bilmediğinden emin olmak, yani token içeride sıkışıp kalmasın diye.

Peki buradaki bit yeniği nerede? Bu hook, daha sen kendi kontratının iç durumunu (state) güncellemeden, işlemin tam ortasında kontrolü dışarıdaki, güvenmediğin bir koda devreder!

Zafiyet Barındıran Mint / Pazar Yeri Kodu

Şu kod parçasına bir baksana. Bunu, yazılımdaki o klasik mimari hatayı—yani Checks-Effects-Interactions pattern'inin ihlalini—net göstermek için bilerek bu şekilde yazdım.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
contract VulnerableNFCLending {
    // Teminat bilgilerini tutuyoruz: Kullanıcı => Token ID => Teminat Aktif mi?
    mapping(address => mapping(uint256 => bool)) public hasCollateral;
    IERC721 public nftToken;
    constructor(address _nft) {
        nftToken = IERC721(_nft);
    }
    // Kullanıcı NFT teminatı verip kredi çekmek istiyor
    function depositCollateral(uint256 tokenId) external {
        // 1. Interactions: NFT'yi kontrata transfer ediyoruz
        // safeTransferFrom alıcının kontratındaki onERC721Received hook'unu tetikler mi?
        // Bir dakika, burada alıcı BİZİZ. Ama ya kontrat safeMint çağırıyorsa...
        // Dur, senaryoyu NFT'yi geri verdiğimiz ya da saldırganın kontrolü ele geçirdiği duruma göre güncelleyelim.
        
        // Senaryoyu değiştirelim: Kontrat NFT'yi geri gönderiyor (mesela teminat çekilirken)
        // ya da bu, önce transferi yapıp durumu sonra güncelleyen bir mint kontratı.
    }
}

En iyisi sana zafiyetli bir mint fonksiyonu üzerinden net bir örnek göstereyim, kafanda daha iyi otursun. Diyelim ki elimizde her cüzdana sadece 1 tane ücretsiz NFT mintleme hakkı veren bir kontrat var.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract VulnerableMint is ERC721 {
    mapping(address => bool) public hasMinted;
    uint256 public currentTokenId;
    constructor() ERC721("DangerNFT", "DNFT") {}
    function freeMint() external {
        // Checks
        require(!hasMinted[msg.sender], "Brother, you already have one!");
        // Interactions (_safeMint içinde dış kontrat çağrısı gizlidir!)
        _safeMint(msg.sender, currentTokenId);
        currentTokenId++;
        // Effects (Durum güncellemesi ÇOK GEÇ yapılıyor)
        hasMinted[msg.sender] = true;
    }
}

Şimdi bir de saldırganın kontratına bakalım. Bu kontrat aradaki hook'u yakalıyor; ana kontrattaki hasMinted değerinin hala false olduğunu görüp, tüm limit bitene veya gas tükenene kadar durmadan freeMint fonksiyonunu tekrar tekrar çağırıyor.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IVulnerableMint {
    function freeMint() external;
}
contract Attacker {
    IVulnerableMint public target;
    uint256 count;
    constructor(address _target) {
        target = IVulnerableMint(_target);
    }
    function attack() external {
        target.freeMint();
    }
    // ERC-721 standardının tetiklediği o lanet olası hook
    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
    ) external returns (bytes4) {
        if (count < 5) {
            count++;
            // Reentrancy gerçekleşti! Kurbanın iç durumu henüz güncellenmedi
            target.freeMint();
        }
        return this.onERC721Received.selector;
    }
}

Ulan denetimlerde (audit) kaç kere karşılaştım şu durumla. Geliştiriciler genelde şöyle düşünüyor: "Ya alt tarafı NFT transferi, yerel ETH göndermiyoruz ki, ne olabilir?" İşte öyle olmuyor; reentrancy bir vuruyor, içeride ne var ne yok boşaltıp götürüyorlar.

Mimarın Altın Kuralı: Önce durumu güncelle (hasMinted[msg.sender] = true;), ondan sonra ne kadar mint veya transfer metodu varsa çağır. Ve lütfen, NFT transferleriyle uğraşan fonksiyonların tepesine OpenZeppelin’in nonReentrant modifier'ını çakmayı asla ihmal etme.

Devam edelim. Madem hook'lar üzerinden dönen Reentrancy mevzusunun röntgenini çektik, şimdi de mimari tasarım aşamasında çok az kişinin aklına gelen, ancak canlar yandıktan sonra fark edilen daha taze ve sinsi bir problemi masaya yatıralım.

3. ERC-2612 (Permit): Hayalet Approve'lar ve Front-running Atakları

ERC-2612 standardı, Web3 dünyasına ilaç gibi gelen permit fonksiyonunu hayatımıza soktu. Bu fonksiyon sayesinde kullanıcılar, token harcama izni (allowance) vermek için offline bir mesajı (EIP-712) imzalayabiliyor ve gas ücretini bir relayer'a veya direkt protokolün kendisine yıkabiliyor. UX (kullanıcı deneyimi) seviye atladı: Kullanıcı iki ayrı işlemle (approve + transferFrom) uğraşmak yerine tek tıkla işi bitiriyor.

Ancak mimarlar bu imzanın arka planda tam olarak nasıl çalıştığını çoğunlukla gözden kaçırıyor ve mantıkta ölümcül hatalar yapıyor.

İmza Front-running'i (Signature Front-running)

permit kullanan klasik bir akıllı sözleşme deposit fonksiyonunu gözümüzde canlandıralım:

function depositWithPermit(
    uint256 amount,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) external {
    // Önce kullanıcının gönderdiği imzayı işleterek permit'i çalıştırıyoruz
    IERC20Permit(token).permit(msg.sender, address(this), amount, deadline, v, r, s);
    
    // Ardından tokenları çekiyoruz
    IERC20(token).transferFrom(msg.sender, address(this), amount);
    
    // Protokol içi puanları veya havuz paylarını mint'liyoruz
    _mintShares(msg.sender, amount);
}

Buradaki açık nerede? Herhangi bir MEV botu public mempool'u tararken bu işlemi havada kapar. Bot, senin işleminden geçerli imzayı (v, r, s) ve parametreleri cımbızla çekip, direkt token sözleşmesi üzerindeki token.permit(...) fonksiyonunu çağıracak kendi işlemini oluşturur. Üstüne bir de gas ücretini (Gas Price) kullanıcınınkinden yüksek girerek işlemi front-run'lar.

Haliyle botun işlemi ilk sırada block'a girer. İmza başarıyla harcanır ve allowance tanımlanır. Hemen arkasından dürüst kullanıcının işlemi zincire ulaşır. Fakat imza zaten kullanıldığı için kullanıcının token sözleşmesindeki nonce değeri çoktan artmıştır! depositWithPermit içindeki permit çağrısı, imza artık geçersiz sayılacağından anında revert yer.

Sonuç mu? Kullanıcının işlemi patlar, ödediği gas boşa gider, UX çöp olur. Eğer bu işlem, kaldıraçlı bir long pozisyonunu likidasyondan kurtarmak için kritik bir teminat ekleme (margin top-up) işlemiyse, yaşanan gecikme yüzünden pozisyon güme gider ve kullanıcı direkt likidite olur.

Mimariyi Nasıl Sağlamlaştırırız?

permit çağrısını bir try/catch bloğu içine alın. Eğer imza bir front-runner tarafından ağa çoktan basılmışsa, token sözleşmesinde zaten gerekli allowance verilmiş demektir. Sözleşmeniz bu mükerrer imza hatasını görmezden gelmeli ve yoluna devam edip direkt transferFrom adımını çalıştırmalıdır.

try IERC20Permit(token).permit(msg.sender, address(this), amount, deadline, v, r, s) {} 
catch {
    // İşlem revert olduysa, imza front-run'lanmış olabilir.
    // Mevcut allowance değerinin işlem için yeterli olup olmadığını kontrol ediyoruz.
    require(IERC20(token).allowance(msg.sender, address(this)) >= amount, "Permit failed and allowance insufficient");
}

"Hayalet Permit" (Phantom Permit) Belası

Geldik sağda solda pek yazılmayan, sadece işin mutfağındakilerin canını yakan asıl mevzuya. DeFi protokolünüzün her türlü tokenı desteklediğini ve kullanıcının aslında ERC-2612 standartını desteklemeyen bir token ile depositWithPermit çağırmaya çalıştığını düşünelim.

Muhtemelen "E sözleşmede permit fonksiyonu yok ki, işlem direkt fail olur" diye düşünüyorsunuz. Kazın ayağı her zaman öyle değil!

Eğer ilgili token sözleşmesinde, bilinmeyen bir fonksiyon seçici (selector) çağrıldığında işlemi revert etmeyen genel bir fallback() veya receive() fonksiyonu varsa (ki bazı proxy mimarilerinde veya eski nesil WETH tokenlarında durum böyledir), permit çağrısı başarılıymış gibi tamamlanır (yani success = true döner). Madalyonun tersi ise şu: Gerçekte ortada verilmiş hiçbir allowance yoktur.

Sözleşmeniz daha sonra transferFrom adımına geçer ve eğer içeride önceden kalma eski bir approve yoksa işlem orada tıkanır. Ancak bu açığı, allowance değerinin farklı akışlar üzerinden doğrulandığı bir mantıkla harmanlarsanız, protokolü ellerinizle reked durumuna düşürürsünüz. Her zaman hedef tokenın ERC-165 üzerinden IERC20Permit arayüzünü gerçekten destekleyip desteklemediğini doğrulayın ya da token whitelist'ini çok sıkı tutun.

4. ERC-3156 (Flash Loans): Tek Bir İşlem İçinde Bakiye Manipülasyonu Riski

Flash loan'lar (flaş krediler) muazzam bir güçtür ancak geleneksel yazılım mimarlarının alışık olduğu tüm zamanlama algılarını altüst ederler. Saldırgan, tek bir atomik işlem (atomic transaction) içinde milyonlarca dolar borç alabilir, sistemin durumunu altüst edebilir ve parayı aynı saniyede geri iade edebilir.

Buradaki en ölümcül mimari hata, havuz payı (share) fiyatını veya varlık değerlemesini hesaplamak için balanceOf(address(this)) fonksiyonuna güvenmektir.

// KATASTROFİK MİMARİ HATA
function getSharePrice() public view returns (uint256) {
    // Pay fiyatı, doğrudan sözleşmedeki anlık token bakiyesine bağımlı
    return token.balanceOf(address(this)) / totalShares;
}

Eğer sözleşmeniz aynı token üzerinden Flash Loan çekilmesine izin veriyorsa, borç alan kişi fonları çektiği anda sözleşmenin bakiyesi neredeyse sıfıra çakılır. Tam o milisaniyede (yani onFlashLoan callback'inin içinde) protokolünüz başka işlemlerin (örneğin likidasyonlar veya ödül dağıtımları) tetiklenmesine izin veriyorsa, hesaplanan pay fiyatı tamamen manipüle edilmiş olur.

Saldırgan flash loan'ı çeker -> havuz bakiyesi dibi görür -> pay fiyatı yerle bir olur -> saldırgan yan bir cüzdanla havuz paylarını bedavadan biraz pahalıya kapatır -> flash loan borcunu iade eder -> havuz bakiyesi eski haline döner -> saldırgan elindeki payları normal fiyattan piyasaya boşaltır. Geçmiş olsun, havuz tamamen boşaltıldı.

Altın kural: Sistemin mantıksal durumunu kalıcı olarak değiştirmeden geçici olarak manipüle edilebilecek bakiyelere, yani balanceOf(address(this)) değerine, kritik ekonomik veya matematiksel hesaplamalarda asla sırtınızı dayamayın. Bunun yerine, yalnızca kontrollü deposit ve withdraw işlemleri esnasında güncellenen uint256 internalReserve gibi dahili bir muhasebe (internal accounting) yapısı kullanın.

Tamam, şimdi borsa altyapı güvenliğinden sorumlu bir herif olarak benim bile uykularımı kaçıran, kelimenin tam anlamıyla tüylerimi diken diken eden konulara gelelim. Henüz yüzlerce geliştirici tarafından tam olarak tecrübelenmemiş, basılmadık mayınların kol gezdiği şu nispeten yeni standartlardan konuşalım biraz.

5. ERC-4337 (Account Abstraction): Toplu İşlemler ve Paymaster Seviyesindeki Tuzaklar

Hesap Soyutlama (Account Abstraction) mevzusu çok fena bir şey, orası kesin. Klasik EOA cüzdanları geride bırakıp, direkt akıllı sözleşmeleri kullanıcı cüzdanı olarak kullanmaya başlıyoruz. Seed phrase saklama derdi bitiyor, social recovery (sosyal kurtarma) yapabiliyorsun ve Paymaster'lar sayesinde gas ücretini stablecoin ile ödeyebiliyorsun.

Gelgelelim, ERC-4337 entegrasyonu yapan bir protokol mimarı gözüyle baktığında, burası ucu bucağı görünmeyen çok spesifik saldırı vektörlerine kapı açıyor.

validateUserOp Fonksiyonundaki İmza Zafiyeti

ERC-4337 standartlarında cüzdanın özel validasyon mantığının kalbi validateUserOp fonksiyonudur. Görevi, işlem imzasını kontrol etmek ve duruma göre özel bir statü kodu dönmektir.

// Akıllı cüzdandaki validasyon mantığının basitleştirilmiş bir örneği
function validateUserOp(
    UserOperation calldata userOp,
    bytes32 userOpHash,
    uint256 missingAccountFunds
) external returns (uint256 validationData) {
    // Kritik hata: HERHANGİ BİR adresten gelen çağrıya öylece güveniyor muyuz?
    // Hayır, normalde Bundler bunu EntryPoint üzerinden çağırır. Ama bu kontrolü unutursak...
    require(msg.sender == entryPoint, "Only EntryPoint can trigger validation");
    
    // Kendi imza doğrulama mantığımız
    if (_verifySignature(userOp, userOpHash)) {
        // Doğrulama başarılıysa 0 dönüyoruz
        return 0; 
    }
    
    // Hata durumunda SIG_VALIDATION_FAILED (genelde 1) dönüyoruz
    return 1; 
}

Bi' dakika, buradaki asıl bit yeniğini fark ettin mi? ERC-4337 spesifikasyonuna göre, eğer imza doğrulaması başarısız olursa fonksiyon sakın revert etmemeli. EntryPoint'in (tüm orkestrayı yöneten ana kontrat) işlemin geçersiz olduğunu anlaması için özel olarak paketlenmiş bir hata kodu (error constant) dönmesi gerekiyor. Böylece EntryPoint cüzdandan gas ücreti kesmiyor ve işlemi direkt Bundler seviyesinde çöpe atıyor.

Eğer bir mimar olarak alışkanlıkla oraya require(isValid, "Invalid signature"); fırlatırsan, geçmiş olsun. İşlemler toplu halde (batch) gönderildiğinde, sert bir revert yüzünden patlayan tek bir işlem, Bundler tarafındaki tüm paketi olduğu gibi kilitler. Günün sonunda cüzdanın ya da Paymaster kontratın bundler'lar tarafından kara listeye (ban) alınır ve kullanıcıların ağa tek bir işlem bile gönderemez hale gelir. Validasyon mantığı atomik olmalı ve klasik Solidity kalıpları yerine kesinlikle ERC-4337'nin dönüş değeri matematiğine sadık kalmalıdır.

Paymaster'a Yönelik Saldırılar (Gas Drain)

Eğer DeFi protokolün bir Paymaster gibi davranıyorsa (örneğin kullanıcılar komisyonsuz trade yapabilsin diye gas ücretlerini sen fonluyorsan), validasyon aşamasını dış dünyadan tamamen izole etmek zorundasın.

Paymaster kontratının içinde validatePaymasterUserOp diye bir fonksiyon bulunur. Bu fonksiyonun içinde, bundler'ın işlemi simüle ettiği an ile işlemin bloğa dahil edildiği an arasında değişebilecek dinamik durumları (dynamic state) kullanmak **kesinlikle yasaktır**. Mesela, kullanıcıdan gas karşılığı kaç token keseceğini hesaplamak için validasyonun tam ortasında fiyat oracle'larını (Chainlink gibi) çağıramazsın.

Neden mi? Saldırgan bir işlem gönderir; simülasyon sırasında oracle düzgün bir fiyat gösterir ve validasyon tıkır tıkır geçer. Fakat işlem tam bloğa girecekken hacker, flash loan ya da hızlı bir trade ile oracle fiyatını manipüle eder. On-chain validasyon patlamaya başlar ama bundler bu işlemi çoktan işleme almış ve gas harcamıştır bile. Gas parası senin Paymaster kasandan çatır çatır eksilir, kullanıcı ise kuruş ödemez. Gas bakiyeni birkaç saat içinde dümdüz ederler.

6. ERC-4626 (Tokenized Vaults): İlk Depozitte Front-running (Inflation Attack)

ERC-4626; tokenize kasalar (staking, yield pool'ları, lending) için biçilmiş kaftan bir standarttır. deposit, mint, withdraw ve redeem fonksiyonlarını tek bir kalıba soktu. Bu harika bir şey çünkü artık Yearn gibi bir yield aggregator, yeni çıkan herhangi bir havuzu 5 dakikada platformuna entegre edebiliyor.

Ancak standardın matematiksel tasarımında, **Inflation Attack** (Varlık Enflasyonu Saldırısı) olarak bilinen sinsi bir saatli bomba gizli. Bu saldırı, havuzların ağa ilk deploy edildiği ve bakiyelerinin henüz sıfır olduğu o ilk savunmasız anı vuruyor.

Saldırının Mekaniği

Bir kullanıcı varlık (assets) yatırdığında karşılığında alacağı pay (shares) miktarı genellikle şu formülle hesaplanır:

$$\text{shares} = \frac{\text{assets} \times \text{totalShares}}{\text{totalAssets}}$$

Eğer havuz bomboşsa (totalShares == 0), varsayılan olarak durum shares == assets olur. Yani bire bir (1-to-1) oran çalışır.

Şimdi hacker'ın el çabukluğuna iyi bak:

  • Temiz bir kullanıcı, çiçeği burnunda ve henüz içi bomboş olan bir ERC-4626 havuzuna 1000 USDC yatırmak için deposit işlemini ateşler.
  • Hacker bu işlemi mempool'da yakalar ve gas ücretini kökleyerek önüne geçer (front-run). Havuza sadece 1 wei USDC yatırır. Havuz da ona karşılık tam 1 wei değerinde pay (shares) mint eder. Son durum: totalShares = 1, totalAssets = 1 olur.
  • Ardından, aynı atomik işlem içinde hacker, kasa kontratının adresine (deposit fonksiyonunu falan kullanmadan, düz bir ERC20 transfer yöntemiyle) yüklü miktarda—mesela 10.000 USDC—direkt transfer çeker.
  • Peki havuzun matematiğine ne oldu? totalShares hala 1 ama havuzdaki toplam varlık (totalAssets) bir anda 10.001 USDC'ye fırladı (direkt transfer kontrat bakiyesini şişirdi ama yeni pay basılmadı). Tek bir payın fiyatı uçuşa geçti.
  • Sonunda, bizim temiz kullanıcının 1000 USDC'lik işlemi sıraya girer ve çalışır. Kontrat yukarıdaki formülle pay hesaplamasını yapar:

    $$\text{shares} = \frac{1000 \times 1}{10\,001} = 0$$

    Solidity'deki tam sayı bölmesindeki aşağı yuvarlama (rounding down) mantığı yüzünden kullanıcıya tam olarak 0 pay düşer! Ama adamın 1000 USDC'si havuzun bakiyesine çoktan cuk oturmuştur.

  • Hacker elindeki o tek payı (1 wei shares) çekmek için withdraw fonksiyonunu çağırır ve kasada ne var ne yoksa götürür: Kendi 10.000 USDC'sini, baştaki 1 wei'yi ve ayıklanan kullanıcının çarpılan 1000 USDC'sini alıp kayıplara karışır.

Mimari Çözüm: Kendini bundan korumanın iki yolu var. Birincisi—havuz oluşturulurken sıfır adresine (zero address) zorunlu olarak "ölü pay" (dead shares) mint etmek (ilk 1000 wei payı orada kilitlemek, tıpkı Uniswap V2'deki mantık gibi). İkincisi ise—OpenZeppelin'in sanal ofset (virtual assets ve virtual shares) koruması içeren güncel kütüphanelerini kullanmak. Bu mekanizma, manipülasyonlar sırasında kesrin paydasının asla sıfıra veya bire düşmesine izin vermez.

Bak, şimdi bir üst seviyeye çıkalım. Token'lar, NFT'ler, permit'ler ve vault'lar hakkında konuştuk. Peki tüm bunları tek bir mimaride nasıl birleştirip entegrasyon sürecinde kafayı yemeyiz?

Yield aggregator veya cross-chain bridge gibi büyük bir sistem tasarlarken, bu standartların hepsiyle aynı anda uğraşman gerekiyor. İşte tam burada "vulnerability synergy" dediğimiz olay devreye giriyor; tek başlarına gayet güvenli olan iki özellik, bir araya geldiklerinde sistemde ölümcül bir açık yaratabiliyor.

7. Sistem Tasarımında Mimari Risk Matrisi

Gözünün önünde net bir tablo olması için şunu hazırladım. Bir sonraki "Architecture Review" toplantın için resmen check-list gibi kullanabilirsin. Notion'a kaydet veya çıktısını al, dursun.

ERC StandardıBaşlıca Gizli TehditMantıkta Nasıl Patlar?Tasarım Seviyesinde Çözüm
ERC-20Dönüş değeri yok / Standart dışı transferİşlemin revert olması veya hatayı sessizce yutmasıSadece SafeERC20 (OpenZeppelin) kullan.
ERC-20 (Weird)Fee-on-Transfer / Bakiye değişimi (Rebase)Sistemin kendi defteriyle kontratın gerçek bakiyesinin uyumsuzluğuamount değerine güvenmek yerine balanceAfter - balanceBefore farkını hesapla.
ERC-721 / 1155onERC...Received hook'ları üzerinden kontrolü çalmaInternal state güncellenmeden reentrancy (tekrarlı giriş)Checks-Effects-Interactions desenine sadık kal + nonReentrant kullan.
ERC-2612Mempool'da imza front-running'iGerçek kullanıcı için servis dışı kalma (DoS)permit çağrılarını try/catch bloklarına al.
ERC-3156Geçici likidite boşaltma (Flash Loan)balanceOf'a bağlı spot fiyatların manipülasyonuDoğrudan bakiye yerine internal tutulan rezerv değişkenlerini (internal reserves) kullan.
ERC-4337Batch validasyonunda sert revertBundler'lar tarafından kontratın veya cüzdanın banlanmasırequire ile işlemi patlatmak yerine "sihirli" hata sabitleri döndür.
ERC-4626Inflation Attack (İlk deposit saldırısı)Hisse paylarının sıfıra yuvarlanması, ilk yatırımcının fonunun çalınmasıBaşlatma sırasında address(0)'a "ölü pay" mint'le veya sanal offset'ler kullan.

8. CTO'nun Not Defteri: Güvenli Mimari İçin Altın Kurallar

Bak, CTO koltuğunda geçirdiğim üç yılda şunu öğrendim: En güvenli kod, yazılmamış olan koddur. Mimari şeman ne kadar karmaşıksa, o kadar çok gizli bağlantı vardır ve sabah kahveni içerken hiç aklına gelmeyen bir açık, hackathon'dan çıkan bir dâhinin gözüne takılır.

Projeni Rekt News manşetlerinden kurtaracak sadece üç kural verecek olsam, bunlar şunlar olurdu:

  • Dış kontratlara asla güvenme. Dünyanın en popüler token'ı bile olsa. Yarın adminleri proxy'yi günceller, blacklist ekler ve sistemin kilitlenir. Kodu, her dış token ağdaki en tehlikeli ve öngörülemez aktörmüş gibi yaz.
  • Önce state'i güncelle, sonra transfer et. Bunu tekrarlamaktan bıkmayacağım. Smart contract derslerinin ilk gününde öğrettikleri temel şey bu ama millet inatla mapping'deki sayıları güncellemeden token gönderiyor. Önce kontrat içinde yetkileri veya bakiyeleri fixle, blockchain'e işle ve en son adımda transfer, safeMint veya call işlemlerini yap.
  • Matematiğini dış bakiyeden izole et. EVM'deki kontrat bakiyen herkese açık ve manipüle etmesi çok kolay. İsteyen herkes flash loan ile milyon dolarları gönderip sistemi bozabilir ya da selfdestruct ile kontratına zorla ETH atabilir. Ödül mantığın veya hisse fiyatın kontratın içindeki token miktarına bağlıysa, zaten kaybetmişsin demektir. Internal muhaseben, uçağın kokpiti gibi izole olmalı.

İşte böyle; mimarın, standartların bu kritik noktalarında zamanında "yama" yapmazsa, en iyi dev ekibinin bile emekleri çöp olabilir.

Oleg Filatov

As the Chief Technology Officer at EXMON Exchange, I focus on building secure, scalable crypto infrastructure and developing systems that protect user assets and privacy.

With over 15 years in cybersecurity, blockchain, and DevOps, I specialize in smart contract analysis, threat modeling, and secure system architecture.

At EXMON Academy, I share practical insights from real-world...

...

Yorumunuzu paylaşın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar işaretlendi *