diff --git a/sessions/timelock/.gitignore b/sessions/timelock/.gitignore new file mode 100644 index 00000000..85198aaa --- /dev/null +++ b/sessions/timelock/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/sessions/timelock/.gitmodules b/sessions/timelock/.gitmodules new file mode 100644 index 00000000..888d42dc --- /dev/null +++ b/sessions/timelock/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/sessions/timelock/README.md b/sessions/timelock/README.md new file mode 100644 index 00000000..8817d6ab --- /dev/null +++ b/sessions/timelock/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/sessions/timelock/foundry.lock b/sessions/timelock/foundry.lock new file mode 100644 index 00000000..bc06b89b --- /dev/null +++ b/sessions/timelock/foundry.lock @@ -0,0 +1,8 @@ +{ + "lib/forge-std": { + "tag": { + "name": "v1.15.0", + "rev": "0844d7e1fc5e60d77b68e469bff60265f236c398" + } + } +} \ No newline at end of file diff --git a/sessions/timelock/foundry.toml b/sessions/timelock/foundry.toml new file mode 100644 index 00000000..7b6b3f60 --- /dev/null +++ b/sessions/timelock/foundry.toml @@ -0,0 +1,16 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +remappings = [ + "@openzeppelin/=lib/openzeppelin-contracts/", + "@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/", +] + +eth_rpc_url = "https://ethereum-sepolia-rpc.publicnode.com" +# fork-block-number = 10382697 10382882 +# fork-block-number = 10382882 +# fork-block-number = 10279235 +fork-block-number = 10383134 diff --git a/sessions/timelock/mappings.txt b/sessions/timelock/mappings.txt new file mode 100644 index 00000000..feaba2dd --- /dev/null +++ b/sessions/timelock/mappings.txt @@ -0,0 +1 @@ +forge-std/=lib/forge-std/src/ diff --git a/sessions/timelock/src/CounterV3.sol b/sessions/timelock/src/CounterV3.sol new file mode 100644 index 00000000..269dfb0c --- /dev/null +++ b/sessions/timelock/src/CounterV3.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract CounterV3 { + uint256 public number; + address public owner; + + mapping(address => bool) public approvedCallers; + mapping(address => bool) public pendingApproval; + + event ApprovalRequested(address indexed caller); + event ApprovalGranted(address indexed caller); + event ApprovalRevoked(address indexed caller); + + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Not owner"); + _; + } + + modifier onlyAuthorized() { + if (msg.sender != owner) { + require(approvedCallers[msg.sender], "Owner consent required"); + } + _; + } + + function requestPrivilege() external { + require(msg.sender != owner, "Owner does not need consent"); + + pendingApproval[msg.sender] = true; + emit ApprovalRequested(msg.sender); + } + + function grantPrivilege(address caller) external onlyOwner { + require(pendingApproval[caller], "No pending request"); + + approvedCallers[caller] = true; + pendingApproval[caller] = false; + emit ApprovalGranted(caller); + } + + function revokePrivilege(address caller) external onlyOwner { + require(approvedCallers[caller], "Caller not approved"); + + approvedCallers[caller] = false; + emit ApprovalRevoked(caller); + } + + function setNumber(uint256 newNumber) public onlyAuthorized { + number = newNumber; + } + + function increment() public onlyAuthorized { + number++; + } +} diff --git a/sessions/timelock/src/TimelockV3.sol b/sessions/timelock/src/TimelockV3.sol new file mode 100644 index 00000000..655c8cb6 --- /dev/null +++ b/sessions/timelock/src/TimelockV3.sol @@ -0,0 +1,239 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; +import {IErc20} from "./interfaces/IERC20.sol"; +import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; +import {console} from "forge-std/console.sol"; + +contract TimeLockV3 { + IErc20 public immutable token; + address public owner = 0xCe4F29b6A3955Fa50b46DFdFAe2F6352F16A77BB; + address public pendingOwner; + AggregatorV3Interface internal priceFeed; + + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + struct Vault { + uint256 balance; + uint256 tokenBalance; + uint256 unlockTime; + bool active; + } + + constructor(address _token) { + token = IErc20(_token); + owner = msg.sender; + priceFeed = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306); // ETH-USD sepolia + } + + modifier onlyOwner() { + require(msg.sender == owner, "Not owner"); + _; + } + + mapping(address => Vault[]) private vaults; + + event Deposited(address indexed user, uint256 vaultId, uint256 amount, uint256 unlockTime); + event Withdrawn(address indexed user, uint256 vaultId, uint256 amount); + event EmergencyWithdrawn(address indexed owner, uint256 amount); + + //INTERNAL FUNCTIONS + function _depositRatio(uint256 _totalDeposit) internal pure returns (uint256 _tokenAmount) { + _tokenAmount = _totalDeposit * 10; // Token Ratio + } + + function deposit(uint256 _unlockTime) external payable { + uint256 amount = ethToUSD(msg.value); + require(amount >= 100, "you must deposit 100 USD worth of ETH"); + require(_unlockTime > block.timestamp, "Unlock time must be in the future"); + // Create new vault + vaults[msg.sender].push(Vault({balance: msg.value, unlockTime: _unlockTime, tokenBalance: 1, active: true})); + + uint256 vaultId = vaults[msg.sender].length - 1; + emit Deposited(msg.sender, vaultId, msg.value, _unlockTime); + } + + function withdraw(uint256 _vaultId) external { + require(_vaultId < vaults[msg.sender].length, "Invalid vault ID"); + + Vault storage userVault = vaults[msg.sender][_vaultId]; + require(userVault.active, "Vault is not active"); + require(userVault.balance > 0, "Vault has zero balance"); + require(block.timestamp >= userVault.unlockTime, "Funds are still locked"); + + uint256 amount = userVault.balance; + uint256 tokenAmount = userVault.tokenBalance; + + // Mark vault as inactive and clear balance + userVault.balance = 0; + userVault.tokenBalance = 0; + userVault.active = false; + + require(token.transferFrom(msg.sender, address(this), tokenAmount), "Token transfer failed"); + + (bool success,) = payable(msg.sender).call{value: amount}(""); + require(success, "Transfer failed"); + + emit Withdrawn(msg.sender, _vaultId, amount); + } + + function withdrawAll() external returns (uint256) { + uint256 totalWithdrawn = 0; + uint256 totalTokens = 0; + Vault[] storage userVaults = vaults[msg.sender]; + + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + uint256 amount = userVaults[i].balance; + uint256 tokenAmount = userVaults[i].tokenBalance; + + userVaults[i].balance = 0; + userVaults[i].tokenBalance = 0; + userVaults[i].active = false; + + totalWithdrawn += amount; + totalTokens += tokenAmount; + emit Withdrawn(msg.sender, i, amount); + } + } + + require(totalWithdrawn > 0, "No unlocked funds available"); + + require(token.transfer(msg.sender, totalTokens), "Token transfer failed"); + (bool success,) = payable(msg.sender).call{value: totalWithdrawn}(""); + require(success, "Transfer failed"); + + return totalWithdrawn; + } + + function emergencyWithdraw() external onlyOwner returns (uint256 amount) { + amount = address(this).balance; + require(amount > 0, "No funds available"); + + (bool success,) = payable(owner).call{value: amount}(""); + if (!success) revert(); + + emit EmergencyWithdrawn(owner, amount); + } + + function getVaultCount(address _user) external view returns (uint256) { + return vaults[_user].length; + } + + function getVault(address _user, uint256 _vaultId) + external + view + returns (uint256 balance, uint256 tokenBalance, uint256 unlockTime, bool active, bool isUnlocked) + { + require(_vaultId < vaults[_user].length, "Invalid vault ID"); + + Vault storage vault = vaults[_user][_vaultId]; + return (vault.balance, vault.tokenBalance, vault.unlockTime, vault.active, block.timestamp >= vault.unlockTime); + } + + function getAllVaults(address _user) external view returns (Vault[] memory) { + return vaults[_user]; + } + + function getActiveVaults(address _user) + external + view + returns ( + uint256[] memory activeVaults, + uint256[] memory balances, + uint256[] memory tokenBalances, + uint256[] memory unlockTimes + ) + { + Vault[] storage userVaults = vaults[_user]; + + // Count active vaults + uint256 activeCount = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeCount++; + } + } + + // Create arrays + activeVaults = new uint256[](activeCount); + balances = new uint256[](activeCount); + tokenBalances = new uint256[](activeCount); + unlockTimes = new uint256[](activeCount); + + // Populate arrays + uint256 index = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeVaults[index] = i; + balances[index] = userVaults[i].balance; + tokenBalances[index] = userVaults[i].tokenBalance; + unlockTimes[index] = userVaults[i].unlockTime; + index++; + } + } + + return (activeVaults, balances, tokenBalances, unlockTimes); + } + + function getTotalBalance(address _user) external view returns (uint256 total) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active) { + total += userVaults[i].balance; + } + } + return total; + } + + function getUnlockedBalance(address _user) external view returns (uint256 unlocked) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + unlocked += userVaults[i].balance; + } + } + return unlocked; + } + + function transferOwnership(address newOwner) external onlyOwner { + require(newOwner != address(0), "New owner is zero address"); + require(newOwner != owner, "New owner is current owner"); + pendingOwner = newOwner; + emit OwnershipTransferStarted(owner, newOwner); + } + + function acceptOwnership() external { + require(msg.sender == pendingOwner, "Not pending owner"); + address previousOwner = owner; + owner = msg.sender; + pendingOwner = address(0); + emit OwnershipTransferred(previousOwner, msg.sender); + } + + /** + * Returns the latest answer. + */ + function getETHUSDPrice() public view returns (int256) { + (, + /* uint80 roundId */ + int256 answer,, + /*uint256 startedAt*/ + , + /*uint256 updatedAt*/ + /*uint80 answeredInRound*/ + ) = priceFeed.latestRoundData(); + uint8 decimals = getDecimals(); + int256 result = answer / int256(10 ** decimals); + return result; + } + + function getDecimals() public view returns (uint8) { + uint8 decimals = priceFeed.decimals(); + return decimals; + } + + function ethToUSD(uint256 amount) public view returns (uint256 returnedValue) { + + } +} diff --git a/sessions/timelock/src/Token.sol b/sessions/timelock/src/Token.sol new file mode 100644 index 00000000..50f223e5 --- /dev/null +++ b/sessions/timelock/src/Token.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.6.0 +pragma solidity ^0.8.27; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; + +contract JasonToken is ERC20, ERC20Burnable, Ownable { + constructor(address recipient, address initialOwner) ERC20("JasonToken", "JKB") Ownable(initialOwner) { + _mint(recipient, 1000 * 10 ** decimals()); + } + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } +} diff --git a/sessions/timelock/src/Vault.sol b/sessions/timelock/src/Vault.sol new file mode 100644 index 00000000..4500dd36 --- /dev/null +++ b/sessions/timelock/src/Vault.sol @@ -0,0 +1,144 @@ +//SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +contract TimeLock { + struct Vault { + uint256 balance; + uint256 unlockTime; + bool active; + } + + mapping(address => Vault[]) private vaults; + + event Deposited(address indexed user, uint256 vaultId, uint256 amount, uint256 unlockTime); + event Withdrawn(address indexed user, uint256 vaultId, uint256 amount); + + function deposit(uint256 _unlockTime) external payable returns (uint256) { + require(msg.value > 0, "Deposit must be greater than zero"); + require(_unlockTime > block.timestamp, "Unlock time must be in the future"); + + // Create new vault + vaults[msg.sender].push(Vault({balance: msg.value, unlockTime: _unlockTime, active: true})); + + uint256 vaultId = vaults[msg.sender].length - 1; + emit Deposited(msg.sender, vaultId, msg.value, _unlockTime); + + return vaultId; + } + + function withdraw(uint256 _vaultId) external { + require(_vaultId < vaults[msg.sender].length, "Invalid vault ID"); + + Vault storage userVault = vaults[msg.sender][_vaultId]; + require(userVault.active, "Vault is not active"); + require(userVault.balance > 0, "Vault has zero balance"); + require(block.timestamp >= userVault.unlockTime, "Funds are still locked"); + + uint256 amount = userVault.balance; + + // Mark vault as inactive and clear balance + userVault.balance = 0; + userVault.active = false; + + (bool success,) = payable(msg.sender).call{value: amount}(""); + require(success, "Transfer failed"); + + emit Withdrawn(msg.sender, _vaultId, amount); + } + + function withdrawAll() external returns (uint256) { + uint256 totalWithdrawn = 0; + Vault[] storage userVaults = vaults[msg.sender]; + + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + uint256 amount = userVaults[i].balance; + userVaults[i].balance = 0; + userVaults[i].active = false; + + totalWithdrawn += amount; + emit Withdrawn(msg.sender, i, amount); + } + } + + require(totalWithdrawn > 0, "No unlocked funds available"); + + (bool success,) = payable(msg.sender).call{value: totalWithdrawn}(""); + require(success, "Transfer failed"); + + return totalWithdrawn; + } + + function getVaultCount(address _user) external view returns (uint256) { + return vaults[_user].length; + } + + function getVault(address _user, uint256 _vaultId) + external + view + returns (uint256 balance, uint256 unlockTime, bool active, bool isUnlocked) + { + require(_vaultId < vaults[_user].length, "Invalid vault ID"); + + Vault storage vault = vaults[_user][_vaultId]; + return (vault.balance, vault.unlockTime, vault.active, block.timestamp >= vault.unlockTime); + } + + function getAllVaults(address _user) external view returns (Vault[] memory) { + return vaults[_user]; + } + + function getActiveVaults(address _user) + external + view + returns (uint256[] memory activeVaults, uint256[] memory balances, uint256[] memory unlockTimes) + { + Vault[] storage userVaults = vaults[_user]; + + // Count active vaults + uint256 activeCount = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeCount++; + } + } + + // Create arrays + activeVaults = new uint256[](activeCount); + balances = new uint256[](activeCount); + unlockTimes = new uint256[](activeCount); + + // Populate arrays + uint256 index = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeVaults[index] = i; + balances[index] = userVaults[i].balance; + unlockTimes[index] = userVaults[i].unlockTime; + index++; + } + } + + return (activeVaults, balances, unlockTimes); + } + + function getTotalBalance(address _user) external view returns (uint256 total) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active) { + total += userVaults[i].balance; + } + } + return total; + } + + function getUnlockedBalance(address _user) external view returns (uint256 unlocked) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + unlocked += userVaults[i].balance; + } + } + return unlocked; + } +} diff --git a/sessions/timelock/src/VaultV2.sol b/sessions/timelock/src/VaultV2.sol new file mode 100644 index 00000000..41a6c350 --- /dev/null +++ b/sessions/timelock/src/VaultV2.sol @@ -0,0 +1,217 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; +import {IErc20} from "./interfaces/IERC20.sol"; + +contract TimeLockV2 { + IErc20 public immutable token; + address public owner; + address public pendingOwner; + + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + struct Vault { + uint256 balance; + uint256 tokenBalance; + uint256 unlockTime; + bool active; + } + + constructor(address _token) { + token = IErc20(_token); + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Not owner"); + _; + } + + function transferOwnership(address newOwner) external onlyOwner { + require(newOwner != address(0), "New owner is zero address"); + require(newOwner != owner, "New owner is current owner"); + pendingOwner = newOwner; + emit OwnershipTransferStarted(owner, newOwner); + } + + function acceptOwnership() external { + require(msg.sender == pendingOwner, "Not pending owner"); + address previousOwner = owner; + owner = msg.sender; + pendingOwner = address(0); + emit OwnershipTransferred(previousOwner, msg.sender); + } + + mapping(address => Vault[]) private vaults; + + event Deposited(address indexed user, uint256 vaultId, uint256 amount, uint256 unlockTime); + event Withdrawn(address indexed user, uint256 vaultId, uint256 amount); + event EmergencyWithdrawn(address indexed owner, uint256 amount); + + //INTERNAL FUNCTIONS + function _depositRatio(uint256 _totalDeposit) internal pure returns (uint256 _tokenAmount) { + _tokenAmount = _totalDeposit * 10; // Token Ratio + } + + function deposit(uint256 _unlockTime) external payable returns (uint256) { + require(msg.value > 0, "Deposit must be greater than zero"); + require(_unlockTime > block.timestamp, "Unlock time must be in the future"); + + uint256 tokenBal = _depositRatio(msg.value); + require(token.transfer(msg.sender, tokenBal), "Token transfer failed"); + + // Create new vault + vaults[msg.sender].push( + Vault({balance: msg.value, unlockTime: _unlockTime, tokenBalance: tokenBal, active: true}) + ); + + uint256 vaultId = vaults[msg.sender].length - 1; + emit Deposited(msg.sender, vaultId, msg.value, _unlockTime); + + return vaultId; + } + + function withdraw(uint256 _vaultId) external { + require(_vaultId < vaults[msg.sender].length, "Invalid vault ID"); + + Vault storage userVault = vaults[msg.sender][_vaultId]; + require(userVault.active, "Vault is not active"); + require(userVault.balance > 0, "Vault has zero balance"); + require(block.timestamp >= userVault.unlockTime, "Funds are still locked"); + + uint256 amount = userVault.balance; + uint256 tokenAmount = userVault.tokenBalance; + + // Mark vault as inactive and clear balance + userVault.balance = 0; + userVault.tokenBalance = 0; + userVault.active = false; + + require(token.transferFrom(msg.sender, address(this), tokenAmount), "Token transfer failed"); + + (bool success,) = payable(msg.sender).call{value: amount}(""); + require(success, "Transfer failed"); + + emit Withdrawn(msg.sender, _vaultId, amount); + } + + function withdrawAll() external returns (uint256) { + uint256 totalWithdrawn = 0; + uint256 totalTokens = 0; + Vault[] storage userVaults = vaults[msg.sender]; + + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + uint256 amount = userVaults[i].balance; + uint256 tokenAmount = userVaults[i].tokenBalance; + + userVaults[i].balance = 0; + userVaults[i].tokenBalance = 0; + userVaults[i].active = false; + + totalWithdrawn += amount; + totalTokens += tokenAmount; + emit Withdrawn(msg.sender, i, amount); + } + } + + require(totalWithdrawn > 0, "No unlocked funds available"); + + require(token.transfer(msg.sender, totalTokens), "Token transfer failed"); + + (bool success,) = payable(msg.sender).call{value: totalWithdrawn}(""); + require(success, "Transfer failed"); + + return totalWithdrawn; + } + + function emergencyWithdraw() external onlyOwner returns (uint256 amount) { + amount = address(this).balance; + require(amount > 0, "No funds available"); + + (bool success,) = payable(owner).call{value: amount}(""); + if (!success) revert(); + + emit EmergencyWithdrawn(owner, amount); + } + + function getVaultCount(address _user) external view returns (uint256) { + return vaults[_user].length; + } + + function getVault(address _user, uint256 _vaultId) + external + view + returns (uint256 balance, uint256 tokenBalance, uint256 unlockTime, bool active, bool isUnlocked) + { + require(_vaultId < vaults[_user].length, "Invalid vault ID"); + + Vault storage vault = vaults[_user][_vaultId]; + return (vault.balance, vault.tokenBalance, vault.unlockTime, vault.active, block.timestamp >= vault.unlockTime); + } + + function getAllVaults(address _user) external view returns (Vault[] memory) { + return vaults[_user]; + } + + function getActiveVaults(address _user) + external + view + returns ( + uint256[] memory activeVaults, + uint256[] memory balances, + uint256[] memory tokenBalances, + uint256[] memory unlockTimes + ) + { + Vault[] storage userVaults = vaults[_user]; + + // Count active vaults + uint256 activeCount = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeCount++; + } + } + + // Create arrays + activeVaults = new uint256[](activeCount); + balances = new uint256[](activeCount); + tokenBalances = new uint256[](activeCount); + unlockTimes = new uint256[](activeCount); + + // Populate arrays + uint256 index = 0; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0) { + activeVaults[index] = i; + balances[index] = userVaults[i].balance; + tokenBalances[index] = userVaults[i].tokenBalance; + unlockTimes[index] = userVaults[i].unlockTime; + index++; + } + } + + return (activeVaults, balances, tokenBalances, unlockTimes); + } + + function getTotalBalance(address _user) external view returns (uint256 total) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active) { + total += userVaults[i].balance; + } + } + return total; + } + + function getUnlockedBalance(address _user) external view returns (uint256 unlocked) { + Vault[] storage userVaults = vaults[_user]; + for (uint256 i = 0; i < userVaults.length; i++) { + if (userVaults[i].active && userVaults[i].balance > 0 && block.timestamp >= userVaults[i].unlockTime) { + unlocked += userVaults[i].balance; + } + } + return unlocked; + } +} diff --git a/sessions/timelock/src/interfaces/IERC20.sol b/sessions/timelock/src/interfaces/IERC20.sol new file mode 100644 index 00000000..92e32621 --- /dev/null +++ b/sessions/timelock/src/interfaces/IERC20.sol @@ -0,0 +1,19 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IErc20 is IERC20 { + function decimals() external view returns (uint8); + + function totalSupply() external view returns (uint256); + + function balanceOf(address _owner) external view returns (uint256 balance); + + function transferFrom(address _from, address _to, uint256 _value) external returns (bool success); + + function mint(address to, uint256 amount) external; + + function burn(uint256 amount) external; + + function burnFrom(address account, uint256 amount) external; +} diff --git a/sessions/timelock/test/CounterV3.t.sol b/sessions/timelock/test/CounterV3.t.sol new file mode 100644 index 00000000..e3384801 --- /dev/null +++ b/sessions/timelock/test/CounterV3.t.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {CounterV3} from "../src/CounterV3.sol"; + +contract CounterV3Test is Test { + CounterV3 public counterv3; + address public addr1; + address public addr2; + + function setUp() public { + counterv3 = new CounterV3(); + addr1 = makeAddr("addr1"); + addr2 = makeAddr("addr2"); + } + + function test_setNumber() public { + counterv3.setNumber(3); + assertEq(counterv3.number(), 3); + } + + function test_increment() public { + counterv3.increment(); + assertEq(counterv3.number(), 1); + } + + function test_revert_increment_withoutPrivilege() public { + vm.prank(addr1); + vm.expectRevert("Owner consent required"); + counterv3.increment(); + + assertEq(counterv3.number(), 0); + } + + function test_revert_setNumber_withoutPrivilege() public { + vm.prank(addr1); + vm.expectRevert("Owner consent required"); + counterv3.setNumber(7); + + assertEq(counterv3.number(), 0); + } + + function test_grantPrivilege() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + assertTrue(counterv3.pendingApproval(addr1)); + + counterv3.grantPrivilege(addr1); + + assertTrue(counterv3.approvedCallers(addr1)); + assertFalse(counterv3.pendingApproval(addr1)); + } + + function test_approvedCaller_increment() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + counterv3.grantPrivilege(addr1); + + vm.prank(addr1); + counterv3.increment(); + + assertEq(counterv3.number(), 1); + } + + function test_revert_grantPrivilege_withoutRequest() public { + vm.expectRevert("No pending request"); + counterv3.grantPrivilege(addr1); + } + + function test_revokePrivilege() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + counterv3.grantPrivilege(addr1); + counterv3.revokePrivilege(addr1); + + assertFalse(counterv3.approvedCallers(addr1)); + } + + function test_revert_revokedCaller_setNumber() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + counterv3.grantPrivilege(addr1); + counterv3.revokePrivilege(addr1); + + vm.prank(addr1); + vm.expectRevert("Owner consent required"); + counterv3.setNumber(7); + } + + function test_revert_grantPrivilege_notOwner() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + vm.prank(addr2); + vm.expectRevert("Not owner"); + counterv3.grantPrivilege(addr1); + } + + function test_revert_revokePrivilege_notOwner() public { + vm.prank(addr1); + counterv3.requestPrivilege(); + + counterv3.grantPrivilege(addr1); + + vm.prank(addr2); + vm.expectRevert("Not owner"); + counterv3.revokePrivilege(addr1); + } + + function test_revert_revokePrivilege_withoutApproval() public { + vm.expectRevert("Caller not approved"); + counterv3.revokePrivilege(addr1); + } + + function test_revert_requestPrivilege_owner() public { + vm.expectRevert("Owner does not need consent"); + counterv3.requestPrivilege(); + } +} diff --git a/sessions/timelock/test/TimelockV3.t.sol b/sessions/timelock/test/TimelockV3.t.sol new file mode 100644 index 00000000..23ac0dfa --- /dev/null +++ b/sessions/timelock/test/TimelockV3.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {TimeLockV3} from "../src/TimelockV3.sol"; +import {console} from "forge-std/console.sol"; +import {IErc20} from "../src/interfaces/IERC20.sol"; + +contract TimelockV3Test is Test { + IErc20 public token; + TimeLockV3 public timelockV3; + address public owner = 0xCe4F29b6A3955Fa50b46DFdFAe2F6352F16A77BB; + address public addr1; + address public addr2; + + function setUp() public { + vm.startPrank(owner); + timelockV3 = new TimeLockV3(0x6434193a115151156d038fB5B61747Cc5b07511F); + addr1 = makeAddr("addr1"); + addr2 = makeAddr("addr2"); + token = IErc20(0x6434193a115151156d038fB5B61747Cc5b07511F); + vm.stopPrank(); + } + + function fromWei(uint256 amount) public pure returns (uint256) { + return (amount / 10 ** 9); + } + + function test_deployment() public view { + assertEq(timelockV3.owner(), owner); + int256 price = timelockV3.getETHUSDPrice(); + console.log("ETH price here", price); + + uint8 decimalResult = timelockV3.getDecimals(); + console.log("decimals here", decimalResult); + + uint256 ownerEthBefore = token.balanceOf(owner); + console.log("token decimals here____", token.decimals()); + assertEq(ownerEthBefore, (2000 * 10 ** 6)); + } + + function test_deposit() public { + vm.startPrank(owner); + uint256 unlockTime = block.timestamp + 1 days; + // uint256 ownerEthBal1 = address(owner).balance; + uint256 ownerEthBal1 = owner.balance; + // console.log("owner balance 1___-", fromWei(ownerEthBal1)); + timelockV3.deposit{value: 0.01 ether}(unlockTime); + + // uint256 vaultEthBefore = address(vault).balance; + // uint256 userTokenBefore = token.balanceOf(user); + // uint256 vaultTokenBefore = token.balanceOf(address(vault)); + + // vm.prank(user); + // vault.deposit{value: 1 ether}(unlockTime); + + // assertEq(user.balance, userEthBefore - 1 ether); + // assertEq(address(vault).balance, vaultEthBefore + 1 ether); + // assertEq(token.balanceOf(user), userTokenBefore + 10 ether); + // assertEq(token.balanceOf(address(vault)), vaultTokenBefore - 10 ether); + // assertEq(vault.getVaultCount(user), 1); + + // (uint256 balance, uint256 tokenBalance, uint256 savedUnlockTime, bool active, bool isUnlocked) = + // vault.getVault(user, 0); + + // assertEq(balance, 1 ether); + // assertEq(tokenBalance, 10 ether); + // assertEq(savedUnlockTime, unlockTime); + // assertTrue(active); + // assertFalse(isUnlocked); + } +} diff --git a/sessions/timelock/test/Vault.t.sol b/sessions/timelock/test/Vault.t.sol new file mode 100644 index 00000000..bdf2655b --- /dev/null +++ b/sessions/timelock/test/Vault.t.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {TimeLock} from "../src/Vault.sol"; + +contract VaultTest is Test { + TimeLock public vault; + address public user; + address public attacker; + + function setUp() public { + vault = new TimeLock(); + user = makeAddr("user"); + attacker = makeAddr("attacker"); + + assertEq(address(vault).balance, 0); + + vm.deal(user, 10 ether); + vm.deal(attacker, 10 ether); + } + + function test_deposit() public { + uint256 unlockTime = block.timestamp + 1 days; + uint256 userBalanceBefore = user.balance; + uint256 vaultBalanceBefore = address(vault).balance; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + assertEq(user.balance, userBalanceBefore - 1 ether); + assertEq(address(vault).balance, vaultBalanceBefore + 1 ether); + assertEq(vault.getVaultCount(user), 1); + + (uint256 balance, uint256 savedUnlockTime, bool active, bool isUnlocked) = vault.getVault(user, 0); + assertEq(balance, 1 ether); + assertEq(savedUnlockTime, unlockTime); + assertTrue(active); + assertFalse(isUnlocked); + } + + function test_revert_deposit_withZeroValue() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vm.expectRevert("Deposit must be greater than zero"); + vault.deposit{value: 0}(unlockTime); + } + + function test_revert_deposit_withNonFutureUnlockTime() public { + vm.prank(user); + vm.expectRevert("Unlock time must be in the future"); + vault.deposit{value: 1 ether}(block.timestamp); + } + + function test_revert_getVault_withInvalidVaultId() public { + vm.expectRevert("Invalid vault ID"); + vault.getVault(user, 0); + } + + function test_getAllVaults() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + TimeLock.Vault[] memory vaults = vault.getAllVaults(user); + + assertEq(vaults.length, 2); + assertEq(vaults[0].balance, 1 ether); + assertEq(vaults[0].unlockTime, firstUnlockTime); + assertTrue(vaults[0].active); + assertEq(vaults[1].balance, 2 ether); + assertEq(vaults[1].unlockTime, secondUnlockTime); + assertTrue(vaults[1].active); + } + + function test_getActiveVaults() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + vm.warp(firstUnlockTime); + + vm.prank(user); + vault.withdraw(0); + + (uint256[] memory activeVaults, uint256[] memory balances, uint256[] memory unlockTimes) = + vault.getActiveVaults(user); + + assertEq(activeVaults.length, 1); + assertEq(balances.length, 1); + assertEq(unlockTimes.length, 1); + assertEq(activeVaults[0], 1); + assertEq(balances[0], 2 ether); + assertEq(unlockTimes[0], secondUnlockTime); + } + + function test_getTotalBalance() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + assertEq(vault.getTotalBalance(user), 3 ether); + + vm.warp(firstUnlockTime); + + vm.prank(user); + vault.withdraw(0); + + assertEq(vault.getTotalBalance(user), 2 ether); + } + + function test_getUnlockedBalance() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + assertEq(vault.getUnlockedBalance(user), 0); + + vm.warp(firstUnlockTime); + assertEq(vault.getUnlockedBalance(user), 1 ether); + + vm.warp(secondUnlockTime); + assertEq(vault.getUnlockedBalance(user), 3 ether); + } + + function test_withdraw() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.warp(unlockTime); + + uint256 userBalanceBefore = user.balance; + uint256 vaultBalanceBefore = address(vault).balance; + + vm.prank(user); + vault.withdraw(0); + + assertEq(user.balance, userBalanceBefore + 1 ether); + assertEq(address(vault).balance, vaultBalanceBefore - 1 ether); + + (uint256 balance,, bool active, bool isUnlocked) = vault.getVault(user, 0); + assertEq(balance, 0); + assertFalse(active); + assertTrue(isUnlocked); + } + + function test_revert_withdraw_whenStillLocked() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(user); + vm.expectRevert("Funds are still locked"); + vault.withdraw(0); + } + + function test_revert_withdraw_fromAnotherUsersVault() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(attacker); + vm.expectRevert("Invalid vault ID"); + vault.withdraw(0); + } + + function test_withdrawAll() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + vm.warp(secondUnlockTime); + + uint256 userBalanceBefore = user.balance; + uint256 vaultBalanceBefore = address(vault).balance; + + vm.prank(user); + uint256 amount = vault.withdrawAll(); + + assertEq(amount, 3 ether); + assertEq(user.balance, userBalanceBefore + 3 ether); + assertEq(address(vault).balance, vaultBalanceBefore - 3 ether); + assertEq(vault.getTotalBalance(user), 0); + } + + function test_revert_withdrawAll_fromUserWithoutUnlockedFunds() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(attacker); + vm.expectRevert("No unlocked funds available"); + vault.withdrawAll(); + } +} diff --git a/sessions/timelock/test/VaultV2.t.sol b/sessions/timelock/test/VaultV2.t.sol new file mode 100644 index 00000000..16f36564 --- /dev/null +++ b/sessions/timelock/test/VaultV2.t.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; +import {TimeLockV2} from "../src/VaultV2.sol"; +import {JasonToken} from "../src/Token.sol"; + +contract VaultV2Test is Test { + TimeLockV2 public vault; + JasonToken public token; + address public owner; + address public user; + address public attacker; + address public newOwner; + + uint256 oneDay = 86400; + + function setUp() public { + owner = makeAddr("owner"); + vm.startPrank(owner); + token = new JasonToken(address(this), address(this)); + vault = new TimeLockV2(address(token)); + vm.stopPrank(); + user = makeAddr("user"); + attacker = makeAddr("attacker"); + newOwner = makeAddr("newOwner"); + + token.transfer(address(vault), 500 * 10 ** token.decimals()); + + vm.deal(user, 10 ether); + vm.deal(attacker, 10 ether); + } + + function test_ownerIsSetOnDeployment() public view { + assertEq(vault.owner(), owner); + console.log("Owner: ", vault.owner()); + console.log("This: ", owner); + } + + function test_emergencyWithdraw() public { + uint256 unlockTime = block.timestamp + 1 days; + uint256 ownerBalanceBefore = owner.balance; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(owner); + uint256 withdrawnAmount = vault.emergencyWithdraw(); + + assertEq(withdrawnAmount, 1 ether); + assertEq(address(vault).balance, 0); + assertEq(owner.balance, ownerBalanceBefore + 1 ether); + } + + function test_revert_emergencyWithdraw_notOwner() public { + uint256 unlockTime = block.timestamp + oneDay; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(attacker); + vm.expectRevert(); + vault.emergencyWithdraw(); + } + + function test_transferOwnership_and_acceptOwnership() public { + vm.prank(owner); + vault.transferOwnership(newOwner); + + assertEq(vault.pendingOwner(), newOwner); + + vm.prank(newOwner); + vault.acceptOwnership(); + + assertEq(vault.owner(), newOwner); + assertEq(vault.pendingOwner(), address(0)); + } + + function test_revert_transferOwnership_notOwner() public { + vm.prank(attacker); + vm.expectRevert("Not owner"); + vault.transferOwnership(newOwner); + } + + function test_revert_acceptOwnership_notPendingOwner() public { + vm.prank(owner); + vault.transferOwnership(newOwner); + + vm.prank(attacker); + vm.expectRevert("Not pending owner"); + vault.acceptOwnership(); + } + + function test_emergencyWithdraw_accessAfterOwnershipTransfer() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(owner); + vault.transferOwnership(newOwner); + + vm.prank(newOwner); + vault.acceptOwnership(); + + vm.prank(owner); + vm.expectRevert("Not owner"); + vault.emergencyWithdraw(); + + uint256 newOwnerBalanceBefore = newOwner.balance; + vm.prank(newOwner); + uint256 withdrawnAmount = vault.emergencyWithdraw(); + + assertEq(withdrawnAmount, 1 ether); + assertEq(address(vault).balance, 0); + assertEq(newOwner.balance, newOwnerBalanceBefore + 1 ether); + } + + function test_deposit() public { + uint256 unlockTime = block.timestamp + 1 days; + uint256 userEthBefore = user.balance; + uint256 vaultEthBefore = address(vault).balance; + uint256 userTokenBefore = token.balanceOf(user); + uint256 vaultTokenBefore = token.balanceOf(address(vault)); + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + assertEq(user.balance, userEthBefore - 1 ether); + assertEq(address(vault).balance, vaultEthBefore + 1 ether); + assertEq(token.balanceOf(user), userTokenBefore + 10 ether); + assertEq(token.balanceOf(address(vault)), vaultTokenBefore - 10 ether); + assertEq(vault.getVaultCount(user), 1); + + (uint256 balance, uint256 tokenBalance, uint256 savedUnlockTime, bool active, bool isUnlocked) = + vault.getVault(user, 0); + + assertEq(balance, 1 ether); + assertEq(tokenBalance, 10 ether); + assertEq(savedUnlockTime, unlockTime); + assertTrue(active); + assertFalse(isUnlocked); + } + + function test_revert_deposit_withZeroValue() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vm.expectRevert("Deposit must be greater than zero"); + vault.deposit{value: 0}(unlockTime); + } + + function test_getActiveVaults() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + token.approve(address(vault), 10 ether); + vm.stopPrank(); + + vm.warp(firstUnlockTime); + + vm.prank(user); + vault.withdraw(0); + + ( + uint256[] memory activeVaults, + uint256[] memory balances, + uint256[] memory tokenBalances, + uint256[] memory unlockTimes + ) = vault.getActiveVaults(user); + + assertEq(activeVaults.length, 1); + assertEq(balances.length, 1); + assertEq(tokenBalances.length, 1); + assertEq(unlockTimes.length, 1); + assertEq(activeVaults[0], 1); + assertEq(balances[0], 2 ether); + assertEq(tokenBalances[0], 20 ether); + assertEq(unlockTimes[0], secondUnlockTime); + } + + function test_withdraw() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(user); + token.approve(address(vault), 10 ether); + + vm.warp(unlockTime); + + uint256 userEthBefore = user.balance; + uint256 vaultEthBefore = address(vault).balance; + uint256 userTokenBefore = token.balanceOf(user); + uint256 vaultTokenBefore = token.balanceOf(address(vault)); + + vm.prank(user); + vault.withdraw(0); + + assertEq(user.balance, userEthBefore + 1 ether); + assertEq(address(vault).balance, vaultEthBefore - 1 ether); + assertEq(token.balanceOf(user), userTokenBefore - 10 ether); + assertEq(token.balanceOf(address(vault)), vaultTokenBefore + 10 ether); + + (uint256 balance, uint256 tokenBalance,, bool active, bool isUnlocked) = vault.getVault(user, 0); + assertEq(balance, 0); + assertEq(tokenBalance, 0); + assertFalse(active); + assertTrue(isUnlocked); + } + + function test_revert_withdraw_withoutApproval() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.warp(unlockTime); + + vm.prank(user); + vm.expectRevert(); + vault.withdraw(0); + } + + function test_withdrawAll() public { + uint256 firstUnlockTime = block.timestamp + 1 days; + uint256 secondUnlockTime = block.timestamp + 2 days; + + vm.startPrank(user); + vault.deposit{value: 1 ether}(firstUnlockTime); + vault.deposit{value: 2 ether}(secondUnlockTime); + vm.stopPrank(); + + vm.warp(secondUnlockTime); + + uint256 userEthBefore = user.balance; + uint256 vaultEthBefore = address(vault).balance; + + vm.prank(user); + uint256 amount = vault.withdrawAll(); + + assertEq(amount, 3 ether); + assertEq(user.balance, userEthBefore + 3 ether); + assertEq(address(vault).balance, vaultEthBefore - 3 ether); + assertEq(vault.getTotalBalance(user), 0); + } + + function test_revert_withdraw_fromAnotherUsersVault() public { + uint256 unlockTime = block.timestamp + 1 days; + + vm.prank(user); + vault.deposit{value: 1 ether}(unlockTime); + + vm.prank(attacker); + vm.expectRevert("Invalid vault ID"); + vault.withdraw(0); + } +}