|
| 1 | +<div align="center"> |
| 2 | + |
1 | 3 | # FundForwarder |
2 | 4 |
|
3 | | -A Solidity smart contract that receives ETH deposits and automatically forwards them to a predefined target wallet. Built with Hardhat and OpenZeppelin v5. |
| 5 | +**A non-custodial ETH forwarding smart contract built on Ethereum** |
| 6 | + |
| 7 | +[](https://soliditylang.org/) |
| 8 | +[](https://hardhat.org/) |
| 9 | +[](https://www.openzeppelin.com/contracts) |
| 10 | +[](https://opensource.org/licenses/ISC) |
| 11 | +[]() |
| 12 | +[]() |
| 13 | + |
| 14 | +</div> |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## Overview |
| 19 | + |
| 20 | +FundForwarder is a production-ready Solidity smart contract that accepts ETH deposits and instantly forwards them to a designated target wallet. It is designed for scenarios where a fixed, public-facing deposit address is needed while retaining the flexibility to update the destination — all secured by timelocks, two-step ownership, and pause controls. |
| 21 | + |
| 22 | +### How It Works |
| 23 | + |
| 24 | +``` |
| 25 | +Sender ──► [ FundForwarder Contract ] ──► Target Wallet |
| 26 | + │ |
| 27 | + ├─ Validates minimum deposit |
| 28 | + ├─ Emits Deposit event |
| 29 | + ├─ Forwards full balance |
| 30 | + └─ Emits Forwarded event |
| 31 | +``` |
| 32 | + |
| 33 | +--- |
4 | 34 |
|
5 | 35 | ## Features |
6 | 36 |
|
7 | | -- Automatic ETH forwarding on deposit |
8 | | -- Configurable minimum deposit amount (capped at 10 ETH) |
9 | | -- Time-locked target wallet changes (24-hour delay) |
10 | | -- Pausable deposits for emergency scenarios |
11 | | -- Owner fund recovery |
12 | | -- Two-step ownership transfer (Ownable2Step) |
13 | | -- Reentrancy protection |
| 37 | +| Feature | Description | |
| 38 | +|---|---| |
| 39 | +| **Instant Forwarding** | ETH is forwarded to the target wallet in the same transaction as the deposit | |
| 40 | +| **Minimum Deposit** | Configurable threshold to filter dust/spam transactions (capped at 10 ETH) | |
| 41 | +| **Time-locked Wallet Changes** | 24-hour mandatory delay before a new target wallet takes effect | |
| 42 | +| **Two-step Ownership (Ownable2Step)** | Ownership transfers require explicit acceptance, preventing irreversible mistakes | |
| 43 | +| **Pausable** | Owner can halt all deposits during emergencies or maintenance | |
| 44 | +| **Emergency Recovery** | Owner can withdraw any ETH stuck in the contract | |
| 45 | +| **Reentrancy Protection** | Guards against reentrancy on deposit and recovery paths | |
| 46 | + |
| 47 | +--- |
| 48 | + |
| 49 | +## Architecture |
| 50 | + |
| 51 | +``` |
| 52 | +contracts/ |
| 53 | +├── FundForwarder.sol # Main contract |
| 54 | +└── test/ |
| 55 | + ├── ForceFeeder.sol # Test helper — force-sends ETH |
| 56 | + └── Reverter.sol # Test helper — always-reverting receiver |
| 57 | +``` |
| 58 | + |
| 59 | +### Contract Inheritance |
| 60 | + |
| 61 | +``` |
| 62 | +Ownable ─► Ownable2Step ─┐ |
| 63 | + Pausable ─────┤── FundForwarder |
| 64 | + ReentrancyGuard ────┘ |
| 65 | +``` |
| 66 | + |
| 67 | +### Access Control |
| 68 | + |
| 69 | +| Function | Access | Description | |
| 70 | +|---|---|---| |
| 71 | +| `receive()` | Public | Accept and forward ETH deposits | |
| 72 | +| `fallback()` | Public | Rejects calls with calldata | |
| 73 | +| `requestTargetWalletChange()` | Owner | Initiate a wallet change (starts 24h timelock) | |
| 74 | +| `finalizeTargetWalletChange()` | Owner | Apply the pending wallet change after timelock | |
| 75 | +| `cancelTargetWalletChange()` | Owner | Cancel a pending wallet change | |
| 76 | +| `updateMinDeposit()` | Owner | Update the minimum deposit threshold | |
| 77 | +| `pause()` / `unpause()` | Owner | Toggle deposit acceptance | |
| 78 | +| `recoverFunds()` | Owner | Withdraw stuck ETH to the owner | |
| 79 | +| `transferOwnership()` | Owner | Initiate two-step ownership transfer | |
| 80 | +| `acceptOwnership()` | Pending Owner | Accept ownership transfer | |
| 81 | + |
| 82 | +--- |
| 83 | + |
| 84 | +## Getting Started |
| 85 | + |
| 86 | +### Prerequisites |
14 | 87 |
|
15 | | -## Setup |
| 88 | +- [Node.js](https://nodejs.org/) >= 18 |
| 89 | +- [npm](https://www.npmjs.com/) >= 9 |
| 90 | + |
| 91 | +### Installation |
16 | 92 |
|
17 | 93 | ```bash |
| 94 | +git clone https://github.com/SP1R4/FundsForwarder.git |
| 95 | +cd FundsForwarder |
18 | 96 | npm install |
| 97 | +``` |
| 98 | + |
| 99 | +### Environment Setup |
| 100 | + |
| 101 | +```bash |
19 | 102 | cp .env.example .env |
20 | | -# Fill in .env with your keys |
21 | 103 | ``` |
22 | 104 |
|
23 | | -## Compile |
| 105 | +Fill in your `.env`: |
| 106 | + |
| 107 | +```env |
| 108 | +SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_KEY |
| 109 | +PRIVATE_KEY=your_deployer_private_key |
| 110 | +ETHERSCAN_API_KEY=your_etherscan_key |
| 111 | +TARGET_WALLET=0xYourTargetWalletAddress |
| 112 | +REPORT_GAS=false |
| 113 | +``` |
| 114 | + |
| 115 | +> **Warning:** Never commit your `.env` file. It is excluded via `.gitignore`. |
| 116 | +
|
| 117 | +--- |
| 118 | + |
| 119 | +## Usage |
| 120 | + |
| 121 | +### Compile |
24 | 122 |
|
25 | 123 | ```bash |
26 | 124 | npx hardhat compile |
27 | 125 | ``` |
28 | 126 |
|
29 | | -## Test |
| 127 | +### Test |
30 | 128 |
|
31 | 129 | ```bash |
32 | 130 | npx hardhat test |
33 | 131 | ``` |
34 | 132 |
|
35 | | -## Coverage |
| 133 | +### Coverage |
36 | 134 |
|
37 | 135 | ```bash |
38 | 136 | npx hardhat coverage |
39 | 137 | ``` |
40 | 138 |
|
41 | | -## Gas Report |
| 139 | +### Gas Report |
42 | 140 |
|
43 | 141 | ```bash |
44 | 142 | REPORT_GAS=true npx hardhat test |
45 | 143 | ``` |
46 | 144 |
|
47 | | -## Deploy |
| 145 | +### Deploy to Sepolia |
48 | 146 |
|
49 | 147 | ```bash |
50 | 148 | npx hardhat run scripts/deploy.js --network sepolia |
51 | 149 | ``` |
52 | 150 |
|
53 | | -## Security Model |
| 151 | +The deploy script reads `TARGET_WALLET` from `.env` and automatically verifies the contract on Etherscan after deployment. |
| 152 | + |
| 153 | +--- |
| 154 | + |
| 155 | +## Test Suite |
| 156 | + |
| 157 | +32 tests across 8 categories with **100% line and function coverage**. |
| 158 | + |
| 159 | +``` |
| 160 | + FundForwarder |
| 161 | + Deployment (6 tests) |
| 162 | + Deposits & Forwarding (4 tests) |
| 163 | + Wallet Change Flow (8 tests) |
| 164 | + MinDeposit Updates (4 tests) |
| 165 | + Pause / Unpause (3 tests) |
| 166 | + Recovery (3 tests) |
| 167 | + Ownable2Step (2 tests) |
| 168 | + Fallback (1 test) |
| 169 | + Edge Cases (1 test) |
| 170 | +
|
| 171 | + 32 passing |
| 172 | +``` |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +## Security Considerations |
| 177 | + |
| 178 | +- **Timelock on wallet changes** — A mandatory 24-hour delay between requesting and finalizing a target wallet change prevents instant fund redirection in the event of a compromised owner key, giving stakeholders time to respond. |
| 179 | +- **Ownable2Step** — Prevents ownership from being accidentally transferred to an incorrect or inaccessible address. The new owner must explicitly call `acceptOwnership()`. |
| 180 | +- **MAX_MIN_DEPOSIT cap** — The minimum deposit is bounded at 10 ETH to prevent misconfiguration that could lock the contract from receiving funds. |
| 181 | +- **No calldata accepted** — The `fallback()` function reverts on any call with data, reducing the attack surface to only plain ETH transfers. |
| 182 | +- **Reentrancy guards** — Applied to both `receive()` and `recoverFunds()` to prevent reentrancy exploits. |
| 183 | + |
| 184 | +### Known Limitations |
| 185 | + |
| 186 | +- The contract does not support ERC-20 token forwarding — ETH only. |
| 187 | +- `selfdestruct`-based force-sends can deposit ETH below `minDeposit` without triggering forwarding. The `recoverFunds()` function handles this case. |
| 188 | + |
| 189 | +--- |
| 190 | + |
| 191 | +## License |
54 | 192 |
|
55 | | -- **Ownable2Step**: Ownership transfers require explicit acceptance by the new owner, preventing accidental transfers to wrong addresses. |
56 | | -- **Timelock**: Target wallet changes require a 24-hour waiting period, giving stakeholders time to react to unauthorized changes. |
57 | | -- **Pausable**: The owner can pause deposits in case of emergency. |
58 | | -- **ReentrancyGuard**: Prevents reentrancy attacks on deposit and recovery functions. |
59 | | -- **MAX_MIN_DEPOSIT**: Caps the minimum deposit at 10 ETH to prevent owner misconfiguration. |
| 193 | +This project is licensed under the [ISC License](https://opensource.org/licenses/ISC). |
0 commit comments