Smart Contracts: Sicherheit als oberste Priorität
Ein Bug in einer Webanwendung ist ärgerlich. Ein Bug in einem Smart Contract kann Millionen kosten — und ist nach dem Deployment nicht mehr zu beheben. Die Immutabilität der Blockchain, die für Vertrauen sorgt, macht Fehler permanent. In diesem Beitrag zeigen wir die häufigsten Schwachstellen anhand von verwundbarem Code und dem jeweiligen Fix.
Reentrancy
Die wohl bekannteste Schwachstelle. Ein externer Call gibt die Kontrolle an den Aufrufer zurück, der die Funktion erneut betritt, bevor der State aktualisiert ist.
Verwundbar:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerableVault {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// BUG: Externer Call VOR State-Update
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] = 0; // Zu spät!
}
}
Fix — Checks-Effects-Interactions-Pattern:
contract SecureVault {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// State-Update VOR dem externen Call
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Zusätzlich empfehlen wir den Einsatz eines Reentrancy Guards:
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract GuardedVault is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Zugriffskontrolle
Fehlende oder fehlerhafte Zugriffskontrolle ist der häufigste Grund für
Exploits in der Praxis. Oft fehlt schlicht ein onlyOwner-Modifier
an einer kritischen Funktion.
Verwundbar:
contract VulnerableToken {
mapping(address => uint256) public balances;
// BUG: Jeder kann sich beliebig viele Token minten
function mint(address to, uint256 amount) external {
balances[to] += amount;
}
}
Fix mit OpenZeppelin AccessControl:
import "@openzeppelin/contracts/access/AccessControl.sol";
contract SecureToken is AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
mapping(address => uint256) public balances;
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
balances[to] += amount;
}
}
Für DAO-Contracts bevorzugen wir rollenbasierte Zugriffskontrolle gegenüber
einfachem Ownable, da DAOs typischerweise mehrere Akteure mit
unterschiedlichen Berechtigungen haben.
Integer Overflow (vor Solidity 0.8)
Seit Solidity 0.8 sind arithmetische Operationen standardmäßig vor Overflow
geschützt. Trotzdem begegnen uns in Audits regelmäßig Contracts, die
unchecked-Blöcke nutzen, um Gas zu sparen — und dabei die
Sicherheit opfern:
// Nur sicher, wenn Sie BEWEISEN können, dass kein Overflow auftritt
function unsafeIncrement(uint256 x) internal pure returns (uint256) {
unchecked { return x + 1; } // OK: uint256 max ist astronomisch
}
// GEFÄHRLICH: Kann bei beliebigen Inputs überlaufen
function unsafeMultiply(uint256 a, uint256 b) internal pure returns (uint256) {
unchecked { return a * b; } // BUG: Overflow möglich
}
Regel: Verwenden Sie unchecked nur für Schleifenzähler und
Inkrement-Operationen, bei denen der Overflow mathematisch ausgeschlossen ist.
Front-Running und Sandwich-Attacks
Jede Transaktion ist im Mempool sichtbar, bevor sie in einen Block aufgenommen wird. Ein Angreifer kann Ihre Transaktion sehen und eine eigene davor platzieren. Bei Governance-Votings kann das bedeuten, dass ein Angreifer Token kauft, abstimmt und direkt danach verkauft.
Gegenmaßnahmen:
// Commit-Reveal-Pattern für Abstimmungen
contract SecureVoting {
mapping(bytes32 => bool) public commits;
mapping(address => bytes32) public userCommits;
// Phase 1: Commit (Hash des Votes)
function commitVote(bytes32 hash) external {
userCommits[msg.sender] = hash;
commits[hash] = true;
}
// Phase 2: Reveal (Vote + Salt)
function revealVote(uint256 proposalId, bool support, bytes32 salt)
external
{
bytes32 hash = keccak256(
abi.encodePacked(proposalId, support, salt, msg.sender)
);
require(userCommits[msg.sender] == hash, "Invalid reveal");
delete userCommits[msg.sender];
// Vote verarbeiten ...
}
}
Audit-Prozess
Unsere interne Sicherheitsprüfung vor jedem Deployment folgt einem festen Schema:
1. Statische Analyse mit Slither:
$ slither src/Vault.sol --filter-paths "node_modules"
# Prüft auf bekannte Patterns: Reentrancy, ungenutzte
# Rückgabewerte, Shadowing, etc.
2. Unit Tests mit Foundry:
function test_ReentrancyProtection() public {
AttackerContract attacker = new AttackerContract(address(vault));
// Attacker deponiert und versucht Reentrancy
vm.deal(address(attacker), 1 ether);
attacker.deposit{value: 1 ether}();
vm.expectRevert("ReentrancyGuard: reentrant call");
attacker.attack();
}
3. Fuzzing mit Foundry:
function testFuzz_WithdrawNeverExceedsBalance(
address user,
uint256 depositAmount
) public {
vm.assume(user != address(0));
vm.assume(depositAmount > 0 && depositAmount <= 100 ether);
vm.deal(user, depositAmount);
vm.prank(user);
vault.deposit{value: depositAmount}();
uint256 vaultBalanceBefore = address(vault).balance;
vm.prank(user);
vault.withdraw();
assertGe(vaultBalanceBefore, depositAmount);
}
4. Externer Audit — Für Contracts, die signifikante Werte verwalten, beauftragen wir externe Auditoren. Ein interner Review ersetzt keinen externen Audit, denn eigene Annahmen werden selten von den eigenen Entwicklern hinterfragt.
Checkliste
Abschließend unsere Kurzcheckliste für jedes Smart-Contract-Deployment:
- Checks-Effects-Interactions eingehalten?
- Reentrancy Guard bei allen externen Calls?
- Zugriffskontrolle auf alle state-verändernden Funktionen?
uncheckednur wo mathematisch bewiesen sicher?- Slither ohne High/Medium-Findings?
- 100 % Branch-Coverage in Tests?
- Fuzz-Tests für alle öffentlichen Funktionen?
- Externer Audit bei Wertverwahrung?
Smart-Contract-Sicherheit ist kein Feature, das man am Ende hinzufügt. Sie muss von der ersten Zeile Code an mitgedacht werden. Die Kosten eines Audits sind ein Bruchteil dessen, was ein Exploit kosten kann.