Best Practices for soliditet for smart kontraktsikkerhet

blogg 1NyheterUtviklereEnterpriseBlockchain ExplainedBegivenheter og konferanserPresseNyhetsbrev

Abonner på vårt nyhetsbrev.

Epostadresse

Vi respekterer personvernet ditt

HjemBloggBlockchain utvikling

Best Practices for soliditet for smart kontraktsikkerhet

Fra overvåking til tidsstempelhensyn, her er noen tips for å sikre at smarte Ethereum-kontrakter blir forsterket. av ConsenSys 21. august 2020 Publisert 21. august 2020

soliditet beste praksis helt

Av ConsenSys Diligence, vårt team av blockchain-sikkerhetseksperter.

Hvis du har tatt den smarte kontraktssikkerhetsinnstillingen til deg og får tak i EVMs egenart, er det på tide å vurdere noen sikkerhetsmønstre som er spesifikke for Solidity-programmeringsspråket. I denne sammendraget vil vi fokusere på sikre utviklingsanbefalinger for soliditet, som også kan være lærerike for utvikling av smarte kontrakter på andre språk. 

Ok, la oss hoppe inn.

Bruk assert (), krever (), reverser () riktig

Bekvemmelighetsfunksjonene hevder og krever kan brukes til å se etter forhold og kaste et unntak hvis vilkåret ikke er oppfylt.

De hevder funksjonen skal bare brukes til å teste for interne feil, og for å sjekke invarianter.

De krever funksjonen skal brukes til å sikre gyldige betingelser, for eksempel innganger, eller kontraktstatusvariabler er oppfylt, eller for å validere returverdier fra samtaler til eksterne kontrakter. 

Å følge dette paradigmet tillater formelle analyseverktøy for å verifisere at den ugyldige opkoden aldri kan nås: det betyr at ingen invarianter i koden blir brutt, og at koden formelt bekreftes.

pragmasoliditet ^ 0,5,0; contract Sharer {funksjon sendHalf (adresse skal betales addr) offentlig betalbar retur (uint-saldo) {krever (msg.verdi% 2 == 0, "Jevn verdi kreves."); // Require () kan ha en valgfri meldingsstreng uint balanceBeforeTransfer = adresse (denne) .balanse; (bool suksess,) = addr.call.value (msg.value / 2) (""); krever (suksess); // Siden vi tilbakeførte hvis overføringen mislyktes, burde det ikke være // noen måte for oss å fortsatt ha halvparten av pengene. påstå (adresse (dette) .balanse == balanceBeforeTransfer – msg.value / 2); // brukes til intern feilkontroll returadresse (dette) .balanse; }} Kodespråk: JavaScript (javascript)

Se SWC-110 & SWC-123

Bruk bare modifikatorer for kontroller

Koden i en modifikator kjøres vanligvis før funksjonsdelen, så eventuelle tilstandsendringer eller eksterne anrop vil bryte med Sjekker-effekter-interaksjoner mønster. Dessuten kan disse utsagnene også forbli ubemerket av utvikleren, da koden for modifikator kan være langt fra funksjonserklæringen. For eksempel kan en ekstern samtale i modifikator føre til angrep på nytt sted:

kontraktsregister {adresse eier; funksjon isVoter (adresse _addr) eksterne returer (bool) {// Kode}} kontraktvalg {Registerregister; modifier isEligible (adresse _addr) {krever (registry.isVoter (_addr)); _; } function vote () isEligible (msg.sender) public {// Code}} Kodespråk: JavaScript (javascript)

I dette tilfellet kan registerkontrakten gjøre et angrep på nytt ved å ringe Election.vote () inni isVoter ().

Merk: Bruk modifikatorer for å erstatte dupliserte tilstandskontroller i flere funksjoner, for eksempel isOwner (), ellers bruk krever eller tilbakestilles i funksjonen. Dette gjør den smarte kontraktskoden din mer lesbar og lettere å overvåke.

Vær forsiktig med avrunding med heltallsdeling

All heltall divisjon runder ned til nærmeste heltall. Hvis du trenger mer presisjon, bør du vurdere å bruke en multiplikator, eller lagre både teller og nevner.

(I fremtiden vil Soliditet ha en fast punkt type, noe som vil gjøre dette lettere.)

// dårlig uint x = 5/2; // Resultatet er 2, alle heltallsinndeling runder NED til nærmeste heltall Kodespråk: JavaScript (javascript)

Ved å bruke en multiplikator forhindres avrunding, må denne multiplikatoren tas hensyn til når du jobber med x i fremtiden:

// god uint multiplikator = 10; uint x = (5 * multiplikator) / 2; Kodespråk: JavaScript (javascript)

Lagring av teller og nevner betyr at du kan beregne resultatet av teller / nevner utenfor kjeden:

// god teller = 5; uint nevner = 2; Kodespråk: JavaScript (javascript)

Vær oppmerksom på kompromissene mellom abstrakte kontrakter og grensesnitt

Både grensesnitt og abstrakte kontrakter gir en en tilpassbar og gjenbrukbar tilnærming for smarte kontrakter. Grensesnitt, som ble introdusert i Solidity 0.4.11, ligner abstrakte kontrakter, men kan ikke ha noen funksjoner implementert. Grensesnitt har også begrensninger som å ikke få tilgang til lagring eller arve fra andre grensesnitt, noe som generelt gjør abstrakte kontrakter mer praktiske. Selv om grensesnitt er absolutt nyttige for utforming av kontrakter før implementering. I tillegg er det viktig å huske at hvis en kontrakt arver fra en abstrakt kontrakt, må den implementere alle ikke-implementerte funksjoner via tilsidesettelse, ellers vil den også være abstrakt.

Fallback-funksjoner

Hold reservefunksjoner enkle

Fallback-funksjoner kalles når en kontrakt sendes en melding uten argumenter (eller når ingen funksjoner stemmer overens), og har bare tilgang til 2300 gass når den ringes fra en .send () eller .transfer (). Hvis du ønsker å kunne motta Ether fra en .send () eller .transfer (), er det meste du kan gjøre i en reservefunksjon, å logge en hendelse. Bruk en riktig funksjon hvis det kreves beregning av mer gass.

// dårlig funksjon () betales {saldoer [msg.sender] + = msg.value; } // god funksjon innskudd () betales eksterne {saldoer [msg.sender] + = msg.value; } funksjon () betales {krever (msg.data.length == 0); send ut LogDepositReceived (msg.sender); } Kodespråk: JavaScript (javascript)

Kontroller datalengden i reservefunksjoner

Siden reservefunksjoner er ikke bare kalt for vanlig eteroverføring (uten data), men også når ingen andre funksjoner samsvarer, bør du sjekke at dataene er tomme hvis reservefunksjonen kun er ment å brukes til logging av mottatt Ether. Ellers vil ikke innringere legge merke til om kontrakten din blir brukt feil og funksjoner som ikke eksisterer blir kalt.

// dårlig funksjon () betales {emit LogDepositReceived (msg.sender); } // god funksjon () betales {krever (msg.data.length == 0); send ut LogDepositReceived (msg.sender); } Kodespråk: JavaScript (javascript)

Merk eksplisitt betalbare funksjoner og tilstandsvariabler

Fra og med soliditet 0.4.0, må hver funksjon som mottar eter bruke modifikator som skal betales, ellers hvis transaksjonen har msg.value > 0 vil gå tilbake (bortsett fra når det er tvunget).

Merk: Noe som kanskje ikke er opplagt: Modifikatoren som skal betales, gjelder bare anrop fra eksterne kontrakter. Hvis jeg kaller en funksjon som ikke skal betales i betalingsfunksjonen i samme kontrakt, mislykkes ikke den ikke-betalbare funksjonen, selv om msg.value fortsatt er satt.

Merk eksplisitt synlighet i funksjoner og tilstandsvariabler

Merk eksplisitt synligheten til funksjoner og tilstandsvariabler. Funksjoner kan spesifiseres som eksterne, offentlige, interne eller private. Vennligst forstå forskjellene mellom dem, for eksempel kan ekstern være tilstrekkelig i stedet for offentlig. For tilstandsvariabler er ekstern ikke mulig. Merking av synligheten eksplisitt vil gjøre det lettere å fange feil antagelser om hvem som kan ringe til funksjonen eller få tilgang til variabelen.

  • Eksterne funksjoner er en del av kontraktsgrensesnittet. En ekstern funksjon f kan ikke kalles internt (dvs. f () fungerer ikke, men dette. F () fungerer). Eksterne funksjoner er noen ganger mer effektive når de mottar store matriser med data.
  • Offentlige funksjoner er en del av kontraktsgrensesnittet og kan enten ringes internt eller via meldinger. For offentlige statlige variabler genereres en automatisk getter-funksjon (se nedenfor).
  • Interne funksjoner og tilstandsvariabler er kun tilgjengelig internt, uten å bruke dette.
  • Private funksjoner og tilstandsvariabler er bare synlige for kontrakten de er definert i, og ikke i avledede kontrakter. Merk: Alt som er inne i en kontrakt er synlig for alle observatører utenfor blockchain, også private variabler.

// dårlig uint x; // standard er intern for tilstandsvariabler, men den bør gjøres eksplisitt funksjon buy () {// standard er offentlig // offentlig kode} // god uint privat y; funksjon kjøp () ekstern {// kan kun kalles eksternt eller ved bruk av this.buy ()} funksjonsverktøy () offentlig {// anropbar eksternt, så vel som internt: å endre denne koden krever å tenke på begge tilfeller. } funksjon internalAction () intern {// intern kode} Kodespråk: PHP (php)

Se SWC-100 og SWC-108

Lås pragmas til spesifikk kompilatorversjon

Kontrakter skal distribueres med samme kompilatorversjon og flagg som de er testet mest med. Å låse pragmaen bidrar til å sikre at kontrakter ikke ved et uhell blir distribuert ved hjelp av for eksempel den siste kompilatoren som kan ha høyere risiko for uoppdagede feil. Kontrakter kan også distribueres av andre, og pragmaet indikerer kompilatorversjonen ment av de opprinnelige forfatterne.

// dårlig pragmasoliditet ^ 0.4.4; // god pragmasoliditet 0.4.4; Kodespråk: JavaScript (javascript)

Merk: en flytende pragmaversjon (dvs. ^ 0.4.25) vil kompilere fint med 0.4.26 nattlig.2018.9.25, men nattlige bygninger skal aldri brukes til å kompilere kode for produksjon.

Advarsel: Pragma-uttalelser kan få lov til å flyte når en kontrakt er ment for forbruk av andre utviklere, som i tilfelle med kontrakter i et bibliotek eller EthPM-pakke. Ellers må utvikleren oppdatere pragmaet manuelt for å kompilere lokalt.

Se SWC-103

Bruk hendelser til å overvåke kontraktsaktivitet

Det kan være nyttig å ha en måte å overvåke kontraktsaktiviteten etter at den ble distribuert. En måte å oppnå dette på er å se på alle transaksjoner i kontrakten, men det kan være utilstrekkelig, ettersom meldingssamtaler mellom kontrakter ikke blir registrert i blockchain. Videre viser den bare inngangsparametrene, ikke de faktiske endringene som blir gjort i staten. Også hendelser kan brukes til å utløse funksjoner i brukergrensesnittet.

kontrakt veldedighet {kartlegging (adresse => uint) balanserer; funksjon donere () betalbare offentlige {saldoer [msg.sender] + = msg.verdi; }} kontrakt Spill {funksjon buyCoins () som skal betales offentlig {// 5% går til veldedighetsorganisasjon veldedighetsdonat.verdi (msg.verdi / 20) (); }} Kodespråk: JavaScript (javascript)

Her vil spillkontrakten ringe internt til Charity.donate (). Denne transaksjonen vises ikke i den eksterne transaksjonslisten for veldedighet, men bare synlig i de interne transaksjonene.

En hendelse er en praktisk måte å logge på noe som skjedde i kontrakten. Hendelser som ble sendt ut forblir i blockchain sammen med andre kontraktsdata, og de er tilgjengelige for fremtidig revisjon. Her er en forbedring av eksemplet ovenfor ved å bruke hendelser til å gi en historie om veldedighetens donasjoner.

kontrakt veldedighet {// definere hendelse hendelse LogDonate (uint _amount); kartlegging (adresse => uint) balanserer; funksjon donere () betalbare offentlige {saldoer [msg.sender] + = msg.verdi; // emit event emit LogDonate (msg.value); }} kontrakt Spill {funksjon buyCoins () som skal betales offentlig {// 5% går til veldedighetsorganisasjon veldedighetsdonat.verdi (msg.verdi / 20) (); }} Kodespråk: JavaScript (javascript)

Her vil alle transaksjoner som går gjennom veldedighetskontrakten, enten direkte eller ikke, vises i hendelseslisten til den kontrakten sammen med mengden donerte penger.

Merk: Foretrekker nyere soliditetskonstruksjoner. Foretrekker konstruksjoner / aliaser som selvdestruksjon (over selvmord) og keccak256 (over sha3). Mønstre som krever (msg.sender.send (1 eter)) kan også forenkles til å bruke transfer (), som i msg.sender.transfer (1 eter). Sjekk ut Soliditet Endringslogg for flere lignende endringer.

Vær oppmerksom på at ‘Innebygd’ kan skygges

Det er for øyeblikket mulig å skygge innebygde globaler i Solidity. Dette gjør at kontrakter kan overstyre funksjonaliteten til innebygde moduler som msg og reversere (). Selv om dette er ment, det kan villede brukere av en kontrakt med hensyn til kontraktens virkelige oppførsel.

kontrakt PretendingToRevert {funksjon revert () intern konstant {}} kontrakt ExampleContract er PretendingToRevert {funksjon somethingBad () offentlig {revert (); }}

Kontraktsbrukere (og revisorer) bør være klar over kildekoden for smart kontrakt for alle applikasjoner de har tenkt å bruke.

Unngå å bruke tx.origin

Bruk aldri tx.origin for autorisasjon, en annen kontrakt kan ha en metode som vil kalle kontrakten din (hvor brukeren for eksempel har noen midler), og kontrakten din vil godkjenne den transaksjonen ettersom adressen din er i tx.origin..

kontrakt MyContract {adresse eier; funksjon MyContract () offentlig {eier = msg.sender; } funksjon sendTo (adressemottaker, uint beløp) offentlig {krever (tx.origin == eier); (bool suksess,) = receiver.call.value (beløp) (""); krever (suksess); }} kontrakt AttackingContract {MyContract myContract; adresse angriper; funksjon AttackingContract (address myContractAddress) public {myContract = MyContract (myContractAddress); angriper = msg.sender; } funksjon () offentlig {myContract.sendTo (angriper, msg.sender.balanse); }} Kodespråk: JavaScript (javascript)

Du bør bruke msg.sender for autorisasjon (hvis en annen kontrakt kaller kontrakten, blir msg.sender adressen til kontrakten og ikke adressen til brukeren som ringte kontrakten).

Du kan lese mer om det her: Soliditet dokumenterer

Advarsel: Foruten problemet med autorisasjon, er det en sjanse for at tx.origin vil bli fjernet fra Ethereum-protokollen i fremtiden, så kode som bruker tx.origin vil ikke være kompatibel med fremtidige utgivelser Vitalik: ‘IKKE anta at tx.origin vil fortsette å være brukbar eller meningsfull. ‘

Det er også verdt å nevne at ved å bruke tx.origin begrenser du interoperabilitet mellom kontrakter, fordi kontrakten som bruker tx.origin ikke kan brukes av en annen kontrakt, ettersom en kontrakt ikke kan være tx.origin.

Se SWC-115

Avhengighet av tidsstempel

Det er tre hovedhensyn når du bruker et tidsstempel for å utføre en kritisk funksjon i en kontrakt, spesielt når handlinger innebærer pengeoverføring.

Tidsstempelmanipulering

Vær oppmerksom på at blokkens tidsstempel kan manipuleres av en gruvearbeider. Vurder dette kontrakt:

uint256 konstant privat salt = block.timestamp; funksjon tilfeldig (uint Maks) konstant privat retur (uint256 resultat) {// få det beste frøet for tilfeldighet uint256 x = salt * 100 / Maks; uint256 y = salt * blokk.nummer / (salt% 5); uint256 seed = block.number / 3 + (salt% 300) + Last_Payout + y; uint256 h = uint256 (block.blockhash (seed)); retur uint256 ((h / x))% Maks + 1; // tilfeldig tall mellom 1 og maks} Kodespråk: PHP (php)

Når kontrakten bruker tidsstempelet for å så et tilfeldig tall, kan gruvearbeideren faktisk legge ut et tidsstempel innen 15 sekunder etter at blokken ble validert, slik at gruvearbeideren effektivt kan beregne et alternativ som er gunstigere for sjansene sine i lotteriet. Tidsstempler er ikke tilfeldige og bør ikke brukes i den sammenheng.

15-sekundersregelen

De Gult papir (Ethereums referansespesifikasjon) spesifiserer ikke en begrensning på hvor mye blokker kan drive i tid, men det spesifiserer at hvert tidsstempel skal være større enn foreldrenes tidsstempel. Populære implementeringer av Ethereum-protokollen Geth og Paritet begge avviser blokker med tidsstempel mer enn 15 sekunder i fremtiden. Derfor er en god tommelfingerregel for å evaluere tidsstempelbruk: Hvis omfanget av den tidsavhengige hendelsen kan variere med 15 sekunder og opprettholde integriteten, er det trygt å bruke en blokk. Tidsstempel.

Unngå å bruke block.number som et tidsstempel

Det er mulig å estimere et tidsdelta ved hjelp av block.number-egenskapen og gjennomsnittlig blokkeringstid, Dette er imidlertid ikke fremtidssikker, da blokkeringstider kan endres (for eksempel gaffel omorganiseringer og vanskelighetsbombe). I et salg som strekker seg over dager, tillater 15-sekundersregelen en å oppnå et mer pålitelig estimat av tid.

Se SWC-116

Forsikring om flere arv

Når du bruker flere arv i soliditet, er det viktig å forstå hvordan kompilatoren komponerer arvediagrammet.

kontrakt Final {uint public a; funksjon Final (uint f) public {a = f; }} kontrakt B er endelig {int offentlig avgift; funksjon B (uint f) Final (f) public {} function setFee () public {fee = 3; }} kontrakt C er endelig {int offentlig avgift; funksjon C (uint f) Final (f) public {} function setFee () public {fee = 5; }} kontrakt A er B, C {funksjon A () offentlig B (3) C (5) {setFee (); }} Kodespråk: PHP (php)

Når en kontrakt blir distribuert, vil kompilatoren linjere arven fra høyre til venstre (etter at nøkkelordet er foreldrene er oppført fra det mest baselignende til det mest avledede). Her er kontrakt A’s linearisering:

Endelig <- B <- C <- EN

Konsekvensen av linearisering vil gi en gebyrverdi på 5, siden C er den mest avledede kontrakten. Dette kan virke åpenbart, men forestill deg scenarier der C er i stand til å skygge viktige funksjoner, omorganisere boolske klausuler og få utvikleren til å skrive utnyttbare kontrakter. Statisk analyse gir foreløpig ikke problemer med overskyggede funksjoner, så den må inspiseres manuelt.

For å bidra, har Solidity’s Github en prosjekt med alle arverelaterte problemer.

Se SWC-125

Bruk grensesnitttype i stedet for adressen for typesikkerhet

Når en funksjon tar en kontraktsadresse som argument, er det bedre å sende et grensesnitt eller kontraktstype i stedet for rå adresse. Hvis funksjonen kalles andre steder innenfor kildekoden, vil kompilatoren gi ytterligere sikkerhetsgarantier.

Her ser vi to alternativer:

contract Validator {funksjon validere (uint) eksterne returer (bool); } kontrakt TypeSafeAuction {// god funksjon validateBet (Validator _validator, uint _value) interne returer (bool) {bool valid = _validator.validate (_value); retur gyldig; }} kontrakt TypeUnsafeAuction {// dårlig funksjon validateBet (adresse _addr, uint _value) interne returer (bool) {Validator validator = Validator (_addr); bool valid = validator.validate (_value); retur gyldig; }} Kodespråk: JavaScript (javascript)

Fordelene ved å bruke TypeSafeAuction-kontrakten ovenfor kan da sees fra følgende eksempel. Hvis validateBet () kalles med et adresseargument, eller en annen kontrakttype enn Validator, vil kompilatoren kaste denne feilen:

kontrakt NonValidator {} kontraktsauksjon er TypeSafeAuction {NonValidator nonValidator; funksjonsinnsats (uint _value) {bool valid = validateBet (nonValidator, _value); // TypeError: Ugyldig type for argument i funksjonsanrop. // Ugyldig implisitt konvertering fra kontrakt NonValidator // til kontrakt Validator forespurt. }} Kodespråk: JavaScript (javascript)

Unngå å bruke extcodesize for å se etter eksternt eide kontoer

Følgende modifikator (eller en lignende sjekk) brukes ofte til å verifisere om det ble ringt fra en eksternt eid konto (EOA) eller en kontrakonto:

// dårlig modifikator isNotContract (adresse _a) {uint størrelse; montering {størrelse: = extcodesize (_a)} krever (størrelse == 0); _; } Kodespråk: JavaScript (javascript)

Ideen er rett frem: Hvis en adresse inneholder kode, er det ikke en EOA, men en kontokonto. men, en kontrakt har ikke kildekode tilgjengelig under bygging. Dette betyr at mens konstruktøren kjører, kan den ringe til andre kontrakter, men ekstkodestørrelse for adressen returnerer null. Nedenfor er et minimalt eksempel som viser hvordan denne sjekken kan omgås:

kontrakt OnlyForEOA {uint offentlig flagg; // dårlig modifikator isNotContract (adresse _a) {uint len; montering {len: = extcodesize (_a)} krever (len == 0); _; } funksjon setFlag (uint i) offentlig isNotContract (msg.sender) {flag = i; }} kontrakt FakeEOA {konstruktør (adresse _a) offentlig {OnlyForEOA c = OnlyForEOA (_a); c.setFlag (1); }} Kodespråk: JavaScript (javascript)

Fordi kontraktadresser kan forhåndsberegnes, kan denne sjekken også mislykkes hvis den sjekker en adresse som er tom i blokk n, men som har en kontrakt distribuert til den i en blokk større enn n.

Advarsel: Dette problemet er nyansert. Hvis målet ditt er å forhindre at andre kontrakter kan ringe til kontrakten, er sannsynligvis ekstrakodestørrelseskontrollen tilstrekkelig. En alternativ tilnærming er å sjekke verdien av (tx.origin == msg.sender), selv om dette også har ulemper.

Det kan være andre situasjoner der ekstrakodesize-kontrollen tjener formålet ditt. Å beskrive dem alle her er utenfor omfanget. Forstå den underliggende oppførselen til EVM og bruk din skjønn.

Er Blockchain-koden din sikker?

Bestill en 1-dagers spotkontroll med våre sikkerhetseksperter. Bestill deg i dag DiligenceSecuritySmart ContractsSolidityNewsletterAbonner på vårt nyhetsbrev for de siste Ethereum-nyhetene, bedriftsløsninger, utviklerressurser og mer.Hvordan lage et vellykket Blockchain-produktWebinar

Hvordan lage et vellykket Blockchain-produkt

Hvordan sette opp og kjøre en Ethereum-nodeWebinar

Hvordan sette opp og kjøre en Ethereum-node

Hvordan lage din egen Ethereum APIWebinar

Hvordan lage din egen Ethereum API

Hvordan lage et sosialt tokenWebinar

Hvordan lage et sosialt token

Bruke sikkerhetsverktøy i smart kontraktutviklingWebinar

Bruke sikkerhetsverktøy i smart kontraktutvikling

Fremtiden for digitale eiendeler og deFiWebinar

Fremtidens økonomi: digitale eiendeler og deFi

Mike Owergreen Administrator
Sorry! The Author has not filled his profile.
follow me