diff --git a/README.md b/README.md index 2f03b34..95094fa 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,13 @@ +## Participant Registration + +Add your information to the below list to officially participate in the workshop challenge (This is the first mission of the whole workshop) + +| Emoji | Name | Github Username | Occupations | +| ----- | -------- | ----------------------------------------------- | ---------------- | +| 🐉 | Wade | [groundedsage](https://github.com/groundedsage) | OpenGuild Member | ## (Optional) Setup environment and register for the challenges TLDR: If you are not familiar with Git & Github, follow these steps below to fork and setup your own repository. diff --git a/challenge-1-vesting/README.md b/challenge-1-vesting/README.md index 9cc7a2c..497697e 100644 --- a/challenge-1-vesting/README.md +++ b/challenge-1-vesting/README.md @@ -10,7 +10,7 @@ Add your information to the below list to officially participate in the workshop | Emoji | Name | Github Username | Occupations | | ----- | ---- | ------------------------------------- | ----------- | -| 🎅 | Ippo | [NTP-996](https://github.com/NTP-996) | DevRel | +| 🐉 | Wade | [groundedsage](https://github.com/groundedsage) | OpenGuild Member | ## 💻 Local development environment setup diff --git a/challenge-1-vesting/contracts/TokenVesting.sol b/challenge-1-vesting/contracts/TokenVesting.sol index 43d4c3a..6a754ec 100644 --- a/challenge-1-vesting/contracts/TokenVesting.sol +++ b/challenge-1-vesting/contracts/TokenVesting.sol @@ -30,19 +30,20 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { struct VestingSchedule { - // TODO: Define the vesting schedule struct + uint256 totalAmount; + uint256 startTime; + uint256 cliffDuration; + uint256 vestingDuration; + uint256 claimedAmount; + bool revoked; } - // Token being vested - // TODO: Add state variables + IERC20 public token; + mapping(address => VestingSchedule) public vestingSchedules; + mapping(address => bool) public whitelist; - // Mapping from beneficiary to vesting schedule - // TODO: Add state variables - - // Whitelist of beneficiaries - // TODO: Add state variables - + // Events event VestingScheduleCreated(address indexed beneficiary, uint256 amount); event TokensClaimed(address indexed beneficiary, uint256 amount); @@ -51,8 +52,8 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { event BeneficiaryRemovedFromWhitelist(address indexed beneficiary); constructor(address tokenAddress) { - // TODO: Initialize the contract - + require(tokenAddress != address(0), "Invalid token address"); + token = IERC20(tokenAddress); } // Modifier to check if beneficiary is whitelisted @@ -79,22 +80,81 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { uint256 vestingDuration, uint256 startTime ) external onlyOwner onlyWhitelisted(beneficiary) whenNotPaused { - // TODO: Implement vesting schedule creation + require(amount > 0, "Amount must be greater than 0"); + require(vestingDuration >= cliffDuration, "Vesting duration must be >= cliff duration"); + + vestingSchedules[beneficiary] = VestingSchedule({ + totalAmount: amount, + startTime: startTime, + cliffDuration: cliffDuration, + vestingDuration: vestingDuration, + claimedAmount: 0, + revoked: false + }); + + require(token.transferFrom(msg.sender, address(this), amount), "Token transfer failed"); + + emit VestingScheduleCreated(beneficiary, amount); } - function calculateVestedAmount( - address beneficiary - ) public view returns (uint256) { - // TODO: Implement vested amount calculation + function calculateVestedAmount(address beneficiary) public view returns (uint256) { + VestingSchedule memory schedule = vestingSchedules[beneficiary]; + uint256 currentTime = block.timestamp; + uint256 vested; + + if (currentTime < schedule.startTime + schedule.cliffDuration) { + vested = 0; + } else if (currentTime >= schedule.startTime + schedule.vestingDuration) { + vested = schedule.totalAmount; + } else { + uint256 timePassed = currentTime - schedule.startTime; + vested = (schedule.totalAmount * timePassed) / schedule.vestingDuration; + } + + if (vested < schedule.claimedAmount) { + return 0; + } + + return vested - schedule.claimedAmount; } function claimVestedTokens() external nonReentrant whenNotPaused { - // TODO: Implement token claiming + VestingSchedule storage schedule = vestingSchedules[msg.sender]; + + require(!schedule.revoked, "Vesting has been revoked"); + + uint256 claimableAmount = calculateVestedAmount(msg.sender); + require(claimableAmount > 0, "No tokens to claim"); + + schedule.claimedAmount += claimableAmount; + + require(token.transfer(msg.sender, claimableAmount), "Token transfer failed"); + + emit TokensClaimed(msg.sender, claimableAmount); } function revokeVesting(address beneficiary) external onlyOwner { - // TODO: Implement vesting revocation + VestingSchedule storage schedule = vestingSchedules[beneficiary]; + require(!schedule.revoked, "Vesting already revoked"); + + uint256 vestedSoFar; + uint256 currentTime = block.timestamp; + + if (currentTime < schedule.startTime + schedule.cliffDuration) { + vestedSoFar = 0; + } else if (currentTime >= schedule.startTime + schedule.vestingDuration) { + vestedSoFar = schedule.totalAmount; + } else { + vestedSoFar = (schedule.totalAmount * (currentTime - schedule.startTime)) / schedule.vestingDuration; + } + + uint256 unvestedTokens = schedule.totalAmount - vestedSoFar; + schedule.revoked = true; + + require(token.transfer(owner(), unvestedTokens), "Token transfer failed"); + + emit VestingRevoked(beneficiary); } function pause() external onlyOwner { @@ -109,6 +169,8 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { /* Solution template (key points to implement): + + 1. VestingSchedule struct should contain: - Total amount - Start time diff --git a/challenge-2-yield-farm/contracts/yeild.sol b/challenge-2-yield-farm/contracts/yeild.sol index 421496a..5282a6e 100644 --- a/challenge-2-yield-farm/contracts/yeild.sol +++ b/challenge-2-yield-farm/contracts/yeild.sol @@ -70,7 +70,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 +88,20 @@ 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; + } + return rewardPerTokenStored + (((block.timestamp - lastUpdateTime) * 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 newReward = (user.amount * (rewardPerToken() - user.rewardDebt)) / 1e18; + + uint256 boost = calculateBoostMultiplier(_user); + newReward = (newReward * boost) / 100; + return user.pendingRewards + newReward; } /** @@ -105,12 +109,28 @@ 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"); + + // Update the reward information for the user + updateReward(msg.sender); + + // If the user has no previous stake, set the start time + if (userInfo[msg.sender].amount == 0) { + userInfo[msg.sender].startTime = block.timestamp; + } + + // Transfer LP tokens from the user to this contract + lpToken.transferFrom(msg.sender, address(this), _amount); + + // Increase the user's staked amount and update the global total + userInfo[msg.sender].amount += _amount; + totalStaked += _amount; + + // Update reward debt to the new total amount + userInfo[msg.sender].rewardDebt = (userInfo[msg.sender].amount * rewardPerTokenStored) / 1e18; + + // Emit the staking event + emit Staked(msg.sender, _amount); } /** @@ -118,35 +138,53 @@ 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 + updateReward(msg.sender); + + require(userInfo[msg.sender].amount >= _amount, "Insufficient balance"); + + userInfo[msg.sender].amount -= _amount; + totalStaked -= _amount; + + userInfo[msg.sender].rewardDebt = (userInfo[msg.sender].amount * rewardPerTokenStored) / 1e18; + + 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); + + uint256 rewards = userInfo[msg.sender].pendingRewards; + require(rewards > 0, "No rewards to claim"); + + userInfo[msg.sender].pendingRewards = 0; + + rewardToken.transfer(msg.sender, rewards); + + emit RewardsClaimed(msg.sender, rewards); } /** * @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 user = userInfo[msg.sender]; + uint256 amount = user.amount; + require(amount > 0, "No tokens to withdraw"); + + user.amount = 0; + user.pendingRewards = 0; + user.rewardDebt = 0; + + totalStaked -= amount; + + lpToken.transfer(msg.sender, amount); + + emit EmergencyWithdrawn(msg.sender, amount); } /** @@ -157,10 +195,15 @@ 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 stakedDuration = block.timestamp - userInfo[_user].startTime; + if (stakedDuration >= BOOST_THRESHOLD_3) { + return 200; // 2x boost + } else if (stakedDuration >= BOOST_THRESHOLD_2) { + return 150; // 1.5x boost + } else if (stakedDuration >= BOOST_THRESHOLD_1) { + return 125; // 1.25x boost + } + return 100; // 1x boost (no boost) } /** @@ -168,10 +211,9 @@ 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; } /** diff --git a/challenge-2-yield-farm/package-lock.json b/challenge-2-yield-farm/package-lock.json index 4cbdfb0..268f2a7 100644 --- a/challenge-2-yield-farm/package-lock.json +++ b/challenge-2-yield-farm/package-lock.json @@ -9,11 +9,12 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@openzeppelin/contracts": "^5.1.0" + "@openzeppelin/contracts": "^5.1.0", + "dotenv": "^16.4.7" }, "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^5.0.0", - "hardhat": "^2.22.17" + "hardhat": "^2.22.19" } }, "node_modules/@adraffy/ens-normalize": { @@ -1069,28 +1070,28 @@ } }, "node_modules/@nomicfoundation/edr": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.6.5.tgz", - "integrity": "sha512-tAqMslLP+/2b2sZP4qe9AuGxG3OkQ5gGgHE4isUuq6dUVjwCRPFhAOhpdFl+OjY5P3yEv3hmq9HjUGRa2VNjng==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.8.0.tgz", + "integrity": "sha512-dwWRrghSVBQDpt0wP+6RXD8BMz2i/9TI34TcmZqeEAZuCLei3U9KZRgGTKVAM1rMRvrpf5ROfPqrWNetKVUTag==", "dev": true, "license": "MIT", "dependencies": { - "@nomicfoundation/edr-darwin-arm64": "0.6.5", - "@nomicfoundation/edr-darwin-x64": "0.6.5", - "@nomicfoundation/edr-linux-arm64-gnu": "0.6.5", - "@nomicfoundation/edr-linux-arm64-musl": "0.6.5", - "@nomicfoundation/edr-linux-x64-gnu": "0.6.5", - "@nomicfoundation/edr-linux-x64-musl": "0.6.5", - "@nomicfoundation/edr-win32-x64-msvc": "0.6.5" + "@nomicfoundation/edr-darwin-arm64": "0.8.0", + "@nomicfoundation/edr-darwin-x64": "0.8.0", + "@nomicfoundation/edr-linux-arm64-gnu": "0.8.0", + "@nomicfoundation/edr-linux-arm64-musl": "0.8.0", + "@nomicfoundation/edr-linux-x64-gnu": "0.8.0", + "@nomicfoundation/edr-linux-x64-musl": "0.8.0", + "@nomicfoundation/edr-win32-x64-msvc": "0.8.0" }, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-darwin-arm64": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.6.5.tgz", - "integrity": "sha512-A9zCCbbNxBpLgjS1kEJSpqxIvGGAX4cYbpDYCU2f3jVqOwaZ/NU761y1SvuCRVpOwhoCXqByN9b7HPpHi0L4hw==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.8.0.tgz", + "integrity": "sha512-sKTmOu/P5YYhxT0ThN2Pe3hmCE/5Ag6K/eYoiavjLWbR7HEb5ZwPu2rC3DpuUk1H+UKJqt7o4/xIgJxqw9wu6A==", "dev": true, "license": "MIT", "engines": { @@ -1098,9 +1099,9 @@ } }, "node_modules/@nomicfoundation/edr-darwin-x64": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.6.5.tgz", - "integrity": "sha512-x3zBY/v3R0modR5CzlL6qMfFMdgwd6oHrWpTkuuXnPFOX8SU31qq87/230f4szM+ukGK8Hi+mNq7Ro2VF4Fj+w==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.8.0.tgz", + "integrity": "sha512-8ymEtWw1xf1Id1cc42XIeE+9wyo3Dpn9OD/X8GiaMz9R70Ebmj2g+FrbETu8o6UM+aL28sBZQCiCzjlft2yWAg==", "dev": true, "license": "MIT", "engines": { @@ -1108,9 +1109,9 @@ } }, "node_modules/@nomicfoundation/edr-linux-arm64-gnu": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.6.5.tgz", - "integrity": "sha512-HGpB8f1h8ogqPHTyUpyPRKZxUk2lu061g97dOQ/W4CxevI0s/qiw5DB3U3smLvSnBHKOzYS1jkxlMeGN01ky7A==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.8.0.tgz", + "integrity": "sha512-h/wWzS2EyQuycz+x/SjMRbyA+QMCCVmotRsgM1WycPARvVZWIVfwRRsKoXKdCftsb3S8NTprqBdJlOmsFyETFA==", "dev": true, "license": "MIT", "engines": { @@ -1118,9 +1119,9 @@ } }, "node_modules/@nomicfoundation/edr-linux-arm64-musl": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.6.5.tgz", - "integrity": "sha512-ESvJM5Y9XC03fZg9KaQg3Hl+mbx7dsSkTIAndoJS7X2SyakpL9KZpOSYrDk135o8s9P9lYJdPOyiq+Sh+XoCbQ==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.8.0.tgz", + "integrity": "sha512-gnWxDgdkka0O9GpPX/gZT3REeKYV28Guyg13+Vj/bbLpmK1HmGh6Kx+fMhWv+Ht/wEmGDBGMCW1wdyT/CftJaQ==", "dev": true, "license": "MIT", "engines": { @@ -1128,9 +1129,9 @@ } }, "node_modules/@nomicfoundation/edr-linux-x64-gnu": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.6.5.tgz", - "integrity": "sha512-HCM1usyAR1Ew6RYf5AkMYGvHBy64cPA5NMbaeY72r0mpKaH3txiMyydcHibByOGdQ8iFLWpyUdpl1egotw+Tgg==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.8.0.tgz", + "integrity": "sha512-DTMiAkgAx+nyxcxKyxFZk1HPakXXUCgrmei7r5G7kngiggiGp/AUuBBWFHi8xvl2y04GYhro5Wp+KprnLVoAPA==", "dev": true, "license": "MIT", "engines": { @@ -1138,9 +1139,9 @@ } }, "node_modules/@nomicfoundation/edr-linux-x64-musl": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.6.5.tgz", - "integrity": "sha512-nB2uFRyczhAvWUH7NjCsIO6rHnQrof3xcCe6Mpmnzfl2PYcGyxN7iO4ZMmRcQS7R1Y670VH6+8ZBiRn8k43m7A==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.8.0.tgz", + "integrity": "sha512-iTITWe0Zj8cNqS0xTblmxPbHVWwEtMiDC+Yxwr64d7QBn/1W0ilFQ16J8gB6RVVFU3GpfNyoeg3tUoMpSnrm6Q==", "dev": true, "license": "MIT", "engines": { @@ -1148,9 +1149,9 @@ } }, "node_modules/@nomicfoundation/edr-win32-x64-msvc": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.6.5.tgz", - "integrity": "sha512-B9QD/4DSSCFtWicO8A3BrsnitO1FPv7axB62wq5Q+qeJ50yJlTmyeGY3cw62gWItdvy2mh3fRM6L1LpnHiB77A==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.8.0.tgz", + "integrity": "sha512-mNRDyd/C3j7RMcwapifzv2K57sfA5xOw8g2U84ZDvgSrXVXLC99ZPxn9kmolb+dz8VMm9FONTZz9ESS6v8DTnA==", "dev": true, "license": "MIT", "engines": { @@ -3383,6 +3384,18 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", @@ -4468,15 +4481,15 @@ } }, "node_modules/hardhat": { - "version": "2.22.17", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.22.17.tgz", - "integrity": "sha512-tDlI475ccz4d/dajnADUTRc1OJ3H8fpP9sWhXhBPpYsQOg8JHq5xrDimo53UhWPl7KJmAeDCm1bFG74xvpGRpg==", + "version": "2.22.19", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.22.19.tgz", + "integrity": "sha512-jptJR5o6MCgNbhd7eKa3mrteR+Ggq1exmE5RUL5ydQEVKcZm0sss5laa86yZ0ixIavIvF4zzS7TdGDuyopj0sQ==", "dev": true, "license": "MIT", "dependencies": { "@ethersproject/abi": "^5.1.2", "@metamask/eth-sig-util": "^4.0.0", - "@nomicfoundation/edr": "^0.6.5", + "@nomicfoundation/edr": "^0.8.0", "@nomicfoundation/ethereumjs-common": "4.0.4", "@nomicfoundation/ethereumjs-tx": "5.0.4", "@nomicfoundation/ethereumjs-util": "9.0.4", diff --git a/challenge-2-yield-farm/package.json b/challenge-2-yield-farm/package.json index 5366ac8..a47d29a 100644 --- a/challenge-2-yield-farm/package.json +++ b/challenge-2-yield-farm/package.json @@ -14,9 +14,10 @@ }, "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^5.0.0", - "hardhat": "^2.22.17" + "hardhat": "^2.22.19" }, "dependencies": { - "@openzeppelin/contracts": "^5.1.0" + "@openzeppelin/contracts": "^5.1.0", + "dotenv": "^16.4.7" } }