diff --git a/.gitignore b/.gitignore index 60f73e90..77ba2a7f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ Thumbs.db .idea/ *.swp *.swo + + +lib/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..feb451eb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "assignments/foundry-test-assignment/lib/openzeppelin-contracts"] + path = assignments/foundry-test-assignment/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/13-04-26-test/assessment1/README.md b/13-04-26-test/assessment1/README.md new file mode 100644 index 00000000..8817d6ab --- /dev/null +++ b/13-04-26-test/assessment1/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/13-04-26-test/assessment1/foundry.toml b/13-04-26-test/assessment1/foundry.toml new file mode 100644 index 00000000..25b918f9 --- /dev/null +++ b/13-04-26-test/assessment1/foundry.toml @@ -0,0 +1,6 @@ +[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 diff --git a/13-04-26-test/assessment1/src/AssessmentContract.sol b/13-04-26-test/assessment1/src/AssessmentContract.sol new file mode 100644 index 00000000..fd4b0ac2 --- /dev/null +++ b/13-04-26-test/assessment1/src/AssessmentContract.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.33; + +// === Vulnerable Contract +contract VulnerableContract { + mapping(address => uint256) public balances; + + function deposit() public payable { + balances[msg.sender] += msg.value; + } + + function withdraw(uint256 amount) public { + require(balances[msg.sender] >= amount, 'Insufficient balance'); + + (bool success, ) = msg.sender.call{value: amount}(''); + require(success, 'Transfer failed'); + + balances[msg.sender] -= amount; + } +} + +// === Attacker Contract +contract AttackerContract { + VulnerableContract public vulnerableContract; + + constructor(address _vulnerableContractAddress) { + vulnerableContract = VulnerableContract(_vulnerableContractAddress); + } + + // === This is called anytime the contract receives ether + fallback() external payable { + if (address(vulnerableContract).balance >= 1 ether) { + vulnerableContract.withdraw(1 ether); + } + } + + function exploit() external payable { + require(msg.value >= 1 ether); + vulnerableContract.deposit{value: 1 ether}(); + vulnerableContract.withdraw(1 ether); + } + + // receive() external payable; +} + +contract ReentrancyGuard { + bool internal _notInteracted = true; + + modifier nonReentrant() { + require(_notInteracted, "ReentrancyGuard: reentrant call"); + _notInteracted = false; + _; + _notInteracted = true; + } + +} + +contract FixedContract is ReentrancyGuard { + mapping(address => uint256) public balances; + + function deposit() external payable nonReentrant { + balances[msg.sender] += msg.value; + } + + function withdraw(uint256 amount) external nonReentrant { + require(balances[msg.sender] >= amount, 'Insufficient balance'); + + balances[msg.sender] -= amount; + + (bool success, ) = msg.sender.call{value: amount}(''); + require(success, 'Transfer failed'); + } +} diff --git a/13-04-26-test/assessment1/test/Assessment.t.sol b/13-04-26-test/assessment1/test/Assessment.t.sol new file mode 100644 index 00000000..f2c05603 --- /dev/null +++ b/13-04-26-test/assessment1/test/Assessment.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import {Test} from 'forge-std/Test.sol'; +import '../src/AssessmentContract.sol'; + +contract AssessmentTest is Test { + VulnerableContract public vulnerableContract; + AttackerContract public attackerContract; + FixedContract public fixedContract; + + address owner = makeAddr('owner'); + address attacker = makeAddr('attacker'); + address user = makeAddr('user'); + + function setUp() public { + vulnerableContract = new VulnerableContract(); + fixedContract = new FixedContract(); + attackerContract = new AttackerContract(address(vulnerableContract)); + + vm.deal(owner, 10 ether); + vm.deal(attacker, 2 ether); + vm.deal(user, 2 ether); + } + + // === Basic Deployment Tests + function test_if_deployment_initialBalance_isZero() public view { + // uint256 contractBalance = vulnerableContract.balances; + assertEq(vulnerableContract.balances(owner), 0); + } + + function test_deposit_will_updateBalance() public { + vm.prank(owner); + vulnerableContract.deposit{value: 1 ether}(); + assertEq(vulnerableContract.balances(owner), 1 ether); + } + + function test_withdraw_will_reduceBalance() public { + vm.prank(owner); + vulnerableContract.deposit{value: 1 ether}(); + + vm.prank(owner); + vulnerableContract.withdraw(1 ether); + + assertEq(vulnerableContract.balances(owner), 0); + } + + function test_withdraw_revertsIfInsufficientBalance() public { + vm.prank(owner); + vm.expectRevert('Insufficient balance'); + vulnerableContract.withdraw(1 ether); + } + + // === Reentrancy attack + function test_reentrancyAttack_to_drain_vulnerable_contract() public { + // I'm depositing into the contract + vm.prank(owner); + vulnerableContract.deposit{value: 5 ether}(); + + // Initial balances + uint256 contractInitialBalance = address(vulnerableContract).balance; + uint256 attackerInitialBalance = address(attackerContract).balance; + + vm.prank(attacker); + attackerContract.exploit{value: 1 ether}(); + + Attacker may now drain more than they put in + assertGt( + address(attackerContract).balance, + attackerBalanceBefore + 1 ether, + "Attacker should have drained extra ETH" + ); + assertLt( + address(vulnerableContract).balance, + contractBalanceBefore, + "Vulnerable contract should have lost ETH" + ); + } + + // === Fixed Contract Tests + function test_if_fixed_deployment_initialBalance_isZero() public view { + assertEq(fixedContract.balances(user), 0); + } + + function test_fixed_deposit_will_updateBalance() public { + vm.prank(user); + fixedContract.deposit{value: 1 ether}(); + assertEq(fixedContract.balances(user), 1 ether); + } + + function test_fixed_withdraw_will_reduceBalance() public { + vm.prank(user); + fixedContract.deposit{value: 1 ether}(); + + vm.prank(user); + fixedContract.withdraw(1 ether); + + assertEq(vulnerableContract.balances(user), 0); + } + + function test_fixed_withdraw_revertsIfInsufficientBalance() public { + vm.prank(user); + vm.expectRevert('Insufficient balance'); + fixedContract.withdraw(1 ether); + } +} diff --git a/assignments/Olorunshogo.md b/assignments/Olorunshogo.md new file mode 100644 index 00000000..f8ad6b5b --- /dev/null +++ b/assignments/Olorunshogo.md @@ -0,0 +1,5 @@ + +1. [Link to the upgradeable contracts using both open zeppelin and the diamond](https://github.com/Olorunshogo/UpgradeableContract) + + +2. [Link to the article](https://medium.com/@shownzy001/upgradeable-contracts-b4d8c5c3d61e) \ No newline at end of file diff --git a/assignments/foundry-test-assignment/.github/workflows/test.yml b/assignments/foundry-test-assignment/.github/workflows/test.yml new file mode 100644 index 00000000..b79c8d4f --- /dev/null +++ b/assignments/foundry-test-assignment/.github/workflows/test.yml @@ -0,0 +1,38 @@ +name: CI + +permissions: {} + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + name: Foundry project + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge version + run: forge --version + + - name: Run Forge fmt + run: forge fmt --check + + - name: Run Forge build + run: forge build --sizes + + - name: Run Forge tests + run: forge test -vvv diff --git a/assignments/foundry-test-assignment/.gitignore b/assignments/foundry-test-assignment/.gitignore new file mode 100644 index 00000000..18d49a69 --- /dev/null +++ b/assignments/foundry-test-assignment/.gitignore @@ -0,0 +1,17 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env + + +lib/ \ No newline at end of file diff --git a/assignments/foundry-test-assignment/.gitmodules b/assignments/foundry-test-assignment/.gitmodules new file mode 100644 index 00000000..888d42dc --- /dev/null +++ b/assignments/foundry-test-assignment/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/assignments/foundry-test-assignment/README.md b/assignments/foundry-test-assignment/README.md new file mode 100644 index 00000000..8817d6ab --- /dev/null +++ b/assignments/foundry-test-assignment/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/assignments/foundry-test-assignment/foundry.lock b/assignments/foundry-test-assignment/foundry.lock new file mode 100644 index 00000000..ab2c6d82 --- /dev/null +++ b/assignments/foundry-test-assignment/foundry.lock @@ -0,0 +1,14 @@ +{ + "lib/forge-std": { + "tag": { + "name": "v1.15.0", + "rev": "0844d7e1fc5e60d77b68e469bff60265f236c398" + } + }, + "lib/openzeppelin-contracts": { + "tag": { + "name": "v5.6.1", + "rev": "5fd1781b1454fd1ef8e722282f86f9293cacf256" + } + } +} \ No newline at end of file diff --git a/assignments/foundry-test-assignment/foundry.toml b/assignments/foundry-test-assignment/foundry.toml new file mode 100644 index 00000000..25b918f9 --- /dev/null +++ b/assignments/foundry-test-assignment/foundry.toml @@ -0,0 +1,6 @@ +[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 diff --git a/assignments/foundry-test-assignment/lib/openzeppelin-contracts b/assignments/foundry-test-assignment/lib/openzeppelin-contracts new file mode 160000 index 00000000..5fd1781b --- /dev/null +++ b/assignments/foundry-test-assignment/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 5fd1781b1454fd1ef8e722282f86f9293cacf256 diff --git a/assignments/foundry-test-assignment/remappings.txt b/assignments/foundry-test-assignment/remappings.txt new file mode 100644 index 00000000..918ed315 --- /dev/null +++ b/assignments/foundry-test-assignment/remappings.txt @@ -0,0 +1,5 @@ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ +forge-std/=lib/forge-std/src/ +halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/ +openzeppelin-contracts/=lib/openzeppelin-contracts/ diff --git a/assignments/foundry-test-assignment/src/CounterV3.sol b/assignments/foundry-test-assignment/src/CounterV3.sol new file mode 100644 index 00000000..269dfb0c --- /dev/null +++ b/assignments/foundry-test-assignment/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/assignments/foundry-test-assignment/src/Token.sol b/assignments/foundry-test-assignment/src/Token.sol new file mode 100644 index 00000000..50f223e5 --- /dev/null +++ b/assignments/foundry-test-assignment/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/assignments/foundry-test-assignment/src/Vault.sol b/assignments/foundry-test-assignment/src/Vault.sol new file mode 100644 index 00000000..4500dd36 --- /dev/null +++ b/assignments/foundry-test-assignment/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/assignments/foundry-test-assignment/src/VaultV2.sol b/assignments/foundry-test-assignment/src/VaultV2.sol new file mode 100644 index 00000000..41a6c350 --- /dev/null +++ b/assignments/foundry-test-assignment/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/assignments/foundry-test-assignment/src/interfaces/IERC20.sol b/assignments/foundry-test-assignment/src/interfaces/IERC20.sol new file mode 100644 index 00000000..92e32621 --- /dev/null +++ b/assignments/foundry-test-assignment/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/assignments/foundry-test-assignment/test/CounterV3.t.sol b/assignments/foundry-test-assignment/test/CounterV3.t.sol new file mode 100644 index 00000000..e3384801 --- /dev/null +++ b/assignments/foundry-test-assignment/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/assignments/foundry-test-assignment/test/Vault.t.sol b/assignments/foundry-test-assignment/test/Vault.t.sol new file mode 100644 index 00000000..bdf2655b --- /dev/null +++ b/assignments/foundry-test-assignment/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/assignments/foundry-test-assignment/test/VaultV2.t.sol b/assignments/foundry-test-assignment/test/VaultV2.t.sol new file mode 100644 index 00000000..16f36564 --- /dev/null +++ b/assignments/foundry-test-assignment/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); + } +} 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); + } +}