Skip to content

MARIA NAKIBUUKA | Register for Open Guild Sub0 Challenges #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
170 changes: 118 additions & 52 deletions challenge-1-vesting/contracts/TokenVesting.sol
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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();
}
Expand Down Expand Up @@ -145,4 +210,5 @@ Solution template (key points to implement):
- Calculate and transfer unvested tokens back
- Mark schedule as revoked
- Emit event
*/
*/
// token contract address - 0xa3Ff912076cD9Caa4ee2244C4a619D86b6E87a87
2 changes: 1 addition & 1 deletion challenge-1-vesting/contracts/token.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading