diff --git a/README.md b/README.md index ee62858..8aa5f42 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ git clone https://github.com/openguild-labs/open-encode-challenges.git Go to **Participant Registration** section and register to be the workshop participants. Add the below to the list, replace any placeholder with your personal information. ``` -| 🦄 | Name | Github username | Your current occupation | +| 🦄 | MARIA NAKIBUUKA | marianakibuuka | Student web3 & Educator(She3) | ``` - Step 5: `Commit` your code and push to the forked Github repository diff --git a/challenge-1-vesting/contracts/TokenVesting.sol b/challenge-1-vesting/contracts/TokenVesting.sol index 43d4c3a..4303c8c 100644 --- a/challenge-1-vesting/contracts/TokenVesting.sol +++ b/challenge-1-vesting/contracts/TokenVesting.sol @@ -1,47 +1,35 @@ -// Challenge: Token Vesting Contract -/* -Create a token vesting contract with the following requirements: - -1. The contract should allow an admin to create vesting schedules for different beneficiaries -2. Each vesting schedule should have: - - Total amount of tokens to be vested - - Cliff period (time before any tokens can be claimed) - - Vesting duration (total time for all tokens to vest) - - Start time -3. After the cliff period, tokens should vest linearly over time -4. Beneficiaries should be able to claim their vested tokens at any time -5. Admin should be able to revoke unvested tokens from a beneficiary - -Bonus challenges: -- Add support for multiple token types -- Implement a whitelist for beneficiaries -- Add emergency pause functionality - -Here's your starter code: -*/ - // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.25; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/utils/Pausable.sol"; -import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/security/Pausable.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { +contract TokenVesting is Ownable, Pausable, ReentrancyGuard { struct VestingSchedule { - // TODO: Define the vesting schedule struct + // Total amount of tokens to vest + uint256 totalAmount; + // Vesting start timestamp + uint256 startTime; + // Cliff period in seconds + uint256 cliffDuration; + // Total vesting duration in seconds + uint256 vestingDuration; + // Amount of tokens already claimed + uint256 amountClaimed; + // Whether vesting has been revoked + bool revoked; } - // Token being vested - // TODO: Add state variables + // ERC20 token being vested + IERC20 public token; + // Mapping from beneficiary address to their vesting schedule + mapping(address => VestingSchedule) public vestingSchedules; - // Mapping from beneficiary to vesting schedule - // TODO: Add state variables - - // Whitelist of beneficiaries - // TODO: Add state variables + // Whitelist of beneficiaries allowed to have vesting schedules + mapping(address => bool) public isWhitelisted; // Events event VestingScheduleCreated(address indexed beneficiary, uint256 amount); @@ -50,57 +38,134 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { event BeneficiaryWhitelisted(address indexed beneficiary); event BeneficiaryRemovedFromWhitelist(address indexed beneficiary); - constructor(address tokenAddress) { - // TODO: Initialize the contract - + /// @notice Constructor sets the ERC20 token address + /// @param _token Address of the ERC20 token to be vested + constructor(address _token) { + require(_token != address(0), "Token address cannot be zero"); + token = IERC20(_token); } - // Modifier to check if beneficiary is whitelisted + /// @notice Modifier to restrict function to whitelisted beneficiaries modifier onlyWhitelisted(address beneficiary) { - require(whitelist[beneficiary], "Beneficiary not whitelisted"); + require(isWhitelisted[beneficiary], "Beneficiary not whitelisted"); _; } + /// @notice Add an address to the whitelist + /// @param beneficiary The address to whitelist function addToWhitelist(address beneficiary) external onlyOwner { require(beneficiary != address(0), "Invalid address"); - whitelist[beneficiary] = true; + isWhitelisted[beneficiary] = true; emit BeneficiaryWhitelisted(beneficiary); } + /// @notice Remove an address from the whitelist + /// @param beneficiary The address to remove from whitelist function removeFromWhitelist(address beneficiary) external onlyOwner { - whitelist[beneficiary] = false; + isWhitelisted[beneficiary] = false; emit BeneficiaryRemovedFromWhitelist(beneficiary); } + /// @notice Create a vesting schedule for a beneficiary + /// @param beneficiary Address of the beneficiary + /// @param totalAmount Total tokens to vest + /// @param cliffDuration Cliff period in seconds + /// @param vestingDuration Total vesting duration in seconds + /// @param startTime Vesting start timestamp (unix time) function createVestingSchedule( address beneficiary, - uint256 amount, + uint256 totalAmount, uint256 cliffDuration, uint256 vestingDuration, uint256 startTime ) external onlyOwner onlyWhitelisted(beneficiary) whenNotPaused { - // TODO: Implement vesting schedule creation + require(beneficiary != address(0), "Invalid beneficiary"); + require(totalAmount > 0, "Total amount must be > 0"); + require(vestingDuration >= cliffDuration, "Vesting duration must be >= cliff duration"); + require(vestingSchedules[beneficiary].totalAmount == 0, "Vesting schedule already exists"); + + // Transfer tokens from admin to contract for vesting + require(token.transferFrom(msg.sender, address(this), totalAmount), "Token transfer failed"); + + vestingSchedules[beneficiary] = VestingSchedule({ + totalAmount: totalAmount, + cliffDuration: cliffDuration, + vestingDuration: vestingDuration, + startTime: startTime, + amountClaimed: 0, + revoked: false + }); + + emit VestingScheduleCreated(beneficiary, totalAmount); } - function calculateVestedAmount( - address beneficiary - ) public view returns (uint256) { - // TODO: Implement vested amount calculation + /// @notice Calculate the amount of vested tokens for a beneficiary + /// @param beneficiary Address of the beneficiary + /// @return vested Amount of tokens vested and available to claim + function calculateVestedAmount(address beneficiary) public view returns (uint256 vested) { + VestingSchedule memory schedule = vestingSchedules[beneficiary]; + + if (schedule.revoked) { + // After revocation, vested amount is fixed to amount already claimed + return schedule.amountClaimed; + } + + if (block.timestamp < schedule.startTime + schedule.cliffDuration) { + // Cliff period not reached yet + return 0; + } else if (block.timestamp >= schedule.startTime + schedule.vestingDuration) { + // Fully vested after vesting duration + return schedule.totalAmount; + } else { + // Linear vesting calculation after cliff but before full vesting + uint256 timeElapsed = block.timestamp - schedule.startTime; + vested = (schedule.totalAmount * timeElapsed) / schedule.vestingDuration; + return vested; + } } - function claimVestedTokens() external nonReentrant whenNotPaused { - // TODO: Implement token claiming + /// @notice Claim vested tokens for the caller + function claimVestedTokens() external nonReentrant whenNotPaused onlyWhitelisted(msg.sender) { + VestingSchedule storage schedule = vestingSchedules[msg.sender]; + require(schedule.totalAmount > 0, "No vesting schedule"); + require(!schedule.revoked, "Vesting revoked"); + + uint256 vested = calculateVestedAmount(msg.sender); + uint256 claimable = vested - schedule.amountClaimed; + require(claimable > 0, "No tokens to claim"); + + schedule.amountClaimed += claimable; + + require(token.transfer(msg.sender, claimable), "Token transfer failed"); + + emit TokensClaimed(msg.sender, claimable); } - function revokeVesting(address beneficiary) external onlyOwner { - // TODO: Implement vesting revocation + /// @notice Revoke vesting schedule for a beneficiary and return unvested tokens to owner + /// @param beneficiary Address of the beneficiary whose vesting is revoked + function revokeVesting(address beneficiary) external onlyOwner whenNotPaused { + VestingSchedule storage schedule = vestingSchedules[beneficiary]; + require(schedule.totalAmount > 0, "No vesting schedule"); + require(!schedule.revoked, "Already revoked"); + uint256 vested = calculateVestedAmount(beneficiary); + uint256 unvested = schedule.totalAmount - vested; + + schedule.revoked = true; + + if (unvested > 0) { + require(token.transfer(owner(), unvested), "Token transfer failed"); + } + + emit VestingRevoked(beneficiary); } + /// @notice Pause the contract (emergency stop) function pause() external onlyOwner { _pause(); } + /// @notice Unpause the contract function unpause() external onlyOwner { _unpause(); } @@ -145,4 +210,5 @@ Solution template (key points to implement): - Calculate and transfer unvested tokens back - Mark schedule as revoked - Emit event -*/ \ No newline at end of file +*/ +// token contract address - 0xa3Ff912076cD9Caa4ee2244C4a619D86b6E87a87 \ No newline at end of file diff --git a/challenge-1-vesting/contracts/token.sol b/challenge-1-vesting/contracts/token.sol index 5f952a1..81bd901 100644 --- a/challenge-1-vesting/contracts/token.sol +++ b/challenge-1-vesting/contracts/token.sol @@ -5,7 +5,7 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract MockERC20 is ERC20, Ownable(msg.sender) { - constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + constructor(string memory name, string memory symbol) ERC20(name, symbol){} function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); diff --git a/challenge-2-yield-farm/contracts/yeild.sol b/challenge-2-yield-farm/contracts/yeild.sol index 421496a..f068e60 100644 --- a/challenge-2-yield-farm/contracts/yeild.sol +++ b/challenge-2-yield-farm/contracts/yeild.sol @@ -5,16 +5,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; -/** - * @title YieldFarm - * @notice Challenge: Implement a yield farming contract with the following requirements: - * - * 1. Users can stake LP tokens and earn reward tokens - * 2. Rewards are distributed based on time and amount staked - * 3. Implement reward boosting mechanism for long-term stakers - * 4. Add emergency withdrawal functionality - * 5. Implement reward rate adjustment mechanism - */ + contract YieldFarm is ReentrancyGuard, Ownable { // LP token that users can stake @@ -70,7 +61,10 @@ contract YieldFarm is ReentrancyGuard, Ownable { address _rewardToken, uint256 _rewardRate ) Ownable(msg.sender) { - // TODO: Initialize contract state + lpToken = IERC20(_lpToken); + rewardToken = IERC20(_rewardToken); + rewardRate = _rewardRate; + lastUpdateTime = block.timestamp; } function updateReward(address _user) internal { @@ -85,19 +79,23 @@ contract YieldFarm is ReentrancyGuard, Ownable { } function rewardPerToken() public view returns (uint256) { - // TODO: Implement pending rewards calculation - // Requirements: - // 1. Calculate rewards since last update - // 2. Apply boost multiplier - // 3. Return total pending rewards + if (totalStaked == 0) { + return rewardPerTokenStored; + } + uint256 timeElapsed = block.timestamp - lastUpdateTime; + return + rewardPerTokenStored + + (timeElapsed * rewardRate * 1e18) / + totalStaked; } function earned(address _user) public view returns (uint256) { - // TODO: Implement pending rewards calculation - // Requirements: - // 1. Calculate rewards since last update - // 2. Apply boost multiplier - // 3. Return total pending rewards + UserInfo storage user = userInfo[_user]; + uint256 boost = calculateBoostMultiplier(_user); + uint256 rewardCalculation = rewardPerToken() - user.rewardDebt; + uint256 currentReward = ((user.amount * rewardCalculation) / 1e18); + uint256 boostedReward = (currentReward * boost) / 100; + return user.pendingRewards + boostedReward; } /** @@ -105,12 +103,21 @@ contract YieldFarm is ReentrancyGuard, Ownable { * @param _amount Amount of LP tokens to stake */ function stake(uint256 _amount) external nonReentrant { - // TODO: Implement staking logic - // Requirements: - // 1. Update rewards - // 2. Transfer LP tokens from user - // 3. Update user info and total staked amount - // 4. Emit Staked event + require(_amount > 0, "Cannot stake 0"); + + updateReward(msg.sender); + + lpToken.transferFrom(msg.sender, address(this), _amount); + + UserInfo storage user = userInfo[msg.sender]; + if (user.amount == 0) { + user.startTime = block.timestamp; + } + + user.amount += _amount; + totalStaked += _amount; + + emit Staked(msg.sender, _amount); } /** @@ -118,35 +125,51 @@ contract YieldFarm is ReentrancyGuard, Ownable { * @param _amount Amount of LP tokens to withdraw */ function withdraw(uint256 _amount) external nonReentrant { - // TODO: Implement withdrawal logic - // Requirements: - // 1. Update rewards - // 2. Transfer LP tokens to user - // 3. Update user info and total staked amount - // 4. Emit Withdrawn event + UserInfo storage user = userInfo[msg.sender]; + require(user.amount >= _amount, "Insufficient balance"); + + updateReward(msg.sender); + + user.amount -= _amount; + totalStaked -= _amount; + lpToken.transfer(msg.sender, _amount); + + emit Withdrawn(msg.sender, _amount); } /** * @notice Claim pending rewards */ function claimRewards() external nonReentrant { - // TODO: Implement reward claiming logic - // Requirements: - // 1. Calculate pending rewards with boost multiplier - // 2. Transfer rewards to user - // 3. Update user reward debt - // 4. Emit RewardsClaimed event + updateReward(msg.sender); + UserInfo storage user = userInfo[msg.sender]; + + uint256 reward = user.pendingRewards; + require(reward > 0, "No rewards"); + + user.pendingRewards = 0; + rewardToken.transfer(msg.sender, reward); + + emit RewardsClaimed(msg.sender, reward); } /** * @notice Emergency withdraw without caring about rewards */ function emergencyWithdraw() external nonReentrant { - // TODO: Implement emergency withdrawal - // Requirements: - // 1. Transfer all LP tokens back to user - // 2. Reset user info - // 3. Emit EmergencyWithdrawn event + UserInfo storage userData = userInfo[msg.sender]; + uint256 amountWithdraw = userData.amount; + + require(amountWithdraw > 0, "Nothing to withdraw"); + + userData.amount = 0; + userData.rewardDebt = 0; + userData.pendingRewards = 0; + totalStaked -= amountWithdraw; + + lpToken.transfer(msg.sender, amountWithdraw); + + emit EmergencyWithdrawn(msg.sender, amountWithdraw); } /** @@ -157,10 +180,13 @@ contract YieldFarm is ReentrancyGuard, Ownable { function calculateBoostMultiplier( address _user ) public view returns (uint256) { - // TODO: Implement boost multiplier calculation - // Requirements: - // 1. Calculate staking duration - // 2. Return appropriate multiplier based on duration thresholds + uint256 durationStaking = block.timestamp - userInfo[_user].startTime; + + if (durationStaking >= BOOST_THRESHOLD_3) return 200; + if (durationStaking >= BOOST_THRESHOLD_2) return 150; + if (durationStaking >= BOOST_THRESHOLD_1) return 125; + + return 100; } /** @@ -168,10 +194,8 @@ contract YieldFarm is ReentrancyGuard, Ownable { * @param _newRate New reward rate per second */ function updateRewardRate(uint256 _newRate) external onlyOwner { - // TODO: Implement reward rate update logic - // Requirements: - // 1. Update rewards before changing rate - // 2. Set new reward rate + updateReward(address(0)); + rewardRate = _newRate; } /** @@ -182,4 +206,4 @@ contract YieldFarm is ReentrancyGuard, Ownable { function pendingRewards(address _user) external view returns (uint256) { return earned(_user); } -} +} \ No newline at end of file