Zum Hauptinhalt springen
BlockchainMar 28, 2026

Deep EVM #4: Sicherheitsprimitive — msg.sender, Zugangskontrolle und Reentrancy

OS
Open Soft Team

Engineering Team

Smart-Contract-Sicherheit beginnt beim Ausfuehrungsmodell

Smart-Contract-Sicherheit bedeutet nicht, Pruefungen ueber funktionierenden Code zu legen — es geht darum zu verstehen, wie das Ausfuehrungsmodell der EVM Angriffsflaechen schafft. Jeder externe Aufruf ist ein potenzieller Wiedereinstiegspunkt. Jeder Delegatecall ist ein Storage-Hijacking-Vektor.

msg.sender vs tx.origin

Der grundlegendste Sicherheitsfehler in Smart Contracts: die Verwechslung von msg.sender und tx.origin.

  • msg.sender — der unmittelbare Aufrufer. Kann ein EOA (Externally Owned Account) oder ein anderer Contract sein.
  • tx.origin — der urspruengliche Absender der Transaktion. Immer ein EOA.
// FALSCH: Jeder Contract kann das ausloesen, wenn er im Namen des Benutzers aufruft
function withdraw() external {
    require(tx.origin == owner, "Not owner");
    payable(owner).transfer(address(this).balance);
}

// RICHTIG: Nur der direkte Aufrufer wird geprueft
function withdraw() external {
    require(msg.sender == owner, "Not owner");
    payable(owner).transfer(address(this).balance);
}

Verwenden Sie niemals tx.origin fuer die Authentifizierung. Ein boesartiger Contract kann den Benutzer dazu bringen, eine Funktion aufzurufen, die dann Ihren Contract mit dem tx.origin des Benutzers aufruft.

Reentrancy-Angriffe

Reentrancy ist der beruechtigtste Angriff in der Smart-Contract-Geschichte. Der DAO-Hack von 2016, bei dem 60 Millionen Dollar gestohlen wurden, nutzte genau diese Schwachstelle.

Das Grundmuster:

// Verwundbarer Contract
contract Vulnerable {
    mapping(address => uint256) public balances;
    
    function withdraw() external {
        uint256 bal = balances[msg.sender];
        // GEFAHR: Externer Aufruf VOR Zustandsaenderung
        (bool success, ) = msg.sender.call{value: bal}("");
        require(success);
        // Diese Zeile wird erst nach dem externen Aufruf erreicht
        balances[msg.sender] = 0;
    }
}

// Angreifer-Contract
contract Attacker {
    Vulnerable target;
    
    receive() external payable {
        // Wird aufgerufen, wenn target.withdraw() ETH sendet
        if (address(target).balance > 0) {
            target.withdraw(); // Reentrancy!
        }
    }
}

Abwehr: Checks-Effects-Interactions

Das goldene Muster fuer sichere Smart Contracts:

  1. Checks — Alle Bedingungen pruefen
  2. Effects — Zustand aendern
  3. Interactions — Externe Aufrufe durchfuehren
function withdraw() external {
    uint256 bal = balances[msg.sender]; // Check
    require(bal > 0, "No balance");
    balances[msg.sender] = 0;           // Effect (VORHER!)
    (bool success, ) = msg.sender.call{value: bal}(""); // Interaction
    require(success);
}

Reentrancy-Guards

Fuer zusaetzliche Sicherheit verwenden Sie einen Reentrancy-Guard:

boolean private locked;

modifier noReentrancy() {
    require(!locked, "Reentrancy");
    locked = true;
    _;
    locked = false;
}

Mit EIP-1153 (Transient Storage) kann das Lock effizienter implementiert werden: TSTORE/TLOAD statt SSTORE/SLOAD, was 4.800 Gas pro Aufruf spart.

delegatecall-Risiken

DELEGATECALL fuehrt den Code eines anderen Contracts im Speicherkontext des aufrufenden Contracts aus. Dies ist die Grundlage fuer Proxy-Muster, birgt aber erhebliche Risiken:

  • Der aufgerufene Code kann den Storage des aufrufenden Contracts beliebig aendern
  • Storage-Slot-Kollisionen zwischen Proxy und Implementierung koennen Daten korrumpieren
  • Wenn die Implementierung eine selfdestruct-Funktion hat, kann der Proxy zerstoert werden
// Gefaehrlich: Ungeprufter delegatecall
function execute(address target, bytes calldata data) external {
    (bool success, ) = target.delegatecall(data);
    require(success);
}

Zugangskontrollmuster

Fuer produktionsreife Smart Contracts verwenden Sie OpenZeppelins AccessControl:

import "@openzeppelin/contracts/access/AccessControl.sol";

contract MyContract is AccessControl {
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
    
    constructor() {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }
    
    function adminFunction() external onlyRole(ADMIN_ROLE) {
        // Nur Admins
    }
}

Fazit

Smart-Contract-Sicherheit erfordert ein tiefes Verstaendnis des EVM-Ausfuehrungsmodells. msg.sender vs tx.origin, Reentrancy-Schutz, sichere delegatecall-Nutzung und rollenbasierte Zugangskontrolle sind die Grundpfeiler. Jeder externe Aufruf ist eine potenzielle Gefahr — behandeln Sie ihn entsprechend.