Advanced
Protocol Security & Auditing — Project — Harden a simple vault
Learn how pros audit.
Developer Advanced
7/8 — Protocol Security & Auditing — Project — Harden a simple vault
What to writeImplement a minimal ERC20 vault with caps, pause, CEI, nonReentrant, and invariant-friendly accounting.
- State
Token, owner, cap, pause flag, totalDeposits, and per-user deposits. - Deposit/withdraw
CEI + nonReentrant; enforce caps; emit events. - Admin
Pause and update cap; rotate owner. - Invariants/tests
Add an invariant for asset accounting.
State and constructor with domain checks.
contract SafeVault is ReentrancyGuard {
IERC20 public immutable token;
address public owner;
uint256 public cap;
bool public paused;
uint256 public totalDeposits;
mapping(address => uint256) public deposits;
event Deposit(address indexed user, uint256 amount, uint256 newBalance);
event Withdraw(address indexed user, uint256 amount, uint256 newBalance);
event Paused(bool status);
event CapUpdated(uint256 cap);
event OwnerUpdated(address indexed oldOwner, address indexed newOwner);
modifier onlyOwner() { require(msg.sender == owner, "owner"); _; }
modifier notPaused() { require(!paused, "paused"); _; }
constructor(address _token, uint256 _cap) {
require(_token != address(0), "token");
token = IERC20(_token);
owner = msg.sender;
cap = _cap;
}
CEI + nonReentrant deposit/withdraw with caps and events.
function deposit(uint256 amount) external notPaused nonReentrant {
require(amount > 0, "amount");
require(totalDeposits + amount <= cap, "cap");
deposits[msg.sender] += amount;
totalDeposits += amount;
token.transferFrom(msg.sender, address(this), amount);
emit Deposit(msg.sender, amount, deposits[msg.sender]);
}
function withdraw(uint256 amount) external notPaused nonReentrant {
uint256 bal = deposits[msg.sender];
require(amount <= bal, "exceeds");
deposits[msg.sender] = bal - amount;
totalDeposits -= amount;
token.transfer(msg.sender, amount);
emit Withdraw(msg.sender, amount, deposits[msg.sender]);
}
Simple admin controls with events.
function setCap(uint256 _cap) external onlyOwner {
cap = _cap;
emit CapUpdated(_cap);
}
function setPaused(bool p) external onlyOwner {
paused = p;
emit Paused(p);
}
function setOwner(address newOwner) external onlyOwner {
require(newOwner != address(0), "zero");
emit OwnerUpdated(owner, newOwner);
owner = newOwner;
}
Add an invariant test to lock in accounting.
function invariant_assetsMatchDeposits() public {
assertEq(token.balanceOf(address(vault)), vault.totalDeposits());
}
Full reference vault for copy/paste:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SafeVault is ReentrancyGuard {
IERC20 public immutable token;
address public owner;
uint256 public cap;
bool public paused;
uint256 public totalDeposits;
mapping(address => uint256) public deposits;
event Deposit(address indexed user, uint256 amount, uint256 newBalance);
event Withdraw(address indexed user, uint256 amount, uint256 newBalance);
event Paused(bool status);
event CapUpdated(uint256 cap);
event OwnerUpdated(address indexed oldOwner, address indexed newOwner);
modifier onlyOwner() { require(msg.sender == owner, "owner"); _; }
modifier notPaused() { require(!paused, "paused"); _; }
constructor(address _token, uint256 _cap) {
require(_token != address(0), "token");
token = IERC20(_token);
owner = msg.sender;
cap = _cap;
}
function deposit(uint256 amount) external notPaused nonReentrant {
require(amount > 0, "amount");
require(totalDeposits + amount <= cap, "cap");
deposits[msg.sender] += amount;
totalDeposits += amount;
token.transferFrom(msg.sender, address(this), amount);
emit Deposit(msg.sender, amount, deposits[msg.sender]);
}
function withdraw(uint256 amount) external notPaused nonReentrant {
uint256 bal = deposits[msg.sender];
require(amount <= bal, "exceeds");
deposits[msg.sender] = bal - amount;
totalDeposits -= amount;
token.transfer(msg.sender, amount);
emit Withdraw(msg.sender, amount, deposits[msg.sender]);
}
function setCap(uint256 _cap) external onlyOwner {
cap = _cap;
emit CapUpdated(_cap);
}
function setPaused(bool p) external onlyOwner {
paused = p;
emit Paused(p);
}
function setOwner(address newOwner) external onlyOwner {
require(newOwner != address(0), "zero");
emit OwnerUpdated(owner, newOwner);
owner = newOwner;
}
}
Try itTests: reentrancy blocked; cap enforced; paused blocks deposit/withdraw; accounting invariant passes; events emitted on admin changes.