From 05535267135c2a812814f5914140231307c0b1ef Mon Sep 17 00:00:00 2001 From: Jishantu Kripal Date: Tue, 16 Jul 2024 18:11:59 +0530 Subject: [PATCH] Zealy Quest --- .../Session 2/ERC20Token.sol | 24 ++ .../Session 3/Foundrytest.sol | 221 ++++++++++++++++++ .../Session 3/StakingAudit.txt | 122 ++++++++++ 3 files changed, 367 insertions(+) create mode 100644 Bootcampers/Jishantu_Kripal_Bordoloi/Session 2/ERC20Token.sol create mode 100644 Bootcampers/Jishantu_Kripal_Bordoloi/Session 3/Foundrytest.sol create mode 100644 Bootcampers/Jishantu_Kripal_Bordoloi/Session 3/StakingAudit.txt diff --git a/Bootcampers/Jishantu_Kripal_Bordoloi/Session 2/ERC20Token.sol b/Bootcampers/Jishantu_Kripal_Bordoloi/Session 2/ERC20Token.sol new file mode 100644 index 0000000..77bde24 --- /dev/null +++ b/Bootcampers/Jishantu_Kripal_Bordoloi/Session 2/ERC20Token.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.0.0 +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts@5.0.2/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts@5.0.2/token/ERC20/extensions/ERC20Burnable.sol"; +import "@openzeppelin/contracts@5.0.2/access/Ownable.sol"; +import "@openzeppelin/contracts@5.0.2/token/ERC20/extensions/ERC20Permit.sol"; + +contract BRBBootcampToken is ERC20, ERC20Burnable, Ownable, ERC20Permit { + constructor(address initialOwner) + ERC20("BRB Bootcamp Token", "BRBBT") + Ownable(initialOwner) + ERC20Permit("BRB Bootcamp Token") + { + _mint(msg.sender, 7777 * 10 ** decimals()); + } + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } +} + + diff --git a/Bootcampers/Jishantu_Kripal_Bordoloi/Session 3/Foundrytest.sol b/Bootcampers/Jishantu_Kripal_Bordoloi/Session 3/Foundrytest.sol new file mode 100644 index 0000000..897e9ae --- /dev/null +++ b/Bootcampers/Jishantu_Kripal_Bordoloi/Session 3/Foundrytest.sol @@ -0,0 +1,221 @@ +// Actors +address owner; +address user; +address uninitializedUser; + +// Constants +uint256 constant rewardAmount = 100 * 10**18; + +function setUp() public { + owner = address(0x111); + user = address(0x222); + uninitializedUser = address(0x333); + + vm.startPrank(owner); + + token = new BRBToken(owner); + stakingContract = new BRBStaking(token); + + token.transfer(user, 1000 ether); + vm.stopPrank(); +} + +// User should be able to initialize their staking profile - JUST ONCE +function testInitializeUserTwice() public { + vm.startPrank(user); + stakingContract.initializeUser(); + + vm.expectRevert("User already initialized"); + stakingContract.initializeUser(); + + vm.stopPrank(); +} + +// TEST initializeUser() function +function testInitializeUser() public { + // Prank as user + vm.prank(user); + stakingContract.initializeUser(); + + // Verify user initialization + (address userAddress, , bool initialized, ,) = stakingContract.userStakeData(user, 0); + assertEq(userAddress, user); + assertTrue(initialized); +} + +// Test stake() function +function testStake() public { + uint256 stakeAmount = 100 * 10**18; + + // User to approve and stake + vm.startPrank(user); + token.approve(address(stakingContract), stakeAmount); + stakingContract.initializeUser(); + stakingContract.stake(stakeAmount); + + vm.stopPrank(); + + // Verify staking + (address userAddress, uint256 stakeAmountStored, bool initialized, ,uint256 stakeID) = stakingContract.userStakeData(user, 1); + assertEq(userAddress, user); + assertEq(stakeAmountStored, stakeAmount); + assertEq(stakeID, 1); + assertTrue(initialized); +} + +// TEST unstake() function +function testUnstakeFunction() public { + uint256 stakeAmount = 100 * 10**18; + + // Prank as user to approve and stake + vm.startPrank(user); + token.approve(address(stakingContract), stakeAmount); + stakingContract.initializeUser(); + stakingContract.stake(stakeAmount); + vm.stopPrank(); + + // Owner Adds Reward + vm.startPrank(owner); + token.approve(address(stakingContract), rewardAmount); + stakingContract.addReward(rewardAmount); + vm.stopPrank(); + + // Fast forward time by 7 days + vm.warp(block.timestamp + 7 days); + + // Check user balance before unstake + uint256 userBalanceBefore = token.balanceOf(user); + + vm.startPrank(user); + stakingContract.unstake(1); + vm.stopPrank(); + + // Check user balance after unstake + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceAfter, userBalanceBefore + stakeAmount + rewardAmount); +} + +// Test that only the owner can call addReward() +function testAddRewardOwnership() public { + uint256 amountToAdd = 200 * 10**18; + + // Attempt to call addReward by a non-owner should revert + vm.startPrank(user); + vm.expectRevert("Ownable: caller is not the owner"); + stakingContract.addReward(amountToAdd); + vm.stopPrank(); + + // Owner should be able to call addReward + vm.startPrank(owner); + token.approve(address(stakingContract), amountToAdd); + stakingContract.addReward(amountToAdd); + vm.stopPrank(); +} + +// Test that uninitialized user cannot stake or unstake +function testUninitializedUserRestrictions() public { + uint256 stakeAmount = 50 * 10**18; + + vm.startPrank(uninitializedUser); + + // Approve tokens for staking + token.approve(address(stakingContract), stakeAmount); + + // Try to stake without initializing + vm.expectRevert("User not initialized"); + stakingContract.stake(stakeAmount); + + // Try to unstake without initializing or staking + vm.expectRevert("User not initialized"); + stakingContract.unstake(1); + + vm.stopPrank(); +} + +// Test that reward is exactly 100 tokens for all stakers +function testRewardDistribution() public { + uint256 stakeAmount = 100 * 10**18; + + vm.startPrank(user); + token.approve(address(stakingContract), stakeAmount); + stakingContract.initializeUser(); + stakingContract.stake(stakeAmount); + vm.stopPrank(); + + // Owner Adds Reward + vm.startPrank(owner); + token.approve(address(stakingContract), rewardAmount); + stakingContract.addReward(rewardAmount); + vm.stopPrank(); + + // Fast forward time by 7 days + vm.warp(block.timestamp + 7 days); + + vm.startPrank(user); + stakingContract.unstake(1); + vm.stopPrank(); + + // Check final balance should be the initial balance + stake + reward + uint256 finalBalance = token.balanceOf(user); + assertEq(finalBalance, 1000 ether + rewardAmount); // Initial balance is 1000 ether +} + +// Test that unstaking before 7 days should revert +function testUnstakeBefore7Days() public { + uint256 stakeAmount = 100 * 10**18; + + // Prank as user to approve and stake + vm.startPrank(user); + token.approve(address(stakingContract), stakeAmount); + stakingContract.initializeUser(); + stakingContract.stake(stakeAmount); + vm.stopPrank(); + + // Try to unstake before 7 days + vm.startPrank(user); + vm.expectRevert("Staking period not yet completed"); + stakingContract.unstake(1); + vm.stopPrank(); +} + +// Test event emissions +function testEventEmissions() public { + uint256 stakeAmount = 100 * 10**18; + + vm.startPrank(user); + token.approve(address(stakingContract), stakeAmount); + stakingContract.initializeUser(); + + // Expect Stake event + vm.expectEmit(true, true, true, true); + emit Stake(user, stakeAmount, 1); + stakingContract.stake(stakeAmount); + + vm.stopPrank(); + + // Owner Adds Reward + vm.startPrank(owner); + token.approve(address(stakingContract), rewardAmount); + + // Expect RewardAdded event + vm.expectEmit(true, true, true, true); + emit RewardAdded(owner, rewardAmount); + stakingContract.addReward(rewardAmount); + vm.stopPrank(); + + // Fast forward time by 7 days + vm.warp(block.timestamp + 7 days); + + vm.startPrank(user); + + // Expect Unstake event + vm.expectEmit(true, true, true, true); + emit Unstake(user, 1); + stakingContract.unstake(1); + vm.stopPrank(); +} + +// Add the missing event definitions +event Stake(address indexed user, uint256 amount, uint256 indexed stakeID); +event Unstake(address indexed user, uint256 indexed stakeID); +event RewardAdded(address indexed owner, uint256 amount); diff --git a/Bootcampers/Jishantu_Kripal_Bordoloi/Session 3/StakingAudit.txt b/Bootcampers/Jishantu_Kripal_Bordoloi/Session 3/StakingAudit.txt new file mode 100644 index 0000000..63ffaf0 --- /dev/null +++ b/Bootcampers/Jishantu_Kripal_Bordoloi/Session 3/StakingAudit.txt @@ -0,0 +1,122 @@ +Final Corrected Code : + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @title StakingContract + * @dev A contract that allows users to stake ERC20 tokens, earn rewards, and unstake after a lockup period. + */ +contract BRBStaking is Ownable { + IERC20 public token; + uint256 public totalStaked; + uint256 public rewardPool; + uint256 public immutable LOCKUP_PERIOD; + uint256 public immutable REWARD_AMOUNT; + + /** + * @dev Struct to represent a user's staking information. + */ + struct User { + address userAddress; + uint256 stakeAmount; + bool initialized; + uint256 timeStamp; + uint256 stakeID; + } + + mapping(address => mapping(uint256 => User)) public userStakeData; + mapping(address => uint256) public userStakeCount; + + event UserInitialized(address indexed user); + event TokensStaked(address indexed user, uint256 amount, uint256 stakeID); + event TokensUnstaked(address indexed user, uint256 amount, uint256 stakeID); + event RewardsAdded(uint256 amount); + + constructor(IERC20 _token, uint256 _lockupPeriod, uint256 _rewardAmount) { + token = _token; + LOCKUP_PERIOD = _lockupPeriod; + REWARD_AMOUNT = _rewardAmount; + } + + function initializeUser() external { + require(!userStakeData[msg.sender][0].initialized, "User already initialized"); + User memory user = User(msg.sender, 0, true, 0, 0); + userStakeData[msg.sender][0] = user; + emit UserInitialized(msg.sender); + } + + function stake(uint256 _amount) external { + require(userStakeData[msg.sender][0].initialized, "User not initialized"); + require(token.transferFrom(msg.sender, address(this), _amount), "Token transfer failed"); + + uint256 stakeID = userStakeCount[msg.sender]; + User memory user = User(msg.sender, _amount, true, block.timestamp, stakeID); + userStakeData[msg.sender][stakeID] = user; + + userStakeCount[msg.sender]++; + totalStaked += _amount; + + emit TokensStaked(msg.sender, _amount, stakeID); + } + + function unstake(uint256 _stakeID) external { + User storage user = userStakeData[msg.sender][_stakeID]; + require(user.initialized, "Stake not found"); + require(block.timestamp >= user.timeStamp + LOCKUP_PERIOD, "Lockup period not completed"); + + uint256 amountToTransfer = user.stakeAmount; + if (rewardPool >= REWARD_AMOUNT) { + amountToTransfer += REWARD_AMOUNT; + rewardPool -= REWARD_AMOUNT; + } + + totalStaked -= user.stakeAmount; + delete userStakeData[msg.sender][_stakeID]; + + require(token.transfer(msg.sender, amountToTransfer), "Token transfer failed"); + + emit TokensUnstaked(msg.sender, user.stakeAmount, _stakeID); + } + + function addReward(uint256 _amount) external onlyOwner { + require(token.transferFrom(msg.sender, address(this), _amount), "Token transfer failed"); + rewardPool += _amount; + + emit RewardsAdded(_amount); + } +} + + +// Issues are discussed below: + +1. Data Type for Stack ID and User Stake Count + +Issue: In the User struct, stack id is initialized as uint8, and userStakeCount is mapped with uint8. This limits the number of stakes. +Improvement: Change the data type to uint256 to avoid limitations on the number of stakes. + +2. Constructor Initialization + +Issue: The constructor includes Ownable(msg.sender), which is unnecessary as the constructor does not take parameters. +Improvement: Remove Ownable(msg.sender) from the constructor. + + +3. Redundant Initialization in initializeUser Function + +Issue: The initializeUser function initializes userStakeData[msg.sender][0] with a zero User struct, which is redundant. +Improvement: Remove the redundant initialization to streamline the function. +Proposed Code Adjustments + +4. Immutability of Lockup Period and Reward Amount + +Issue: Lockup period and Reward Amount are not set as immutable despite being constants. +Improvement: Use the immutable keyword for these variables to ensure they remain constant after deployment. + + +5. Reward Pool Logic + +Issue: The current logic for adding rewards to the stake amount is unclear and could lead to confusion or manipulation. +Improvement: Clarify the reward distribution logic to ensure it is straightforward and secure. \ No newline at end of file