Naar hoofdinhoud gaan
Change page

Smart contract veiligheid

Smart contracts zijn extreem flexibel en kunnen grote hoeveelheden waarden en gegevens beheren, terwijl ze onveranderlijke logica uitvoeren op basis van code die op de blockchain is geïnstalleerd. Hierdoor is een levendig ecosysteem ontstaan van vertrouwensloze en gedecentraliseerde toepassingen die veel voordelen bieden ten opzichte van oudere systemen. Ze vormen ook kansen voor aanvallers die winst willen maken door kwetsbaarheden in smart contracts uit te buiten.

Openbare blockchains, zoals Ethereum, maken de beveiliging van smart contracts nog ingewikkelder. Ingezette contractcode kan gewoonlijk niet worden gewijzigd om beveiligingsproblemen op te lossen, terwijl activa die zijn gestolen van smart contracts extreem moeilijk te traceren zijn en meestal niet kunnen worden teruggehaald vanwege de onveranderlijkheid.

Hoewel de cijfers variëren, wordt geschat dat het totale bedrag aan waarde dat gestolen of verloren is gegaan door beveiligingsfouten in smart contracts al gauw meer dan $1 miljard bedraagt. Hieronder vallen ook opvallende incidenten, zoals de DAO-hack(opens in a new tab) (3,6M ETH gestolen, met een waarde van meer dan 1 miljard dollar in de huidige prijzen), Parity multisig-wallethack(opens in a new tab) ($30M verloren aan hackers), en het Parity frozen wallet-probleem(opens in a new tab) (meer dan $300M in ETH voor altijd vergrendeld).

De bovengenoemde problemen maken het noodzakelijk voor ontwikkelaars om te investeren in het bouwen van veilige, robuuste en veerkrachtige smart contracts. De beveiliging van smart contracts is een serieuze zaak en iedere ontwikkelaar doet er goed aan om dit te leren. Deze gids behandelt de beveiligingsaspecten voor Ethereum-ontwikkelaars en gaat in op bronnen voor het verbeteren van de beveiliging van smart contracts.

Vereisten

Zorg ervoor dat u zich eerst inleest over de basisprincipes van de ontwikkeling van smart contracts voordat u met de beveiliging ervan begint.

Richtlijnen voor het bouwen van veilige smart contracts met Ethereum

1. Ontwerp de juiste toegangscontroles

In smart contracts kunnen functies met de markering public of external worden aangeroepen door accounts in externe eigendom (EOA's) of contractaccounts. Het is noodzakelijk om de openbare zichtbaarheid van functies op te geven als u wilt dat anderen kunnen communiceren met uw contract. Functies met de markering private kunnen echter alleen worden opgeroepen door functies binnen het smart contract, en niet door externe accounts. Elke netwerkdeelnemer toegang geven tot contractfuncties kan problemen veroorzaken, vooral als dit betekent dat iedereen gevoelige handelingen kan uitvoeren (zoals het "minten" van nieuwe tokens).

Om ongeoorloofd gebruik van smart contract-functies te voorkomen, is het noodzakelijk om veilige toegangscontroles te implementeren. Mechanismen voor toegangscontrole beperken de mogelijkheid om bepaalde functies in een smart contract te gebruiken tot goedgekeurde entiteiten, zoals accounts die verantwoordelijk zijn voor het beheer van het contract. Het Ownable-patroon en rolgebaseerde controle zijn twee patronen die nuttig zijn voor het implementeren van toegangscontrole in smart contracts:

Ownable-patroon

In het Ownable-patroon wordt een adres ingesteld als de “eigenaar” van het contract tijdens het aanmaken van het contract. Beschermde functies krijgen een OnlyOwner-modifier, die ervoor zorgt dat het contract de identiteit van het oproepende adres verifieert voordat de functie wordt uitgevoerd. Oproepen naar beschermde functies vanaf andere adressen dan de eigenaar van het contract worden altijd teruggedraaid, waardoor ongewenste toegang wordt voorkomen.

Op rol gebaseerde toegangscontrole

Het registreren van een enkel adres als Owner in een smart contract brengt het risico van centralisatie met zich mee en vormt een single point-of-failure. Als de accountsleutels van de eigenaar in gevaar komen, kunnen aanvallers het contract dat eigendom is van de eigenaar aanvallen. Daarom kan het gebruik van een op rollen gebaseerd toegangscontrolepatroon met verschillende beheerdersaccounts een betere optie zijn.

Bij een op rol gebaseerd toegangsbeheer wordt de toegang tot gevoelige functies verdeeld over een reeks vertrouwde deelnemers. Eén account kan bijvoorbeeld verantwoordelijk zijn voor het minten van tokens, terwijl een andere account upgrades uitvoert of het contract pauzeert. Door toegangscontrole op deze manier te decentraliseren, worden single points of failure geëlimineerd en worden aannames ten aanzien van vertrouwen van gebruikers verminderd.

Wallets met verschillende handtekeningen gebruiken

Een andere benadering voor het implementeren van een veilige toegangscontrole is het gebruik van een account met verschillende handtekeningen om een contract te beheren. In tegenstelling tot een gewone EOA, zijn accounts met verschillende handtekeningen eigendom van verschillende entiteiten en vereisen ze handtekeningen van een minimum aantal accounts. Bijvoorbeeld tussen de 3 en de 5 om transacties uit te voeren.

Het gebruik van een multisig (multi signature) voor toegangscontrole zorgt voor een extra beveiligingslaag, omdat acties op het doelcontract toestemming vereisen van verschillende partijen. Dit is vooral handig als het Ownable-patroon gebruikt moet worden, omdat het dan voor een aanvaller of kwaadwillende insider moeilijker wordt om gevoelige contractfuncties te manipuleren voor kwaadwillende doeleinden.

2. Gebruik de elementen require(), assert() en revert() om contracthandelingen te bewaken

Zoals gezegd kan iedereen publieke functies in uw smart contract oproepen zodra het is ingezet op de blockchain. Omdat u niet van tevoren kunt weten hoe externe accounts met een contract zullen omgaan, is het ideaal om interne beveiligingen tegen problematische handelingen te implementeren voordat u het contract inzet. U kunt correct gedrag afdwingen in smart contracts door de elementen require(), assert(), en revert() te gebruiken om uitzonderingen te activeren en statusveranderingen terug te draaien als de uitvoering niet aan bepaalde eisen voldoet.

require(): require worden gedefinieerd aan het begin van functies en zorgen ervoor dat aan vooraf gedefinieerde voorwaarden wordt voldaan voordat de opgeroepen functie wordt uitgevoerd. Een require-element kan worden gebruikt om gebruikersinvoer te valideren, toestandsvariabelen te controleren of de identiteit van het oproepende account te verifiëren voordat er verder wordt gegaan met een functie.

assert(): assert() wordt gebruikt om interne fouten op te sporen en te controleren op schendingen van “invarianten” in uw code. Een invariant is een logische bewering over de status van een contract die waar moet zijn voor alle functie-uitvoeringen. Een voorbeeld van een invariant is het maximale totale aanbod of saldo van een tokencontract. Het gebruik van assert() zorgt ervoor dat uw contract nooit een kwetsbare status bereikt, en als dat toch gebeurt, worden alle wijzigingen aan statusvariabelen teruggedraaid.

revert(): revert() kan worden gebruikt in een if-else-element dat een uitzondering activeert als niet aan de vereiste voorwaarde wordt voldaan. Het voorbeeldcontract hieronder gebruikt revert() om de uitvoering van functies te bewaken:

1pragma solidity ^0.8.4;
2
3contract VendingMachine {
4 address owner;
5 error Unauthorized();
6 function buy(uint amount) public payable {
7 if (amount > msg.value / 2 ether)
8 revert("Not enough Ether provided.");
9 // Perform the purchase.
10 }
11 function withdraw() public {
12 if (msg.sender != owner)
13 revert Unauthorized();
14
15 payable(msg.sender).transfer(address(this).balance);
16 }
17}
Toon alle

3. Smart contracts testen en de juistheid van de code verifiëren

De onveranderlijkheid van code die wordt uitgevoerd in de Ethereum Virtual Machine betekent dat smart contracts een hoger niveau van kwaliteitsbeoordeling vereisen tijdens de ontwikkelingsfase. Door uw contract uitgebreid te testen en te observeren op onverwachte resultaten, verbetert u de beveiliging aanzienlijk en beschermt u uw gebruikers op lange termijn.

De gebruikelijke methode is om kleine unit tests te schrijven met behulp van nepgegevens die het contract naar verwachting zal ontvangen van gebruikers. Unit testing is goed voor het testen van de functionaliteit van bepaalde functies en om ervoor te zorgen dat een smart contract werkt zoals verwacht.

Helaas is unit testing minimaal effectief voor het verbeteren van de veiligheid van smart contracts als het geïsoleerd wordt gebruikt. Een unit test kan bewijzen dat een functie correct wordt uitgevoerd voor nepgegevens, maar unit tests zijn slechts zo effectief als de tests die worden geschreven. Dit maakt het moeilijk om gemiste randgevallen en kwetsbaarheden op te sporen die de veiligheid van uw smart contract kunnen doorbreken.

Een betere aanpak is om unit testing te combineren met eigendomsgerichte testing, uitgevoerd met statische en dynamische analyse. Statische analyse vertrouwt op representaties op laag niveau, zoals controlestroomdiagrammen(opens in a new tab) en abstracte syntaxistrees(opens in a new tab) om bereikbare programmatoestanden en uitvoeringspaden te analyseren. Ondertussen voeren dynamische analysetechnieken, zoals smart contract fuzzing(opens in a new tab), contractcode uit met willekeurige invoerwaarden om bewerkingen te detecteren die de beveiligingseigenschappen schenden.

Formele verificatie is een andere techniek voor het verifiëren van beveiligingseigenschappen in smart contracts. In tegenstelling tot gewone tests, kan formele verificatie onomstotelijk de afwezigheid van fouten in een smart contract aantonen. Dit wordt bereikt door een formele specificatie te creëren die de gewenste beveiligingseigenschappen vastlegt en te bewijzen dat een formeel model van de contracten aan deze specificatie voldoet.

4. Vraag om een onafhankelijke beoordeling van uw code

Na het testen van uw contract is het goed om anderen te vragen de broncode te controleren op eventuele beveiligingsproblemen. Tests zullen niet elke fout in een smart contract blootleggen, maar een onafhankelijke beoordeling vergroot de kans op het ontdekken van kwetsbaarheden.

Audits

Het laten uitvoeren van een audit voor uw smart contract is een mogelijkheid om een onafhankelijke codebeoordeling uit te voeren. Auditors spelen een belangrijke rol om ervoor te zorgen dat smart contracts veilig zijn en vrij van kwaliteitsdefecten en ontwerpfouten.

Toch moet u audits niet zien als een wondermiddel. Audits op smart contracts kunnen niet elke bug opsporen en zijn vooral bedoeld voor een extra beoordelingsronde, die kan helpen bij het opsporen van problemen die ontwikkelaars hebben gemist tijdens de initiële ontwikkeling en het testen. U moet ook best practices volgen voor het werken met auditors, zoals het goed documenteren van code en het toevoegen van inline opmerkingen, om het voordeel van een audit op uw smart contract te maximaliseren.

Bug bounties

Een bug bounty programma organiseren is een andere manier om externe codebeoordelingen te implementeren. Een bug bounty is een financiële beloning die wordt gegeven aan individuele personen (meestal whitehat hackers) die kwetsbaarheden ontdekken in een applicatie.

Als bug bounties op de juiste manier worden gebruikt, geven ze leden van de hackergemeenschap een stimulans om uw code te inspecteren op kritieke fouten. Een voorbeeld uit het echte leven is de “infinite money bug” waarmee een aanvaller een onbeperkte hoeveelheid Ether zou kunnen creëren op Optimism(opens in a new tab), een protocol op laag 2 dat functioneert op Ethereum. Gelukkig ontdekte een whitehat hacker de fout(opens in a new tab) en bracht deze het team op de hoogte. Zo verdiende de hacker een groot bedrag(opens in a new tab).

Een nuttige strategie is om de uitbetaling van een bug bounty-programma vast te stellen in verhouding tot het bedrag dat op het spel staat. Deze benadering, die wordt omschreven als de “scaling bug bounty(opens in a new tab)”, biedt een financiële stimulans voor individuen om kwetsbaarheden op verantwoorde wijze op te sporen in plaats van ze uit te buiten.

5. Volg best practices tijdens de ontwikkeling van smart contracts

Het bestaan van audits en bug bounties is geen excuus voor uw verantwoordelijkheid om code van hoge kwaliteit te schrijven. Een goede beveiliging van smart contracts begint met het volgen van de juiste ontwerp- en ontwikkelingsprocessen:

6. Implementeer robuuste noodherstelplannen

Het ontwerpen van veilige toegangscontroles, het implementeren van functiemodifiers en andere suggesties kunnen de veiligheid van smart contracts verbeteren, maar ze kunnen de mogelijkheid van kwaadaardige uitbuitingen niet uitsluiten. Het bouwen van veilige smart contracts vereist “voorbereiding op mislukking”. Daarnaast moet er een noodplan bestaan om effectief te kunnen reageren op aanvallen. Een goed rampherstelplan bevat enkele of alle van de volgende onderdelen:

Contractupgrades

Hoewel smart contracts van Ethereum standaard onveranderlijk zijn, is het mogelijk om een zekere mate van veranderlijkheid te verkrijgen door upgradepatronen te gebruiken. Het upgraden van contracten is nodig in gevallen waar een kritieke fout uw oude contract onbruikbaar maakt en het inzetten van nieuwe logica de meest haalbare optie is.

Contractupgrademechanismes werken verschillend, maar het “proxypatroon” is één van de meer populaire benaderingen voor het upgraden van smart contracts. Proxypatronen(opens in a new tab) splitsen de status en logica van een applicatie op tussen twee contracten. Het eerste contract (een “proxycontract” genoemd) slaat statusvariabelen op (bijv. gebruikerssaldi), terwijl het tweede contract (een “logisch contract” genoemd) de code bevat voor het uitvoeren van contractfuncties.

Accounts werken samen met het proxycontract, dat alle functie-oproepen doorstuurt naar het logisch contract met behulp van de oproep op laag niveau delegatecall()(opens in a new tab). In tegenstelling tot een gewone berichtoproep, zorgt delegatecall() ervoor dat de code die wordt uitgevoerd op het adres van het logische contract wordt uitgevoerd in de context van het oproepende contract. Dit betekent dat het logische contract altijd naar de opslag van de proxy schrijft (in plaats van de eigen opslag) en dat de oorspronkelijke waarden van msg.sender en msg.value behouden blijven.

Het delegeren van oproepen naar het logische contract vereist het opslaan van het adres in de opslag van het proxycontract. Daarom is het upgraden van de logica van het contract slechts een kwestie van een ander logisch contract inzetten en het nieuwe adres opslaan in het proxycontract. Aangezien volgende oproepen naar het proxycontract automatisch worden doorgestuurd naar het nieuwe logische contract, hebt u het contract “geüpgraded” zonder de code te wijzigen.

Meer over het upgraden van contracten.

Noodstops

Zoals gezegd kunnen uitgebreide controles en tests onmogelijk alle bugs in een smart contract opsporen. Als er na de inzet een kwetsbaarheid in uw code verschijnt, is patchen onmogelijk omdat u de code die op het contractadres wordt uitgevoerd niet kunt wijzigen. Ook kunnen upgrademechanismen (zoals proxypatronen) tijd kosten om te implementeren (ze vereisen vaak goedkeuring van verschillende partijen), wat aanvallers alleen maar meer tijd geeft om meer schade aan te richten.

De nucleaire optie is om een “noodstop”-functie te implementeren die oproepen naar kwetsbare functies in een contract blokkeert. Noodstops bestaan meestal uit de volgende onderdelen:

  1. Een globale Booleaanse variabele die aangeeft of het smart contract zich in een gestopte status bevindt of niet. Deze variabele wordt ingesteld op false bij het opzetten van het contract, maar zal terugkeren naar true zodra het contract wordt gestopt.

  2. Functies die in hun uitvoering verwijzen naar de Booleaanse variabele. Dergelijke functies zijn toegankelijk als het smart contract niet gestopt is, en worden ontoegankelijk als de noodstopfunctie geactiveerd wordt.

  3. Een entiteit die toegang heeft tot de noodstopfunctie, die de Booleaanse variabele op true zet. Om kwaadaardige acties te voorkomen, kunnen oproepen naar deze functie beperkt worden tot een vertrouwd adres (bijv. de eigenaar van het contract).

Zodra het contract de noodstop activeert, kunnen bepaalde functies niet meer worden opgeroepen. Dit wordt bereikt door selectiefuncties te omhullen met een modifier die verwijst naar de globale variabele. Hieronder staat een voorbeeld(opens in a new tab) dat een implementatie van dit patroon in contracten beschrijft:

1// This code has not been professionally audited and makes no promises about safety or correctness. Use at your own risk.
2
3contract EmergencyStop {
4
5 bool isStopped = false;
6
7 modifier stoppedInEmergency {
8 require(!isStopped);
9 _;
10 }
11
12 modifier onlyWhenStopped {
13 require(isStopped);
14 _;
15 }
16
17 modifier onlyAuthorized {
18 // Check for authorization of msg.sender here
19 _;
20 }
21
22 function stopContract() public onlyAuthorized {
23 isStopped = true;
24 }
25
26 function resumeContract() public onlyAuthorized {
27 isStopped = false;
28 }
29
30 function deposit() public payable stoppedInEmergency {
31 // Deposit logic happening here
32 }
33
34 function emergencyWithdraw() public onlyWhenStopped {
35 // Emergency withdraw happening here
36 }
37}
Toon alle
Kopiëren

Dit voorbeeld toont de basisfuncties van noodstops:

  • isStopped is een Boolean die evalueert naar false aan het begin en true wanneer het contract in noodmodus gaat.

  • De functiemodifiers onlyWhenStopped en stoppedInEmergency controleren de variabele isStopped. stoppedInEmergency wordt gebruikt om functies te controleren die ontoegankelijk moeten zijn als het contract kwetsbaar is (bv. deposit()). Oproepen naar deze functies worden gewoon teruggedraaid.

onlyWhenStopped wordt gebruikt voor functies die oproepbaar moeten zijn tijdens een noodgeval (bv. emergencyWithdraw()). Dergelijke functies kunnen helpen om de situatie op te lossen, vandaar hun uitsluiting van de lijst met “beperkte functies”.

Het gebruik van een noodstopfunctie zorgt voor een effectieve noodoplossing voor het omgaan met ernstige kwetsbaarheden in uw smart contract. Het vergroot echter de noodzaak voor gebruikers om ontwikkelaars te vertrouwen dat ze het niet om zelfzuchtige redenen activeren. Daarom is het mogelijk om de controle over de noodstop te decentraliseren door deze te onderwerpen aan een on-chain stemmechanisme, tijdslot of goedkeuring van een multisig-wallet.

Evenementenmonitoring

Met Evenementen(opens in a new tab) kunt u oproepen naar functies van smart contracts opvolgen en wijzigingen aan statusvariabelen controleren. Het is ideaal om uw smart contract te programmeren om een evenement uit te zenden wanneer een partij een veiligheidskritieke actie uitvoert (bv. middelen opnemen).

Gebeurtenissen loggen en ze off-chain monitoren biedt inzicht in contracthandelingen en helpt bij het sneller ontdekken van kwaadaardige acties. Dit betekent dat uw team sneller kan reageren op hacks en actie kan ondernemen om de gevolgen voor gebruikers te beperken, zoals het pauzeren van functies of het uitvoeren van een upgrade.

U kunt ook kiezen voor een kant-en-klare bewakingstool die automatisch waarschuwingen doorstuurt wanneer iemand interactie heeft met uw contracten. Met deze tools kunt u aangepaste waarschuwingen creëren op basis van verschillende triggers, zoals transactievolume, frequentie van functieoproepen of de betrokken specifieke functies. U kunt bijvoorbeeld een waarschuwing programmeren die wordt weergegeven wanneer het opgenomen bedrag in een enkele transactie een bepaalde drempel overschrijdt.

7. Ontwerp van veilige bestuurssystemen

Mogelijk wilt u uw applicatie decentraliseren door de controle over de belangrijkste smart contracts over te dragen aan leden van de gemeenschap. In dit geval bevat het smart contractsysteem een bestuursmodule. Dit is een mechanisme waarmee leden van de gemeenschap bestuurlijke acties kunnen goedkeuren via een on-chain bestuurssysteem. Er kan bijvoorbeeld door tokenhouders worden gestemd over een voorstel om een proxycontract te upgraden naar een nieuwe implementatie.

Gedecentraliseerd bestuur kan gunstig zijn, vooral omdat het de belangen van ontwikkelaars en eindgebruikers op één lijn brengt. Desondanks kunnen bestuursmechanismen voor smart contracts leiden tot nieuwe risico's als ze verkeerd worden geïmplementeerd. Een aannemelijk scenario is als een aanvaller enorme stemmacht verwerft (gemeten in aantal tokens in bewaring) door een snelle lening (flash loan) af te sluiten en een kwaadaardig voorstel doordrukt.

Een manier om problemen met betrekking tot on-chain bestuur te voorkomen is het gebruiken van een tijdslot(opens in a new tab). Een tijdslot zorgt ervoor dat een smart contract bepaalde acties niet kan uitvoeren tot een bepaalde tijd voorbij is. Andere strategieën zijn onder andere het toekennen van een “stemgewicht” aan elke token op basis van hoe lang het is vergrendeld, of het meten van de stemkracht van een adres op een historische periode (bijvoorbeeld 2-3 blocks in het verleden) in plaats van de huidige block. Beide methoden verminderen de mogelijkheid om snel stemmacht te vergaren om de stemmen op de chain te beïnvloeden.

Meer over het ontwerpen van veilige bestuurssystemen(opens in a new tab), verschillende stemmechanismen in DAO's(opens in a new tab), en de veelvoorkomende DAO-aanvalsvectoren die gebruikmaken van DeFi(opens in a new tab) in de gedeelde links.

8. Beperk complexiteit in code tot een minimum

Traditionele software-ontwikkelaars zijn bekend met het KISS-principe (“keep it simple, stupid”), dat adviseert om geen onnodige complexiteit te introduceren in het ontwerp van software. Dit sluit aan bij de lang gekoesterde gedachte dat “complexe systemen op complexe manieren defect raken” en vatbaarder zijn voor kostbare fouten.

Dingen eenvoudig houden is van bijzonder belang bij het schrijven van smart contracts, aangezien smart contracts potentieel grote hoeveelheden waarde controleren. Een tip om alles eenvoudig te houden bij het schrijven van smart contracts, is het waar mogelijk hergebruiken van bestaande bibliotheken, zoals OpenZeppelin Contracts(opens in a new tab). Omdat deze bibliotheken uitgebreid zijn gecontroleerd en getest door ontwikkelaars, verkleint het gebruik ervan de kans op het introduceren van bugs door nieuwe functionaliteit vanaf nul te schrijven.

Een ander veelgebruikt advies is om kleine functies te schrijven en contracten modulair te houden door bedrijfslogica te verdelen over verschillende contracten. Niet alleen verkleint het schrijven van eenvoudigere code het aanvalsoppervlak in een smart contract, het maakt het ook eenvoudiger om te redeneren over de correctheid van het totale systeem en mogelijke ontwerpfouten vroegtijdig op te sporen.

9. Verdediging tegen veelvoorkomende kwetsbaarheden in smart contracts

Reentrancy

De EVM staat geen gelijktijdigheid toe, wat betekent dat twee contracten die betrokken zijn bij een berichtoproep niet gelijktijdig kunnen worden uitgevoerd. Een externe oproep pauzeert de uitvoering en het geheugen van het oproepende contract totdat de oproep terugkomt, waarna de uitvoering op de normale manier verdergaat. Dit proces kan formeel beschreven worden als het overdragen van besturingsstroom(opens in a new tab) naar een ander contract.

Hoewel dit meestal geen kwaad kan, kan het overbrengen van besturingsstroom naar niet-vertrouwde contracten problemen veroorzaken, zoals reentrancy. Een reentrancy-aanval treedt op wanneer een kwaadaardig contract terug een kwetsbaar contract oproept voordat de oorspronkelijke oproeping van de functie is voltooid. Dit type aanval kan het beste worden uitgelegd met een voorbeeld.

Denk aan een eenvoudig smart contract ('Victim') waarmee iedereen Ether kan storten en opnemen:

1// This contract is vulnerable. Do not use in production
2
3contract Victim {
4 mapping (address => uint256) public balances;
5
6 function deposit() external payable {
7 balances[msg.sender] += msg.value;
8 }
9
10 function withdraw() external {
11 uint256 amount = balances[msg.sender];
12 (bool success, ) = msg.sender.call.value(amount)("");
13 require(success);
14 balances[msg.sender] = 0;
15 }
16}
Toon alle
Kopiëren

Dit contract heeft een withdraw()-functie waarmee gebruikers ETH kunnen opnemen die ze eerder in het contract hebben gestort. Bij het verwerken van een opname voert het contract de volgende handelingen uit:

  1. Controleert het ETH-saldo van de gebruiker
  2. Stuurt middelen naar het oproepadres
  3. Zet zijn/haar saldo terug op 0, zodat er geen middelen meer kunnen worden opgenomen van de gebruiker

De withdraw()-functie in het Victim-contract volgt een “controles-interacties-effecten”-patroon. Het controleert of aan de voorwaarden voor uitvoering is voldaan (d.w.z. de gebruiker heeft een positief ETH-saldo) en voert de interactie uit door ETH naar het adres van de oproeper te sturen, voordat de effecten van de transactie worden toegepast (d.w.z. het saldo van de gebruiker wordt verlaagd).

Als withdraw() wordt opgeroepen vanaf een account in externe eigendom (EOA), wordt de functie uitgevoerd zoals verwacht: msg.sender.call.value() stuurt ETH naar de oproeper. Maar als msg.sender een smart contract-account is dat withdraw() aanroept, zal het verzenden van middelen met msg.sender.call.value() ook code activeren die op dat adres is opgeslagen om uit te voeren.

Stel dat dit de code is die wordt ingezet op het contractadres:

1 contract Attacker {
2 function beginAttack() external payable {
3 Victim(victim_address).deposit.value(1 ether)();
4 Victim(victim_address).withdraw();
5 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(victim_address).withdraw();
10 }
11 }
12}
Toon alle
Kopiëren

Dit contract is ontworpen om drie dingen te doen:

  1. Een storting accepteren van een ander account (waarschijnlijk de EOA van de aanvaller)
  2. 1 ETH storten in het Victim-contract
  3. De 1 ETH opnemen die is opgeslagen in het smart contract

Er is hier niets aan de hand, behalve dat Attacker een andere functie heeft die withdraw() in Victim opnieuw oproept als het overgebleven gas van de inkomende msg.sender.call.value meer dan 40.000 bedraagt. Dit geeft Attacker de mogelijkheid om Victim opnieuw binnen te dringen en meer middelen op te nemen voordat de eerste aanroep van withdraw voltooid is. De cyclus ziet er als volgt uit:

1- Attacker's EOA calls `Attacker.beginAttack()` with 1 ETH
2- `Attacker.beginAttack()` deposits 1 ETH into `Victim`
3- `Attacker` calls `withdraw() in `Victim`
4- `Victim` checks `Attacker`’s balance (1 ETH)
5- `Victim` sends 1 ETH to `Attacker` (which triggers the default function)
6- `Attacker` calls `Victim.withdraw()` again (note that `Victim` hasn’t reduced `Attacker`’s balance from the first withdrawal)
7- `Victim` checks `Attacker`’s balance (which is still 1 ETH because it hasn’t applied the effects of the first call)
8- `Victim` sends 1 ETH to `Attacker` (which triggers the default function and allows `Attacker` to reenter the `withdraw` function)
9- The process repeats until `Attacker` runs out of gas, at which point `msg.sender.call.value` returns without triggering additional withdrawals
10- `Victim` finally applies the results of the first transaction (and subsequent ones) to its state, so `Attacker`’s balance is set to 0
Toon alle
Kopiëren

De conclusie is dat omdat het saldo van de oproeper niet op 0 wordt gezet totdat de uitvoering van de functie is voltooid, latere oproepen zullen slagen en de oproeper in staat stellen zijn/haar saldo meerdere keren op te nemen. Dit soort aanval kan gebruikt worden om middelen van een smart contract leeg te halen, zoals gebeurde in de 2016 DAO hack(opens in a new tab). Reentrancy-aanvallen zijn vandaag de dag nog steeds een kritiek probleem voor smart contracts, zoals blijkt uit openbare lijsten van reentrancy-exploitaties(opens in a new tab).

Hoe reentrancy-aanvallen voorkomen

U kunt best het patroon controles-effecten-interacties(opens in a new tab) volgen om om te gaan met reentrancy. Dit patroon ordent de uitvoering van functies op dusdanige wijze dat code die noodzakelijke controles uitvoert voordat er verder wordt gegaan met de uitvoering als eerste komt, gevolgd door code die de contractstatus manipuleert, waarbij code die interactie heeft met andere contracten of EOA's als laatste komt.

Het patroon controles-effecten-interacties wordt gebruikt in een herziene versie van het Victim-contract dat hieronder wordt weergegeven:

1contract NoLongerAVictim {
2 function withdraw() external {
3 uint256 amount = balances[msg.sender];
4 balances[msg.sender] = 0;
5 (bool success, ) = msg.sender.call.value(amount)("");
6 require(success);
7 }
8}
Kopiëren

Dit contract voert een controle uit op het saldo van de gebruiker, past de effecten toe van de functie withdraw() (door het saldo van de gebruiker op 0 te zetten) en gaat verder met het uitvoeren van de interactie (het verzenden van ETH naar het adres van de gebruiker). Dit zorgt ervoor dat het contract zijn opslag bijwerkt voor de externe oproep, waardoor de reentrancy conditie die de eerste aanval mogelijk maakte, wordt geëlimineerd. Het Attacker-contract kan nog terug een oproep doen naar NoLongerAVictim, maar omdat balances[msg.sender] op 0 is gezet, zullen extra opnames een fout geven.

Een andere optie is het gebruik van een wederzijdse uitsluitingsvergrendeling (vaak omschreven als "mutual exclusion lock" of "mutex") die een deel van de status van een contract vergrendelt totdat een oproep voor een functie is voltooid. Dit wordt geïmplementeerd met behulp van een Booleaanse variabele die wordt ingesteld op true voordat de functie wordt uitgevoerd en wordt teruggezet naar false nadat de oproep is uitgevoerd. Zoals in het onderstaande voorbeeld te zien is, beschermt het gebruik van een mutex een functie tegen recursieve oproepen terwijl de oorspronkelijke oproep nog steeds wordt verwerkt, waardoor reentrancy effectief wordt gestopt.

1pragma solidity ^0.7.0;
2
3contract MutexPattern {
4 bool locked = false;
5 mapping(address => uint256) public balances;
6
7 modifier noReentrancy() {
8 require(!locked, "Blocked from reentrancy.");
9 locked = true;
10 _;
11 locked = false;
12 }
13 // This function is protected by a mutex, so reentrant calls from within `msg.sender.call` cannot call `withdraw` again.
14 // The `return` statement evaluates to `true` but still evaluates the `locked = false` statement in the modifier
15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {
16 require(balances[msg.sender] >= _amount, "No balance to withdraw.");
17
18 balances[msg.sender] -= _amount;
19 bool (success, ) = msg.sender.call{value: _amount}("");
20 require(success);
21
22 return true;
23 }
24}
Toon alle
Kopiëren

U kunt ook een pull payments(opens in a new tab)-systeem gebruiken waarbij gebruikers middelen moeten opnemen van de smart contracts, in plaats van een "push payments"-systeem dat middelen naar accounts stuurt. Dit voorkomt de mogelijkheid om onbedoeld code op onbekende adressen te activeren (en kan ook bepaalde denial-of-service-aanvallen voorkomen).

Integer underflows en overflows

Een integer overflow treedt op wanneer de resultaten van een rekenkundige bewerking buiten het aanvaardbare waardenbereik vallen, waardoor het “doorrolt” naar de laagste representeerbare waarde. Een uint8 kan bijvoorbeeld alleen waarden tot 2^8-1=255 opslaan. Rekenkundige bewerkingen die resulteren in waarden hoger dan 255 zullen overgaan tot een overflow en uint resetten naar 0, vergelijkbaar met hoe de kilometerteller van een auto wordt gereset naar 0 zodra deze de maximale kilometerstand bereikt (999999).

Integer underflows gebeuren om vergelijkbare redenen: de resultaten van een rekenkundige bewerking vallen onder het acceptabele bereik. Stel dat u 0 probeert te verlagen in een uint8, dan rolt het resultaat gewoon door naar de maximaal representeerbare waarde (255).

Zowel integer overflows als underflows kunnen leiden tot onverwachte veranderingen in de statusvariabelen van een contract en resulteren in ongeplande uitvoering. Hieronder staat een voorbeeld dat toont hoe een aanvaller een rekenkundige overloop in een smart contract kan misbruiken om een ongeldige handeling uit te voeren:

1pragma solidity ^0.7.6;
2
3// This contract is designed to act as a time vault.
4// User can deposit into this contract but cannot withdraw for at least a week.
5// User can also extend the wait time beyond the 1 week waiting period.
6
7/*
81. Deploy TimeLock
92. Deploy Attack with address of TimeLock
103. Call Attack.attack sending 1 ether. You will immediately be able to
11 withdraw your ether.
12
13What happened?
14Attack caused the TimeLock.lockTime to overflow and was able to withdraw
15before the 1 week waiting period.
16*/
17
18contract TimeLock {
19 mapping(address => uint) public balances;
20 mapping(address => uint) public lockTime;
21
22 function deposit() external payable {
23 balances[msg.sender] += msg.value;
24 lockTime[msg.sender] = block.timestamp + 1 weeks;
25 }
26
27 function increaseLockTime(uint _secondsToIncrease) public {
28 lockTime[msg.sender] += _secondsToIncrease;
29 }
30
31 function withdraw() public {
32 require(balances[msg.sender] > 0, "Insufficient funds");
33 require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
34
35 uint amount = balances[msg.sender];
36 balances[msg.sender] = 0;
37
38 (bool sent, ) = msg.sender.call{value: amount}("");
39 require(sent, "Failed to send Ether");
40 }
41}
42
43contract Attack {
44 TimeLock timeLock;
45
46 constructor(TimeLock _timeLock) {
47 timeLock = TimeLock(_timeLock);
48 }
49
50 fallback() external payable {}
51
52 function attack() public payable {
53 timeLock.deposit{value: msg.value}();
54 /*
55 if t = current lock time then we need to find x such that
56 x + t = 2**256 = 0
57 so x = -t
58 2**256 = type(uint).max + 1
59 so x = type(uint).max + 1 - t
60 */
61 timeLock.increaseLockTime(
62 type(uint).max + 1 - timeLock.lockTime(address(this))
63 );
64 timeLock.withdraw();
65 }
66}
Toon alle
Hoe integer underflows en overflows voorkomen

Vanaf versie 0.8.0 weigert de Solidity-compiler code die resulteert in integer underflows en overflows. Contracten die gecompileerd zijn met een lagere compilerversie moeten echter controles uitvoeren op functies met rekenkundige bewerkingen of een bibliotheek gebruiken (bv. SafeMath(opens in a new tab)) die controleert op underflow/overflow.

Oracle-manipulatie

Oracles verzamelen off-chain-informatie en sturen het on-chain zodat smart contracts het kunnen gebruiken. Met oracles kunt u smart contracts ontwerpen die samenwerken met off-chain-systemen, zoals kapitaalmarkten, waardoor hun toepassing enorm wordt uitgebreid.

Maar als het orakel beschadigd is en onjuiste informatie op de chain zet, zullen smart contracts worden uitgevoerd op basis van onjuiste invoer, wat problemen kan veroorzaken. Dit is de basis van het “oracle-probleem”, dat zich richt op de taak om ervoor te zorgen dat informatie van een blockchain-oracle correct, up-to-date en tijdig is.

Een gerelateerd beveiligingsprobleem is het gebruik van een on-chain oracle, zoals een gedecentraliseerde crypto-uitwisseling, om de spotprijs voor een activum te verkrijgen. Uitleenplatforms in de decentrale financiering (DeFi)-sector doen dit vaak om de waarde van het onderpand van een gebruiker te bepalen om vast te stellen hoeveel ze kunnen lenen.

DEX-prijzen zijn vaak nauwkeurig, grotendeels dankzij arbitrageurs die de pariteit in de markten herstellen. Ze zijn echter vatbaar voor manipulatie, vooral als de on-chain oracle activaprijzen berekent op basis van historische handelspatronen (zoals meestal het geval is).

Een aanvaller kan bijvoorbeeld de spotprijs van een activum kunstmatig opdrijven door een snelle lening af te sluiten vlak voordat er interactie plaatsvindt met uw uitleencontract. De DEX vragen naar de prijs van het activum zou een hoger dan normale waarde opleveren (door de grote “kooporder” van de aanvaller die de vraag naar het activum verstoort), waardoor ze meer kunnen lenen dan ze eigenlijk mogen. Dergelijke “snelle leningsaanvallen” zijn gebruikt om misbruik te maken van het vertrouwen op prijs-oracles bij DeFi-toepassingen, wat protocollen miljoenen aan verloren geld heeft gekost.

Hoe oracle-manipulatie voorkomen

De minimale vereiste om oracle-manipulatie(opens in a new tab) te voorkomen is het gebruik van een gedecentraliseerd oracle-netwerk dat informatie opvraagt uit verschillende bronnen om single points-of-failure te vermijden. In de meeste gevallen hebben gedecentraliseerde oracles ingebouwde crypto-economische stimulansen om oracle-nodes aan te moedigen om correcte informatie te rapporteren, waardoor ze veiliger zijn dan gecentraliseerde oracles.

Als u van plan bent om een on-chain oracle te raadplegen voor activaprijzen, gebruik dan een oracle die een mechanisme voor tijdgewogen gemiddelde prijzen (Time-Weighted Average Price TWAP) implementeert. Een TWAP-oracle(opens in a new tab) vraagt de prijs op van een activum op twee verschillende tijdstippen (die u kunt wijzigen) en berekent de spotprijs op basis van het verkregen gemiddelde. Door langere tijdsperioden te kiezen, beschermt u uw protocol tegen prijsmanipulatie, omdat grote orders die recent zijn uitgevoerd de activaprijzen niet kunnen beïnvloeden.

Beveiligingsbronnen slimme contracten voor ontwikkelaars

Tools voor het analyseren van smart contracts en het verifiëren van de correctheid van code

  • Testtools en bibliotheken - Verzameling van tools met industriestandaard en bibliotheken voor het uitvoeren van unit tests, statische analyse en dynamische analyse op smart contracts.

  • Formele verificatietools - Tools voor het verifiëren van functionele correctheid in smart contracts en het controleren van invarianten.

  • Auditingservices voor smart contracts - Lijst van organisaties die auditingservices voor smart contracts leveren voor Ethereum-ontwikkelingsprojecten.

  • Bug bounty-platforms - Platforms voor het coördineren van bug bounty's en het belonen van verantwoorde openbaarmaking van kritieke kwetsbaarheden in smart contracts.

  • Fork Checker(opens in a new tab) - Een gratis online tool voor het controleren van alle beschikbare informatie over een forked contract.

  • ABI Encoder(opens in a new tab) - Een gratis online service voor het programmeren van uw Solidity-contractfuncties en constructorargumenten.

  • Aderyn(opens in a new tab) - Statische analysator van Solidity, die de abstracte syntaxistrees (Abstract Syntax Trees, AST) doorzoekt om vermoedelijke kwetsbaarheden op te sporen en problemen af te drukken in een gemakkelijk te gebruiken markdown-formaat.

Tools voor het monitoren van smart contracts

Tools voor veilig beheer van smart contracts

Auditingservices voor smart contracts

Bug bounty-platformen

Publicaties van bekende kwetsbaarheden in en uitbuitingen van smart contracts

Uitdagingen voor het leren beveiligen van smart contracts

Beste praktijken voor het beveiligen van smart contracts

Tutorials over de beveiliging van smart contracts

  • Hoe veilige smart contracts schrijven

  • Hoe u Slither gebruikt om bugs in smart contracts te vinden

  • How to use Manticore to find smart contract bugs

  • Richtlijnen voor de beveiliging van smart contracts

  • Hoe uw tokencontract veilig integreren met willekeurige tokens

  • Cyfrin Updraft - Volledige cursus over de beveiliging en auditing van smart contracts(opens in a new tab)

Was dit artikel nuttig?