Skip to content

marlowl | Register for OpenGuild x Encode Club Solidity Challenges #8

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 3 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
6 changes: 3 additions & 3 deletions challenge-1-vesting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ OpenGuild Labs makes the repository to introduce OpenHack workshop participants

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 |
| ----- | ---- | ------------------------------------- | ----------- |
| 🎅 | Ippo | [NTP-996](https://github.com/NTP-996) | DevRel |
| Emoji | Name | Github Username | Occupations |
| ----- | ------- | ------------------------------------- | ----------------- |
| 🔧 | marlowl | [marlowl](https://github.com/marlowl) | Software Engineer |

## 💻 Local development environment setup

Expand Down
197 changes: 121 additions & 76 deletions challenge-1-vesting/contracts/TokenVesting.sol
Original file line number Diff line number Diff line change
@@ -1,25 +1,3 @@
// 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;

Expand All @@ -30,18 +8,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; // Total amount of tokens to be vested
uint256 startTime; // Start time of the vesting schedule
uint256 cliffDuration; // Duration of cliff in seconds
uint256 vestingDuration; // Duration of vesting in seconds
uint256 claimedAmount; // Amount of tokens already claimed
bool revoked; // Whether the vesting has been revoked
}

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

IERC20 public token;

// Mapping from beneficiary to vesting schedule
// TODO: Add state variables
// Mapping from beneficiary to their vesting schedule
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 @@ -51,8 +33,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
Expand All @@ -79,22 +61,126 @@ 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 beneficiary address");
require(amount > 0, "Vesting amount must be greater than 0");
require(vestingDuration > 0, "Vesting duration must be greater than 0");
require(
cliffDuration <= vestingDuration,
"Cliff duration must be less than or equal to vesting duration"
);

// Use current time if startTime is 0 or in the past
uint256 effectiveStartTime = startTime == 0 ||
startTime < block.timestamp
? block.timestamp
: startTime;

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

// Transfer tokens from sender to contract
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
VestingSchedule memory schedule = vestingSchedules[beneficiary];

// Return 0 if the schedule has been revoked or doesn't exist
if (schedule.revoked || schedule.totalAmount == 0) {
return 0;
}

uint256 currentTime = block.timestamp;
uint256 vestedAmount;
uint256 cliffEndTime = schedule.startTime + schedule.cliffDuration;
uint256 vestingEndTime = schedule.startTime + schedule.vestingDuration;

// Return 0 if the cliff hasn't been reached
if (currentTime < cliffEndTime) {
vestedAmount = 0;
}
// If vesting is completed, return the total amount
else if (currentTime >= vestingEndTime) {
vestedAmount = schedule.totalAmount;
}
// Calculate linearly vested amount based on time passed
else {
uint256 timeFromStart = currentTime - schedule.startTime;
vestedAmount =
(schedule.totalAmount * timeFromStart) /
schedule.vestingDuration;
}

// Return available amount to claim
if (vestedAmount <= schedule.claimedAmount) {
return 0;
}

return vestedAmount - 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");
require(schedule.totalAmount > 0, "No vesting schedule found");

uint256 currentTime = block.timestamp;
uint256 vestedAmount;
uint256 cliffEndTime = schedule.startTime + schedule.cliffDuration;
uint256 vestingEndTime = schedule.startTime + schedule.vestingDuration;

if (currentTime < cliffEndTime) {
vestedAmount = 0;
} else if (currentTime >= vestingEndTime) {
vestedAmount = schedule.totalAmount;
} else {
vestedAmount =
(schedule.totalAmount * (currentTime - schedule.startTime)) /
schedule.vestingDuration;
}

uint256 unvestedAmount = schedule.totalAmount - vestedAmount;

// Mark schedule as revoked
schedule.revoked = true;

// Return unvested tokens to owner
require(
token.transfer(owner(), unvestedAmount),
"Token transfer failed"
);

emit VestingRevoked(beneficiary);
}

function pause() external onlyOwner {
Expand All @@ -105,44 +191,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