Skip to content
Merged
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
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,60 @@ $ cast --help
alchemy sepolia rpc url

<!-- https://eth-sepolia.g.alchemy.com/v2/${key} -->


Updated README file

////////

# 🧠 TimeLockedVault

A TimeLockedVault smart contract that allows users to deposit ERC20 tokens with a time lock. Users can only withdraw their tokens after the lock period has passed.

✅ Features:

- Lock ERC20 tokens for a fixed duration.

- Prevent early withdrawals.

- Fully tested using Foundry's Forge framework.
## ✍️ Project Structure Update
This repository includes a custom smart contract called TimeLockedVault.sol, created as part of a feature implementation for locking ERC-20 token deposits with time-based withdrawal restrictions.

📄 File: src/TimeLockedVault.sol

This contract allows users to:

- Deposit ERC-20 tokens with a lock duration

- Enforce time-based withdrawal logic

- Access locked balances and check unlock time

Unit tests are provided in test/TimeLockedVault.t.sol.
## 🧪 How to Run the Tests:

To run tests, run the following command

```bash
forge test --match-path test/TimeLockedVault.t.sol
```

Ensure your .env is set up with RPC URL and private key if needed.
## 📁 Files:
- Contract: src/TimeLockedVault.sol

- Test: test/TimeLockedVault.t.sol
## 🛠 Technologies:
- Solidity ^0.8.20

- Foundry (Forge)
## ✅ Optional (only if you add a script):
If you decide to write a deploy script later (like TimeLockedVault.s.sol)

```bash
forge script script/TimeLockedVault.s.sol:TimeLockedVaultScript --rpc-url <your_rpc_url> --private-key <your_private_key>

```

////////
71 changes: 71 additions & 0 deletions src/TimeLockedVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title TimeLockedVault
/// @notice Vault to deposit WLD tokens and lock them for a user-defined time period.
interface IERC20 {
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
}

contract TimeLockedVault {
IERC20 public immutable wldToken;

struct Lock {
uint256 amount;
uint256 unlockTime;
}

mapping(address => Lock[]) public userLocks;

event TokensLocked(address indexed user, uint256 amount, uint256 unlockTime);
event TokensWithdrawn(address indexed user, uint256 amount);

constructor(address _wldTokenAddress) {
require(_wldTokenAddress != address(0), "Invalid token address");
wldToken = IERC20(_wldTokenAddress);
}

/// @notice Lock tokens for a specified duration
/// @param amount Amount of tokens to lock
/// @param lockDuration Duration in seconds to lock tokens
function lockTokens(uint256 amount, uint256 lockDuration) external {
require(amount > 0, "Amount must be greater than zero");
require(lockDuration > 0, "Lock duration must be greater than zero");

bool success = wldToken.transferFrom(msg.sender, address(this), amount);
require(success, "Token transfer failed");

uint256 unlockTime = block.timestamp + lockDuration;
userLocks[msg.sender].push(Lock(amount, unlockTime));

emit TokensLocked(msg.sender, amount, unlockTime);
}

/// @notice Withdraw tokens if the lock time has expired
/// @param index Index of the user's lock to withdraw
function withdrawTokens(uint256 index) external {
require(index < userLocks[msg.sender].length, "Invalid lock index");

Lock memory lock = userLocks[msg.sender][index];
require(block.timestamp >= lock.unlockTime, "Tokens are still locked");

// Remove the lock entry by swapping with the last and popping
uint256 amount = lock.amount;
uint256 lastIndex = userLocks[msg.sender].length - 1;
if (index != lastIndex) {
userLocks[msg.sender][index] = userLocks[msg.sender][lastIndex];
}
userLocks[msg.sender].pop();

bool success = wldToken.transfer(msg.sender, amount);
require(success, "Token transfer failed");

emit TokensWithdrawn(msg.sender, amount);
}

/// @notice Get all lock records for a user
function getLocks(address user) external view returns (Lock[] memory) {
return userLocks[user];
}
}
144 changes: 144 additions & 0 deletions test/TimeLockedVault.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/TimeLockedVault.sol";

contract MockERC20 is IERC20 {
string public name = "Mock Token";
string public symbol = "MTKN";
uint8 public decimals = 18;
uint256 public totalSupply;

mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowance;

function mint(address to, uint256 amount) external {
balances[to] += amount;
totalSupply += amount;
}

function transfer(address recipient, uint256 amount) external override returns (bool) {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[recipient] += amount;
return true;
}

function transferFrom(address sender, address recipient, uint256 amount) external override returns (bool) {
require(balances[sender] >= amount, "Insufficient balance");
require(allowance[sender][msg.sender] >= amount, "Insufficient allowance");
balances[sender] -= amount;
balances[recipient] += amount;
allowance[sender][msg.sender] -= amount;
return true;
}

function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}

function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
}

contract TimeLockedVaultTest is Test {
TimeLockedVault vault;
MockERC20 token;
address user = address(0xBEEF);
uint256 initialBalance = 1000 ether;

function setUp() public {
token = new MockERC20();
vault = new TimeLockedVault(address(token));
token.mint(user, initialBalance);
vm.prank(user);
token.approve(address(vault), initialBalance);
}

function testLockTokensSuccess() public {
vm.prank(user);
vault.lockTokens(100 ether, 1 days);

TimeLockedVault.Lock[] memory locks = vault.getLocks(user);
assertEq(locks.length, 1);
assertEq(locks[0].amount, 100 ether);
}

function testWithdrawFailsIfStillLocked() public {
vm.prank(user);
vault.lockTokens(100 ether, 1 days);

vm.prank(user);
vm.expectRevert("Tokens are still locked");
vault.withdrawTokens(0);
}

function testWithdrawSucceedsAfterUnlock() public {
vm.prank(user);
vault.lockTokens(100 ether, 1 days);

// Fast forward time
vm.warp(block.timestamp + 2 days);

vm.prank(user);
vault.withdrawTokens(0);

assertEq(token.balanceOf(user), initialBalance);
}

function testLockFailsIfAmountZero() public {
vm.prank(user);
vm.expectRevert("Amount must be greater than zero");
vault.lockTokens(0, 1 days);
}

function testLockFailsIfDurationZero() public {
vm.prank(user);
vm.expectRevert("Lock duration must be greater than zero");
vault.lockTokens(100 ether, 0);
}

function testWithdrawFailsWithInvalidIndex() public {
vm.prank(user);
vm.expectRevert("Invalid lock index");
vault.withdrawTokens(0);
}

// --- FUZZ TESTS ---

function testFuzz_LockAndWithdraw(uint256 amount, uint256 duration) public {
amount = bound(amount, 1 ether, 100 ether); // Limit fuzz range
duration = bound(duration, 1, 30 days);

token.mint(address(this), amount);
token.approve(address(vault), amount);
vault.lockTokens(amount, duration);

vm.warp(block.timestamp + duration + 1);

vault.withdrawTokens(0);
assertEq(token.balanceOf(address(this)), amount);
}

function testFuzz_LockMultipleEntries(uint256 a1, uint256 a2) public {
a1 = bound(a1, 1 ether, 50 ether);
a2 = bound(a2, 1 ether, 50 ether);

token.mint(user, a1 + a2);
vm.prank(user);
token.approve(address(vault), a1 + a2);

vm.startPrank(user);
vault.lockTokens(a1, 1 days);
vault.lockTokens(a2, 2 days);
vm.stopPrank();

TimeLockedVault.Lock[] memory locks = vault.getLocks(user);
assertEq(locks.length, 2);
assertEq(locks[0].amount, a1);
assertEq(locks[1].amount, a2);
}
}