Die Solidität wurde im Oktober 2014 begonnen, als weder das Ethereum-Netzwerk noch die virtuelle Maschine reale Tests hatten, die Gaskosten zu diesem Zeitpunkt sogar drastisch von dem, was sie heute sind, unterschieden. Darüber hinaus wurden einige der frühen Entwurfsentscheidungen von der Schlange übernommen. In den letzten Monaten wurden Beispiele und Muster, die ursprünglich als Best-Praction angesehen wurden, der Realität ausgesetzt, und einige von ihnen stellten sich tatsächlich als Anti-Muster heraus. Aus diesem Grund haben wir kürzlich einige der aktualisiert SoliditätsdokumentationAber da die meisten Menschen wahrscheinlich nicht dem Strom von Github -Commits zu diesem Repository folgen, möchte ich einige der Ergebnisse hier hervorheben.
Ich werde hier nicht über die kleinen Probleme sprechen, bitte lesen Sie sie in der Dokumentation.
Äther senden
Das Senden von Ether soll eines der einfachsten Dinge in der Solidität sein, aber es stellt sich heraus, dass die meisten Menschen nicht erkennen.
Es ist wichtig, dass der Empfänger des Äthers im besten Fall die Auszahlung initiiert. Das Folgende ist a SCHLECHT Beispiel eines Auktionsvertrags:
// THIS IS A NEGATIVE EXAMPLE! DO NOT USE! contract auction { address highestBidder; uint highestBid; function bid() { if (msg.value < highestBid) throw; if (highestBidder != 0) highestBidder.send(highestBid); // refund previous bidder highestBidder = msg.sender; highestBid = msg.value; } }
Aufgrund der maximalen Stapeltiefe von 1024 kann der neue Bieter die Stapelgröße immer auf 1023 erhöhen und dann anrufen Gebot() das verursacht die Senden (höchstbid) Rufen Sie an, um schweigend zu scheitern (dh der vorherige Bieter erhält die Rückerstattung nicht), aber der neue Bieter wird immer noch der höchste Bieter sein. Eine Möglichkeit, ob man überprüfen, ob schicken erfolgreich war es, den Rückgabewert zu überprüfen:
/// THIS IS STILL A NEGATIVE EXAMPLE! DO NOT USE! if (highestBidder != 0) if (!highestBidder.send(highestBid)) throw;Der
throwDie Erklärung bewirkt, dass der aktuelle Anruf zurückgekehrt wird. Dies ist eine schlechte Idee, denn der Empfänger, z. B. durch die Implementierung der Fallback -Funktion als
function() { throw; }Kann immer die Ätherübertragung zum Scheitern erzwingen, und dies hätte den Effekt, dass niemand sie überbieten kann.
Der einzige Weg, um beide Situationen zu verhindern, besteht darin, das Sendungsmuster in ein Rückzugsmuster umzuwandeln, indem die Empfängersteuerung über die Übertragung verleiht:
/// THIS IS STILL A NEGATIVE EXAMPLE! DO NOT USE! contract auction { address highestBidder; uint highestBid; mapping(address => uint) refunds; function bid() { if (msg.value < highestBid) throw; if (highestBidder != 0) refunds[highestBidder] += highestBid; highestBidder = msg.sender; highestBid = msg.value; } function withdrawRefund() { if (msg.sender.send(refunds[msg.sender])) refunds[msg.sender] = 0; } }
Warum sagt es immer noch “negatives Beispiel” über dem Vertrag? Aufgrund der Gasmechanik ist der Vertrag tatsächlich in Ordnung, aber es ist immer noch kein gutes Beispiel. Der Grund dafür ist, dass es unmöglich ist, die Codeausführung beim Empfänger als Teil eines Sendens zu verhindern. Dies bedeutet, dass der Empfänger, während die Sendungsfunktion noch läuft, zurück in das Rückzug zurückrufen kann. Zu diesem Zeitpunkt ist der Rückerstattungsbetrag immer noch der gleiche und sie würden den Betrag wieder und so weiter bekommen. In diesem spezifischen Beispiel funktioniert es nicht, da der Empfänger nur das Gasstipendium (2100 Gas) erhält und es unmöglich ist, einen weiteren Senden mit dieser Menge an Gas durchzuführen. Der folgende Code ist jedoch anfällig für diesen Angriff: msg.sender.call.value (Rückerstattungen[msg.sender]) ()).
Nachdem der folgende Code in Ordnung ist (natürlich ist es immer noch kein vollständiges Beispiel für einen Auktionsvertrag):
contract auction { address highestBidder; uint highestBid; mapping(address => uint) refunds; function bid() { if (msg.value < highestBid) throw; if (highestBidder != 0) refunds[highestBidder] += highestBid; highestBidder = msg.sender; highestBid = msg.value; } function withdrawRefund() { uint refund = refunds[msg.sender]; refunds[msg.sender] = 0; if (!msg.sender.send(refund)) refunds[msg.sender] = refund; } }
Beachten Sie, dass wir keinen fehlgeschlagenen Versand angewendet haben, da wir alle Zustandsänderungen manuell zurückversetzen können und nicht Wurf verwendet, um viel weniger Nebenwirkungen zu verwenden.
Verwenden von Wurf
Die Wurfanweisung ist oft sehr bequem, um Änderungen am Zustand als Teil des Anrufs zurückzuversetzen (oder die gesamte Transaktion, je nachdem, wie die Funktion aufgerufen wird). Man muss sich jedoch bewusst sein, dass es auch dazu führt, dass alle Gas ausgegeben werden und somit teuer ist und möglicherweise die aktuelle Funktion aufruft. Aus diesem Grund möchte ich empfehlen, es zu verwenden nur In den folgenden Situationen:
1. Die Ätherübertragung in die aktuelle Funktion zurückkehren
Wenn eine Funktion nicht im aktuellen Status oder mit den aktuellen Argumenten ein Ether empfangen soll oder nicht, sollten Sie Wurf verwenden, um den Äther abzulehnen. Die Verwendung von Wurf ist der einzige Weg, um Ether aufgrund von Gas- und Stapel -Tiefenproblemen zuverlässig zurückzuschicken: Der Empfänger kann einen Fehler in der Fallback -Funktion haben, der zu viel Gas erfordert und daher den Äther nicht empfangen kann oder die Funktion möglicherweise in einem böswilligen Kontext mit zu hoher Stapeltiefe (möglicherweise sogar der Anruffunktion) aufgerufen worden sein kann.
Beachten Sie, dass das Versand von Äther an einen Vertrag nicht immer ein UX -Fehler ist: Sie können niemals vorhersagen, in welcher Reihenfolge oder zu welchen Zeittransaktionen einem Block hinzugefügt werden. Wenn der Vertrag geschrieben wird, um nur die erste Transaktion zu akzeptieren, muss der in den andere Transaktionen enthaltene Äther abgelehnt werden.
2. REUVENT Effekte von sogenannten Funktionen
Wenn Sie Funktionen für andere Verträge anrufen, können Sie nie wissen, wie sie implementiert werden. Dies bedeutet, dass die Auswirkungen dieser Aufrufe ebenfalls nicht bekannt sind und daher die einzige Möglichkeit, diese Effekte zurückzuversetzen, besteht darin, Wurf zu verwenden. Natürlich sollten Sie Ihren Vertrag immer schreiben, um diese Funktionen überhaupt nicht aufzurufen, wenn Sie wissen, dass Sie die Effekte zurückversetzen müssen, aber es gibt einige Anwendungsfälle, in denen Sie das erst nach der Tatsache wissen.
Schleifen und die Blockgasgrenze
Es gibt eine Grenze dafür, wie viel Gas in einem einzigen Block verbracht werden kann. Diese Grenze ist flexibel, aber es ist ziemlich schwierig, sie zu erhöhen. Dies bedeutet, dass jede einzelne Funktion in Ihrem Vertrag in allen (angemessenen) Situationen unter einer bestimmten Menge Gas bleiben sollte. Das Folgende ist ein schlechtes Beispiel für einen Abstimmungsvertrag:
/// THIS IS STILL A NEGATIVE EXAMPLE! DO NOT USE! contract Voting { mapping(address => uint) voteWeight; address[] yesVotes; uint requiredWeight; address beneficiary; uint amount; function voteYes() { yesVotes.push(msg.sender); } function tallyVotes() { uint yesVotes; for (uint i = 0; i < yesVotes.length; ++i) yesVotes += voteWeight[yesVotes[i]]; if (yesVotes > requiredWeight) beneficiary.send(amount); } }
Der Vertrag hat tatsächlich mehrere Probleme, aber das, das ich hier hervorheben möchte, ist das Problem der Schleife: Angenommen, die Stimmengewichte sind wie Token übertragbar und splittierbar (denken Sie an die Dao -Token als Beispiel). Dies bedeutet, dass Sie eine willkürliche Anzahl von Klonen von sich selbst erstellen können. Das Erstellen solcher Klone erhöht die Länge der Schleife in der Funktion “TallyVoten”, bis mehr Gas als in einem einzigen Block verfügbar ist.
Dies gilt für alles, was Schleifen verwendet, auch wenn Schleifen im Vertrag nicht ausdrücklich sichtbar sind, beispielsweise wenn Sie Arrays oder Zeichenfolgen im Speicher kopieren. Auch hier ist es in Ordnung, Schleifen von beliebigen Längen zu haben, wenn die Länge der Schleife vom Anrufer gesteuert wird, beispielsweise wenn Sie über ein Array iterieren, das als Funktionsargument übergeben wurde. Aber niemals Erstellen Sie eine Situation, in der die Schleifenlänge von einer Partei gesteuert wird, die nicht die einzige wäre, die an ihrem Versagen leidet.
Als Randnotiz war dies ein Grund, warum wir jetzt das Konzept von blockierten Konten im DAO -Vertrag haben: Das Stimmengewicht wird an dem Punkt gezählt, an dem die Abstimmung abgegeben wird, um zu verhindern, dass die Schleife stecken bleibt und wenn das Stimmengewicht erst am Ende der Abstimmung festgelegt wird, können Sie eine zweite Abstimmung abgeben, indem Sie nur Ihre Token übertragen und dann erneut wählen.
Erhalten Sie Äther / die Fallback -Funktion
Wenn Sie möchten, dass Ihr Vertrag über den regulären Send () -Anruf Ether erhält, müssen Sie seine Fallback -Funktion billig machen. Es kann nur 2300 verwenden, Gas, das weder Speicherschreib- noch Funktionsaufrufe zulässt, die entlang des Äthers senden. Grundsätzlich sollten Sie in der Fallback -Funktion nur ein Ereignis protokollieren, damit externe Prozesse auf die Tatsache reagieren können. Natürlich kann jede Funktion eines Vertrags Ether erhalten und ist nicht an diese Gasbeschränkung gebunden. Funktionen müssen tatsächlich Äther ablehnen, die ihnen gesendet werden, wenn sie keine erhalten möchten, aber wir denken darüber nach, dieses Verhalten in einer zukünftigen Veröffentlichung potenziell zu invertieren.

