examples/VulnerableVault.sol:24
withdraw() sends ether before zeroing balance; attacker re-enters via receive() to drain the vault.
function withdraw() external {
uint256 amt = balance[msg.sender];
(bool ok,) = msg.sender.call{value: amt}("");
require(ok);
balance[msg.sender] = 0;
}
Loss of 100% of user deposits in one tx. PoC demonstrates 1 ETH attacker → 11 ETH withdrawn.
Move state update before the external call (CEI). Or add nonReentrant.
- (bool ok,) = msg.sender.call{value: amt}("");
- require(ok);
- balance[msg.sender] = 0;
+ balance[msg.sender] = 0;
+ (bool ok,) = msg.sender.call{value: amt}("");
+ require(ok);
examples/VulnerableVault.sol:34
Anyone can call setPaused(bool) — pause state controllable by adversary.
function setPaused(bool p) external { paused = p; }
State pollution today; full DoS if any future function gains whenNotPaused gating.
Add onlyOwner modifier.
examples/VulnerableVault.sol:40
Owner can transfer entire contract balance (= user deposits) anywhere with no allowlist or timelock.
function rescue(address payable to) external onlyOwner { to.transfer(address(this).balance); }
Single-key compromise = 100% user fund loss.
Remove or scope to non-deposit assets; add Timelock + multi-sig.
examples/VulnerableVault.sol:46
depositor.call(data) silently swallows failure.
depositor.call(data);
Silent notification failures; gas-grief surface.
Capture return value; require(ok).
examples/VulnerableVault.sol:50
transferOwnership applies immediately; typo can permanently brick contract.
function transferOwnership(address newOwner) external onlyOwner { owner = newOwner; }
Operational risk on owner rotation.
Use Ownable2Step.
examples/VulnerableVault.sol:2
Pin compiler version to match audited bytecode.
pragma solidity ^0.8.20;
Future minor compiler updates can change codegen.
pragma solidity 0.8.24;