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 diff --git a/challenge-1-vesting/contracts/TokenVesting.sol b/challenge-1-vesting/contracts/TokenVesting.sol index 43d4c3a..7c4840e 100644 --- a/challenge-1-vesting/contracts/TokenVesting.sol +++ b/challenge-1-vesting/contracts/TokenVesting.sol @@ -30,20 +30,18 @@ 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(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); event VestingRevoked(address indexed beneficiary); @@ -51,11 +49,9 @@ 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 modifier onlyWhitelisted(address beneficiary) { require(whitelist[beneficiary], "Beneficiary not whitelisted"); _; @@ -79,22 +75,78 @@ 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 + }); + + require( + token.transferFrom(msg.sender, address(this), amount), + "Transfer failed" + ); + 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 { @@ -104,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]; + } } /* @@ -145,4 +227,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 +*/