From 1614cdd84863e1ff10af4e9a7b22be293b8b3225 Mon Sep 17 00:00:00 2001 From: Hammad Iftikhar Date: Thu, 28 Nov 2024 23:16:55 +0500 Subject: [PATCH] Initial commit --- .gitmodules | 3 + Makefile | 50 ++++++++++ foundry.toml | 1 + lib/openzeppelin-contracts | 1 + script/Counter.s.sol | 19 ---- script/DeployOurToken.s.sol | 16 ++++ src/Counter.sol | 14 --- src/ManualToken.sol | 30 ++++++ src/OurToken.sol | 11 +++ test/Counter.t.sol | 24 ----- test/OurTokenTest.t.sol | 184 ++++++++++++++++++++++++++++++++++++ 11 files changed, 296 insertions(+), 57 deletions(-) create mode 100644 Makefile create mode 160000 lib/openzeppelin-contracts delete mode 100644 script/Counter.s.sol create mode 100644 script/DeployOurToken.s.sol delete mode 100644 src/Counter.sol create mode 100644 src/ManualToken.sol create mode 100644 src/OurToken.sol delete mode 100644 test/Counter.t.sol create mode 100644 test/OurTokenTest.t.sol diff --git a/.gitmodules b/.gitmodules index 888d42d..e80ffd8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/openzeppelin/openzeppelin-contracts diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..905e676 --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +-include .env + +.PHONY: all test test-zk clean deploy fund help install snapshot format anvil install deploy deploy-zk deploy-zk-sepolia deploy-sepolia verify + +DEFAULT_ANVIL_KEY := 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +DEFAULT_ZKSYNC_LOCAL_KEY := 0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 + +all: clean remove install update build + +# Clean the repo +clean :; forge clean + +# Remove modules +remove :; rm -rf .gitmodules && rm -rf .git/modules/* && rm -rf lib && touch .gitmodules && git add . && git commit -m "modules" + +install :; forge install cyfrin/foundry-devops@0.2.2 --no-commit && forge install foundry-rs/forge-std@v1.8.2 --no-commit && forge install openzeppelin/openzeppelin-contracts@v5.0.2 --no-commit + +# Update Dependencies +update:; forge update + +build:; forge build + +test :; forge test + +test-zk :; foundryup-zksync && forge test --zksync && foundryup + +snapshot :; forge snapshot + +format :; forge fmt + +anvil :; anvil -m 'test test test test test test test test test test test junk' --steps-tracing --block-time 1 + +deploy: + @forge script script/DeployOurToken.s.sol:DeployOurToken --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast + + +deploy-sepolia: + @forge script script/DeployOurToken.s.sol:DeployOurToken --rpc-url $(SEPOLIA_RPC_URL) --account $(ACCOUNT) --sender $(SENDER) --etherscan-api-key $(ETHERSCAN_API_KEY) --broadcast --verify + +deploy-zk: + @forge script script/DeployOurToken.s.sol --rpc-url http://127.0.0.1:8011 --private-key $(DEFAULT_ZKSYNC_LOCAL_KEY) --legacy --zksync + +deploy-zk-sepolia: + @forge script script/DeployOurToken.s.sol --rpc-url $(ZKSYNC_SEPOLIA_RPC_URL) --account $(ACCOUNT) --legacy --zksync + +deploy-zk-bad: + @forge script script/DeployOurToken.s.sol --rpc-url https://sepolia.era.zksync.dev --private-key $(PRIVATE_KEY) --legacy --zksync + +verify: + @forge verify-contract --chain-id 11155111 --num-of-optimizations 200 --watch --constructor-args 0x00000000000000000000000000000000000000000000d3c21bcecceda1000000 --etherscan-api-key $(ETHERSCAN_API_KEY) --compiler-version v0.8.19+commit.7dd6d404 0x089dc24123e0a27d44282a1ccc2fd815989e3300 src/OurToken.sol:OurToken \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 25b918f..6508796 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,6 @@ src = "src" out = "out" libs = ["lib"] +remappings = ["@openzeppelin=lib/openzeppelin-contracts"] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..dbb6104 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index cdc1fe9..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/script/DeployOurToken.s.sol b/script/DeployOurToken.s.sol new file mode 100644 index 0000000..e27780f --- /dev/null +++ b/script/DeployOurToken.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import {Script} from "forge-std/Script.sol"; +import {OurToken} from "src/OurToken.sol"; + +contract DeployOurToken is Script { + uint256 public constant INITIAL_SUPPLY = 1000 ether; + + function run() external returns (OurToken) { + vm.startBroadcast(); + OurToken ot = new OurToken(INITIAL_SUPPLY); + vm.stopBroadcast(); + return ot; + } +} diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/ManualToken.sol b/src/ManualToken.sol new file mode 100644 index 0000000..5f43e6c --- /dev/null +++ b/src/ManualToken.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract ManualToken { + mapping(address => uint256) private s_balances; + + function name() public pure returns (string memory) { + return "ManualToken"; + } + + // string public name = "ManualToken"; //Another way of doing the same thing + function totalSupply() public pure returns (uint256) { + return 100 ether; + } + + function decimals() public pure returns (uint8) { + return 18; + } + + function balanceOf(address _owner) public view returns (uint256) { + return s_balances[_owner]; + } + + function transfer(address _to, uint256 _amount) public { + uint256 previousBalances = balanceOf(msg.sender) + balanceOf(_to); + s_balances[msg.sender] -= _amount; + s_balances[_to] += _amount; + require(balanceOf(msg.sender) + balanceOf(_to) == previousBalances); + } +} diff --git a/src/OurToken.sol b/src/OurToken.sol new file mode 100644 index 0000000..8e6a054 --- /dev/null +++ b/src/OurToken.sol @@ -0,0 +1,11 @@ +//SPDX-License-Identifier:MIT + +pragma solidity ^0.8.18; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract OurToken is ERC20 { + constructor(uint256 initialSupply) ERC20("OurToken", "OT") { + _mint(msg.sender, initialSupply); + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/OurTokenTest.t.sol b/test/OurTokenTest.t.sol new file mode 100644 index 0000000..a429cbe --- /dev/null +++ b/test/OurTokenTest.t.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import {DeployOurToken} from "../script/DeployOurToken.s.sol"; +import {OurToken} from "../src/OurToken.sol"; +import {Test, console} from "forge-std/Test.sol"; +import {StdCheats} from "forge-std/StdCheats.sol"; + +contract OurTokenTest is StdCheats, Test { + OurToken public ourToken; + DeployOurToken public deployer; + + address public user1 = address(0x1); + address public user2 = address(0x2); + + function setUp() public { + deployer = new DeployOurToken(); + ourToken = deployer.run(); + vm.deal(user1, 1 ether); // Fund user1 with ETH for gas + vm.deal(user2, 1 ether); // Fund user2 with ETH for gas + } + + // Test the initial supply is set correctly + function testInitialSupply() public view { + assertEq(ourToken.totalSupply(), deployer.INITIAL_SUPPLY()); + } + + // Test that users cannot mint new tokens + function testUsersCantMint() public { + vm.expectRevert("function selector was not recognized"); + // Attempt to call a non-existent mint function on the contract + (bool success, ) = address(ourToken).call( + abi.encodeWithSignature("mint(address,uint256)", address(this), 1) + ); + assertFalse(success); + } + + // Test transferring tokens between accounts + function testTransferTokens() public { + uint256 transferAmount = 1000 ether; + + // Mint some tokens to user1 for testing + deal(address(ourToken), user1, transferAmount); + + // Ensure user1 balance is updated + assertEq(ourToken.balanceOf(user1), transferAmount); + + // Transfer tokens from user1 to user2 + vm.prank(user1); + ourToken.transfer(user2, transferAmount); + + // Check final balances + assertEq(ourToken.balanceOf(user1), 0); + assertEq(ourToken.balanceOf(user2), transferAmount); + } + + // Test transfers revert when the sender has insufficient balance + function testTransferRevertsOnInsufficientBalance() public { + vm.prank(user1); + vm.expectRevert(); + ourToken.transfer(user2, 1 ether); + } + + // Test allowances: approve and transferFrom + function testAllowanceWorks() public { + uint256 approveAmount = 500 ether; + + // Approve user2 to spend tokens on behalf of user1 + vm.prank(user1); + ourToken.approve(user2, approveAmount); + + // Check allowance is set + assertEq(ourToken.allowance(user1, user2), approveAmount); + + // Mint tokens to user1 for testing + deal(address(ourToken), user1, approveAmount); + + // Perform transferFrom using allowance + vm.prank(user2); + ourToken.transferFrom(user1, user2, approveAmount); + + // Check balances and allowance after transfer + assertEq(ourToken.balanceOf(user1), 0); + assertEq(ourToken.balanceOf(user2), approveAmount); + assertEq(ourToken.allowance(user1, user2), 0); + } + + // Test that transferFrom reverts when the spender tries to exceed allowance + function testTransferFromRevertsOnExceedingAllowance() public { + uint256 approveAmount = 500 ether; + + // Approve user2 to spend tokens on behalf of user1 + vm.prank(user1); + ourToken.approve(user2, approveAmount); + + // Attempt transfer exceeding allowance + vm.prank(user2); + vm.expectRevert(); + ourToken.transferFrom(user1, user2, approveAmount + 1 ether); + } + + // Test that approve overwrites previous allowance + function testApproveOverwritesPreviousAllowance() public { + uint256 firstApproveAmount = 500 ether; + uint256 secondApproveAmount = 300 ether; + + // Approve user2 with the first amount + vm.prank(user1); + ourToken.approve(user2, firstApproveAmount); + assertEq(ourToken.allowance(user1, user2), firstApproveAmount); + + // Overwrite allowance with a second approval + vm.prank(user1); + ourToken.approve(user2, secondApproveAmount); + assertEq(ourToken.allowance(user1, user2), secondApproveAmount); + } + + function testBurnTokens() public { + uint256 initialBalance = 1000 ether; + uint256 burnAmount = 500 ether; + + // Mint tokens to user1 for testing + deal(address(ourToken), user1, initialBalance); + + // Attempt to burn tokens by transferring to the zero address + vm.prank(user1); + vm.expectRevert( + abi.encodeWithSignature("ERC20InvalidReceiver(address)", address(0)) + ); + ourToken.transfer(address(0), burnAmount); + + // Verify that the balance and total supply remain unchanged + assertEq(ourToken.balanceOf(user1), initialBalance); + assertEq(ourToken.totalSupply(), deployer.INITIAL_SUPPLY()); + } + + // Test approve and transferFrom when allowance is partially used + function testPartialAllowanceUsage() public { + uint256 approveAmount = 1000 ether; + uint256 transferAmount = 500 ether; + + // Approve user2 to spend tokens on behalf of user1 + vm.prank(user1); + ourToken.approve(user2, approveAmount); + + // Mint tokens to user1 + deal(address(ourToken), user1, approveAmount); + + // Use part of the allowance + vm.prank(user2); + ourToken.transferFrom(user1, user2, transferAmount); + + // Check remaining allowance + assertEq( + ourToken.allowance(user1, user2), + approveAmount - transferAmount + ); + assertEq(ourToken.balanceOf(user1), approveAmount - transferAmount); + assertEq(ourToken.balanceOf(user2), transferAmount); + } + + function testTransferToZeroAddressReverts() public { + uint256 transferAmount = 100 ether; + + // Mint tokens to user1 for testing + deal(address(ourToken), user1, transferAmount); + + // Expect the transfer to zero address to revert with the custom error + vm.prank(user1); + vm.expectRevert( + abi.encodeWithSignature("ERC20InvalidReceiver(address)", address(0)) + ); + ourToken.transfer(address(0), transferAmount); + + // Verify that the balance of user1 remains unchanged + assertEq(ourToken.balanceOf(user1), transferAmount); + } + + function testConstructorInitializesCorrectly() public view { + uint256 expectedSupply = 1e21; // Expected initial supply + assertEq(ourToken.totalSupply(), expectedSupply); // Check total supply + assertEq(ourToken.balanceOf(address(msg.sender)), expectedSupply); // Check deployer’s balance + } +}