From e64a2d1e7a0789bcabd4922162106e092b2f6982 Mon Sep 17 00:00:00 2001 From: MisterDefender Date: Mon, 1 Jul 2024 23:08:01 +0530 Subject: [PATCH 1/5] Fix: Made vesting inclusive of cliff period --- src/TokenVesting.sol | 84 ++++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/src/TokenVesting.sol b/src/TokenVesting.sol index 890a4e1..33a19e9 100644 --- a/src/TokenVesting.sol +++ b/src/TokenVesting.sol @@ -94,6 +94,7 @@ contract TokenVesting is Owned, ReentrancyGuard { getWithdrawableAmount() >= _amount, "TokenVesting: cannot create vesting schedule because not sufficient tokens" ); + require(_start >= getCurrentTime(), "TokenVesting: Invalid start time for schedule vesting"); require(_duration > 0, "TokenVesting: duration must be > 0"); require(_amount > 0, "TokenVesting: amount must be > 0"); require( @@ -116,10 +117,11 @@ contract TokenVesting is Owned, ReentrancyGuard { 0, false ); - vestingSchedulesTotalAmount = vestingSchedulesTotalAmount + _amount; + vestingSchedulesTotalAmount += _amount; vestingSchedulesIds.push(vestingScheduleId); - uint256 currentVestingCount = holdersVestingCount[_beneficiary]; - holdersVestingCount[_beneficiary] = currentVestingCount + 1; + unchecked { + holdersVestingCount[_beneficiary]++; + } } /** @@ -167,31 +169,31 @@ contract TokenVesting is Owned, ReentrancyGuard { * @param amount the amount to release */ function release( - bytes32 vestingScheduleId, - uint256 amount - ) public nonReentrant onlyIfVestingScheduleNotRevoked(vestingScheduleId) { - VestingSchedule storage vestingSchedule = vestingSchedules[ - vestingScheduleId - ]; - bool isBeneficiary = msg.sender == vestingSchedule.beneficiary; + bytes32 vestingScheduleId, + uint256 amount +) public nonReentrant onlyIfVestingScheduleNotRevoked(vestingScheduleId) { + VestingSchedule storage vestingSchedule = vestingSchedules[vestingScheduleId]; + address beneficiary = vestingSchedule.beneficiary; + + require( + msg.sender == beneficiary || msg.sender == owner, + "TokenVesting: only beneficiary and owner can release vested tokens" + ); + + uint256 vestedAmount = _computeReleasableAmount(vestingSchedule); + require( + vestedAmount >= amount, + "TokenVesting: cannot release tokens, not enough vested tokens" + ); - bool isReleasor = (msg.sender == owner); - require( - isBeneficiary || isReleasor, - "TokenVesting: only beneficiary and owner can release vested tokens" - ); - uint256 vestedAmount = _computeReleasableAmount(vestingSchedule); - require( - vestedAmount >= amount, - "TokenVesting: cannot release tokens, not enough vested tokens" - ); - vestingSchedule.released = vestingSchedule.released + amount; - address payable beneficiaryPayable = payable( - vestingSchedule.beneficiary - ); - vestingSchedulesTotalAmount = vestingSchedulesTotalAmount - amount; - SafeTransferLib.safeTransfer(_token, beneficiaryPayable, amount); + unchecked { + vestingSchedule.released += amount; + vestingSchedulesTotalAmount -= amount; } + + SafeTransferLib.safeTransfer(_token, payable(beneficiary), amount); +} + /** * @dev Returns the number of vesting schedules associated to a beneficiary. @@ -337,21 +339,41 @@ contract TokenVesting is Owned, ReentrancyGuard { ) internal view returns (uint256) { // Retrieve the current time. uint256 currentTime = getCurrentTime(); - // If the current time is before the cliff, no tokens are releasable. - if ((currentTime < vestingSchedule.cliff) || vestingSchedule.revoked) { + // If the current time is before or equals to the cliff, no tokens are releasable. + if ((currentTime <= vestingSchedule.cliff) || vestingSchedule.revoked) { + + /* (block.timestamp) + ------------------------------------------------------------------------------------ + | | | | + Start-Time **Curent Timestamp** Cliff Vesting Duration end + (vesting started) (vesting ended) + */ return 0; } // If the current time is after the vesting period, all tokens are releasable, // minus the amount already released. - else if ( - currentTime >= vestingSchedule.cliff + vestingSchedule.duration + else if ( + /* (block.timestamp) + ------------------------------------------------------------------------------------ + | | | | + Start-Time Cliff Vesting Duration end **Curent Timestamp** + (vesting started) (vesting ended) + */ + currentTime > vestingSchedule.start + vestingSchedule.duration ) { return vestingSchedule.amountTotal - vestingSchedule.released; } // Otherwise, some tokens are releasable. else { + + /* (block.timestamp) + ------------------------------------------------------------------------------------ + | | | | + Start-Time Cliff **Curent Timestamp** Vesting Duration end + (vesting started) (vesting ended) + */ // Compute the number of full vesting periods that have elapsed. - uint256 timeFromStart = currentTime - vestingSchedule.cliff; + uint256 timeFromStart = currentTime - vestingSchedule.start; uint256 secondsPerSlice = vestingSchedule.slicePeriodSeconds; uint256 vestedSlicePeriods = timeFromStart / secondsPerSlice; uint256 vestedSeconds = vestedSlicePeriods * secondsPerSlice; From b0f44a30cafcb7242cdc3e8b3a7418c05d8731ce Mon Sep 17 00:00:00 2001 From: MisterDefender Date: Mon, 1 Jul 2024 23:08:35 +0530 Subject: [PATCH 2/5] Fix: Tests for inclusive cliff period with custom test --- test/TokenVesting.js | 96 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 8 deletions(-) diff --git a/test/TokenVesting.js b/test/TokenVesting.js index b96ff3d..2ad0991 100644 --- a/test/TokenVesting.js +++ b/test/TokenVesting.js @@ -350,12 +350,94 @@ describe("TokenVesting", function () { ).to.be.equal(0); // set time to the cliff - await tokenVesting.setCurrentTime(baseTime + cliff + 200); - + let additionalTS = 200; // extra time after cliff TS milestone + await tokenVesting.setCurrentTime(baseTime + cliff + additionalTS); + // Here cliff + additionalTS because startTime == baseTime + let expectedReleasableAmount = + ((cliff + additionalTS) * amount) / duration; // check that vested amount is greater than 0 at the cliff expect( await tokenVesting.computeReleasableAmount(vestingScheduleId) - ).to.be.equal(20); + ).to.be.equal(expectedReleasableAmount); + }); + + /* + 1000 tokens are allocated for 10 months duration with 3 months cliff period. + For first 3 months, vested / claimable amount should be 0. + On the first day of 4th month, claimable amount should be 300. + */ + it("Should test for custom scenario", async function () { + // deploy vesting contract + const tokenVesting = await TokenVesting.deploy(testToken.address); + await tokenVesting.deployed(); + + // send tokens to vesting contract + const amountToVest = 1000; + await expect(testToken.transfer(tokenVesting.address, amountToVest)) + .to.emit(testToken, "Transfer") + .withArgs(owner.address, tokenVesting.address, amountToVest); + const TEN_MONTHS_IN_SEC = 26_298_240; + const THREE_MONTHS_IN_SEC = 7_889_472; + const ONE_MONTHS_IN_SEC = 2_629_824; + const baseTime = 1622551248; + const beneficiary = addr1; + const startTime = baseTime; + const cliff = THREE_MONTHS_IN_SEC; + const duration = TEN_MONTHS_IN_SEC; + const slicePeriodSeconds = 1; + const revokable = true; + const amount = 1000; + + // create new vesting schedule + await tokenVesting.createVestingSchedule( + beneficiary.address, + startTime, + cliff, + duration, + slicePeriodSeconds, + revokable, + amount + ); + + // compute vesting schedule id + const vestingScheduleId = + await tokenVesting.computeVestingScheduleIdForAddressAndIndex( + beneficiary.address, + 0 + ); + + // check that vested amount is 0 before cliff + expect( + await tokenVesting.computeReleasableAmount(vestingScheduleId) + ).to.be.equal(0); + + // set time to just before the cliff + const justBeforeCliff = startTime + cliff - 1; + await tokenVesting.setCurrentTime(justBeforeCliff); + + // check that vested amount is still 0 just before the cliff + expect( + await tokenVesting.computeReleasableAmount(vestingScheduleId) + ).to.be.equal(0); + + // set time to cliff + const AtCliff = startTime + cliff; + await tokenVesting.setCurrentTime(AtCliff); + + // check that vested amount is still 0 at cliff + expect( + await tokenVesting.computeReleasableAmount(vestingScheduleId) + ).to.be.equal(0); + + // set time to the cliff + one month + let FourMonthAfterVesting = startTime + cliff + ONE_MONTHS_IN_SEC; // Current-TS + await tokenVesting.setCurrentTime(FourMonthAfterVesting); + // Here cliff + ONE_MONTHS_IN_SEC = 4 months + let expectedReleasableAmount = + ((cliff + ONE_MONTHS_IN_SEC) * amount) / duration; + expect( + await tokenVesting.computeReleasableAmount(vestingScheduleId) + ).to.be.equal(expectedReleasableAmount); }); it("Should vest tokens correctly with a 6-months cliff and 12-months duration", async function () { @@ -435,7 +517,6 @@ describe("TokenVesting", function () { await tokenVesting.computeReleasableAmount(vestingScheduleId) ).to.be.equal(amount); }); - }); describe("Withdraw", function () { @@ -464,9 +545,9 @@ describe("TokenVesting", function () { await tokenVesting.deployed(); await testToken.transfer(tokenVesting.address, 1000); - await expect( - tokenVesting.withdraw(1500) - ).to.be.revertedWith("TokenVesting: not enough withdrawable funds"); + await expect(tokenVesting.withdraw(1500)).to.be.revertedWith( + "TokenVesting: not enough withdrawable funds" + ); }); it("Should emit a Transfer event when withdrawing tokens", async function () { @@ -487,6 +568,5 @@ describe("TokenVesting", function () { await tokenVesting.withdraw(300); expect(await tokenVesting.getWithdrawableAmount()).to.equal(700); }); - }); }); From c77a07293e6c82fbe3c069ea786980f30629515c Mon Sep 17 00:00:00 2001 From: MisterDefender Date: Mon, 1 Jul 2024 23:26:27 +0530 Subject: [PATCH 3/5] Fix: Refactor hardhat.config.js, add dotenv, sepolia network support --- hardhat.config.js | 23 +++++++++++++++++++++-- package.json | 6 ++++-- yarn.lock | 5 +++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/hardhat.config.js b/hardhat.config.js index cb49d0f..689bb65 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -7,6 +7,7 @@ require("hardhat-tracer"); require("hardhat-gas-reporter"); require("solidity-coverage"); require("hardhat-preprocessor"); +require('dotenv').config() const fs = require("fs"); const etherscanApiKey = getEtherscanApiKey(); @@ -52,8 +53,9 @@ module.exports = { }), }, networks: { - mainnet: mainnetNetworkConfig(), - goerli: goerliNetworkConfig(), + // mainnet: mainnetNetworkConfig(), + // goerli: goerliNetworkConfig(), + sepolia: sepoliaNetworkConfig() }, abiExporter: { path: "./build/abi", @@ -109,6 +111,23 @@ function goerliNetworkConfig() { accounts: [accountPrivateKey], }; } +function sepoliaNetworkConfig() { + // console.log("Hello"); + let url = "https://goerli.infura.io/v3/"; + let accountPrivateKey = + "0x0000000000000000000000000000000000000000000000000000000000000000"; + if (process.env.SEPOLIA_ENDPOINT) { + url = `${process.env.SEPOLIA_ENDPOINT}`; + } + + if (process.env.SEPOLIA_PRIVATE_KEY) { + accountPrivateKey = `${process.env.SEPOLIA_PRIVATE_KEY}`; + } + return { + url: url, + accounts: [accountPrivateKey], + }; +} function getEtherscanApiKey() { let apiKey = ""; diff --git a/package.json b/package.json index bbfbc40..6737e10 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "flatten": "npx hardhat flatten", "check:format": "solhint contracts/*.sol --fix", "deploy:mainnet": "npx hardhat run scripts/deploy.js --network mainnet", - "deploy:goerli": "npx hardhat run scripts/deploy.js --network goerli" + "deploy:goerli": "npx hardhat run scripts/deploy.js --network goerli", + "deploy:sepolia": "npx hardhat run scripts/deploy.js --network sepolia" }, "files": [ "build/abi", @@ -36,6 +37,7 @@ "homepage": "https://github.com/abdelhamidbakhta/token-vesting-contracts#readme", "dependencies": { "@openzeppelin/contracts": "^5.0.1", + "dotenv": "^16.4.5", "prettier": "^2.3.0", "solc": "npm:solc@^0.8.4", "solidity-coverage": "^0.7.16", @@ -51,7 +53,6 @@ "@nomiclabs/hardhat-etherscan": "^3.1.7", "@nomiclabs/hardhat-solhint": "^2.0.0", "@nomiclabs/hardhat-waffle": "^2.0.1", - "hardhat-preprocessor": "^0.1.5", "chai": "^4.3.4", "ethereum-waffle": "^3.3.0", "ethers": "^5.2.0", @@ -59,6 +60,7 @@ "hardhat-abi-exporter": "^2.2.1", "hardhat-docgen": "^1.1.1", "hardhat-gas-reporter": "^1.0.4", + "hardhat-preprocessor": "^0.1.5", "hardhat-tracer": "^1.0.0-alpha.6" } } diff --git a/yarn.lock b/yarn.lock index b3d5a31..25db34c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4725,6 +4725,11 @@ dot-prop@^6.0.1: dependencies: is-obj "^2.0.0" +dotenv@^16.4.5: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + dotignore@~0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz" From aba1e73ac42e7eca2260e509d8115dc8ded99c2f Mon Sep 17 00:00:00 2001 From: MisterDefender Date: Mon, 1 Jul 2024 23:40:46 +0530 Subject: [PATCH 4/5] lint and test for just after the cliff duration --- hardhat.config.js | 4 ++-- test/TokenVesting.js | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/hardhat.config.js b/hardhat.config.js index 689bb65..edff268 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -7,7 +7,7 @@ require("hardhat-tracer"); require("hardhat-gas-reporter"); require("solidity-coverage"); require("hardhat-preprocessor"); -require('dotenv').config() +require("dotenv").config(); const fs = require("fs"); const etherscanApiKey = getEtherscanApiKey(); @@ -55,7 +55,7 @@ module.exports = { networks: { // mainnet: mainnetNetworkConfig(), // goerli: goerliNetworkConfig(), - sepolia: sepoliaNetworkConfig() + sepolia: sepoliaNetworkConfig(), }, abiExporter: { path: "./build/abi", diff --git a/test/TokenVesting.js b/test/TokenVesting.js index 2ad0991..4369e52 100644 --- a/test/TokenVesting.js +++ b/test/TokenVesting.js @@ -429,6 +429,16 @@ describe("TokenVesting", function () { await tokenVesting.computeReleasableAmount(vestingScheduleId) ).to.be.equal(0); + // set time to the cliff + one second + let JustAfterCliffPeriod = startTime + cliff + 1; // Current-TS + await tokenVesting.setCurrentTime(JustAfterCliffPeriod); + let expectedReleasableAmountAfterCliff = Math.round( + ((cliff + 1) * amount) / duration + ); + expect( + await tokenVesting.computeReleasableAmount(vestingScheduleId) + ).to.be.equal(expectedReleasableAmountAfterCliff); + // set time to the cliff + one month let FourMonthAfterVesting = startTime + cliff + ONE_MONTHS_IN_SEC; // Current-TS await tokenVesting.setCurrentTime(FourMonthAfterVesting); From f7b74fec4128da3707d6c73e6eae89c3de265c4e Mon Sep 17 00:00:00 2001 From: MisterDefender Date: Mon, 1 Jul 2024 23:42:38 +0530 Subject: [PATCH 5/5] Doc: added readme.md --- README.md | 126 ++++++++---------------------------------------------- 1 file changed, 17 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index 14a4292..202324f 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,29 @@ -[![Actions Status](https://github.com/abdelhamidbakhta/token-vesting-contracts/workflows/test/badge.svg)](https://github.com/abdelhamidbakhta/token-vesting-contracts/actions/workflows/test.yml) -[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) -[![license](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -[![npm version](https://badge.fury.io/js/erc20-token-vesting.svg)](https://badge.fury.io/js/erc20-token-vesting) - # Token Vesting Contracts +--- -## Overview - -On-Chain vesting scheme enabled by smart contracts. - -`TokenVesting` contract can release its token balance gradually like a typical vesting scheme, with a cliff and vesting period. -The vesting schedules are optionally revocable by the owner. - -## 🎭🧑‍💻 Security audits - -- [Security audit](https://github.com/abdelhamidbakhta/token-vesting-contracts/blob/main/audits/hacken_audit_report.pdf) from [Hacken](https://hacken.io) - -This repository is compatible with both Forge and Hardhat. -Forge needs to be ran (install and build) before Hardhat is used in order to load dependency contracts. -You can find the specific instructions for each tool below. - -### Forge - -#### 📦 Installation - -```console -forge install -``` - -#### ⛏️ Compile - -```console -forge build -``` - -#### 🌡️ Testing - -```console -$ forge test -``` - -### Hardhat - -#### 📦 Installation +This project is a fork of the ERC20 token vesting contract from [AbdelStark's repository](https://github.com/AbdelStark/token-vesting-contracts/tree/main). The current implementation has been modified to make the cliff period inclusive of the vesting duration. -```console -$ yarn -``` - -#### ⛏️ Compile - -```console -$ yarn compile -``` - -This task will compile all smart contracts in the `contracts` directory. -ABI files will be automatically exported in `build/abi` directory. - -#### 📚 Documentation - -Documentation is auto-generated after each build in `docs` directory. - -The generated output is a static website containing smart contract documentation. - -#### 🌡️ Testing - -Note: make sure to have ran forge build and compile before you run tests. - -```console -$ yarn test -``` - -#### 📊 Code coverage - -```console -$ yarn coverage -``` - -The report will be printed in the console and a static website containing full report will be generated in `coverage` directory. - -#### ✨ Code style - -```console -$ yarn prettier -``` - -#### 🐱‍💻 Verify & Publish contract source code +## Overview -```console -$ npx hardhat verify --network mainnet $CONTRACT_ADDRESS $CONSTRUCTOR_ARGUMENTS -``` +In the modified implementation, tokens are not vested during the cliff period, making it inclusive of the vesting duration. For example, if 1000 tokens are allocated for a 10-month duration with a 3-month cliff period, the vested/claimable amount for the first 3 months will be 0. On the first day of the 4th month, the claimable amount should be 300 tokens. -## 📄 License +## Deployed Contracts -**Token Vesting Contracts** is released under the [Apache-2.0](LICENSE). +- **Token Address:** [0x480222Fd55597BB7EFc414a9C6d4E103820520Eb](https://sepolia.etherscan.io/address/0x480222Fd55597BB7EFc414a9C6d4E103820520Eb) +- **TokenVesting Address:** [0xDFf34D3960804DA62734d095419fD19F65229b0C](https://sepolia.etherscan.io/address/0xDFf34D3960804DA62734d095419fD19F65229b0C) +- **Deployer Address:** [0x14E9511285a10950f3c968Fa371F3b87182C9ef5](https://sepolia.etherscan.io/address/0x14E9511285a10950f3c968Fa371F3b87182C9ef5) -## Contributors ✨ +## Modifications -Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): +1. **Cliff Period Inclusion:** + - The logic has been modified to include the cliff period within the vesting duration. - - - - - - - - - - - - -
Abdel @ StarkWare
Abdel @ StarkWare

💻
Vaclav Pavlin
Vaclav Pavlin

💻
Brendan Baker
Brendan Baker

💻
Oren Gampel
Oren Gampel

💻
+2. **Example Scenario:** + - For a 1000 tokens allocation over a 10-month duration with a 3-month cliff period: + - The claimable amount is 0 for the first 3 months. + - On the first day of the 4th month, the claimable amount is 300 tokens. - - +## Testing - +A test case has been written to pass the scenario mentioned above. -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!