Skip to content

Emmanuel Bagoole | Register for OpenGuild Sub0 Challenges #29

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 6 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 |
| 🦄 | Emmanuel Bagoole | ManuelPrhyme | Solidity Smart Contracts Instructor/Auditor |
```

- Step 5: `Commit` your code and push to the forked Github repository
Expand Down
1 change: 1 addition & 0 deletions challenge-1-vesting/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 |
| 🎅 | Manuel | [ManuelPrhyme](https://github.com/ManuelPrhyme) | Smart-Contracts Engineer |

## 💻 Local development environment setup

Expand Down
228 changes: 138 additions & 90 deletions challenge-1-vesting/contracts/TokenVesting.sol
Original file line number Diff line number Diff line change
@@ -1,148 +1,196 @@
// 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";

// Token Address
// Vesting ["VST"] -> 0xD2f45381d4aAd3B29C26E9FD86dAb8B98E2FE238
// Staked ["SKT"] -> 0xcac8028b3361da8729a6ce521503ce88a50a4806
// Vested ["VES"] -> 0x14574a3d70554722a14c0cbc50ab0e7834ac66a5

contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard {
struct VestingSchedule {
// TODO: Define the vesting schedule struct
}

address Owner;

// Token being vested
// TODO: Add state variables
IERC20 public Vestings;
IERC20 internal Staked;
IERC20 internal Vested;

//Vesting schedule for each beneficiary
struct VestingSchedule {
uint256 totalAmount;
uint256 startTime;
uint256 cliffDuration;
uint256 vestingDuration;
uint256 amountClaimed;
string Token;
bool revokedStatus;
}

// Mapping from beneficiary to vesting schedule
// TODO: Add state variables
//Mapping from beneficiary to vesting schedule created
mapping(address=>VestingSchedule) public beneficiaryScheduled;

// Whitelist of beneficiaries
// TODO: Add state variables
//Whitelist of beneficiaries
mapping(address=>bool) public beneficiaryWhitelist;

// Events
event VestingScheduleCreated(address indexed beneficiary, uint256 amount);
event VestingScheduleCreated(address indexed beneficiary, string token, uint256 amount);
event TokensClaimed(address indexed beneficiary, uint256 amount);
event VestingRevoked(address indexed beneficiary);
event BeneficiaryWhitelisted(address indexed beneficiary);
event BeneficiaryRemovedFromWhitelist(address indexed beneficiary);
event BeneficiaryWhitelisted(address indexed beneficiary);
event BeneficiaryRemovedFromWhitelist(address indexed beneficiary);

constructor(address tokenAddress) {
// TODO: Initialize the contract
//constructor functions assigns the actual token addresses to the instances created
constructor(address _vst, address _stk, address _ves){
Vestings = IERC20(_vst);
Staked = IERC20(_stk);
Vested = IERC20(_ves);

Owner = msg.sender;
}

// Modifier to check if beneficiary is whitelisted
modifier onlyWhitelisted(address beneficiary) {
require(whitelist[beneficiary], "Beneficiary not whitelisted");
require(beneficiaryWhitelist[beneficiary], "Beneficiary not whitelisted");
_;
}

function addToWhitelist(address beneficiary) external onlyOwner {
require(beneficiary != address(0), "Invalid address");
whitelist[beneficiary] = true;
beneficiaryWhitelist[beneficiary] = true;
emit BeneficiaryWhitelisted(beneficiary);
}

function removeFromWhitelist(address beneficiary) external onlyOwner {
whitelist[beneficiary] = false;
beneficiaryWhitelist[beneficiary] = false;
emit BeneficiaryRemovedFromWhitelist(beneficiary);
}


function createVestingSchedule(
address beneficiary,
uint256 amount,
uint256 cliffDuration,
uint256 vestingDuration,
uint256 startTime
uint256 startTime,
string calldata tokenChoice
) external onlyOwner onlyWhitelisted(beneficiary) whenNotPaused {
// TODO: Implement vesting schedule creation
//Validation
require(amount > 0,'Stake amount greater than 0');
require( beneficiary != address(0), 'Enter correct address');
require( vestingDuration > cliffDuration, "Enter a valid Vesting duration [Vesting duration must be greater than Cliff duration]");
require(vestingDuration > 0,"Vesting duration must be greater that 0");

//Effects
if(startTime == 0 || startTime <= block.timestamp){
startTime = block.timestamp;
}

//Interactions
//For mutiple token transfer I used an implict references for each token => ['USDT','USDC','DAI']. They could be picked fron a list on the
// interface and then a transaction is senn to this function with that string in calldata, to enable with the conditional logic of calling
// the trasferFrom method from the correct ERC20 contract
if(bytes32(bytes(tokenChoice)) == bytes32('VST')) {
Vestings.transferFrom(msg.sender, address(this),amount);
}else if(bytes32(bytes(tokenChoice)) == bytes32('STK')){
Staked.transferFrom(msg.sender, address(this), amount);
} else if(bytes32(bytes(tokenChoice)) == bytes32('VES')) {
Vested.transferFrom(msg.sender, address(this),amount);
} else {
revert('Please choose one of these ["VST","STK","VES"]');
}

//Note: This is an effect (simply a state mutability statemet) but it is necessary that it comes after an interaction of the contract with external
// signers, because its records determine how much tokens to a particular beneficiary is released later on. Even though it might jeopardise the
// reentracy guard of the contract coming after transfer, the risk of registering a Schedule and not sending actual tokens is just as high. So just incase a
// wrong token's transfer is attempted or somehow the transfer fails, the contract shouldn't create a Vesting Schedule, instead it should revert without the line below being executed.

beneficiaryScheduled[beneficiary] = VestingSchedule(amount,startTime,cliffDuration,vestingDuration,0,tokenChoice,false);

emit VestingScheduleCreated(beneficiary, tokenChoice, amount);
}

function calculateVestedAmount(
address beneficiary
) public view returns (uint256) {
// TODO: Implement vested amount calculation
function calculateVestedAmount() public view returns (uint256) {
require(beneficiaryWhitelist[msg.sender],"Please connect with the vesting address");
require(!beneficiaryScheduled[msg.sender].revokedStatus,"Please initiate a vesting schedule");

address beneficiary = msg.sender;

if(block.timestamp <= beneficiaryScheduled[beneficiary].startTime + beneficiaryScheduled[beneficiary].cliffDuration){
return 0;
}

if(block.timestamp >= beneficiaryScheduled[beneficiary].startTime + beneficiaryScheduled[beneficiary].vestingDuration){
return beneficiaryScheduled[beneficiary].totalAmount;

}

uint timeCount = block.timestamp - beneficiaryScheduled[beneficiary].cliffDuration;

return (beneficiaryScheduled[beneficiary].totalAmount * timeCount) / beneficiaryScheduled[beneficiary].vestingDuration;

}

function claimVestedTokens() external nonReentrant whenNotPaused {
// TODO: Implement token claiming
require(beneficiaryWhitelist[msg.sender],"Please connect with the vesting address");
require(!beneficiaryScheduled[msg.sender].revokedStatus,"Please initiate a vesting schedule");
require(beneficiaryScheduled[msg.sender].amountClaimed < beneficiaryScheduled[msg.sender].totalAmount ,"All claimable amount has been withdrawn");

uint claimableAmount = calculateVestedAmount() - beneficiaryScheduled[msg.sender].amountClaimed;
beneficiaryScheduled[msg.sender].amountClaimed = claimableAmount;

require(claimableAmount > 0,"All claimble tokens have been claimed");

string memory VestedToken = beneficiaryScheduled[msg.sender].Token;

if(bytes32(bytes(VestedToken)) == bytes32('VST')){
require(Vestings.transfer(msg.sender,claimableAmount),'Transaction failed');

} else if(bytes32(bytes(VestedToken)) == bytes32('STK')){
require(Staked.transfer(msg.sender,claimableAmount),'Transaction failed');

} else {
require(Vested.transfer(msg.sender,claimableAmount),'Transaction failed');

}

emit TokensClaimed(msg.sender, claimableAmount);
}

function revokeVesting(address beneficiary) external onlyOwner {
// TODO: Implement vesting revocation
function revokeVesting() external onlyOwner {
require(beneficiaryWhitelist[msg.sender],"Please connect with the vesting address");
require(!beneficiaryScheduled[msg.sender].revokedStatus,"No vesting schedule found");

uint Unvested = beneficiaryScheduled[msg.sender].totalAmount - calculateVestedAmount();
string memory VestedToken = beneficiaryScheduled[msg.sender].Token;

if (Unvested > 0){
if(bytes32(bytes(VestedToken)) == bytes32('VST')){
require(Vestings.transfer(msg.sender,Unvested),'Transaction failed');

} else if(bytes32(bytes(VestedToken)) == bytes32('STK')){
require(Staked.transfer(msg.sender,Unvested),'Transaction failed');

} else {
require(Vested.transfer(msg.sender,Unvested),'Transaction failed'); }
}

beneficiaryScheduled[msg.sender].revokedStatus = true;

emit VestingRevoked(msg.sender);
}

function pause() external onlyOwner {
function Pause() external onlyOwner {
_pause();
}

function unpause() external onlyOwner {
function Unpause() external onlyOwner {
_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