Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions rayeberechi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Node modules
/node_modules

# Compilation output
/dist

# pnpm deploy output
/bundle

# Hardhat Build Artifacts
/artifacts

# Hardhat compilation (v2) support directory
/cache

# Typechain output
/types

# Hardhat coverage reports
/coverage
17 changes: 17 additions & 0 deletions rayeberechi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Timelocked Savings Vault

A smart contract system that forces users to save ETH until a specific date.

## Features
- **Time Lock**: Users set an `unlockTime`. Funds cannot be withdrawn before this timestamp.
- **Factory Pattern**: Each user gets their own personal Vault contract.
- **Single Active Vault**: A user cannot create a second vault if they already have funds locked in an existing one.
- **Security**:
- Only the owner can withdraw.
- Direct ETH transfers to the vault are rejected (no `receive` function).
- Full balance is withdrawn at once to reset the vault.

## Usage
1. Call `createVault(unlockTime)` on the Factory with ETH attached.
2. Wait until `block.timestamp >= unlockTime`.
3. Call `withdraw()` on your specific Vault address.
71 changes: 71 additions & 0 deletions rayeberechi/contracts/TimeLockedVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

// --- 1. THE CHILD CONTRACT (The Vault) ---
contract TimeLockedVault {

address public owner;
uint256 public unlockTime;

// Requirement: "Contract must reject direct ETH transfers"

constructor(address _owner, uint256 _unlockTime) payable {
require(msg.value > 0, "Must deposit ETH to create vault");
require(_unlockTime > block.timestamp, "Unlock time must be in the future");

owner = _owner;
unlockTime = _unlockTime;
}

function withdraw() external {
// Requirement: "No one can withdraw another user's funds"
require(msg.sender == owner, "Only owner can withdraw");

// Requirement: "User can withdraw only after block.timestamp >= unlockTime"
require(block.timestamp >= unlockTime, "Vault is still locked");

uint256 amount = address(this).balance;
require(amount > 0, "Vault is empty");

// Requirement: "Full balance must be withdrawn at once"
(bool success, ) = owner.call{value: amount}("");
require(success, "Transfer failed");
}

// Helper to check balance
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}

// --- 2. THE FACTORY CONTRACT (The Manager) ---
contract VaultFactory {

// Requirement: "Each user can have only one active vault at a time"
mapping(address => TimeLockedVault) public userVaults;

event VaultCreated(address indexed user, address vaultAddress, uint256 unlockTime, uint256 amount);

function createVault(uint256 _unlockTime) external payable {
// 1. Check if user already has an ACTIVE vault (one with money in it)
if (address(userVaults[msg.sender]) != address(0)) {
uint256 existingBalance = userVaults[msg.sender].getBalance();
require(existingBalance == 0, "You already have an active vault with funds");
}

require(msg.value > 0, "You must deposit ETH to create a vault");

// 2. Create the new vault and pass the ETH
TimeLockedVault newVault = new TimeLockedVault{value: msg.value}(msg.sender, _unlockTime);

// 3. Update the registry
userVaults[msg.sender] = newVault;

emit VaultCreated(msg.sender, address(newVault), _unlockTime, msg.value);
}

// Helper to find your vault
function getMyVault() external view returns (address) {
return address(userVaults[msg.sender]);
}
}
38 changes: 38 additions & 0 deletions rayeberechi/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers";
import { configVariable, defineConfig } from "hardhat/config";

export default defineConfig({
plugins: [hardhatToolboxMochaEthersPlugin],
solidity: {
profiles: {
default: {
version: "0.8.28",
},
production: {
version: "0.8.28",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
},
},
networks: {
hardhatMainnet: {
type: "edr-simulated",
chainType: "l1",
},
hardhatOp: {
type: "edr-simulated",
chainType: "op",
},
sepolia: {
type: "http",
chainType: "l1",
url: configVariable("SEPOLIA_RPC_URL"),
accounts: [configVariable("SEPOLIA_PRIVATE_KEY")],
},
},
});
9 changes: 9 additions & 0 deletions rayeberechi/ignition/modules/Counter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

export default buildModule("CounterModule", (m) => {
const counter = m.contract("Counter");

m.call(counter, "incBy", [5n]);

return { counter };
});
28 changes: 28 additions & 0 deletions rayeberechi/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "rayeberechi",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"devDependencies": {
"@nomicfoundation/hardhat-ethers": "^4.0.4",
"@nomicfoundation/hardhat-ignition": "^3.0.7",
"@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2",
"@types/chai": "^4.3.20",
"@types/chai-as-promised": "^8.0.2",
"@types/mocha": "^10.0.10",
"@types/node": "^22.19.11",
"chai": "^5.3.3",
"ethers": "^6.16.0",
"forge-std": "github:foundry-rs/forge-std#v1.9.4",
"hardhat": "^3.1.8",
"mocha": "^11.7.5",
"typescript": "~5.8.0"
}
}
22 changes: 22 additions & 0 deletions rayeberechi/scripts/send-op-tx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { network } from "hardhat";

const { ethers } = await network.connect({
network: "hardhatOp",
chainType: "op",
});

console.log("Sending transaction using the OP chain type");

const [sender] = await ethers.getSigners();

console.log("Sending 1 wei from", sender.address, "to itself");

console.log("Sending L2 transaction");
const tx = await sender.sendTransaction({
to: sender.address,
value: 1n,
});

await tx.wait();

console.log("Transaction sent successfully");
36 changes: 36 additions & 0 deletions rayeberechi/test/Counter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect } from "chai";
import { network } from "hardhat";

const { ethers } = await network.connect();

describe("Counter", function () {
it("Should emit the Increment event when calling the inc() function", async function () {
const counter = await ethers.deployContract("Counter");

await expect(counter.inc()).to.emit(counter, "Increment").withArgs(1n);
});

it("The sum of the Increment events should match the current value", async function () {
const counter = await ethers.deployContract("Counter");
const deploymentBlockNumber = await ethers.provider.getBlockNumber();

// run a series of increments
for (let i = 1; i <= 10; i++) {
await counter.incBy(i);
}

const events = await counter.queryFilter(
counter.filters.Increment(),
deploymentBlockNumber,
"latest",
);

// check that the aggregated events match the current value
let total = 0n;
for (const event of events) {
total += event.args.by;
}

expect(await counter.x()).to.equal(total);
});
});
13 changes: 13 additions & 0 deletions rayeberechi/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* Based on https://github.com/tsconfig/bases/blob/501da2bcd640cf95c95805783e1012b992338f28/bases/node22.json */
{
"compilerOptions": {
"lib": ["es2023"],
"module": "node16",
"target": "es2022",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "node16",
"outDir": "dist"
}
}