paint-brush
Intelligente Vertragssicherheit in Soliditätvon@rareskills
6,103 Lesungen
6,103 Lesungen

Intelligente Vertragssicherheit in Solidität

von RareSkills53m2023/05/16
Read on Terminal Reader
Read this story w/o Javascript

Zu lang; Lesen

Ein Sicherheitsproblem in Solidity besteht darin, dass sich Smart Contracts nicht wie vorgesehen verhalten. Dies kann in vier große Kategorien eingeteilt werden: Gelder, die gestohlen werden, Gelder, die in einem Vertrag eingesperrt oder eingefroren werden. Menschen erhalten weniger Belohnungen als erwartet (Belohnungen werden verzögert oder reduziert). Es ist nicht möglich, eine umfassende Liste aller Dinge zu erstellen, die schief gehen können.

People Mentioned

Mention Thumbnail
featured image - Intelligente Vertragssicherheit in Solidität
RareSkills HackerNoon profile picture
0-item
1-item
2-item


Eine Liste von Solidity-Schwachstellen

Dieser Artikel dient als Minikurs zur Sicherheit intelligenter Verträge und bietet eine ausführliche Liste der Probleme und Schwachstellen, die bei intelligenten Verträgen von Solidity häufig auftreten. Dies sind die Art von Problemen, die bei einem Qualitätsaudit auftreten können.


Ein Sicherheitsproblem in Solidity besteht darin, dass sich Smart Contracts nicht wie vorgesehen verhalten.


Dies lässt sich in vier große Kategorien einteilen:

  • Gelder werden gestohlen

  • Gelder werden in einem Vertrag gesperrt oder eingefroren

  • Menschen erhalten weniger Belohnungen als erwartet (Belohnungen werden verzögert oder reduziert)

  • Menschen erhalten mehr Belohnungen als erwartet (was zu Inflation und Abwertung führt)


Es ist nicht möglich, eine umfassende Liste aller Dinge zu erstellen, die schief gehen können. Doch ebenso wie bei der traditionellen Softwareentwicklung häufig Schwachstellen wie SQL-Injection, Pufferüberläufe und Cross-Site-Scripting auftreten, weisen Smart Contracts wiederkehrende Anti-Patterns auf, die dokumentiert werden können.


Betrachten Sie diesen Leitfaden eher als Referenz. Es ist nicht möglich, alle Konzepte im Detail zu besprechen, ohne daraus ein Buch zu machen (Vorsicht: Dieser Artikel ist mehr als 10.000 Wörter lang, Sie können ihn also gerne mit einem Lesezeichen versehen und in Abschnitten lesen). Es dient jedoch als Liste dessen, worauf man achten und was man studieren sollte. Wenn sich ein Thema fremd anfühlt, sollte dies als Indikator dafür dienen, dass es sich lohnt, Zeit in das Üben der Identifizierung dieser Schwachstellenklasse zu investieren.

Voraussetzungen

Dieser Artikel setzt grundlegende Kenntnisse in Solidity voraus. Wenn Sie neu bei Solidity sind, sehen Sie sich bitte unser kostenloses Solidity-Tutorial an.

Wiedereintritt

Wir haben ausführlich über den Wiedereintritt in Smart Contracts geschrieben, deshalb wiederholen wir es hier nicht. Aber hier ist eine kurze Zusammenfassung:


Immer wenn ein Smart Contract die Funktion eines anderen Smart Contracts aufruft, Ether an ihn sendet oder einen Token an ihn überträgt, besteht die Möglichkeit eines Wiedereintritts.


  • Bei der Übertragung von Ether wird die Fallback- oder Empfangsfunktion des empfangenden Vertrags aufgerufen. Dadurch wird die Kontrolle an den Empfänger übergeben.
  • Einige Token-Protokolle benachrichtigen den empfangenden Smart Contract, dass er den Token erhalten hat, indem sie eine vorgegebene Funktion aufrufen. Dadurch wird der Kontrollfluss an diese Funktion übergeben.
  • Wenn ein angreifender Vertrag die Kontrolle übernimmt, muss er nicht dieselbe Funktion aufrufen, die die Kontrolle übergeben hat. Es könnte eine andere Funktion im intelligenten Vertrag des Opfers (funktionsübergreifender Wiedereintritt) oder sogar einen anderen Vertrag (vertragsübergreifender Wiedereintritt) aufrufen.
  • Ein schreibgeschützter Wiedereintritt erfolgt, wenn auf eine Ansichtsfunktion zugegriffen wird, während sich der Vertrag in einem Zwischenzustand befindet.


Obwohl der Wiedereintritt wahrscheinlich die bekannteste Schwachstelle bei Smart Contracts ist, macht er nur einen kleinen Prozentsatz der Hacks aus, die in freier Wildbahn passieren. Der Sicherheitsforscher Pascal Caversaccio (pcaveraccio) führt eine aktuelle Github- Liste der Wiedereintrittsangriffe . Bis April 2023 wurden in diesem Repository 46 Wiedereintrittsangriffe dokumentiert.

Zugangskontrolle

Es scheint ein einfacher Fehler zu sein, aber es passiert überraschend oft, dass man vergisst, Beschränkungen dafür festzulegen, wer eine sensible Funktion aufrufen kann (wie z. B. das Abheben von Ether oder den Wechsel des Besitzers).


Auch wenn ein Modifikator vorhanden ist, gab es Fälle, in denen der Modifikator nicht korrekt implementiert wurde, wie zum Beispiel im folgenden Beispiel, wo die require-Anweisung fehlte.

 // DO NOT USE! modifier onlyMinter { minters[msg.sender] == true_; }

Der obige Code ist ein echtes Beispiel aus diesem Audit: https://code4rena.com/reports/2023-01-rabbithole/#h-01-bad-implementation-in-minter-access-control-for-rabbitholereceipt-and- Rabbitholetickets-Verträge


Auch hier kann die Zugangskontrolle schiefgehen

 function claimAirdrop(bytes32 calldata proof[]) { bool verified = MerkleProof.verifyCalldata(proof, merkleRoot, keccak256(abi.encode(msg.sender))); require(verified, "not verified"); require(alreadyClaimed[msg.sender], "already claimed"); _transfer(msg.sender, AIRDROP_AMOUNT); }

In diesem Fall wird „alreadyClaimed“ niemals auf „true“ gesetzt, sodass der Antragsteller die Funktion mehrmals aufrufen kann.

Beispiel aus dem wirklichen Leben: Trader-Bot ausgenutzt

Ein relativ aktuelles Beispiel für unzureichende Zugriffskontrolle war eine ungeschützte Funktion zum Empfang von Flashloans durch einen Trading-Bot (die den Namen 0xbad trug, da die Adresse mit dieser Sequenz begann). Es brachte einen Gewinn von über einer Million Dollar ein, bis eines Tages ein Angreifer bemerkte, dass jede Adresse die Flashloan-Empfangsfunktion aufrufen konnte, nicht nur der Flashloan-Anbieter.


Wie bei Trading-Bots üblich, wurde der Smart-Contract-Code zur Ausführung der Trades nicht verifiziert, der Angreifer entdeckte die Schwachstelle aber trotzdem. Weitere Infos in der rekt-Berichterstattung .

Unsachgemäße Eingabevalidierung

Wenn es bei der Zugriffskontrolle darum geht, zu kontrollieren, wer eine Funktion aufruft, geht es bei der Eingabevalidierung darum, zu kontrollieren, womit der Vertrag aufgerufen wird.


Dies liegt in der Regel daran, dass vergessen wird, die richtigen „require“-Anweisungen einzufügen.

Hier ist ein rudimentäres Beispiel:

 contract UnsafeBank { mapping(address => uint256) public balances; // allow depositing on other's behalf function deposit(address for) public payable { balances += msg.value; } function withdraw(address from, uint256 amount) public { require(balances[from] <= amount, "insufficient balance"); balances[from] -= amount; msg.sender.call{value: amout}(""); } }

Der obige Vertrag stellt zwar sicher, dass Sie nicht mehr abheben, als auf Ihrem Konto vorhanden ist, hindert Sie jedoch nicht daran, von einem beliebigen Konto abzuheben.

Beispiel aus dem wirklichen Leben: Sushiswap

Sushiswap erlebte einen Hack dieser Art, weil einer der Parameter einer externen Funktion nicht bereinigt wurde.

https://twitter.com/peckshield/status/1644907207530774530

Was ist der Unterschied zwischen unsachgemäßer Zugriffskontrolle und unsachgemäßer Eingabevalidierung?

Eine unsachgemäße Zugriffskontrolle bedeutet, dass msg.sender nicht über ausreichende Einschränkungen verfügt. Eine unsachgemäße Eingabevalidierung bedeutet, dass die Argumente der Funktion nicht ausreichend bereinigt werden. Es gibt auch eine Umkehrung dieses Anti-Musters: einem Funktionsaufruf zu starke Einschränkungen auferlegen.

Übermäßige Funktionseinschränkung

Eine übermäßige Validierung bedeutet wahrscheinlich, dass keine Gelder gestohlen werden, aber es könnte bedeuten, dass Gelder an den Vertrag gebunden werden. Es ist auch nicht gut, zu viele Sicherheitsvorkehrungen zu treffen.

Beispiel aus dem wirklichen Leben: Akutars NFT

Einer der bekanntesten Vorfälle war der Akutars NFT, bei dem Eth im Wert von 34 Millionen Dollar im Smart Contract steckte und nicht mehr abgehoben werden konnte.


Der Vertrag enthielt einen gut gemeinten Mechanismus, um den Vertragsinhaber am Rücktritt zu hindern, bis alle Rückerstattungen für Zahlungen über dem niederländischen Auktionspreis erfolgt waren. Aufgrund eines Fehlers, der im unten verlinkten Twitter-Thread dokumentiert ist, war der Eigentümer jedoch nicht in der Lage, das Geld abzuheben.

https://twitter.com/0xInuarashi/status/1517674505975394304

Die richtige Balance finden

Sushiswap gab nicht vertrauenswürdigen Benutzern zu viel Macht, und der Akutars NFT gab dem Administrator zu wenig Macht. Bei der Gestaltung intelligenter Verträge muss eine subjektive Beurteilung darüber getroffen werden, wie viel Freiheit jede Benutzerklasse haben muss, und diese Entscheidung kann nicht automatisierten Tests und Tools überlassen werden. Es gibt erhebliche Kompromisse bei Dezentralisierung, Sicherheit und UX, die berücksichtigt werden müssen.


Für den Smart-Contract-Programmierer ist es ein wichtiger Teil des Entwicklungsprozesses, explizit festzulegen, was Benutzer mit bestimmten Funktionen tun können und was nicht.

Wir werden später noch einmal auf das Thema übermächtige Administratoren zurückkommen.

Bei der Sicherheit geht es oft darum, die Art und Weise zu verwalten, wie Geld aus dem Vertrag austritt

Wie in der Einleitung erwähnt, gibt es hauptsächlich vier Möglichkeiten, wie Smart Contracts gehackt werden:


  • Geld gestohlen
  • Geld eingefroren
  • Unzureichende Belohnungen
  • Übermäßige Belohnungen


„Geld“ bedeutet hier alles von Wert, wie zum Beispiel Token, nicht nur Kryptowährung. Bei der Codierung oder Prüfung eines Smart Contracts muss sich der Entwickler darüber im Klaren sein, auf welche Weise der Wert in den Vertrag ein- und aus ihm herausfließen soll. Die oben aufgeführten Probleme sind die Hauptursachen für das Hacken intelligenter Verträge, es gibt jedoch noch viele andere Grundursachen, die zu schwerwiegenden Problemen führen können, die im Folgenden dokumentiert werden.

Doppelte Abstimmung oder msg.sender-Spoofing

Die Verwendung von Vanilla-ERC20-Tokens oder NFTs als Tickets zur Abstimmungsabwägung ist unsicher, da Angreifer mit einer Adresse abstimmen, die Token an eine andere Adresse übertragen und von dieser Adresse aus erneut abstimmen können.

Hier ist ein Minimalbeispiel:

 // A malicious voter can simply transfer their tokens to // another address and vote again. contract UnsafeBallot { uint256 public proposal1VoteCount; uint256 public proposal2VoteCount; IERC20 immutable private governanceToken; constructor(IERC20 _governanceToken) { governanceToken = _governanceToken; } function voteFor1() external notAlreadyVoted { proposal1VoteCount += governanceToken.balanceOf(msg.sender); } function voteFor2() external notAlreadyVoted { proposal2VoteCount += governanceToken.balanceOf(msg.sender); } // prevent the same address from voting twice, // however the attacker can simply // transfer to a new address modifier notAlreadyVoted { require(!alreadyVoted[msg.sender], "already voted"); _; alreadyVoted[msg.sender] = true; } }

Um diesen Angriff zu verhindern, sollten ERC20 Snapshot oder ERC20 Votes verwendet werden. Durch die Momentaufnahme eines Zeitpunkts in der Vergangenheit können die aktuellen Token-Salden nicht manipuliert werden, um illegale Stimmrechte zu erlangen.

Flashloan-Governance-Angriffe

Die Verwendung eines ERC20- Tokens mit Snapshot- oder Abstimmungsfunktion löst das Problem jedoch nicht vollständig, wenn jemand einen Kurzkredit aufnehmen kann, um sein Guthaben vorübergehend zu erhöhen, und dann in derselben Transaktion einen Snapshot seines Guthabens erstellen kann. Wenn dieser Schnappschuss für die Abstimmung verwendet wird, stehen ihnen unverhältnismäßig viele Stimmen zur Verfügung.


Bei einem Flashloan wird einer Adresse eine große Menge Ether oder Token verliehen, die jedoch zurückerstattet wird, wenn das Geld nicht in derselben Transaktion zurückgezahlt wird.

 contract SimpleFlashloan { function borrowERC20Tokens() public { uint256 before = token.balanceOf(address(this)); // send tokens to the borrower token.transfer(msg.sender, amount); // hand control back to the borrower to // let them do something IBorrower(msg.sender).onFlashLoan(); // require that the tokens got returned require(token.balanceOf(address(this) >= before); } }

Ein Angreifer kann einen Flashloan nutzen, um plötzlich viele Stimmen zu gewinnen, Vorschläge zu seinen Gunsten auszusprechen und/oder etwas Böswilliges zu tun.

Flashloan-Preisangriffe

Dies ist wohl der häufigste (oder zumindest bekannteste) Angriff auf DeFi, der Verluste in Höhe von Hunderten Millionen Dollar verursacht. Hier ist eine Liste der bekanntesten.


Der Preis eines Vermögenswerts auf der Blockchain wird häufig als aktueller Wechselkurs zwischen Vermögenswerten berechnet. Wenn ein Kontrakt beispielsweise derzeit 1 USDC für 100 k9coin handelt, dann könnte man sagen, dass k9coin einen Preis von 0,01 USDC hat. Allerdings bewegen sich die Preise im Allgemeinen als Reaktion auf den Kauf- und Verkaufsdruck, und Schnellkredite können einen massiven Kauf- und Verkaufsdruck erzeugen.


Bei der Abfrage eines anderen Smart-Vertrags zum Preis eines Vermögenswerts muss der Entwickler sehr vorsichtig sein, da er davon ausgeht, dass der von ihm aufgerufene Smart-Vertrag immun gegen Flash-Darlehensmanipulationen ist.

Umgehen der Vertragsprüfung

Sie können „überprüfen“, ob es sich bei einer Adresse um einen Smart Contract handelt, indem Sie sich die Bytecodegröße ansehen. Externe Konten (normale Wallets) haben keinen Bytecode. Hier sind einige Möglichkeiten, dies zu tun

 import "@openzeppelin/contracts/utils/Address.sol" contract CheckIfContract { using Address for address; function addressIsContractV1(address _a) { return _a.code.length == 0; } function addressIsContractV2(address _a) { // use the openzeppelin libraryreturn _a.isContract(); } }


Dies hat jedoch einige Einschränkungen

  • Wenn ein Vertrag einen externen Aufruf von einem Konstruktor durchführt, ist seine Bytecodegröße offensichtlich Null, da der Smart-Contract-Bereitstellungscode den Laufzeitcode noch nicht zurückgegeben hat
  • Der Speicherplatz ist derzeit möglicherweise leer, aber ein Angreifer weiß möglicherweise, dass er dort in Zukunft mithilfe von create2 einen Smart Contract bereitstellen kann


Im Allgemeinen ist die Überprüfung, ob es sich bei einer Adresse um einen Vertrag handelt, normalerweise (aber nicht immer) ein Antimuster. Multisignatur-Wallets sind selbst intelligente Verträge, und alles, was Multisignatur-Wallets beschädigen könnte, beeinträchtigt die Zusammensetzbarkeit.


Die Ausnahme hiervon ist die Prüfung, ob das Ziel ein Smart Contract ist, bevor ein Transfer-Hook aufgerufen wird. Mehr dazu später.

tx.origin

Es gibt selten einen guten Grund, tx.origin zu verwenden. Wenn tx.origin zur Identifizierung des Absenders verwendet wird, ist ein Man-in-the-Middle-Angriff möglich. Wenn der Benutzer dazu verleitet wird, einen böswilligen Smart-Vertrag aufzurufen, kann der Smart-Vertrag alle Befugnisse nutzen, die tx.origin hat, um Chaos anzurichten.


Betrachten Sie die folgende Übung und die Kommentare über dem Code.

 contract Phish { function phishingFunction() public { // this fails, because this contract does not have approval/allowance token.transferFrom(msg.sender, address(this), token.balanceOf(msg.sender)); // this also fails, because this creates approval for the contract,// not the wallet calling this phishing function token.approve(address(this), type(uint256).max); } }


Dies bedeutet nicht, dass Sie sicher sind, beliebige Smart Contracts aufzurufen. In den meisten Protokollen ist jedoch eine Sicherheitsebene eingebaut, die umgangen wird, wenn tx.origin zur Authentifizierung verwendet wird.

Manchmal sehen Sie möglicherweise Code, der so aussieht:

 require(msg.sender == tx.origin, "no contracts");


Wenn ein Smart Contract einen anderen Smart Contract aufruft, ist msg.sender der Smart Contract und tx.origin die Wallet des Benutzers, was einen zuverlässigen Hinweis darauf gibt, dass der eingehende Anruf von einem Smart Contract stammt. Dies gilt auch dann, wenn der Aufruf vom Konstruktor erfolgt.


Meistens ist dieses Designmuster keine gute Idee. Multisignatur-Wallets und Wallets von EIP 4337 können nicht mit einer Funktion interagieren, die diesen Code enthält. Dieses Muster ist häufig bei NFT-Mints zu beobachten, wo man davon ausgehen kann, dass die meisten Benutzer ein traditionelles Wallet verwenden. Da die Kontoabstraktion jedoch immer beliebter wird, wird dieses Muster mehr behindern als helfen.

Gas Griefing oder Denial of Service

Ein Trauerangriff bedeutet, dass der Hacker versucht, anderen Menschen „Kummer zu bereiten“, auch wenn sie dadurch keinen wirtschaftlichen Gewinn erzielen.


Ein Smart Contract kann in böswilliger Absicht das gesamte an ihn weitergeleitete Gas verbrauchen, indem er in eine Endlosschleife gerät. Betrachten Sie das folgende Beispiel:

 contract Mal { fallback() external payable { // infinite loop uses up all the gas while (true) { } } }


Wenn ein anderer Vertrag Ether an eine Liste von Adressen wie die folgende verteilt:

 contract Distribute { funtion distribute(uint256 total) public nonReentrant { for (uint i; i < addresses.length; ) { (bool ok, ) addresses.call{value: total / addresses.length}(""); // ignore ok, if it reverts we move on // traditional gas saving trick for for loops unchecked { ++i; } } } }


Dann kehrt die Funktion zurück, wenn sie Ether an Mal sendet. Der Aufruf im obigen Code leitet 63/64 des verfügbaren Gases weiter, sodass wahrscheinlich nicht genug Gas vorhanden ist, um den Vorgang abzuschließen, wenn nur noch 1/64 des Gases übrig ist.


Ein Smart Contract kann ein großes Speicherarray zurückgeben, das viel Gas verbraucht

Betrachten Sie das folgende Beispiel

 function largeReturn() public { // result might be extremely long! (book ok, bytes memory result) = otherContract.call(abi.encodeWithSignature("foo()")); require(ok, "call failed"); }

Speicherarrays verbrauchen nach 724 Bytes eine quadratische Menge an Gas, sodass eine sorgfältig ausgewählte Rückgabedatengröße den Aufrufer verärgern kann.


Auch wenn die Variable result nicht verwendet wird, wird sie dennoch in den Speicher kopiert. Wenn Sie die Rückgabegröße auf einen bestimmten Betrag beschränken möchten, können Sie die Montage verwenden

 function largeReturn() public { assembly { let ok := call(gas(), destinationAddress, value, dataOffset, dataSize, 0x00, 0x00); // nothing is copied to memory until you // use returndatacopy() } }

Das Löschen von Arrays, zu denen andere etwas hinzufügen können, stellt ebenfalls einen Denial-of-Service-Angriff dar

Obwohl das Löschen von Speicher ein gaseffizienter Vorgang ist, verursacht es dennoch Nettokosten. Wenn ein Array zu lang wird, kann es nicht mehr gelöscht werden. Hier ist ein Minimalbeispiel

 contract VulnerableArray { address[] public stuff; function addSomething(address something) public { stuff.push(something); } // if stuff is too long, this will become undeletable due to // the gas cost function deleteEverything() public onlyOwner { delete stuff; } }

ERC777, ERC721 und ERC1155 können ebenfalls Trauervektoren sein

Wenn ein Smart Contract Token mit Transfer-Hooks überträgt, kann ein Angreifer einen Vertrag einrichten, der das Token nicht akzeptiert (entweder verfügt er nicht über eine onReceive-Funktion oder er programmiert die Funktion so, dass sie zurückgesetzt wird). Dadurch wird der Token unübertragbar und die gesamte Transaktion wird rückgängig gemacht.


Berücksichtigen Sie vor der Verwendung von „safeTransfer“ oder „transfer“ die Möglichkeit, dass der Empfänger das Zurücksetzen der Transaktion erzwingen könnte.

 contract Mal is IERC721Receiver, IERC1155Receiver, IERC777Receiver { // this will intercept any transfer hook fallback() external payable { // infinite loop uses up all the gaswhile (true) { } } // we could also selectively deny transactions function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4) { if (wakeUpChooseViolence()) { revert(); } else { return IERC721Receiver.onERC721Received.selector; } } }

Unsicherer Zufall

Derzeit ist es nicht möglich, mit einer einzelnen Transaktion auf der Blockchain sicher Zufälligkeiten zu erzeugen. Blockchains müssen vollständig deterministisch sein, sonst wären verteilte Knoten nicht in der Lage, einen Konsens über den Zustand zu erzielen. Da sie vollständig deterministisch sind, kann jede „zufällige“ Zahl vorhergesagt werden. Die folgende Würfelfunktion kann ausgenutzt werden.


 contract UnsafeDice { function randomness() internal returns (uint256) { return keccak256(abi.encode(msg.sender, tx.origin, block.timestamp, tx.gasprice, blockhash(block.number - 1); } // our dice can land on one of {0,1,2,3,4,5}function rollDice() public payable { require(msg.value == 1 ether); if (randomness() % 6) == 5) { msg.sender.call{value: 2 ether}(""); } } } contract ExploitDice { function randomness() internal returns (uint256) { return keccak256(abi.encode(msg.sender, tx.origin, block.timestamp, tx.gasprice, blockhash(block.number - 1); } function betSafely(IUnsafeDice game) public payable { if (randomness % 6) == 5)) { game.betSafely{value: 1 ether}() } // else don't do anything } }


Es spielt keine Rolle, wie Sie den Zufall erzeugen, denn ein Angreifer kann ihn genau reproduzieren. Das Einfügen weiterer „Entropiequellen“ wie msg.sender, Zeitstempel usw. hat keine Auswirkung, da der Smart Contract zwei davon messen kann.

Die Verwendung des Chainlink Randomness Oracle ist falsch

Chainlink ist eine beliebte Lösung, um sichere Zufallszahlen zu erhalten. Dies geschieht in zwei Schritten. Zuerst senden die Smart Contracts eine Zufallsanfrage an das Orakel, einige Blöcke später antwortet das Orakel mit einer Zufallszahl.


Da ein Angreifer die Zukunft nicht vorhersagen kann, kann er die Zufallszahl nicht vorhersagen.

Es sei denn, der Smart Contract nutzt das Orakel falsch.


  • Der Smart Contract, der Zufälligkeit anfordert, darf nichts tun, bis die Zufallszahl zurückgegeben wird. Andernfalls kann ein Angreifer den Mempool auf das Orakel überwachen, das die Zufälligkeit zurückgibt, und das Orakel an vorderster Front ausführen, da er weiß, wie hoch die Zufallszahl sein wird.
  • Die Zufallsorakel selbst könnten versuchen, Ihre Anwendung zu manipulieren. Sie können ohne Zustimmung anderer Knoten keine Zufallszahlen auswählen, sie können jedoch Zufallszahlen zurückhalten und neu anordnen, wenn Ihre Anwendung mehrere gleichzeitig anfordert.
  • Die Endgültigkeit ist bei Ethereum oder den meisten anderen EVM-Ketten nicht sofort gegeben. Nur weil ein Block der aktuellste ist, heißt das nicht, dass er nicht unbedingt auch so bleiben wird. Dies wird als „Kettenreorganisation“ bezeichnet. Tatsächlich kann die Kette mehr als nur den letzten Block verändern. Dies wird als „Reorganisationstiefe“ bezeichnet. Etherscan meldet Neuorganisationen für verschiedene Ketten, zum Beispiel Ethereum-Neuorganisationen und Polygon-Neuorganisationen. Reorgs können auf Polygon bis zu 30 oder mehr Blöcke tief sein, sodass das Warten auf weniger Blöcke die Anwendung angreifbar machen kann (dies kann sich ändern, wenn zk-evm zum Standardkonsens auf Polygon wird, da die Endgültigkeit mit der von Ethereum übereinstimmt, aber dies ist eine Zukunftsprognose , keine Tatsache über die Gegenwart).

Abrufen veralteter Daten von einem Preis-Oracle

Es gibt kein SLA (Service Level Agreement) für Chainlink, um seine Preisorakel innerhalb eines bestimmten Zeitrahmens auf dem neuesten Stand zu halten. Wenn die Kette stark überlastet ist (z. B. als die Mint Yuga Labs Otherside Ethereum so weit überwältigte , dass keine Transaktionen mehr durchgeführt wurden), könnten sich die Preisaktualisierungen verzögern.


Ein intelligenter Vertrag, der ein Preisorakel verwendet, muss explizit prüfen, ob die Daten nicht veraltet sind, d. h. ob sie kürzlich innerhalb eines bestimmten Schwellenwerts aktualisiert wurden. Andernfalls kann sie keine verlässliche Preisentscheidung treffen.


Erschwerend kommt hinzu, dass das Orakel den Preis möglicherweise nicht aktualisiert, um Gas zu sparen, wenn sich der Preis nicht über einen Abweichungsschwellenwert hinaus ändert. Dies könnte sich also darauf auswirken, welcher Zeitschwellenwert als „veraltet“ gilt.


Es ist wichtig, das SLA eines Orakels zu verstehen, auf dem ein Smart Contract basiert.

Sich nur auf ein Orakel verlassen

Egal wie sicher ein Orakel scheint, in der Zukunft kann ein Angriff entdeckt werden. Der einzige Schutz dagegen besteht darin, mehrere unabhängige Orakel zu verwenden.

Generell ist es schwierig, Orakel richtig hinzubekommen

Die Blockchain kann ziemlich sicher sein, aber um Daten überhaupt in die Kette zu übertragen, ist eine Art Off-Chain-Vorgang erforderlich, der auf alle Sicherheitsgarantien verzichtet, die Blockchains bieten. Auch wenn Orakel ehrlich bleiben, kann ihre Datenquelle manipuliert werden. Beispielsweise kann ein Orakel zuverlässig Preise von einer zentralen Börse melden, diese können jedoch durch große Kauf- und Verkaufsaufträge manipuliert werden. Ebenso sind Orakel, die auf Sensordaten oder einer Web2-API angewiesen sind, traditionellen Hackerangriffen ausgesetzt.


Eine gute Smart-Contract-Architektur vermeidet den Einsatz von Orakeln nach Möglichkeit gänzlich.

Gemischte Buchhaltung

Betrachten Sie den folgenden Vertrag

 contract MixedAccounting { uint256 myBalance; function deposit() public payable { myBalance = myBalance + msg.value; } function myBalanceIntrospect() public view returns (uint256) { return address(this).balance; } function myBalanceVariable() public view returns (uint256) { return myBalance; } function notAlwaysTrue() public view returns (bool) { return myBalanceIntrospect() == myBalanceVariable(); } }


Der obige Vertrag verfügt nicht über eine Empfangs- oder Fallback-Funktion, daher wird die direkte Übertragung von Ether dorthin zurückgesetzt. Ein Vertrag kann jedoch durch Selbstzerstörung gewaltsam Ether dorthin schicken.


In diesem Fall ist myBalanceIntrospect() größer als myBalanceVariable(). Die Ether-Buchhaltungsmethode ist in Ordnung, aber wenn Sie beide verwenden, weist der Vertrag möglicherweise ein inkonsistentes Verhalten auf.


Gleiches gilt für ERC20-Token.

 contract MixedAccountingERC20 { IERC20 token; uint256 myTokenBalance; function deposit(uint256 amount) public { token.transferFrom(msg.sender, address(this), amount); myTokenBalance = myTokenBalance + amount; } function myBalanceIntrospect() public view returns (uint256) { return token.balanceOf(address(this)); } function myBalanceVariable() public view returns (uint256) { return myTokenBalance; } function notAlwaysTrue() public view returns (bool) { return myBalanceIntrospect() == myBalanceVariable(); } }

Auch hier können wir nicht davon ausgehen, dass myBalanceIntrospect() und myBalanceVariable() immer denselben Wert zurückgeben. Es ist möglich, ERC20-Token direkt an MixedAccountingERC20 zu übertragen, wobei die Einzahlungsfunktion umgangen wird und die Variable myTokenBalance nicht aktualisiert wird.


Bei der Überprüfung der Bilanzen durch Selbstbeobachtung sollten strikte Gleichheitsprüfungen vermieden werden, da die Bilanz von einem Außenstehenden nach Belieben geändert werden kann.

Behandeln Sie kryptografische Beweise wie Passwörter

Dies ist keine Eigenart von Solidity, sondern eher ein häufiges Missverständnis unter Entwicklern darüber, wie man Kryptografie verwendet, um Adressen besondere Privilegien zu erteilen. Der folgende Code ist unsicher

 contract InsecureMerkleRoot { bytes32 merkleRoot; function airdrop(bytes[] calldata proof, bytes32 leaf) external { require(MerkleProof.verifyCalldata(proof, merkleRoot, leaf), "not verified"); require(!alreadyClaimed[leaf], "already claimed airdrop"); alreadyClaimed[leaf] = true; mint(msg.sender, AIRDROP_AMOUNT); } }


Dieser Code ist aus drei Gründen unsicher:

  1. Jeder, der die Adressen kennt, die für den Airdrop ausgewählt werden, kann den Merkle-Baum nachbilden und einen gültigen Beweis erstellen.
  2. Das Blatt ist nicht gehasht. Ein Angreifer kann ein Blatt einreichen, das der Merkle-Wurzel entspricht, und die Anforderungsanweisung umgehen.
  3. Selbst wenn die beiden oben genannten Probleme behoben sind, kann jemand, sobald er einen gültigen Nachweis einreicht, an vorderster Front stehen.


Kryptografische Beweise (Merkle-Bäume, Signaturen usw.) müssen an msg.sender gebunden werden, was ein Angreifer nicht manipulieren kann, ohne den privaten Schlüssel zu erhalten.

Die Solidität wird nicht auf die endgültige uint-Größe hochgestuft

 function limitedMultiply(uint8 a, uint8 b) public pure returns (uint256 product) { product = a * b; }

Obwohl „product“ eine uint256 -Variable ist, darf das Multiplikationsergebnis nicht größer als 255 sein, da sonst der Code zurückgesetzt wird.


Dieses Problem kann durch ein individuelles Upcasting jeder Variablen gemildert werden.

 function unlimitedMultiply(uint8 a, uint8 b) public pure returns (uint256 product) { product = uint256(a) * uint256(b); }

Eine solche Situation kann auftreten, wenn in einer Struktur gepackte Ganzzahlen multipliziert werden. Dies sollten Sie beachten, wenn Sie kleine Werte multiplizieren, die in einer Struktur verpackt sind

 struct Packed { uint8 time; uint16 rewardRate } //... Packed p; p.time * p.rewardRate; // this might revert!

Das Downcasting der Solidität wird bei einem Überlauf nicht wiederhergestellt

Solidity prüft nicht, ob es sicher ist, eine Ganzzahl in eine kleinere umzuwandeln. Sofern keine Geschäftslogik gewährleistet, dass das Downcasting sicher ist, sollte eine Bibliothek wie SafeCast verwendet werden.

 function test(int256 value) public pure returns (int8) { return int8(value + 1); // overflows and does not revert }

Beim Schreiben auf Speicherzeiger werden keine neuen Daten gespeichert.

Der Code sieht so aus, als ob er die Daten in myArray[1] nach myArray[0] kopiert, aber das ist nicht der Fall. Wenn Sie die letzte Zeile der Funktion auskommentieren, sagt der Compiler, dass die Funktion in eine Ansichtsfunktion umgewandelt werden sollte. Beim Schreiben in „foo“ wird nicht in den zugrunde liegenden Speicher geschrieben.


 contract DoesNotWrite { struct Foo { uint256 bar; } Foo[] public myArray; function moveToSlot0() external { Foo storage foo = myArray[0]; foo = myArray[1]; // myArray[0] is unchanged // we do this to make the function a state // changing operation // and silence the compiler warning myArray[1] = Foo({bar: 100}); } }

Schreiben Sie also nicht auf Speicherzeiger.

Durch das Löschen von Strukturen, die dynamische Datentypen enthalten, werden die dynamischen Daten nicht gelöscht

Wenn sich eine Zuordnung (oder ein dynamisches Array) innerhalb einer Struktur befindet und die Struktur gelöscht wird, wird die Zuordnung oder das Array nicht gelöscht.


Mit Ausnahme des Löschens eines Arrays kann das Schlüsselwort delete nur einen Speichersteckplatz löschen. Wenn der Speicherplatz Verweise auf andere Speicherplätze enthält, werden diese nicht gelöscht.

 contract NestedDelete { mapping(uint256 => Foo) buzz; struct Foo { mapping(uint256 => uint256) bar; } Foo foo; function addToFoo(uint256 i) external { buzz[i].bar[5] = 6; } function getFromFoo(uint256 i) external view returns (uint256) { return buzz[i].bar[5]; } function deleteFoo(uint256 i) external { // internal map still holds the data in the // mapping and array delete buzz[i]; } }


Lassen Sie uns nun die folgende Transaktionssequenz durchführen

  1. addToFoo(1)
  2. getFromFoo(1) gibt 6 zurück
  3. deleteFoo(1)
  4. getFromFoo(1) gibt immer noch 6 zurück!


Denken Sie daran, dass Karten in Solidity niemals „leer“ sind. Wenn also jemand auf ein gelöschtes Element zugreift, wird die Transaktion nicht rückgängig gemacht, sondern gibt stattdessen den Nullwert für diesen Datentyp zurück.

Probleme mit dem ERC20-Token

Wenn Sie nur mit vertrauenswürdigen ERC20-Tokens arbeiten, treffen die meisten dieser Probleme nicht zu. Wenn Sie jedoch mit einem beliebigen oder teilweise nicht vertrauenswürdigen ERC20-Token interagieren, sollten Sie Folgendes beachten.

ERC20: Gebühr bei Überweisung

Beim Umgang mit nicht vertrauenswürdigen Tokens sollten Sie nicht davon ausgehen, dass sich Ihr Guthaben zwangsläufig um den Betrag erhöht. Es ist möglich, dass ein ERC20-Token seine Übertragungsfunktion wie folgt implementiert:

 contract ERC20 { // internally called by transfer() and transferFrom() // balance and approval checks happen in the caller function _transfer(address from, address to, uint256 amount) internal returns (bool) { fee = amount * 100 / 99; balanceOf[from] -= to; balanceOf[to] += (amount - fee); balanceOf[TREASURY] += fee; emit Transfer(msg.sender, to, (amount - fee)); return true; } }


Dieser Token erhebt auf jede Transaktion eine Steuer von 1 %. Wenn also ein Smart Contract wie folgt mit dem Token interagiert, erhalten wir entweder unerwartete Rückerstattungen oder gestohlenes Geld.

 contract Stake { mapping(address => uint256) public balancesInContract; function stake(uint256 amount) public { token.transferFrom(msg.sender, address(this), amount); balancesInContract[msg.sender] += amount; // THIS IS WRONG! } function unstake() public { uint256 toSend = balancesInContract[msg.sender]; delete balancesInContract[msg.sender]; // this could revert because toSend is 1% greater than// the amount in the contract. Otherwise, 1% will be "stolen"// from other depositors. token.transfer(msg.sender, toSend); } }

ERC20: Rebasing-Token

Der Rebasing-Token wurde durch den sOhm-Token von Olympus DAO und den AMPL-Token von Ampleforth populär gemacht. Coingecko führt eine Liste der umbasierenden ERC20-Token.


Wenn ein Token umbasiert wird, ändert sich der Gesamtvorrat und der Kontostand aller Spieler erhöht oder verringert sich je nach Rebase-Richtung.


Der folgende Code kann beim Umgang mit einem Rebasing-Token nicht funktionieren

 contract WillBreak { mapping(address => uint256) public balanceHeld; IERC20 private rebasingToken function deposit(uint256 amount) external { balanceHeld[msg.sender] = amount; rebasingToken.transferFrom(msg.sender, address(this), amount); } function withdraw() external { amount = balanceHeld[msg.sender]; delete balanceHeld[msg.sender]; // ERROR, amount might exceed the amount // actually held by the contract rebasingToken.transfer(msg.sender, amount); } }


Die Lösung vieler Verträge besteht darin, Rebasing-Tokens einfach zu verbieten. Man könnte jedoch den obigen Code ändern, um balanceOf(address(this)) zu prüfen, bevor der Kontostand an den Absender übertragen wird. Dann würde es auch dann noch funktionieren, wenn sich das Gleichgewicht ändert.

ERC20: ERC777 in ERC20-Kleidung

ERC20: Wenn ERC20-Token gemäß dem Standard implementiert werden, haben sie keine Transfer-Hooks, und daher gibt es bei Transfer und TransferFrom kein Wiedereintrittsproblem.


Token mit Transfer-Hooks bieten bedeutende Vorteile, weshalb sie in allen NFT-Standards implementiert sind und ERC777 fertiggestellt wurde. Es hat jedoch genug Verwirrung gestiftet, dass Openzeppelin die ERC777-Bibliothek als veraltet markiert hat .


Wenn Sie möchten, dass Ihr Protokoll mit Token kompatibel ist, die sich wie ERC20-Token verhalten, aber über Übertragungs-Hooks verfügen, müssen Sie die Funktionen transfer und transferFrom einfach so behandeln, als würden sie einen Funktionsaufruf an den Empfänger senden.


Dieser ERC777-Wiedereintritt ist Uniswap passiert (Openzeppelin hat den Exploit hier dokumentiert, falls Sie neugierig sind).

ERC20: Nicht alle ERC20-Token geben „true“ zurück

Die ERC20-Spezifikation schreibt vor, dass ein ERC20-Token „true“ zurückgeben muss, wenn eine Übertragung erfolgreich ist. Da die meisten ERC20-Implementierungen nicht fehlschlagen können, es sei denn, die Zuteilung reicht nicht aus oder der übertragene Betrag ist zu hoch, haben sich die meisten Entwickler daran gewöhnt, den Rückgabewert von ERC20-Tokens zu ignorieren und davon auszugehen, dass eine fehlgeschlagene Übertragung rückgängig gemacht wird.


Ehrlich gesagt hat dies keine Konsequenzen, wenn Sie nur mit einem vertrauenswürdigen ERC20-Token arbeiten, dessen Verhalten Sie kennen. Beim Umgang mit beliebigen ERC20-Tokens muss diese Verhaltensvarianz jedoch berücksichtigt werden.


In vielen Verträgen wird implizit erwartet, dass fehlgeschlagene Übertragungen immer rückgängig gemacht werden und nicht „Falsch“ zurückgeben, da die meisten ERC20-Token keinen Mechanismus haben, um „Falsch“ zurückzugeben, was zu großer Verwirrung geführt hat.


Erschwerend kommt hinzu, dass einige ERC20-Tokens nicht dem Protokoll zur Rückgabe von „true“ folgen, insbesondere Tether. Einige Token werden zurückgesetzt, wenn die Übertragung fehlschlägt, was dazu führt, dass die Rückerstattung an den Anrufer weitergegeben wird. Daher umschließen einige Bibliotheken ERC20-Token-Übertragungsaufrufe, um die Wiederherstellung abzufangen und stattdessen einen booleschen Wert zurückzugeben.


Hier sind einige Implementierungen

Openzeppelin SafeTransfer

Solady SafeTransfer (deutlich gaseffizienter)

ERC20: Adressvergiftung

Hierbei handelt es sich nicht um eine Schwachstelle bei Smart Contracts, der Vollständigkeit halber erwähnen wir sie hier jedoch.

Die Spezifikation erlaubt die Übertragung von null ERC20-Tokens. Dies kann zu Verwirrung bei Frontend-Anwendungen führen und möglicherweise Benutzer darüber täuschen, an wen sie kürzlich Token gesendet haben. Metamask hat mehr dazu in diesem Thread .

ERC20: Einfach absolut robust

(Im Web3-Sprachgebrauch bedeutet „robust“ „man hat den Boden unter den Füßen weggezogen.“)

Nichts hindert jemanden daran, einem ERC20-Token eine Funktion hinzuzufügen, die es ihm ermöglicht, nach Belieben Token zu erstellen, zu übertragen und zu brennen – oder sich selbst zu zerstören oder zu aktualisieren. Grundsätzlich gibt es also eine Grenze dafür, wie „nicht vertrauenswürdig“ ein ERC20-Token sein kann.


Logische Fehler in Kreditprotokollen

Wenn man darüber nachdenkt, wie kredit- und leihbasierte DeFi-Protokolle kaputt gehen können, ist es hilfreich, darüber nachzudenken, dass sich Fehler auf Softwareebene ausbreiten und die Geschäftslogikebene beeinflussen. Der Abschluss und Abschluss eines Anleihevertrags erfordert viele Schritte. Hier sind einige Angriffsvektoren, die Sie berücksichtigen sollten.

Wie Kreditgeber verlieren

  • Fehler, die es ermöglichen, den Kapitalbetrag zu reduzieren (möglicherweise auf Null), ohne Zahlungen zu leisten.
  • Die Sicherheiten des Käufers können nicht verwertet werden, wenn das Darlehen nicht zurückgezahlt wird oder die Sicherheiten unter den Schwellenwert fallen.
  • Wenn das Protokoll über einen Mechanismus zur Übertragung des Schuldeigentums verfügt, könnte dies ein Vektor für den Diebstahl von Anleihen von Kreditgebern sein.
  • Die Fälligkeit des Darlehensbetrags oder der Zahlungen wird unrechtmäßig auf einen späteren Zeitpunkt verschoben.

Wie Kreditnehmer Verluste erleiden

  • Ein Fehler, bei dem die Rückzahlung des Kapitals nicht zu einer Reduzierung des Kapitals führt.
  • Ein Fehler oder ein Griefing-Angriff hindert den Benutzer daran, eine Zahlung vorzunehmen.
  • Der Kapital- oder Zinssatz wird unrechtmäßig erhöht.
  • Oracle-Manipulationen führen zu einer Entwertung der Sicherheiten.
  • Das Fälligkeitsdatum des Darlehensbetrags oder der Zahlungen wird fälschlicherweise auf einen früheren Zeitpunkt verschoben.


Wenn Sicherheiten aus dem Protokoll entfernt werden, verlieren sowohl der Kreditgeber als auch der Kreditnehmer, da der Kreditnehmer keinen Anreiz hat, den Kredit zurückzuzahlen, und der Kreditnehmer verliert den Kapitalbetrag.


Wie oben zu sehen ist, gibt es viel mehr Ebenen, auf denen ein DeFi-Protokoll „gehackt“ wird, als nur, dass eine Menge Geld aus dem Protokoll abgezogen wird (die Art von Ereignissen, die normalerweise für Schlagzeilen sorgen). Dies ist ein Bereich, in dem CTF-Sicherheitsübungen (Capture the Flag) irreführend sein können. Obwohl der Diebstahl von Protokollgeldern das katastrophalste Ergebnis ist, ist es keineswegs das einzige, gegen das man sich wehren kann.

Ungeprüfte Rückgabewerte

Es gibt zwei Möglichkeiten, einen externen Smart Contract aufzurufen: 1) Aufrufen der Funktion mit einer Schnittstellendefinition; 2) mit der .call-Methode. Dies wird unten veranschaulicht

 contract A { uint256 public x; function setx(uint256 _x) external { require(_x > 10, "x must be bigger than 10"); x = _x; } } interface IA { function setx(uint256 _x) external; } contract B { function setXV1(IA a, uint256 _x) external { a.setx(_x); } function setXV2(address a, uint256 _x) external { (bool success, ) = a.call(abi.encodeWithSignature("setx(uint256)", _x)); // success is not checked! } }

In Vertrag B kann setXV2 stillschweigend fehlschlagen, wenn _x kleiner als 10 ist. Wenn eine Funktion über die .call-Methode aufgerufen wird, kann der Aufgerufene zurücksetzen, der übergeordnete jedoch nicht. Der Erfolgswert muss überprüft werden und das Codeverhalten muss entsprechend verzweigt werden.

Private Variablen

Private Variablen sind weiterhin in der Blockchain sichtbar, daher sollten sensible Informationen niemals dort gespeichert werden. Wenn sie nicht zugänglich wären, wie könnten die Validatoren dann Transaktionen verarbeiten, die von ihren Werten abhängen? Private Variablen können nicht aus einem externen Solidity-Vertrag gelesen werden, sie können jedoch mit einem Ethereum-Client außerhalb der Kette gelesen werden.


Um eine Variable lesen zu können, müssen Sie ihren Speicherplatz kennen. Im folgenden Beispiel ist der Speichersteckplatz von myPrivateVar 0.

 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract PrivateVarExample { uint256 private myPrivateVar; constructor(uint256 _initialValue) { myPrivateVar = _initialValue; } }

Hier ist der Javascript-Code zum Lesen der privaten Variablen des bereitgestellten Smart Contracts

 const Web3 = require("web3"); const PRIVATE_VAR_EXAMPLE_ADDRESS = "0x123..."; // Replace with your contract address async function readPrivateVar() { const web3 = new Web3("http://localhost:8545"); // Replace with your provider's URL // Read storage slot 0 (where 'myPrivateVar' is stored) const storageSlot = 0; const privateVarValue = await web3.eth.getStorageAt( PRIVATE_VAR_EXAMPLE_ADDRESS, storageSlot ); console.log("Value of private variable 'myPrivateVar':", web3.utils.hexToNumberString(privateVarValue)); } readPrivateVar();

Unsicherer Delegiertenanruf

Delegatecall sollte niemals mit nicht vertrauenswürdigen Verträgen verwendet werden, da dadurch die gesamte Kontrolle an den Delegatecalle übergeben wird. In diesem Beispiel stiehlt der nicht vertrauenswürdige Vertrag den gesamten Ether im Vertrag.

 contract UntrustedDelegateCall { constructor() payable { require(msg.value == 1 ether); } function doDelegateCall(address _delegate, bytes calldata data) public { (bool ok, ) = _delegate.delegatecall(data); require(ok, "delegatecall failed"); } } contract StealEther { function steal() public { // you could also selfdestruct here // if you really wanted to be mean (bool ok,) = tx.origin.call{value: address(this).balance}(""); require(ok); } function attack(address victim) public { UntrustedDelegateCall(victim).doDelegateCall( address(this), abi.encodeWithSignature("steal()")); } }

Upgrade-Fehler im Zusammenhang mit Proxys

Wir können diesem Thema nicht in einem einzigen Abschnitt gerecht werden. Die meisten Upgrade-Fehler können im Allgemeinen vermieden werden, indem man das Hardhat-Plugin von Openzeppelin verwendet und sich darüber informiert, vor welchen Problemen es schützt. ( https://docs.openzeppelin.com/upgrades-plugins/1.x/ ).


Als kurze Zusammenfassung sind hier Probleme im Zusammenhang mit Smart-Contract-Upgrades:

  • Selfdestruct und Delegatecall sollten nicht in Implementierungsverträgen verwendet werden
  • Es ist darauf zu achten, dass sich Speichervariablen bei Upgrades niemals gegenseitig überschreiben
  • Der Aufruf externer Bibliotheken sollte in Implementierungsverträgen vermieden werden, da nicht vorhersehbar ist, wie sie sich auf den Speicherzugriff auswirken
  • Der Bereitsteller darf es niemals versäumen, die Initialisierungsfunktion aufzurufen
  • Keine Lückenvariable in Basisverträge aufnehmen, um Speicherkonflikte zu verhindern, wenn neue Variablen zum Basisvertrag hinzugefügt werden (dies wird vom Hardhat-Plugin automatisch gehandhabt)
  • Die Werte in unveränderlichen Variablen bleiben zwischen Upgrades nicht erhalten
  • Es wird dringend davon abgeraten, irgendetwas im Konstruktor zu tun, da zukünftige Upgrades zur Aufrechterhaltung der Kompatibilität identische Konstruktorlogik ausführen müssten.

Übermächtige Administratoren

Nur weil ein Vertrag einen Eigentümer oder Administrator hat, heißt das nicht, dass dessen Macht unbegrenzt sein muss. Betrachten Sie einen NFT. Es ist vernünftig, dass nur der Eigentümer die Einnahmen aus dem NFT-Verkauf abzieht, aber die Möglichkeit, den Vertrag zu pausieren (Blocktransfers), könnte verheerende Folgen haben, wenn die privaten Schlüssel des Eigentümers kompromittiert werden. Im Allgemeinen sollten Administratorrechte so gering wie möglich sein, um unnötige Risiken zu minimieren.


Apropos Vertragseigentum …

Verwenden Sie Ownable2Step anstelle von Ownable

Dies ist technisch gesehen keine Schwachstelle, aber OpenZeppelin Ownable kann zum Verlust des Vertragseigentums führen, wenn das Eigentum an eine nicht existierende Adresse übertragen wird. Ownable2step verlangt vom Empfänger eine Eigentumsbestätigung. Dadurch wird verhindert, dass der Besitz versehentlich an eine falsch eingegebene Adresse gesendet wird.

Rundungsfehler

Solidity hat keine Gleitkommazahlen, daher sind Rundungsfehler unvermeidlich. Der Designer muss sich darüber im Klaren sein, ob das Auf- oder Abrunden richtig ist und zu wessen Gunsten das Runden sein sollte.


Die Teilung sollte immer zuletzt durchgeführt werden. Der folgende Code konvertiert fälschlicherweise zwischen Stablecoins, die eine unterschiedliche Anzahl von Dezimalstellen haben. Der folgende Umtauschmechanismus ermöglicht es einem Benutzer, eine kleine Menge USDC (mit 6 Dezimalstellen) kostenlos zu nehmen, wenn er gegen Dai (mit 18 Dezimalstellen) umtauscht. Die Variable daiToTake rundet auf Null ab und entnimmt dem Benutzer nichts im Austausch für einen usdcAmount ungleich Null.

 contract Exchange { uint256 private constant CONVERSION = 1e12; function swapDAIForUSDC(uint256 usdcAmount) external pure returns (uint256 a) { uint256 daiToTake = usdcAmount / CONVERSION; conductSwap(daiToTake, usdcAmount); } }

Frontrunning

Frontrunning im Kontext von Etheruem (und ähnlichen Ketten) bedeutet, eine ausstehende Transaktion zu beobachten und davor eine andere Transaktion auszuführen, indem man einen höheren Gaspreis zahlt. Das heißt, der Angreifer ist der Transaktion „vorbeigelaufen“. Wenn es sich bei der Transaktion um ein profitables Geschäft handelt, ist es sinnvoll, die Transaktion genau zu kopieren, außer einen höheren Gaspreis zu zahlen. Dieses Phänomen wird manchmal als MEV bezeichnet, was den vom Bergmann extrahierbaren Wert bedeutet, in anderen Zusammenhängen jedoch manchmal den maximalen extrahierbaren Wert. Blockproduzenten haben unbegrenzte Macht, Transaktionen neu zu ordnen und eigene einzufügen, und in der Vergangenheit waren Blockproduzenten Miner, bevor Ethereum zum Proof of Stake überging, daher der Name.

Frontrunning: Ungeschützter Rückzug

Der Entzug von Ether aus einem Smart Contract kann als „gewinnbringender Handel“ betrachtet werden. Sie führen eine Null-Kosten-Transaktion durch (abgesehen vom Benzin) und erhalten am Ende mehr Kryptowährung als zu Beginn.

 contract UnprotectedWithdraw { constructor() payable { require(msg.value == 1 ether, "must create with 1 eth"); } function unsafeWithdraw() external { (bool ok, ) = msg.sender.call{value: address(this).value}(""); require(ok, "transfer failed"). } }


Wenn Sie diesen Vertrag bereitstellen und versuchen, ihn zurückzuziehen, wird ein Frontrunner-Bot Ihren Aufruf von „unsafeWithdraw“ im Mempool bemerken und ihn kopieren, um zuerst den Ether zu erhalten.

Frontrunning: ERC4626 Inflationsangriff, eine Kombination aus Frontrunning- und Rundungsfehlern

In unserem ERC4626-Tutorial haben wir ausführlich über den ERC-4626- Inflationsangriff geschrieben. Das Wesentliche dabei ist jedoch, dass ein ERC4626-Vertrag „Aktien“-Token basierend auf dem Prozentsatz der „Vermögenswerte“ verteilt, die ein Händler beisteuert.


Im Großen und Ganzen funktioniert es wie folgt:

 function getShares(...) external { // code shares_received = assets_contributed / total_assets; // more code }

Natürlich wird niemand Vermögenswerte beisteuern und keine Aktien zurückbekommen, aber sie können nicht vorhersagen, dass das passieren wird, wenn jemand den Handel vorantreiben kann, um die Aktien zu erhalten.


Sie bringen beispielsweise 200 Vermögenswerte ein, wenn der Pool 20 hat, und erwarten, 100 Aktien zu erhalten. Wenn jedoch jemand die Transaktion vorantreibt, um 200 Vermögenswerte einzuzahlen, lautet die Formel 200/220, was auf Null abgerundet wird, was dazu führt, dass das Opfer Vermögenswerte verliert und null Aktien zurückerhält.

Spitzenreiter: ERC20-Zulassung

Es ist am besten, dies anhand eines realen Beispiels zu veranschaulichen, anstatt es abstrakt zu beschreiben


  1. Angenommen, Alice genehmigt Eve für 100 Token. (Eva ist immer die böse Person, nicht Bob, also bleiben wir bei der Konvention.)
  2. Alice ändert ihre Meinung und sendet eine Transaktion, um Eves Zustimmung auf 50 zu ändern.
  3. Bevor die Transaktion zur Änderung der Genehmigung auf 50 in den Block aufgenommen wird, liegt sie im Mempool, wo Eve sie sehen kann.
  4. Eve sendet eine Transaktion, um ihre 100 Token einzufordern, um der Genehmigung für 50 zuvorzukommen.
  5. Die Genehmigung für 50 geht durch
  6. Eve sammelt die 50 Token ein.


Jetzt verfügt Eve über 150 Token statt 100 oder 50. Die Lösung hierfür besteht darin, die Genehmigung auf Null zu setzen, bevor sie erhöht oder verringert wird, wenn es sich um nicht vertrauenswürdige Genehmigungen handelt.

Frontrunning: Sandwich-Angriffe

Der Preis eines Vermögenswerts bewegt sich als Reaktion auf Kauf- und Verkaufsdruck. Wenn sich ein großer Auftrag im Mempool befindet, haben Händler einen Anreiz, den Auftrag zu kopieren, allerdings zu einem höheren Gaspreis. Auf diese Weise kaufen sie den Vermögenswert, lassen den Preis durch den Großauftrag steigen und verkaufen ihn dann sofort. Der Verkaufsauftrag wird manchmal als „Backrunning“ bezeichnet. Der Verkaufsauftrag kann dadurch erledigt werden, dass ein Verkaufsauftrag mit einem niedrigeren Gaspreis erteilt wird, sodass die Reihenfolge wie folgt aussieht


  1. Frontrun kaufen
  2. großer Kauf
  3. verkaufen


Die primäre Verteidigung gegen diesen Angriff besteht darin, einen „Slippage“-Parameter bereitzustellen. Wenn der „Frontrun-Kauf“ selbst den Preis über eine bestimmte Schwelle hinaus treibt, wird die „Großkauf“-Order zurückgesetzt, wodurch der Spitzenreiter im Handel scheitert.


Es wird als Sandwhich bezeichnet, weil der große Kauf durch den Frontrun-Kauf und den Backrun-Verkauf gesandwicht wird. Dieser Angriff funktioniert auch bei großen Verkaufsaufträgen, nur in die entgegengesetzte Richtung.

Erfahren Sie mehr über Frontrunning

Frontrunning ist ein riesiges Thema. Flashbots hat das Thema umfassend recherchiert und mehrere Tools und Forschungsartikel veröffentlicht, um die negativen externen Auswirkungen zu minimieren.


Ob Frontrunning mit einer geeigneten Blockchain-Architektur „weggestaltet“ werden kann, ist ein Diskussionsgegenstand, der noch nicht abschließend geklärt ist. Die folgenden zwei Artikel sind dauerhafte Klassiker zu diesem Thema:


Ethereum ist ein dunkler Wald

Dem dunklen Wald entfliehen

Signaturbezogen

Digitale Signaturen haben im Zusammenhang mit Smart Contracts zwei Verwendungszwecke:

  • Es ermöglicht Adressen, bestimmte Transaktionen auf der Blockchain zu autorisieren, ohne eine tatsächliche Transaktion durchzuführen
  • Beweisen Sie einem Smart Contract, dass der Absender eine gewisse Befugnis hat, entsprechend einer vordefinierten Adresse etwas zu tun

Hier ist ein Beispiel für die sichere Verwendung digitaler Signaturen, um einem Benutzer die Berechtigung zum Prägen eines NFT zu erteilen:

 import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract NFT is ERC721("name", "symbol") { function mint(bytes calldata signature) external { address recovered = keccak256(abi.encode(msg.sender)).toEthSignedMessageHash().recover(signature); require(recovered == authorizer, "signature does not match"); } }

Ein klassisches Beispiel ist die Genehmigungsfunktion in ERC20. Um eine Adresse zum Abheben einer bestimmten Menge an Token von unserem Konto zu genehmigen, müssen wir eine tatsächliche Ethereum-Transaktion durchführen, die Benzin kostet.


Manchmal ist es effizienter, eine digitale Signatur außerhalb der Kette an den Empfänger weiterzuleiten. Anschließend übermittelt der Empfänger die Signatur an den Smart-Vertrag, um zu beweisen, dass er zur Durchführung der Transaktion berechtigt ist.


ERC20Permit ermöglicht Genehmigungen mit digitaler Signatur. Die Funktion wird wie folgt beschrieben

 function permit(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) public

Anstatt eine tatsächliche Genehmigungstransaktion zu senden, kann der Eigentümer die Genehmigung für den Spender „unterzeichnen“ (zusammen mit einer Frist). Der genehmigte Spender kann dann die Genehmigungsfunktion mit den bereitgestellten Parametern aufrufen.

Anatomie einer Signatur

Sie werden die Variablen v, r und s häufig sehen. Sie werden durch die Datentypen uint8, bytes32 bzw. bytes32 solide dargestellt. Manchmal werden Signaturen als 65-Byte-Array dargestellt, bei dem alle diese Werte wie folgt verkettet sind: abi.encodePacked(r, s, v);


Die beiden anderen wesentlichen Bestandteile einer Signatur sind der Nachrichten-Hash (32 Byte) und die Signaturadresse. Die Reihenfolge sieht so aus


  1. Ein privater Schlüssel (privKey) wird verwendet, um eine öffentliche Adresse (ethAddress) zu generieren.

  2. Ein Smart Contract speichert ethAddress im Voraus

  3. Ein Off-Chain-Benutzer hasht eine Nachricht und signiert den Hash. Dadurch entsteht das Paar msgHash und die Signatur (r, s, v)

  4. Der Smart Contract empfängt eine Nachricht, hasht sie, um msgHash zu erzeugen, und kombiniert sie dann mit (r, s, v), um zu sehen, welche Adresse herauskommt.

  5. Wenn die Adresse mit ethAddress übereinstimmt, ist die Signatur gültig (unter bestimmten Annahmen, die wir bald sehen werden!)


Smart Contracts nutzen den vorkompilierten Vertrag ecrecover in Schritt 4, um die sogenannte Kombination durchzuführen und die Adresse zurückzubekommen.


Es gibt viele Schritte in diesem Prozess, bei denen die Dinge schiefgehen können.

Signaturen: ecrecover gibt die Adresse (0) zurück und wird nicht wiederhergestellt, wenn die Adresse ungültig ist

Dies kann zu einer Sicherheitslücke führen, wenn eine nicht initialisierte Variable mit der Ausgabe von ecrecover verglichen wird.

Dieser Code ist anfällig

 contract InsecureContract { address signer; // defaults to address(0) // who lets us give the beneficiary the airdrop without them// spending gas function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external { // ecrecover returns address(0) if the signature is invalid require(signer == ecrecover(keccak256(abi.encode(who, amount)), v, r, s), "invalid signature"); mint(msg.sender, AIRDROP_AMOUNT); } }

Signaturwiedergabe

Die Signaturwiedergabe erfolgt, wenn ein Vertrag nicht nachverfolgt, ob eine Signatur zuvor verwendet wurde. Im folgenden Code beheben wir das vorherige Problem, es ist jedoch immer noch nicht sicher.

 contract InsecureContract { address signer; function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external { address recovered == ecrecover(keccak256(abi.encode(who, amount)), v, r, s); require(recovered != address(0), "invalid signature"); require(recovered == signer, "recovered signature not equal signer"); mint(msg.sender, amount); } }

Die Leute können den Airdrop so oft beanspruchen, wie sie wollen!


Wir könnten die folgenden Zeilen hinzufügen

 bytes memory signature = abi.encodePacked(v, r, s); require(!used[signature], "signature already used"); // mapping(bytes => bool); used[signature] = true;

Leider ist der Code immer noch nicht sicher!

Charakteristische Formbarkeit

Bei einer gültigen Signatur kann ein Angreifer schnell rechnen, um eine andere abzuleiten. Der Angreifer kann diese geänderte Signatur dann „wiedergeben“. Aber stellen wir zunächst einen Code bereit, der zeigt, dass wir mit einer gültigen Signatur beginnen, diese ändern und zeigen können, dass die neue Signatur weiterhin gültig ist.

 contract Malleable { // v = 28 // r = 0xf8479d94c011613baeffe9239e4ff65e2adbac744c34217ca7d51378e72c5204 // s = 0x57af17590a914b759c45aaeabaf513d5ef72d7da1bdd19d9f2e1bc371ece5b86 // m = 0x0000000000000000000000000000000000000000000000000000000000000003 function foo(bytes calldata msg, uint8 v, bytes32 r, bytes32 s) public pure returns (address, address){ bytes32 h = keccak256(msg); address a = ecrecover(h, v, r, s); // The following is math magic to invert the // signature and create a valid one // flip s bytes32 s2 = bytes32(uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141) - uint256(s)); // invert v uint8 v2; require(v == 27 || v == 28, "invalid v"); v2 = v == 27 ? 28 : 27; address b = ecrecover(h, v2, r, s2); assert(a == b); // different signatures, same address!; return (a, b); } }

Daher ist unser Laufbeispiel immer noch anfällig. Sobald jemand eine gültige Unterschrift vorlegt, kann eine spiegelbildliche Unterschrift erstellt werden und die Prüfung der verwendeten Unterschrift umgangen werden.

 contract InsecureContract { address signer; function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external { address recovered == ecrecover(keccak256(abi.encode(who, amount)), v, r, s); require(recovered != address(0), "invalid signature"); require(recovered == signer, "recovered signature not equal signer"); bytes memory signature = abi.encodePacked(v, r, s); require(!used[signature], "signature already used"); // this can be bypassed used[signature] = true; mint(msg.sender, amount); } }

Sichere Signaturen

An dieser Stelle benötigen Sie wahrscheinlich einen sicheren Signaturcode, oder? Wir verweisen Sie auf unser Tutorial zum Erstellen von Signaturen in Solidität und zum Testen dieser in der Gießerei.


Aber hier ist die Checkliste.


  • Nutzen Sie die Openzeppelin-Bibliothek, um Formbarkeitsangriffe zu verhindern und eine Wiederherstellung auf null Probleme zu ermöglichen
  • Verwenden Sie keine Signaturen als Passwort. Die Nachrichten müssen Informationen enthalten, die Angreifer nicht einfach wiederverwenden können (z. B. msg.sender).
  • Hashen Sie, was Sie in der Kette signieren
  • Verwenden Sie eine Nonce, um Wiederholungsangriffe zu verhindern. Besser noch: Befolgen Sie EIP712, damit Benutzer sehen können, was sie signieren, und Sie verhindern können, dass Signaturen zwischen Verträgen und verschiedenen Ketten wiederverwendet werden.

Signaturen können ohne entsprechende Sicherheitsmaßnahmen gefälscht oder manipuliert werden

Der obige Angriff kann weiter verallgemeinert werden, wenn das Hashing nicht in der Kette erfolgt. In den obigen Beispielen wurde das Hashing im Smart Contract durchgeführt, sodass die obigen Beispiele nicht anfällig für den folgenden Exploit sind.


Schauen wir uns den Code zum Wiederherstellen von Signaturen an

 // this code is vulnerable! function recoverSigner(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public returns (address signer) { require(signer == ecrecover(hash, v, r, s), "signer does not match"); // more actions }


Der Benutzer stellt sowohl den Hash als auch die Signaturen bereit. Wenn der Angreifer bereits eine gültige Signatur des Unterzeichners gesehen hat, kann er einfach den Hash und die Signatur einer anderen Nachricht wiederverwenden.

Aus diesem Grund ist es sehr wichtig, die Nachricht im Smart Contract und nicht außerhalb der Kette zu hashen.

Um diesen Exploit in Aktion zu sehen, sehen Sie sich das CTF an, das wir auf Twitter gepostet haben.


Ursprüngliche Herausforderung:

Teil 1: https://twitter.com/RareSkills_io/status/1650869999266037760

Teil 2: https://twitter.com/RareSkills_io/status/1650897671543197701

Lösungen:

https://twitter.com/RareSkills_io/status/1651527648676573185 https://twitter.com/RareSkills_io/status/1651224817465540611

Signaturen als Identifikatoren

Signaturen sollten nicht zur Identifizierung von Benutzern verwendet werden. Aufgrund ihrer Formbarkeit kann nicht davon ausgegangen werden, dass sie einzigartig sind. Msg.sender hat viel stärkere Eindeutigkeitsgarantien.

Einige Solidity-Compilerversionen weisen Fehler auf

Sehen Sie sich hier eine Sicherheitsübung an, die wir auf Twitter gehostet haben. Wenn Sie eine Codebasis prüfen, vergleichen Sie die Solidity-Version mit den Veröffentlichungsankündigungen auf der Solidity-Seite, um festzustellen, ob möglicherweise ein Fehler vorliegt.

Vorausgesetzt, Smart Contracts sind unveränderlich

Intelligente Verträge können mit dem Proxy-Muster (oder seltener mit dem metamorphen Muster) aktualisiert werden. Intelligente Verträge sollten sich nicht darauf verlassen, dass die Funktionalität eines beliebigen intelligenten Vertrags unverändert bleibt.

Transfer() und send() können bei Multi-Signatur-Wallets kaputt gehen

Die Soliditätsfunktionen transfer und send sollten nicht verwendet werden. Sie begrenzen absichtlich die mit der Transaktion weitergeleitete Gasmenge auf 2.300, was dazu führen wird, dass den meisten Betrieben das Gas ausgeht.


Das häufig verwendete Gnosis-sichere Multi-Signatur-Wallet unterstützt in der Fallback-Funktion die Weiterleitung des Anrufs an eine andere Adresse. Wenn jemand Transfer oder Send verwendet, um Ether an die Multisig-Wallet zu senden, könnte der Fallback-Funktion der Treibstoff ausgehen und die Übertragung würde fehlschlagen. Unten finden Sie einen Screenshot der Gnosis-Safe-Fallback-Funktion. Der Leser erkennt deutlich, dass es mehr als genug Vorgänge gibt, um das 2300-Gas zu verbrauchen.

Gnosis sicherer Fallback

Wenn Sie mit einem Vertrag interagieren müssen, der Übertragung und Senden verwendet, lesen Sie unseren Artikel über Ethereum-Zugriffslistentransaktionen , mit dem Sie die Gaskosten für Speicher- und Vertragszugriffsvorgänge senken können.

Ist der arithmetische Überlauf noch relevant?

Solidity 0.8.0 verfügt über einen integrierten Überlauf- und Unterlaufschutz. Sofern also kein ungeprüfter Block vorhanden ist oder Low-Level-Code in Yul verwendet wird, besteht keine Gefahr eines Überlaufs. Daher sollten SafeMath-Bibliotheken nicht verwendet werden, da sie bei den zusätzlichen Prüfungen unnötig viel Geld verschwenden.

Was ist mit block.timestamp?

Einige Literaturdokumente belegen, dass block.timestamp ein Verwundbarkeitsvektor ist, da Miner ihn manipulieren können. Dies gilt normalerweise für die Verwendung von Zeitstempeln als Zufallsquelle, was, wie bereits erwähnt, ohnehin nicht erfolgen sollte. Nach der Zusammenführung aktualisiert Ethereum den Zeitstempel in genau 12-Sekunden-Intervallen (oder einem Vielfachen von 12 Sekunden). Allerdings ist die Messung der Zeit in der Granularität der zweiten Ebene ein Anti-Muster. Auf der Skala von einer Minute besteht ein erhebliches Fehlerrisiko, wenn ein Validator seinen Blockslot verpasst und es zu einer Lücke von 24 Sekunden in der Blockproduktion kommt.

Eckfälle, Randfälle und Off-By-One-Fehler

Eckfälle lassen sich nicht einfach definieren, aber sobald man genug davon gesehen hat, beginnt man, eine Intuition dafür zu entwickeln. Ein Eckfall kann so etwas sein, als ob jemand versucht, eine Belohnung einzufordern, aber nichts auf dem Spiel hat. Das ist gültig, wir sollten ihnen einfach keine Belohnung geben. Ebenso wollen wir die Belohnungen im Allgemeinen gleichmäßig aufteilen, aber was ist, wenn es nur einen Empfänger gibt und technisch gesehen keine Aufteilung erfolgen sollte?

Eckfall: Beispiel 1

Dieses Beispiel wurde dem Twitter-Thread von Akshay Srivastav entnommen und geändert.

Stellen Sie sich den Fall vor, dass jemand eine privilegierte Aktion ausführen kann, wenn eine Reihe privilegierter Adressen eine Signatur dafür bereitstellen.

 contract VulnerableMultisigAuthorization { struct Authorization { bytes signature; address authorizer; bytes32 hashOfAction; // more fields } // more codef unction takeAction(Authorization[] calldata auths, bytes calldata action) public { // logic for avoiding replay attacks for (uint256 i; i < auths.length; ++i) { require(validateSignature(auths[i].signature, auths[i].authorizer), "invalid signature"); require(authorizers[auths[i].authorizer], "address is not an authorizer"); } doTheAction(action) } }

Wenn eine der Signaturen ungültig ist oder die Signaturen nicht mit einer gültigen Adresse übereinstimmen, erfolgt die Wiederherstellung. Was aber, wenn das Array leer ist? In diesem Fall springt es ganz nach unten zu doTheAction, ohne dass Signaturen erforderlich sind.

Off-By-One: Beispiel 2

 contract ProportionalRewards { mapping(address => uint256) originalId; address[] stakers; function stake(uint256 id) public { nft.transferFrom(msg.sender, address(this), id); stakers.append(msg.sender); } function unstake(uint256 id) public { require(originalId[id] == msg.sender, "not the owner"); removeFromArray(msg.sender, stakers); sendRewards(msg.sender, totalRewardsSinceLastclaim() / stakers.length()); nft.transferFrom(address(this), msg.sender, id); } }

Obwohl der obige Code nicht alle Funktionsimplementierungen zeigt, liegt dennoch ein Fehler vor, selbst wenn sich die Funktionen wie in ihren Namen beschrieben verhalten. Kannst du es erkennen? Hier ist ein Bild, um Ihnen etwas Platz zu geben, damit Sie die Antwort nicht sehen können, bevor Sie nach unten scrollen.

Diese Ziege wird Sie vorerst beschäftigen

Die Funktion „removeFromArray“ und „sendRewards“ sind in der falschen Reihenfolge. Wenn es nur einen Benutzer im Stakers-Array gibt, kommt es zu einem Division-durch-Null-Fehler und der Benutzer kann seine NFT nicht abheben. Darüber hinaus sind die Belohnungen wahrscheinlich nicht so aufgeteilt, wie es der Autor beabsichtigt. Wenn es ursprünglich vier Spieler gab und eine Person aussteigt, erhält sie ein Drittel der Belohnungen, da die Feldlänge zum Zeitpunkt des Ausstiegs 3 beträgt.

Eckfallbeispiel 3: Fehlkalkulation der Compound Finance-Belohnung

Nehmen wir ein reales Beispiel, das Schätzungen zufolge einen Schaden von über 100 Millionen US-Dollar verursacht hat. Machen Sie sich keine Sorgen, wenn Sie das Compound-Protokoll nicht vollständig verstehen. Wir konzentrieren uns nur auf die relevanten Teile. (Außerdem ist das Compound-Protokoll eines der wichtigsten und folgenreichsten Protokolle in der Geschichte von DeFi. Wir lehren es in unserem DeFi-Bootcamp . Wenn dies also Ihr erster Eindruck vom Protokoll ist, lassen Sie sich nicht täuschen.)


Wie auch immer, der Zweck von Compound besteht darin, Benutzer dafür zu belohnen, dass sie ihre ungenutzte Kryptowährung an andere Händler verleihen, die möglicherweise eine Verwendung dafür haben könnten. Die Kreditgeber werden sowohl mit Zinsen als auch mit COMP-Tokens bezahlt (die Kreditnehmer könnten eine COMP-Token-Belohnung beanspruchen, aber darauf werden wir uns jetzt nicht konzentrieren).

Der Compound Comptroller ist ein Proxy-Vertrag, der Aufrufe an Implementierungen delegiert, die von der Compound Governance festgelegt werden können.


Im Governance- Vorschlag 62 vom 30. September 2021 wurde der Implementierungsvertrag auf einen Implementierungsvertrag festgelegt, der die Schwachstelle aufwies. Am selben Tag, an dem es live ging, wurde auf Twitter beobachtet, dass einige Transaktionen COMP-Belohnungen forderten, obwohl keine Token eingesetzt wurden.

Die anfällige Funktion „distributSupplierComp()“


Hier ist der Originalcode

 /** * @notice Calculate COMP accrued by a supplier and possibly transfer it to them * @param cToken The market in which the supplier is interacting * @param supplier The address of the supplier to distribute COMP to */ function distributeSupplierComp(address cToken, address supplier) internal { // TODO: Don't distribute supplier COMP if the user is not in the supplier market. // This check should be as gas efficient as possible as distributeSupplierComp is called in many places. // - We really don't want to call an external contract as that's quite expensive. CompMarketState storage supplyState = compSupplyState[cToken]; uint supplyIndex = supplyState.index; uint supplierIndex = compSupplierIndex[cToken][supplier]; // Update supplier's index to the current index since we are distributing accrued COMP compSupplierIndex[cToken][supplier] = supplyIndex; if (supplierIndex == 0 && supplyIndex > compInitialIndex) { // Covers the case where users supplied tokens before the market's supply state index was set. // Rewards the user with COMP accrued from the start of when supplier rewards were first // set for the market. supplierIndex = compInitialIndex; } // Calculate change in the cumulative sum of the COMP per cToken accrued Double memory deltaIndex = Double({mantissa: sub_(supplyIndex, supplierIndex)}); uint supplierTokens = CToken(cToken).balanceOf(supplier); // Calculate COMP accrued: cTokenAmount * accruedPerCToken uint supplierDelta = mul_(supplierTokens, deltaIndex); uint supplierAccrued = add_(compAccrued[supplier], supplierDelta); compAccrued[supplier] = supplierAccrued; emit DistributedSupplierComp(CToken(cToken), supplier, supplierDelta, supplyIndex); }


Ironischerweise liegt der Fehler im TODO-Kommentar. „Verteilen Sie kein Lieferanten-COMP, wenn der Benutzer nicht im Lieferantenmarkt tätig ist.“ Dafür gibt es aber keine Überprüfung im Code. Solange der Benutzer einen Stake-Token in seiner Wallet hat (CToken(cToken).balanceOf(supplier);), dann

Vorschlag 64 behebt den Fehler am 9. Oktober 2021.


Obwohl man argumentieren könnte, dass es sich hierbei um einen Fehler bei der Eingabevalidierung handelt, haben die Benutzer nichts Bösartiges übermittelt. Wenn jemand versucht, eine Belohnung dafür einzufordern, dass er nichts eingesetzt hat, sollte die korrekte Berechnung Null sein. Es handelt sich wohl eher um einen Geschäftslogik- oder Eckfallfehler.

Hacks aus der realen Welt

DeFi-Hacks, die in der realen Welt passieren, fallen oft nicht in die oben genannten netten Kategorien.

Pairity Wallet Freeze (November 2017)

Das Paritäts-Wallet war nicht für die direkte Verwendung gedacht. Es handelte sich um eine Referenzimplementierung, auf die intelligente Vertragsklone verweisen würden. Die Implementierung ermöglichte es den Klonen, sich auf Wunsch selbst zu zerstören, was jedoch die Zustimmung aller Wallet-Besitzer erforderte.

 // throw unless the contract is not yet initialized.modifier only_uninitialized { if (m_numOwners > 0) throw; _; } function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized { initDaylimit(_daylimit); initMultiowned(_owners, _required); }

Die Wallet-Inhaber werden deklariert

 // kills the contract sending everything to `_to`.function kill(address _to) onlymanyowners(sha3(msg.data)) external { suicide(_to); }

In der Literatur wird dies als „ungeschützte Selbstzerstörung“ beschrieben, also als Fehler bei der Zugangskontrolle, aber das ist nicht ganz korrekt. Das Problem bestand darin, dass die initWallet-Funktion nicht im Implementierungsvertrag aufgerufen wurde und jemand die initWallet-Funktion selbst aufrufen und sich selbst zum Eigentümer machen konnte. Das gab ihnen die Befugnis, die Kill-Funktion aufzurufen. Die Hauptursache war, dass die Implementierung nicht initialisiert wurde. Daher wurde der Fehler nicht aufgrund eines fehlerhaften Soliditätscodes, sondern aufgrund eines fehlerhaften Bereitstellungsprozesses eingeführt.

Badger DAO Hack (Dezember 2021)

Bei diesem Hack wurde kein Solidity-Code ausgenutzt. Stattdessen beschaffen sich die Angreifer den Cloudflare-API-Schlüssel und injizieren ein Skript in das Frontend der Website, das Benutzertransaktionen so ändert, dass Auszahlungen an die Adresse des Angreifers weitergeleitet werden. Lesen Sie mehr in diesem Artikel .

Angriffsvektoren für Wallets

Private Schlüssel mit unzureichender Zufälligkeit

Der Grund für die Suche nach Adressen mit vielen führenden Nullen liegt darin, dass sie energieeffizienter zu verwenden sind. Für eine Ethereum-Transaktion werden 4 Gas für ein Null-Byte in den Transaktionsdaten und 16 Gas für ein Nicht-Null-Byte berechnet.


Aus diesem Grund wurde Wintermute gehackt, weil es die obszöne Adresse ( Writeup ) verwendete. Hier ist der Bericht von 1inch darüber, wie der Schimpfwort-Adressgenerator kompromittiert wurde.


Das Trust Wallet wies eine ähnliche Sicherheitslücke auf, die in diesem Artikel dokumentiert ist ( https://blog.ledger.com/Funds-of-every-wallet-created-with-the-Trust-Wallet-browser-extension-could-have-been- gestohlen/ )


Beachten Sie, dass dies nicht für Smart Contracts mit führenden Nullen gilt, die durch Ändern des Salts in create2 entdeckt werden, da Smart Contracts keine privaten Schlüssel haben.

Wiederverwendete Nonces oder nicht ausreichend zufällige Nonces.

Der Punkt „r“ und „s“ auf der Signatur der elliptischen Kurve wird wie folgt generiert

 r = k * G (mod N) s = k^-1 * (h + r * privateKey) (mod N)


G, r, s, h und ein N sind alle öffentlich bekannt. Wenn „k“ öffentlich wird, ist „privateKey“ die einzige unbekannte Variable und kann aufgelöst werden. Aus diesem Grund müssen Wallets k vollkommen zufällig generieren und dürfen sie niemals wiederverwenden. Wenn die Zufälligkeit nicht vollkommen zufällig ist, kann auf k geschlossen werden.


Die unsichere Zufallsgenerierung in der Java-Bibliothek machte 2013 viele Android-Bitcoin-Wallets anfällig. (Bitcoin verwendet denselben Signaturalgorithmus wie Ethereum.)


( https://arstechnica.com/information-technology/2013/08/all-android-created-bitcoin-wallets-vulnerable-to-theft/ ).

Die meisten Schwachstellen sind anwendungsspezifisch

Wenn Sie sich darin schulen, die Anti-Patterns in dieser Liste schnell zu erkennen, werden Sie zu einem effektiveren Smart-Contract-Programmierer, aber die meisten Smart-Contract-Fehler mit schwerwiegenden Folgen sind auf eine Diskrepanz zwischen der beabsichtigten Geschäftslogik und dem, was der Code tatsächlich tut, zurückzuführen.


Weitere Bereiche, in denen Fehler auftreten können:

  • schlechte symbolische Anreize
  • um einen Fehler abweichen
  • Schreibfehler
  • Administratoren oder Benutzern werden ihre privaten Schlüssel gestohlen

Viele Schwachstellen hätten durch Unit-Tests entdeckt werden können

Das Testen von Smart-Contract-Einheiten ist wohl die grundlegendste Schutzmaßnahme für Smart Contracts, aber einer erschreckenden Anzahl von Smart Contracts fehlen sie entweder oder sie verfügen über eine unzureichende Testabdeckung .

Unit-Tests testen jedoch in der Regel nur den „glücklichen Weg“ (erwartetes/gestaltetes Verhalten) von Verträgen. Um die überraschenden Fälle zu testen, müssen zusätzliche Testmethoden angewendet werden.


Bevor ein Smart Contract zur Prüfung gesendet wird, sollte zunächst Folgendes erledigt werden:

  • Statische Analyse mit Tools wie Slither, um sicherzustellen, dass grundlegende Fehler nicht übersehen werden
  • 100 % Leitungs- und Zweigstellenabdeckung durch Unit-Tests
  • Mutationstests, um sicherzustellen, dass die Unit-Tests über robuste Assert-Anweisungen verfügen
  • Fuzz-Testen, insbesondere für Arithmetik
  • Invariantes Testen für zustandsbehaftete Eigenschaften
  • Gegebenenfalls formelle Überprüfung


Für diejenigen, die mit einigen der hier vorgestellten Methoden nicht vertraut sind: Patrick Collins von Cyfrin Audits hat in seinem Video eine humorvolle Einführung in Stateful und Stateless Fuzzing.


Tools zur Bewältigung dieser Aufgaben werden immer weiter verbreitet und einfacher zu verwenden.

Mehr Ressourcen

Einige Autoren haben in diesen Repos eine Liste früherer DeFi-Hacks zusammengestellt:


Secureum wird häufig zum Erlernen und Üben von Sicherheit verwendet. Bedenken Sie jedoch, dass das Repo seit zwei Jahren nicht wesentlich aktualisiert wurde


Mit unserem Solidity Riddles- Repository können Sie das Ausnutzen von Solidity-Schwachstellen üben.


DamnVulnerableDeFi ist ein klassisches Kriegsspiel, das jeder Entwickler üben sollte


Capture The Ether und Ethernaut sind Klassiker, aber bedenken Sie, dass einige der Aufgaben unrealistisch einfach sind oder veraltete Solidity-Konzepte vermitteln


Einige seriöse Crowdsourcing-Sicherheitsfirmen verfügen über eine nützliche Liste vergangener Audits, die es zu studieren gilt.

Werden Sie ein intelligenter Vertragsprüfer

Wenn Sie Solidity nicht fließend beherrschen, können Sie die intelligenten Verträge von Ethereum nicht prüfen.


Es gibt keine branchenweit anerkannte Zertifizierung zum Smart-Contract-Auditor. Jeder kann eine Website und Social-Media-Profile erstellen und sich als Soliditätsprüfer ausgeben und mit dem Verkauf von Dienstleistungen beginnen, und viele haben dies auch getan. Seien Sie daher vorsichtig und holen Sie Empfehlungen ein, bevor Sie einen Mitarbeiter einstellen.

Um ein Smart-Contract-Auditor zu werden, müssen Sie beim Erkennen von Fehlern wesentlich besser sein als der durchschnittliche Solidity-Entwickler. Daher besteht der „Fahrplan“ auf dem Weg zum Wirtschaftsprüfer aus nichts weiter als Monaten unermüdlichen und bewussten Übens, bis Sie ein besserer Fehlerfänger bei intelligenten Verträgen sind als die meisten anderen.


Wenn es Ihnen an der Entschlossenheit mangelt, Ihre Kollegen bei der Identifizierung von Schwachstellen zu übertreffen, ist es unwahrscheinlich, dass Sie die kritischen Probleme erkennen, bevor es die gut ausgebildeten und motivierten Kriminellen tun.

Kalte Wahrheit über Ihre Erfolgschancen als Sicherheitsprüfer für intelligente Verträge

Die Prüfung intelligenter Verträge gilt in jüngster Zeit aufgrund der Wahrnehmung, dass sie lukrativ ist, als wünschenswertes Arbeitsfeld. Tatsächlich haben einige Bug-Bounty-Auszahlungen die Grenze von 1 Million Dollar überschritten, aber das ist eine äußerst seltene Ausnahme und nicht die Norm.


Code4rena verfügt über eine öffentliche Rangliste der Auszahlungen von Wettbewerbern bei ihren Prüfungswettbewerben, die uns einige Daten über Erfolgsquoten liefert.


Bisher stehen 1171 Namen auf der Tafel

  • Nur 29 Konkurrenten haben einen Lebensverdienst von über 100.000 US-Dollar (2,4 %).
  • Nur 57 haben ein Lebenseinkommen von über 50.000 US-Dollar (4,9 %).
  • Nur 170 haben ein Lebenseinkommen von über 10.000 US-Dollar (14,5 %).


Bedenken Sie auch Folgendes: Als Openzeppelin eine Bewerbung für ein Sicherheitsforschungsstipendium (keine Stelle, sondern ein Screening und eine Schulung vor der Einstellung) einreichte, erhielten sie über 300 Bewerbungen, nur um weniger als 10 Kandidaten auszuwählen, von denen noch weniger eine volle Stelle erhielten Zeitjob.

https://twitter.com/David_Bessin/status/1625167906328944640

Das ist eine niedrigere Zulassungsquote als in Harvard.


Smart Contract Auditing ist ein kompetitives Nullsummenspiel. Es gibt nur eine begrenzte Menge an Projekten, die geprüft werden müssen, ein begrenztes Budget für Sicherheit und nur eine begrenzte Anzahl von Fehlern, die es zu finden gilt. Wenn Sie jetzt mit dem Studium der Sicherheit beginnen, gibt es Dutzende hochmotivierter Einzelpersonen und Teams, die einen enormen Vorsprung vor Ihnen haben. Die meisten Projekte sind bereit, für einen Prüfer mit gutem Ruf einen Aufpreis zu zahlen, statt für einen ungeprüften neuen Prüfer.


In diesem Artikel haben wir mindestens 20 verschiedene Kategorien von Schwachstellen aufgelistet. Wenn Sie eine Woche damit verbracht haben, jedes einzelne zu meistern (was einigermaßen optimistisch ist), fangen Sie gerade erst an zu verstehen, was erfahrenen Prüfern allgemein bekannt ist. Wir haben in diesem Artikel nicht auf Gasoptimierung oder Tokenomics eingegangen, beides wichtige Themen, die ein Prüfer verstehen muss. Rechnen Sie nach und Sie werden sehen, dass dies keine kurze Reise ist.


Allerdings ist die Community im Allgemeinen freundlich und hilfsbereit gegenüber Neulingen und es gibt jede Menge Tipps und Tricks. Aber für diejenigen, die diesen Artikel lesen und hoffen, mit der Sicherheit intelligenter Verträge Karriere zu machen, ist es wichtig, sich darüber im Klaren zu sein, dass die Chancen auf eine lukrative Karriere nicht gut stehen. Erfolg ist nicht das Standardergebnis.


Das ist natürlich machbar , und nicht wenige Leute, die Solidity nicht kannten, haben sich zu einer lukrativen Karriere in der Wirtschaftsprüfung entwickelt. Es ist wohl einfacher, innerhalb von zwei Jahren einen Job als Smart Contract Auditor zu bekommen, als zum Jurastudium zugelassen zu werden und die Anwaltsprüfung zu bestehen. Im Vergleich zu vielen anderen Berufswahlmöglichkeiten bietet es sicherlich mehr Vorteile.


Dennoch wird es von Ihrer Seite herkulische Ausdauer erfordern, den Berg an sich schnell entwickelndem Wissen, der vor Ihnen liegt, zu meistern und Ihre Intuition für das Erkennen von Fehlern zu schärfen.

Das soll nicht heißen, dass es sich nicht lohnt, die Sicherheit intelligenter Verträge zu erlernen. Das ist es absolut. Aber wenn Sie mit Dollarzeichen im Blick auf das Spielfeld zugehen, sollten Sie Ihre Erwartungen im Zaum halten.

Abschluss

Es ist wichtig, sich der bekannten Anti-Patterns bewusst zu sein. Allerdings sind die meisten Fehler in der realen Welt anwendungsspezifisch. Das Erkennen beider Kategorien von Schwachstellen erfordert kontinuierliche und bewusste Übung.


Lernen Sie die Sicherheit intelligenter Verträge und viele weitere Themen zur Ethereum-Entwicklung mit unserem branchenführenden Solidity-Training .


Das Leitbild für diesen Artikel wurde vomAI Image Generator von HackerNoon über die Eingabeaufforderung „ein Roboter, der einen Computer schützt“ generiert.