Dinge über Solidity Call und Delegatecall

Basiskonzept

Die Verwendung von Anruf

call ist ein Aufruf auf unterster Ebene für die Interaktion zwischen Verträgen in Solidität, es wird jedoch offiziell empfohlen, diese Methode nur beim Senden von Ethereum zu verwenden. Der Vertrag muss einen Empfangs- oder Zahlungsfallback definieren, um die Übertragung zu akzeptieren (beim Aufrufen einer Methode, die nicht vorhanden ist). Im Vertrag ist diese Methode der Standardaufruf. Es wird nicht empfohlen, im Vertrag vorhandene Call-to-Call-Methoden zu verwenden. Informationen zum Unterschied zwischen Empfangen und Fallback finden Sie in der remoteCall-Methode des Beispielvertragsaufrufers unten. Ausführlichere Anweisungen finden Sie hier .

Anrufbeispiel

Als nächstes demonstrieren wir den Versand von Ethereum und den Aufruf von Vertragsmethoden. Hinweis: Um den Aufruf von hardhats console.sol-Vertrag zu erleichtern, der in unserem Beispiel zum Drucken von Protokollinformationen verwendet wird.

Mustervertrag

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Callee {
    
    
    fallback() external payable {
    
    
        console.log("In fallback Ether", msg.sender, msg.value);
    }

    receive() external payable {
    
    
        console.log("In Receive Ether", msg.sender, msg.value);
    }

    function foo(string memory _message, uint256 _x)
        public
        view
        returns (uint256)
    {
    
    
        console.log("foo invoking", msg.sender, _message);
        return _x + 1;
    }
}

contract Caller {
    
    
    constructor() payable{
    
    

    }
    function remoteCall(address instance) public payable {
    
    
        //触发fallback调用
        (bool sucess, ) = instance.call{
    
    value: 200}(abi.encodeWithSignature('nonExistingFunction()'));
        require(sucess, "call error");

        // //触发receive调用
        (bool sucess2, ) = instance.call{
    
    value: 200}('');
        require(sucess2, "call error");

        //调用foo
        (bool sucess3, ) = instance.call(abi.encodeWithSignature('foo(string,uint256)', 'hello foo', 100));
        require(sucess3, "call error");
    }
}

Anruf zwischen Verträgen

  • Lösen Sie Vertragsaufrufe mithilfe von JS-Skriptaufrufen aus
const hre = require('hardhat');
async function main () {
    
    

  let Callee = await hre.ethers.getContractFactory("Callee");
  let Caller = await hre.ethers.getContractFactory("Caller");
  let callee = await Callee.deploy();
  //由于remoteCall方法向其它合约转以太坊,因此该合约部署时需要转入
  let caller = await Caller.deploy({
    
    value:10000});
  await caller.remoteCall(callee.address);
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    
    
    console.error(error);
    process.exit(1);
  });
  • Hinweis zu den Ausführungsergebnissen Fügen Sie hier eine Bildbeschreibung ein
    : Bei Verwendung npx hardhat run <script>führt das Hardhat-Framework standardmäßig die Hardhat-Umgebungsvariable in das JS-Skript ein, sodass die hre-Variable nicht im Skript definiert werden muss. Das hier gezeigte führt die Hardhat-Umgebungsvariable (const hre = require('hardhat');) ein, damit das Skript wie ein normales JS-Skript ( node <script>) ausgeführt werden kann.

Die offizielle Beschreibung von Hardhat lautet wie folgt:

Wir benötigen hier explizit die Hardhat Runtime Environment. Dies ist optional, aber nützlich, um das Skript eigenständig über auszuführen node <script>.
Sie können auch ein Skript mit ausführen npx hardhat run <script>. Wenn Sie dies tun, kompiliert Hardhat Ihre Verträge, fügt die Mitglieder der Hardhat Runtime Environment zum globalen Bereich hinzu und führt das Skript aus.

Remix- Aufruf

  • Übertragen Sie 300wei in den Vertrag, und die Anrufdaten sind nicht leer (Fallback wird aufgerufen):
    Fügen Sie hier eine Bildbeschreibung ein
  • Übertragen Sie 300wei in den Vertrag und die Anrufdaten sind leer (Empfangen wird aufgerufen):
    Fügen Sie hier eine Bildbeschreibung ein
  • Methodenaufruf
    Fügen Sie hier eine Bildbeschreibung ein

Hinweise zum Anruf

Wenn Sie Call-to-Call-Methoden anderer Verträge verwenden, überprüfen Sie unbedingt die Ausführungsergebnisse, z. B. die Call-Methode in remoteCall im Caller-Vertrag im obigen Beispiel.

//触发fallback调用
(bool sucess, ) = instance.call{
    
    value: 200}(abi.encodeWithSignature('nonExistingFunction()'));
require(sucess, "call error");

Während „Call“ in manchen Situationen ein nützliches Werkzeug sein kann, wird im Allgemeinen davon abgeraten, ihn beim Aufruf vorhandener Funktionen in anderen Verträgen zu verwenden. Die Gründe sind wie folgt:

  • Revert kann den Aufrufstapel nicht aufblähen

Wenn Sie eine Funktion mit call aufrufen, wird ein Revert, das innerhalb der aufgerufenen Funktion auftritt, nicht in den aufrufenden Vertrag übertragen. Dies bedeutet, dass der aufrufende Vertrag vom Auftreten eines Reverts nichts mitbekommt und möglicherweise weiterhin fehlerhaft ausgeführt wird.

  • Typprüfung ignoriert

Solidity bietet ein Typsystem, das Datenintegrität und -sicherheit gewährleistet. Bei Verwendung von call wird jedoch die Typprüfung der Funktionsparameter umgangen. Dies kann zu potenziellen Schwachstellen führen, wenn Eingabetypen nicht korrekt verarbeitet werden.

  • Prüfung auf Funktionsexistenz fehlt

Indem Sie call zum Aufrufen einer Funktion verwenden, können Sie die von Solidity durchgeführte automatische Existenzprüfung umgehen. Wenn die Funktion nicht vorhanden ist oder umbenannt wurde, löst der Aufruf die Fallback-Funktion aus, was möglicherweise zu unerwartetem Verhalten führt.

Anruf vs. Delegatecall

Sowohl „call“ als auch „delegatecall“ sind Low-Level-Funktionen, die für die Interaktion zwischen Verträgen verwendet werden. Der Unterschied liegt im Ausführungskontext der beiden. Der Kontext des ersteren ist der aufgerufene Vertrag, während der Kontext des letzteren der Vertrag ist, der den Aufruf initiiert hat . Es klingt etwas abstrakt, also geben wir ein Beispiel:

Beispielbeschreibung

Aufrufausführungskontext

AufrufausführungskontextWie in der Abbildung gezeigt, befindet sich der Ausführungskontext der aufgerufenen Methode (Methode in B) in Vertrag B, wenn Vertrag A eine Methode in Vertrag B in Form eines Aufrufs aufruft. Den Wert der Adresse (dieser) Vertragsadresse sehen Sie im Bild.

  • Der Quellcode des Testvertrags lautet wie folgt:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
import "hardhat/console.sol";
contract A {
    
    
    constructor() {
    
    
        console.log("Contract A's address:", address(this));
    }

    function remoteCall(address instance) public {
    
    
        (bool sucess, ) = instance.call(abi.encodeWithSignature("myAddress()"));
        require(sucess, "call error");
    }
}

contract B {
    
    
    constructor() {
    
    
        console.log("Contract B's address:", address(this));
    }

    function myAddress() public view {
    
    
        console.log("In contract B's myAddress", address(this));
    }
}

Im Beispiel ruft Vertrag A die myAddress-Methode von Vertrag B auf, und diese Methode gibt einfach die Adresse des Ausführungskontextvertrags aus. Da A die Methode von B in Form eines Aufrufs aufruft, ist der Ausführungskontext der Methode myAddress gemäß dem, was wir zuvor erwähnt haben, Vertrag B, sodass die Ausgabe dieser Methode die Adresse von Vertrag B sein sollte. Wir stellen die Verträge A und B bereit in Remix und rufen Sie sie nacheinander auf. remoteCall in A.

  • Ausführungsergebnisse in Remix:

Fügen Sie hier eine Bildbeschreibung ein

Ausführungskontext des Delegatecall-Aufrufs

Delegatecall

Ändern Sie einfach den Aufruf im Beispiel in „delegatecall“. Beim Aufruf in „delegatecall“ ist der Ausführungskontext der Methode „myAddress“ gemäß dem, was wir zuvor erwähnt haben, Vertrag A, sodass die Adresse von Vertrag A in der Methode „myAddress“ gedruckt werden sollte.

  • Der Vertrags-A-Code wird wie folgt geändert:
function remoteCall(address instance) public {
    
    
        (bool sucess, ) = instance.delegatecall(abi.encodeWithSignature("myAddress()"));
        require(sucess, "call error");
    }
  • Die Ausführungsergebnisse in Remix sind wie folgt:

Fügen Sie hier eine Bildbeschreibung ein

Delegatecall-Sicherheitslücke

Merkmale der Solidität

Der Grund, warum Delegatecall-Aufrufe anfällig für Schwachstellen sind, hängt mit den folgenden zwei Merkmalen der Solidität zusammen:

  1. Wenn ein Vertrag aufgerufen wird, befindet sich der Ausführungskontext in dem Vertrag, der den Aufruf initiiert hat.
  2. Das Layout der Zustandsvariablen des Anrufers und des Angerufenen muss konsistent sein;

Beispiele für Sicherheitslücken

Lassen Sie uns anhand von Beispielen näher darauf eingehen.

  • Anfälliger Vertragscode
contract Vulnerable {
    
    
    address public owner;
    Lib public lib;

    constructor(Lib _lib) {
    
    
        owner = msg.sender;
        lib = Lib(_lib);
    }

    fallback() external payable {
    
    
        address(lib).delegatecall(msg.data);
    }
}

contract Lib {
    
    
    address public owner;

    function setowner() public {
    
    
        owner = msg.sender;
    }
}

Der obige Vertrag ist relativ einfach zu verstehen. Wenn Sie den Vulnerable-Vertrag bereitstellen, müssen Sie die Adresse des bereitgestellten Lib-Vertrags übergeben. Der Fallback im Vulnerable-Vertrag ruft die Methode in der Vertrags-Lib in Form eines Delegataufrufs auf (die aufrufende Methode wird angegeben). durch den Anrufer über msg.data). Hier gibt es im Lib-Vertrag nur eine Methode setOwner, mit der der Eigentümerwert geändert wird.

  • Angriffscode
    Als nächstes verwenden wir Code, um den Eigentümer des oben genannten anfälligen Vertrags zu ändern.
contract AttackVulnerable {
    
    
    address public vulnerable;

    constructor(address _vulnerable) {
    
    
        vulnerable = _vulnerable;
    }

    function attack() public {
    
    
        vulnerable.call(abi.encodeWithSignature("setowner()"));
    }
}

Der Angreifer übergibt bei der Bereitstellung des AttackVulnerable-Vertrags die Adresse des Vulnerable-Vertrags und ruft nach Abschluss der Bereitstellung seine eigene Angriffsmethode auf, um den Besitzerwert des Vulnerable-Vertrags zu ändern.

  • Angriffsprozess und -prinzip
  1. Übergeben Sie beim Bereitstellen des AttackVulnerable-Vertrags die Vulnerable-Adresse und setzen Sie deren Statusvariable auf die Adresse des Vulnerable-Vertrags.
  2. Rufen Sie die Angriffsmethode von AttackVulnerable auf, die die Setowner-Methode des Vulnerable-Vertrags aufruft. Da diese Methode im Vulnerable-Vertrag nicht vorhanden ist, wird ihr Fallback ausgelöst.
  3. In der Fallback-Methode msg.datawird die Methode in der Vertragsbibliothek in Form eines Delegataufrufs aufgerufen, und der Wert von msg.data lautet hier abi.encodeWithSignature("setowner()"). Daher wird die Setowner-Methode in der Bibliothek aufgerufen.
  4. Die setowner-Methode in der Vertragsbibliothek wird verwendet, um den Eigentümer im Vertrag auf den Wert von msg.sender zu ändern, wobei msg.sender die Adresse des Angreifers ist.
  5. Da der Fallback im Vulnerable-Vertrag den Delegatecall-Aufruf ausführt, ist der Ausführungskontext von setowner in Schritt 4 der Vulnerable-Vertrag, sodass er den Wert des Eigentümers im Vulnerable-Vertrag ändert. Zu diesem Zeitpunkt alle Eigentümer des Vulnerable-Vertrags sind zu Angreifern geworden;

Hinweis: Die Änderung des Eigentümers von Vulnerable in Schritt 5 wird durch die Merkmale der Solidität bestimmt (das Layout der Statusvariablen des Anrufers und des Angerufenen muss konsistent sein).

  • Ausführungsergebnisse
    Fügen Sie hier eine Bildbeschreibung ein
    Hinweis: Der Eigentümer von Vulnerable wird hier auf die Vertragsadresse des Angriffsvertrags AttackVulnerable festgelegt. Dies liegt daran, dass der Test in Remix ausgeführt wird und der Aufruf direkt vom AttackVulnerable-Vertrag initiiert wird. Wir können die Angriffsmethode des AttackVulnerable-Vertrags in Form eines Skripts aufrufen, um den Besitzer der Schwachstelle auf eine beliebige vom Angreifer angegebene Adresse festzulegen.

Expandieren

Die Lücken in den folgenden Verträgen sind relativ versteckt und der Leser kann sie anhand der oben genannten Diskussionsmethode selbst analysieren.

contract Lib {
    
    
    uint public num;

    function performOperation(uint _num) public {
    
    
        num = _num;
    }
}

contract Vulnerable {
    
    
    address public lib;
    address public owner;
    uint public num;

    constructor(address _lib) {
    
    
        lib = _lib;
        owner = msg.sender;
    }

    function performOperation(uint _num) public {
    
    
        lib.delegatecall(abi.encodeWithSignature("performOperation(uint256)", _num));
    }
}

//攻击者
contract AttackVulnerable {
    
    

    address public lib;
    address public owner;
    uint public num;

    Vulnerable public vulnerable;

    constructor(Vulnerable _vulnerable) {
    
    
        vulnerable = Vulnerable(_vulnerable);
    }

    function attack() public {
    
    
        vulnerable.performOperation(uint(address(this)));
        vulnerable.performOperation(9);
    }

    // function signature must match Vulnerable.performOperation()
    function performOperation(uint _num) public {
    
    
        owner = msg.sender;
    }
}

Abschluss

Vertragssicherheitsvorkehrungen

Oben haben wir erwähnt, dass die Verwundbarkeit von Delegatecall mit zwei Merkmalen der Solidität zusammenhängt. Um sicherere Verträge zu schreiben, stellt Solidity das Schlüsselwort „Library“ bereit. Durch das Wort „Library“ definierte Verträge müssen zustandslos sein (Statusvariablen dürfen innerhalb des Vertrags nicht vorhanden sein). Dadurch wird vermieden, dass Zustandsvariablen in externen Verträgen geändert werden. In unserer Praxis versuchen wir, es beim Schreiben von Shared-Function-Verträgen als „Bibliothek“ zu definieren.

Zusammenfassung von Anruf und Delegatecall

Die Unterschiede zwischen Call und Delegatecall sind subtil und es ist wichtig, sie zu verstehen, um sie effektiv und sicher nutzen zu können. Wenn Sie den Ausführungskontext und den Unterschied zwischen Call und Delegatecall verstehen, können Sie in Solidity effizientere, modularere und sicherere Smart Contracts schreiben.

Referenz:
https://medium.com/0xmantle/solidity-series-part-3-call-vs-delegatecall-8113b3c76855
https://celo.academy/t/preventing-vulnerabilities-in-solidity-delegate-call/38
https://docs.soliditylang.org/en/v0.6.2/contracts.html#receive-ether-function

Supongo que te gusta

Origin blog.csdn.net/haifeng_zhang_it/article/details/135273703
Recomendado
Clasificación