Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| 👾 | Daniel | [danielbui12](https://github.com/danielbui12) | Dev |

## 💻 Local development environment setup

Expand Down
167 changes: 91 additions & 76 deletions contracts/TokenVesting.sol
Original file line number Diff line number Diff line change
@@ -1,27 +1,5 @@
// 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.7.0 <0.9.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
Expand All @@ -30,18 +8,23 @@ 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 amountClaimed;
bool revoked;
}

// Token being vested
// TODO: Add state variables

// Token being vested
IERC20 public immutable i_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);
Expand All @@ -50,9 +33,18 @@ 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

error ZeroAddress(string field);
error ZeroValue(string field);
error VestingDurationMustGteThanCliffDuration();
error ExistedVesting();
error TooEarly();
error TransferFailed();
error NoVesting();
error Revoked();

constructor(address vestingToken_) {
if (vestingToken_ == address(0)) revert ZeroAddress("vestingToken_");
i_token = IERC20(vestingToken_);
}

// Modifier to check if beneficiary is whitelisted
Expand All @@ -79,21 +71,85 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard {
uint256 vestingDuration,
uint256 startTime
) external onlyOwner onlyWhitelisted(beneficiary) whenNotPaused {
// TODO: Implement vesting schedule creation
if (beneficiary == address(0)) revert ZeroAddress("beneficiary");
if (amount == 0) revert ZeroValue("amount");
if (vestingDuration == 0) revert ZeroValue("vestingDuration");
if (vestingDuration < cliffDuration) revert VestingDurationMustGteThanCliffDuration();
if (vestingSchedules[beneficiary].totalAmount > 0) revert ExistedVesting();
if (startTime <= block.timestamp) revert TooEarly();

vestingSchedules[beneficiary] = VestingSchedule({
totalAmount: amount,
startTime: startTime,
cliffDuration: cliffDuration,
vestingDuration: vestingDuration,
amountClaimed: 0,
revoked: false
});

if (i_token.allowance(msg.sender, address(this)) < amount) {
i_token.approve(address(this), amount);
}
if (i_token.transferFrom(msg.sender, address(this), amount) == false) {
revert TransferFailed();
}
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 == true) {
return 0;
}

if (block.timestamp < schedule.startTime + schedule.cliffDuration) {
return 0;
}

if (block.timestamp >= schedule.startTime + schedule.vestingDuration) {
return schedule.totalAmount;
}

uint256 timeFromStart = block.timestamp - schedule.startTime;
uint256 vestedAmount = (schedule.totalAmount * timeFromStart) / schedule.vestingDuration;

return vestedAmount;
}

function claimVestedTokens() external nonReentrant whenNotPaused {
// TODO: Implement token claiming
address sender = msg.sender;
VestingSchedule storage schedule = vestingSchedules[sender];
if (schedule.totalAmount == 0) revert NoVesting();
if (schedule.revoked == true) revert Revoked();

uint256 vestedAmount = calculateVestedAmount(sender);
uint256 claimableAmount = vestedAmount - schedule.amountClaimed;
if (claimableAmount == 0) revert ZeroValue("claimableAmount");

schedule.amountClaimed += claimableAmount;
if (i_token.transfer(sender, claimableAmount) == false) revert TransferFailed();

emit TokensClaimed(sender, claimableAmount);
}

function revokeVesting(address beneficiary) external onlyOwner {
// TODO: Implement vesting revocation
VestingSchedule storage schedule = vestingSchedules[beneficiary];
if (schedule.totalAmount == 0) revert NoVesting();
if (schedule.revoked == true) revert Revoked();

uint256 vestedAmount = calculateVestedAmount(beneficiary);
uint256 unclaimedAmount = schedule.totalAmount - vestedAmount;

schedule.revoked = true;

if (unclaimedAmount > 0 && i_token.transfer(owner(), unclaimedAmount) == false) {
revert TransferFailed();
}

emit VestingRevoked(beneficiary);

}

Expand All @@ -105,44 +161,3 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard {
_unpause();
}
}

/*
Solution template (key points to implement):

1. VestingSchedule struct should contain:
- Total amount
- Start time
- Cliff duration
- Vesting duration
- Amount claimed
- Revoked status

2. State variables needed:
- Mapping of beneficiary address to VestingSchedule
- ERC20 token reference
- Owner/admin address

3. createVestingSchedule should:
- Validate input parameters
- Create new vesting schedule
- Transfer tokens to contract
- Emit event

4. calculateVestedAmount should:
- Check if cliff period has passed
- Calculate linear vesting based on time passed
- Account for already claimed tokens
- Handle revoked status

5. claimVestedTokens should:
- Calculate claimable amount
- Update claimed amount
- Transfer tokens
- Emit event

6. revokeVesting should:
- Only allow admin
- Calculate and transfer unvested tokens back
- Mark schedule as revoked
- Emit event
*/
Loading