masonzeng702550/secure-vault-solidity

GitHub: masonzeng702550/secure-vault-solidity

Stars: 0 | Forks: 0

# SecureVault A secure Solidity ETH vault contract with multi-layered defense against common smart contract attacks. Built and tested with Foundry. [![Solidity](https://img.shields.io/badge/Solidity-0.8.20-blue)](https://docs.soliditylang.org/) [![Foundry](https://img.shields.io/badge/Built%20with-Foundry-orange)](https://book.getfoundry.sh/) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) [![Tests](https://img.shields.io/badge/Tests-14%2F14%20passing-brightgreen)](test-results/forge-test-output.txt) ## Features - **ETH deposits / withdrawals** with per-transaction withdrawal cap (`MAX_WITHDRAW = 10 ether`) - **Internal balance transfers** between users (gas-efficient, no external call) - **Two-tier access control** — `Owner` + `Operator` role - **Pausable** — Operator can halt deposits/withdrawals in emergencies - **Emergency drain** — Owner-only fallback to rescue funds - **Custom errors** — gas-optimized revert reasons (Solidity 0.8.4+) - **Reentrancy guard** — handcrafted 1/2 status pattern (no OZ dependency) - **Safe ETH transfer** — uses `call{value:..}` with success check - **Defensive fallback** — unknown function calls revert ## Security mechanisms | Threat | Defense | | ----------------------------------- | -------------------------------------------- | | Reentrancy attack | `nonReentrant` modifier + CEI pattern | | Integer overflow / underflow | Solidity 0.8+ built-in checks | | Unauthorized access | `onlyOwner` / `onlyOperator` modifiers | | ETH transfer failure (silent) | `call{value:..}` + boolean check | | Emergency lockdown | `Pausable` (`whenNotPaused`) | | Zero address / zero amount | `ZeroAddress` / `ZeroAmount` custom errors | | Unknown function invocation | `fallback()` reverts | ## Project layout secure-vault-solidity/ ├── src/ │ └── SecureVault.sol # Main contract ├── test/ │ └── SecureVault.t.sol # Foundry test suite + ReentrancyAttacker ├── test-results/ │ ├── forge-test-output.txt # Captured `forge test -vv` output │ └── forge-gas-report.txt # Captured `forge test --gas-report` output ├── foundry.toml ├── LICENSE └── README.md ## Getting started ### Prerequisites - [Foundry](https://book.getfoundry.sh/getting-started/installation) (forge, anvil, cast) Install Foundry if you haven't: curl -L https://foundry.paradigm.xyz | bash foundryup ### Clone and install dependencies git clone https://github.com/masonzeng702550/secure-vault-solidity.git cd secure-vault-solidity forge install foundry-rs/forge-std --no-commit ### Build forge build ### Run tests # Standard run with verbose output forge test -vv # With gas report forge test --gas-report # Run only the reentrancy attack test forge test --match-test test_ReentrancyAttack_IsBlocked -vvvv ## Test suite 14 tests covering every public function and the major attack vectors: | # | Test | Category | Status | | -- | ------------------------------------------------- | ----------------- | ------ | | 1 | `test_Deposit_UpdatesBalance` | Deposit | PASS | | 2 | `test_Deposit_RevertsOnZero` | Input validation | PASS | | 3 | `test_Receive_AccountsAsDeposit` | Receive fallback | PASS | | 4 | `test_Withdraw_Success` | Withdraw | PASS | | 5 | `test_Withdraw_RevertsOnExceedLimit` | Limit check | PASS | | 6 | `test_Withdraw_RevertsOnInsufficientBalance` | Balance check | PASS | | 7 | `test_TransferTo_MovesInternalBalance` | Internal transfer | PASS | | 8 | `test_OnlyOwner_CanTransferOwnership` | Access control | PASS | | 9 | `test_Operator_CanPause_NonOperator_Cannot` | Role permission | PASS | | 10 | `test_WhenPaused_DepositReverts` | Pausable | PASS | | 11 | **`test_ReentrancyAttack_IsBlocked`** | **Security** | **PASS** | | 12 | `test_EmergencyWithdraw_OnlyOwner` | Owner-only | PASS | | 13 | `test_EmergencyWithdraw_DrainsContract` | Emergency drain | PASS | | 14 | `test_Fallback_Reverts` | Fallback | PASS | ### Reentrancy test highlight The suite includes a real `ReentrancyAttacker` contract that: 1. Deposits ETH into the vault 2. Calls `withdraw()`, which triggers ETH transfer to the attacker 3. In `receive()`, immediately calls `withdraw()` again — attempting the classic recursive drain Result: the second call is blocked by `nonReentrant`, the outer ETH transfer fails with `TransferFailed`, the attack reverts atomically, and the victim's deposit remains untouched. [PASS] test_ReentrancyAttack_IsBlocked() (gas: 385,801) See [`test-results/forge-test-output.txt`](test-results/forge-test-output.txt) for the full raw run. ## Gas report (excerpt) | Function | Min | Avg | Max | Calls | | ------------------- | ------ | ------ | ------ | ----- | | `deposit` | 23,489 | 60,501 | 69,752 | 10 | | `withdraw` | 29,288 | 35,362 | 45,287 | 3 | | `transferTo` | 54,156 | 54,156 | 54,156 | 1 | | `emergencyWithdraw` | 24,171 | 43,470 | 62,769 | 2 | | `pause` | 25,733 | 27,817 | 29,974 | 3 | | `setOperator` | 48,254 | 48,254 | 48,254 | 1 | | `transferOwnership` | 24,170 | 24,170 | 24,170 | 1 | | `fallback` | 21,552 | 21,552 | 21,552 | 1 | Full report: [`test-results/forge-gas-report.txt`](test-results/forge-gas-report.txt) ## Design notes ### Why a hand-rolled `nonReentrant`? Using a 1/2 status flag (instead of `bool`) saves gas — toggling between two non-zero values avoids the cold SSTORE cost on every call. This is the same pattern OpenZeppelin uses internally, replicated here to keep the contract dependency-free. ### Why custom errors instead of `require(.., "msg")`? Custom errors (Solidity 0.8.4+) are cheaper at both deploy time and runtime, and they can carry typed parameters for easier debugging: revert InsufficientBalance(requested, available); ### Why `call{value:..}` instead of `transfer`? `transfer` and `send` forward only 2,300 gas, which breaks when the recipient is a contract with non-trivial fallback logic. `call` forwards all remaining gas — paired with the CEI pattern and `nonReentrant`, this is safe and forward-compatible. ### Checks-Effects-Interactions Every state-changing function follows the strict CEI pattern: validate inputs, update state, then make external calls. Even if `nonReentrant` were removed, a reentrant call would see already-updated balances and fail naturally — defense in depth. ## Out of scope (room for improvement) The contract intentionally focuses on a single, well-defended ETH vault. Topics not covered: - Cross-function and read-only reentrancy - Front-running / MEV mitigation (commit-reveal, TWAP) - Signature replay protection (EIP-712 / EIP-1271) - Upgradeable patterns (UUPS / Transparent Proxy) and storage layout These would each warrant their own contracts and dedicated test files. ## License [MIT](LICENSE)