Deep EVM #4: Sicherheitsprimitive — msg.sender, Zugangskontrolle und Reentrancy
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:
- Checks — Alle Bedingungen pruefen
- Effects — Zustand aendern
- 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.