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
2 changes: 1 addition & 1 deletion challenge-1-vesting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,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 |
| 🎅 | Chuhsin Chen | [bamboochen92518](https://github.com/bamboochen92518) | Student |

## 💻 Local development environment setup

Expand Down
178 changes: 74 additions & 104 deletions challenge-1-vesting/contracts/TokenVesting.sol
Original file line number Diff line number Diff line change
@@ -1,148 +1,118 @@
// 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;

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";

contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard {
contract TokenVesting is Ownable {
error EnforcedPause();

struct VestingSchedule {
// TODO: Define the vesting schedule struct
uint128 totalAmount;
uint64 startTime;
uint32 cliffDuration;
uint32 vestingDuration;
uint128 amountClaimed;
bool revoked;
}

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


// Mapping from beneficiary to vesting schedule
// TODO: Add state variables
IERC20 public immutable token;
mapping(address => VestingSchedule) public vestingSchedules;
mapping(address => bool) public whitelist;
bool public paused;

// Whitelist of beneficiaries
// TODO: Add state variables

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

constructor(address tokenAddress) {
// TODO: Initialize the contract

event Whitelisted(address indexed beneficiary);
event Unwhitelisted(address indexed beneficiary);
event Paused();
event Unpaused();

constructor(address tokenAddress) Ownable(msg.sender) {
require(tokenAddress != address(0));
token = IERC20(tokenAddress);
}

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

modifier whenNotPaused() {
if (paused) revert EnforcedPause();
_;
}

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

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

function createVestingSchedule(
address beneficiary,
uint256 amount,
uint256 cliffDuration,
uint256 vestingDuration,
uint256 startTime
uint128 amount,
uint32 cliffDuration,
uint32 vestingDuration,
uint64 startTime
) external onlyOwner onlyWhitelisted(beneficiary) whenNotPaused {
// TODO: Implement vesting schedule creation
require(beneficiary != address(0) && amount > 0 && vestingDuration > 0 && cliffDuration <= vestingDuration);
require(vestingSchedules[beneficiary].totalAmount == 0);
require(token.transferFrom(msg.sender, address(this), amount));

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

emit VestingScheduleCreated(beneficiary, amount);
}

function calculateVestedAmount(
address beneficiary
) public view returns (uint256) {
// TODO: Implement vested amount calculation
function calculateVestedAmount(address beneficiary) public view returns (uint256) {
VestingSchedule memory s = vestingSchedules[beneficiary];
if (s.totalAmount == 0 || s.revoked || block.timestamp < s.startTime + s.cliffDuration) return 0;
if (block.timestamp >= s.startTime + s.vestingDuration) return s.totalAmount - s.amountClaimed;
return ((s.totalAmount * (block.timestamp - s.startTime)) / s.vestingDuration) - s.amountClaimed;
}

function claimVestedTokens() external nonReentrant whenNotPaused {
// TODO: Implement token claiming
function claimVestedTokens() external whenNotPaused {
uint256 claimable = calculateVestedAmount(msg.sender);
require(claimable > 0, "No tokens to claim");

vestingSchedules[msg.sender].amountClaimed += uint128(claimable);
require(token.transfer(msg.sender, claimable));
emit TokensClaimed(msg.sender, uint128(claimable));
}

function revokeVesting(address beneficiary) external onlyOwner {
// TODO: Implement vesting revocation
VestingSchedule storage s = vestingSchedules[beneficiary];
require(s.totalAmount > 0 && !s.revoked);

uint256 claimable = calculateVestedAmount(beneficiary);
uint256 unvested = s.totalAmount - s.amountClaimed - claimable;
s.revoked = true;

if (unvested > 0) require(token.transfer(owner(), unvested));
emit VestingRevoked(beneficiary);
}

function pause() external onlyOwner {
_pause();
paused = true;
emit Paused();
}

function unpause() external onlyOwner {
_unpause();
paused = false;
emit Unpaused();
}
}

/*
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
*/
}
14 changes: 11 additions & 3 deletions challenge-1-vesting/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@nomicfoundation/hardhat-ignition";
import "dotenv/config";
import "hardhat-contract-sizer";

const config: HardhatUserConfig = {
solidity: {
Expand All @@ -11,19 +12,26 @@ const config: HardhatUserConfig = {
enabled: true,
runs: 1, // Lower optimization runs for simpler bytecode
},
evmVersion: "london", // Use an older EVM version for better compatibility
viaIR: false, // Disable IR-based compilation
evmVersion: "london", // Try istanbul for better compatibility
viaIR: true,
},
},
networks: {
"asset-hub-westend": {
url: "https://westend-asset-hub-eth-rpc.polkadot.io",
chainId: 420420421,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
gasPrice: "auto",
gasPrice: "auto", // Dynamic pricing to avoid gas issues
gas: 15000000, // High gas limit for deployment
timeout: 100000,
},
},
contractSizer: {
alphaSort: true,
disambiguatePaths: false,
runOnCompile: true,
unit: "B",
},
};

export default config;
Loading