From a3ecc3e5fcf35af8ad739f5ef2254027b1c8304c Mon Sep 17 00:00:00 2001 From: Niklas P Date: Mon, 14 Apr 2025 16:06:34 +0200 Subject: [PATCH 1/3] Register for OpenGuild Lost Tribes Challenges --- challenge-1-vesting/.prettierignore | 1 + challenge-1-vesting/README.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 challenge-1-vesting/.prettierignore diff --git a/challenge-1-vesting/.prettierignore b/challenge-1-vesting/.prettierignore new file mode 100644 index 0000000..fa29cdf --- /dev/null +++ b/challenge-1-vesting/.prettierignore @@ -0,0 +1 @@ +** \ No newline at end of file diff --git a/challenge-1-vesting/README.md b/challenge-1-vesting/README.md index 9cc7a2c..e1b987b 100644 --- a/challenge-1-vesting/README.md +++ b/challenge-1-vesting/README.md @@ -11,6 +11,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 | +| 🫐 | niftesty | [niklasp](https://github.com/niklasp) | Fullstack Web3 Dev | ## 💻 Local development environment setup From fc2a441e26fb98fca074fc2be0b6675e727f9972 Mon Sep 17 00:00:00 2001 From: Niklas P Date: Mon, 14 Apr 2025 16:08:33 +0200 Subject: [PATCH 2/3] solve token vesting --- .../contracts/TokenVesting.sol | 87 ++++++++++++++++--- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/challenge-1-vesting/contracts/TokenVesting.sol b/challenge-1-vesting/contracts/TokenVesting.sol index 43d4c3a..13a64bb 100644 --- a/challenge-1-vesting/contracts/TokenVesting.sol +++ b/challenge-1-vesting/contracts/TokenVesting.sol @@ -30,18 +30,22 @@ 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 cliffDuration; + uint256 vestingDuration; + uint256 startTime; + uint256 amountClaimed; + bool revoked; } // Token being vested - // TODO: Add state variables - + IERC20 public immutable token; // Mapping from beneficiary to vesting schedule - // TODO: Add state variables + mapping(address => VestingSchedule) public vestingSchedules; // Whitelist of beneficiaries - // TODO: Add state variables + mapping(address => bool) public whitelist; // Events event VestingScheduleCreated(address indexed beneficiary, uint256 amount); @@ -51,8 +55,7 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { event BeneficiaryRemovedFromWhitelist(address indexed beneficiary); constructor(address tokenAddress) { - // TODO: Initialize the contract - + token = IERC20(tokenAddress); } // Modifier to check if beneficiary is whitelisted @@ -79,22 +82,82 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { uint256 vestingDuration, uint256 startTime ) external onlyOwner onlyWhitelisted(beneficiary) whenNotPaused { - // TODO: Implement vesting schedule creation + require(beneficiary != address(0), "Invalid address"); + require(amount > 0, "Amount must be greater than 0"); + require(cliffDuration > 0, "Cliff duration must be greater than 0"); + require(vestingDuration > 0, "Vesting duration must be greater than 0"); + require( + startTime > block.timestamp, + "Start time must be in the future" + ); + + vestingSchedules[beneficiary] = VestingSchedule({ + totalAmount: amount, + cliffDuration: cliffDuration, + vestingDuration: vestingDuration, + startTime: startTime, + amountClaimed: 0, + revoked: false + }); + + token.transferFrom(msg.sender, address(this), amount); + emit VestingScheduleCreated(beneficiary, amount); } function calculateVestedAmount( address beneficiary ) public view returns (uint256) { - // TODO: Implement vested amount calculation + VestingSchedule memory schedule = vestingSchedules[beneficiary]; + if (schedule.totalAmount == 0 || schedule.revoked) { + return 0; + } + + if (block.timestamp < schedule.startTime + schedule.cliffDuration) { + return 0; + } + + if (block.timestamp >= schedule.startTime + schedule.vestingDuration) { + return schedule.totalAmount - schedule.amountClaimed; + } + + uint256 timeFromStart = block.timestamp - schedule.startTime; + uint256 vestedAmount = (schedule.totalAmount * timeFromStart) / + schedule.vestingDuration; + + return vestedAmount - schedule.amountClaimed; } function claimVestedTokens() external nonReentrant whenNotPaused { - // TODO: Implement token claiming + VestingSchedule storage schedule = vestingSchedules[msg.sender]; + require(schedule.totalAmount > 0, "No vesting schedule"); + require(!schedule.revoked, "Vesting revoked"); + + uint256 vestedAmount = calculateVestedAmount(msg.sender); + require(vestedAmount > 0, "No tokens to claim"); + + schedule.amountClaimed += vestedAmount; + require(token.transfer(msg.sender, vestedAmount), "Transfer failed"); + + emit TokensClaimed(msg.sender, vestedAmount); } function revokeVesting(address beneficiary) external onlyOwner { - // TODO: Implement vesting revocation + VestingSchedule storage schedule = vestingSchedules[beneficiary]; + require(schedule.totalAmount > 0, "No vesting schedule"); + require(!schedule.revoked, "Already revoked"); + + uint256 vestedAmount = calculateVestedAmount(beneficiary); + uint256 unvestedAmount = schedule.totalAmount - + schedule.amountClaimed - + vestedAmount; + schedule.revoked = true; + + if (unvestedAmount > 0) { + require(token.transfer(owner(), unvestedAmount), "Transfer failed"); + } + + emit VestingRevoked(beneficiary); } function pause() external onlyOwner { @@ -145,4 +208,4 @@ Solution template (key points to implement): - Calculate and transfer unvested tokens back - Mark schedule as revoked - Emit event -*/ \ No newline at end of file +*/ From eb3227c339dc73100913cf47890ff669663d2f57 Mon Sep 17 00:00:00 2001 From: Niklas P Date: Mon, 14 Apr 2025 16:37:53 +0200 Subject: [PATCH 3/3] solve challenge 1 --- .../contracts/TokenVesting.sol | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/challenge-1-vesting/contracts/TokenVesting.sol b/challenge-1-vesting/contracts/TokenVesting.sol index 13a64bb..7c4840e 100644 --- a/challenge-1-vesting/contracts/TokenVesting.sol +++ b/challenge-1-vesting/contracts/TokenVesting.sol @@ -38,16 +38,10 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { bool revoked; } - // Token being vested IERC20 public immutable token; - - // Mapping from beneficiary to vesting schedule mapping(address => VestingSchedule) public vestingSchedules; - - // Whitelist of beneficiaries mapping(address => bool) public whitelist; - // Events event VestingScheduleCreated(address indexed beneficiary, uint256 amount); event TokensClaimed(address indexed beneficiary, uint256 amount); event VestingRevoked(address indexed beneficiary); @@ -58,7 +52,6 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { token = IERC20(tokenAddress); } - // Modifier to check if beneficiary is whitelisted modifier onlyWhitelisted(address beneficiary) { require(whitelist[beneficiary], "Beneficiary not whitelisted"); _; @@ -100,7 +93,10 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { revoked: false }); - token.transferFrom(msg.sender, address(this), amount); + require( + token.transferFrom(msg.sender, address(this), amount), + "Transfer failed" + ); emit VestingScheduleCreated(beneficiary, amount); } @@ -108,14 +104,9 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { address beneficiary ) public view returns (uint256) { VestingSchedule memory schedule = vestingSchedules[beneficiary]; - if (schedule.totalAmount == 0 || schedule.revoked) { + if (schedule.totalAmount == 0 || schedule.revoked) return 0; + if (block.timestamp < schedule.startTime + schedule.cliffDuration) return 0; - } - - if (block.timestamp < schedule.startTime + schedule.cliffDuration) { - return 0; - } - if (block.timestamp >= schedule.startTime + schedule.vestingDuration) { return schedule.totalAmount - schedule.amountClaimed; } @@ -123,7 +114,6 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { uint256 timeFromStart = block.timestamp - schedule.startTime; uint256 vestedAmount = (schedule.totalAmount * timeFromStart) / schedule.vestingDuration; - return vestedAmount - schedule.amountClaimed; } @@ -137,7 +127,6 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { schedule.amountClaimed += vestedAmount; require(token.transfer(msg.sender, vestedAmount), "Transfer failed"); - emit TokensClaimed(msg.sender, vestedAmount); } @@ -167,6 +156,36 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { function unpause() external onlyOwner { _unpause(); } + + // View functions + function getVestingSchedule( + address beneficiary + ) + external + view + returns ( + uint256 totalAmount, + uint256 cliffDuration, + uint256 vestingDuration, + uint256 startTime, + uint256 amountClaimed, + bool revoked + ) + { + VestingSchedule memory schedule = vestingSchedules[beneficiary]; + return ( + schedule.totalAmount, + schedule.cliffDuration, + schedule.vestingDuration, + schedule.startTime, + schedule.amountClaimed, + schedule.revoked + ); + } + + function isWhitelisted(address beneficiary) external view returns (bool) { + return whitelist[beneficiary]; + } } /*