From 1d4672845eecb19b1fb935db56f957e55754b214 Mon Sep 17 00:00:00 2001 From: mzxyz <8177474+mzxyz@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:05:33 +1300 Subject: [PATCH 01/16] Update Vesting contract to support mint vtToken --- contracts/root/SQToken.sol | 2 +- contracts/root/VTSQtoken.sol | 38 +++++++++++++++++++++++ contracts/{ => root}/Vesting.sol | 53 ++++++++++++++++++++++++++++---- publish/revertcode.json | 3 ++ 4 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 contracts/root/VTSQtoken.sol rename contracts/{ => root}/Vesting.sol (70%) diff --git a/contracts/root/SQToken.sol b/contracts/root/SQToken.sol index 650d002b..3c1509cf 100644 --- a/contracts/root/SQToken.sol +++ b/contracts/root/SQToken.sol @@ -17,7 +17,7 @@ contract SQToken is ERC20, Ownable, ERC20Burnable { _; } - constructor(address _minter, uint256 totalSupply) ERC20('SubQueryToken', 'SQT') Ownable() { + constructor(address _minter, uint256 totalSupply) ERC20('VTSubQueryToken', 'vtSQT') Ownable() { minter = _minter; _mint(msg.sender, totalSupply); } diff --git a/contracts/root/VTSQtoken.sol b/contracts/root/VTSQtoken.sol new file mode 100644 index 00000000..0f2640c7 --- /dev/null +++ b/contracts/root/VTSQtoken.sol @@ -0,0 +1,38 @@ +// Copyright (C) 2020-2023 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity 0.8.15; + +import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; +import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import '@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol'; +import '@openzeppelin/contracts/access/Ownable.sol'; + +contract VTSQToken is ERC20, Ownable, ERC20Burnable { + using SafeERC20 for IERC20; + address public minter; + + modifier isMinter() { + require(minter == msg.sender, 'Not minter'); + _; + } + + constructor(address _minter, uint256 totalSupply) ERC20('SubQueryToken', 'SQT') Ownable() { + minter = _minter; + _mint(msg.sender, totalSupply); + } + + function mint(address destination, uint256 amount) external isMinter { + _mint(destination, amount); + } + + /// #if_succeeds {:msg "minter should be set"} minter == _minter; + /// #if_succeeds {:msg "owner functionality"} old(msg.sender == address(owner)); + function setMinter(address _minter) external onlyOwner { + minter = _minter; + } + + function getMinter() external view returns (address) { + return minter; + } +} diff --git a/contracts/Vesting.sol b/contracts/root/Vesting.sol similarity index 70% rename from contracts/Vesting.sol rename to contracts/root/Vesting.sol index b5432cb0..59e65f4e 100644 --- a/contracts/Vesting.sol +++ b/contracts/root/Vesting.sol @@ -6,6 +6,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "../interfaces/ISQToken.sol"; contract Vesting is Ownable { using SafeERC20 for IERC20; @@ -17,6 +18,7 @@ contract Vesting is Ownable { } address public token; + address public vtToken; uint256 public vestingStartDate; uint256 public totalAllocation; uint256 public totalClaimed; @@ -25,13 +27,18 @@ contract Vesting is Ownable { mapping(address => uint256) public userPlanId; mapping(address => uint256) public allocations; mapping(address => uint256) public claimed; + mapping(address => uint256) public vtSQTAllocations; event VestingPlanAdded(uint256 planId, uint256 lockPeriod, uint256 vestingPeriod, uint256 initialUnlockPercent); event VestingAllocated(address indexed user, uint256 planId, uint256 allocation); event VestingClaimed(address indexed user, uint256 amount); + event TokenDeposited(address indexed user, uint256 amount); + event TokenWithdrawn(address indexed user, uint256 amount); - constructor(address _token) Ownable() { + constructor(address _token, address _vtToken) Ownable() { require(_token != address(0x0), "G009"); + require(_vtToken != address(0x0), "G009"); + vtToken = _vtToken; token = _token; } @@ -56,6 +63,7 @@ contract Vesting is Ownable { userPlanId[addr] = planId; allocations[addr] = allocation; + vtSQTAllocations[addr] = allocation; totalAllocation += allocation; emit VestingAllocated(addr, planId, allocation); @@ -72,11 +80,13 @@ contract Vesting is Ownable { function depositByAdmin(uint256 amount) external onlyOwner { require(amount > 0, "V007"); + ISQToken(vtToken).mint(address(this), amount); require(IERC20(token).transferFrom(msg.sender, address(this), amount), "V008"); } function withdrawAllByAdmin() external onlyOwner { uint256 amount = IERC20(token).balanceOf(address(this)); + ISQToken(vtToken).burn(amount); require(IERC20(token).transfer(msg.sender, amount), "V008"); } @@ -86,23 +96,54 @@ contract Vesting is Ownable { vestingStartDate = _vestingStartDate; uint256 amount = IERC20(token).balanceOf(address(this)); + uint256 vtTokenAmount = IERC20(vtToken).balanceOf(address(this)); + require(amount == vtTokenAmount, "V013"); require(amount == totalAllocation, "V010"); transferOwnership(address(this)); } + function deposit(uint256 amount) external { + require(amount > 0, "V007"); + + vtSQTAllocations[msg.sender] += amount; + IERC20(vtToken).transferFrom(msg.sender, address(this), amount); + + emit TokenDeposited(msg.sender, amount); + } + + function withdraw(uint256 amount) external { + require(amount > 0, "V007"); + require(vtSQTAllocations[msg.sender] >= amount, "V014"); + + vtSQTAllocations[msg.sender] -= amount; + IERC20(vtToken).transferFrom(address(this), msg.sender, amount); + + emit TokenWithdrawn(msg.sender, amount); + } + function claim() external { require(allocations[msg.sender] != 0, "V011"); - uint256 claimAmount = claimableAmount(msg.sender); - claimed[msg.sender] += claimAmount; - totalClaimed += claimAmount; + uint256 amount = claimableAmount(msg.sender); + require(amount > 0, "V012"); + + ISQToken(vtToken).burn(amount); + vtSQTAllocations[msg.sender] -= amount; + claimed[msg.sender] += amount; + totalClaimed += amount; - require(IERC20(token).transfer(msg.sender, claimAmount), "V008"); - emit VestingClaimed(msg.sender, claimAmount); + require(IERC20(token).transfer(msg.sender, amount), "V008"); + emit VestingClaimed(msg.sender, amount); } function claimableAmount(address user) public view returns (uint256) { + uint256 amount = unlockedAmount(user); + uint256 vtSQTAmount = vtSQTAllocations[msg.sender]; + return vtSQTAmount >= amount ? amount : vtSQTAmount; + } + + function unlockedAmount(address user) public view returns (uint256) { // vesting start date is not set or allocation is empty if (vestingStartDate == 0 || allocations[user] == 0) { return 0; diff --git a/publish/revertcode.json b/publish/revertcode.json index bf1242eb..29e2008f 100644 --- a/publish/revertcode.json +++ b/publish/revertcode.json @@ -170,6 +170,9 @@ "V009": "vesting start date must in the future", "V010": "balance not enough for allocation", "V011": "vesting is not set on the account", + "V012": "no token available to claim", + "V013": "inconsistent amount between SQT and vtSQT", + "V014": "insufficient vtSQT to widthdraw", "OR001": "invalid asset price", "OR002": "not meet the block number limitation", "OR003": "invalid price size change", From 24dfbfc9e32d0fdf781a52562e0c6c2c87612f2a Mon Sep 17 00:00:00 2001 From: mzxyz <8177474+mzxyz@users.noreply.github.com> Date: Tue, 12 Dec 2023 17:31:36 +1300 Subject: [PATCH 02/16] Update token names --- contracts/root/SQToken.sol | 2 +- contracts/root/VTSQtoken.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/root/SQToken.sol b/contracts/root/SQToken.sol index 3c1509cf..650d002b 100644 --- a/contracts/root/SQToken.sol +++ b/contracts/root/SQToken.sol @@ -17,7 +17,7 @@ contract SQToken is ERC20, Ownable, ERC20Burnable { _; } - constructor(address _minter, uint256 totalSupply) ERC20('VTSubQueryToken', 'vtSQT') Ownable() { + constructor(address _minter, uint256 totalSupply) ERC20('SubQueryToken', 'SQT') Ownable() { minter = _minter; _mint(msg.sender, totalSupply); } diff --git a/contracts/root/VTSQtoken.sol b/contracts/root/VTSQtoken.sol index 0f2640c7..907c5264 100644 --- a/contracts/root/VTSQtoken.sol +++ b/contracts/root/VTSQtoken.sol @@ -17,7 +17,7 @@ contract VTSQToken is ERC20, Ownable, ERC20Burnable { _; } - constructor(address _minter, uint256 totalSupply) ERC20('SubQueryToken', 'SQT') Ownable() { + constructor(address _minter, uint256 totalSupply) ERC20('VTSubQueryToken', 'vtSQT') Ownable() { minter = _minter; _mint(msg.sender, totalSupply); } From 1c2112477b9fe64c7fa25d6382d2ed006fe32ebc Mon Sep 17 00:00:00 2001 From: mzxyz <8177474+mzxyz@users.noreply.github.com> Date: Tue, 12 Dec 2023 18:08:08 +1300 Subject: [PATCH 03/16] Update `Vesting Contract` --- contracts/root/Vesting.sol | 33 ++++----------------------------- publish/ABI/Vesting.json | 37 +++++++++++++++++++++++++++++++++++++ scripts/abi.ts | 2 +- src/contracts.ts | 2 +- 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/contracts/root/Vesting.sol b/contracts/root/Vesting.sol index 59e65f4e..7c997341 100644 --- a/contracts/root/Vesting.sol +++ b/contracts/root/Vesting.sol @@ -27,13 +27,10 @@ contract Vesting is Ownable { mapping(address => uint256) public userPlanId; mapping(address => uint256) public allocations; mapping(address => uint256) public claimed; - mapping(address => uint256) public vtSQTAllocations; event VestingPlanAdded(uint256 planId, uint256 lockPeriod, uint256 vestingPeriod, uint256 initialUnlockPercent); event VestingAllocated(address indexed user, uint256 planId, uint256 allocation); event VestingClaimed(address indexed user, uint256 amount); - event TokenDeposited(address indexed user, uint256 amount); - event TokenWithdrawn(address indexed user, uint256 amount); constructor(address _token, address _vtToken) Ownable() { require(_token != address(0x0), "G009"); @@ -63,9 +60,10 @@ contract Vesting is Ownable { userPlanId[addr] = planId; allocations[addr] = allocation; - vtSQTAllocations[addr] = allocation; totalAllocation += allocation; + ISQToken(vtToken).mint(address(this), allocation); + emit VestingAllocated(addr, planId, allocation); } @@ -86,7 +84,6 @@ contract Vesting is Ownable { function withdrawAllByAdmin() external onlyOwner { uint256 amount = IERC20(token).balanceOf(address(this)); - ISQToken(vtToken).burn(amount); require(IERC20(token).transfer(msg.sender, amount), "V008"); } @@ -96,40 +93,18 @@ contract Vesting is Ownable { vestingStartDate = _vestingStartDate; uint256 amount = IERC20(token).balanceOf(address(this)); - uint256 vtTokenAmount = IERC20(vtToken).balanceOf(address(this)); - require(amount == vtTokenAmount, "V013"); require(amount == totalAllocation, "V010"); transferOwnership(address(this)); } - function deposit(uint256 amount) external { - require(amount > 0, "V007"); - - vtSQTAllocations[msg.sender] += amount; - IERC20(vtToken).transferFrom(msg.sender, address(this), amount); - - emit TokenDeposited(msg.sender, amount); - } - - function withdraw(uint256 amount) external { - require(amount > 0, "V007"); - require(vtSQTAllocations[msg.sender] >= amount, "V014"); - - vtSQTAllocations[msg.sender] -= amount; - IERC20(vtToken).transferFrom(address(this), msg.sender, amount); - - emit TokenWithdrawn(msg.sender, amount); - } - function claim() external { require(allocations[msg.sender] != 0, "V011"); uint256 amount = claimableAmount(msg.sender); require(amount > 0, "V012"); - ISQToken(vtToken).burn(amount); - vtSQTAllocations[msg.sender] -= amount; + ISQToken(vtToken).burnFrom(msg.sender, amount); claimed[msg.sender] += amount; totalClaimed += amount; @@ -139,7 +114,7 @@ contract Vesting is Ownable { function claimableAmount(address user) public view returns (uint256) { uint256 amount = unlockedAmount(user); - uint256 vtSQTAmount = vtSQTAllocations[msg.sender]; + uint256 vtSQTAmount = IERC20(vtToken).balanceOf(user); return vtSQTAmount >= amount ? amount : vtSQTAmount; } diff --git a/publish/ABI/Vesting.json b/publish/ABI/Vesting.json index b1ea9e22..fcc50779 100644 --- a/publish/ABI/Vesting.json +++ b/publish/ABI/Vesting.json @@ -5,6 +5,11 @@ "internalType": "address", "name": "_token", "type": "address" + }, + { + "internalType": "address", + "name": "_vtToken", + "type": "address" } ], "stateMutability": "nonpayable", @@ -364,6 +369,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "unlockedAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -396,6 +420,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "vtToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "withdrawAllByAdmin", diff --git a/scripts/abi.ts b/scripts/abi.ts index 3506a5e9..03f7a4cb 100644 --- a/scripts/abi.ts +++ b/scripts/abi.ts @@ -20,7 +20,6 @@ const main = async () => { 'StateChannel', 'Airdropper', 'PermissionedExchange', - 'Vesting', 'ConsumerHost', 'DisputeManager', 'ConsumerRegistry', @@ -32,6 +31,7 @@ const main = async () => { ] const rootContracts = [ 'SQToken', + 'Vesting', 'InflationController', ] const proxyContracts = [ diff --git a/src/contracts.ts b/src/contracts.ts index 8f89c6f0..90927c9f 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -28,7 +28,7 @@ import StakingManager from './artifacts/contracts/StakingManager.sol/StakingMana import StateChannel from './artifacts/contracts/StateChannel.sol/StateChannel.json'; import VSQToken from './artifacts/contracts/VSQToken.sol/VSQToken.json'; import ChildERC20 from './artifacts/contracts/polygon/ChildERC20.sol/ChildERC20.json'; -import Vesting from './artifacts/contracts/Vesting.sol/Vesting.json'; +import Vesting from './artifacts/contracts/root/Vesting.sol/Vesting.json'; import PolygonDestination from './artifacts/contracts/root/PolygonDestination.sol/PolygonDestination.json'; export default { From 2694fb1c5428a3e47de652669bf7db69b6edc5e4 Mon Sep 17 00:00:00 2001 From: mzxyz <8177474+mzxyz@users.noreply.github.com> Date: Tue, 12 Dec 2023 18:08:58 +1300 Subject: [PATCH 04/16] Update mint account --- contracts/root/Vesting.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/root/Vesting.sol b/contracts/root/Vesting.sol index 7c997341..dde86d0d 100644 --- a/contracts/root/Vesting.sol +++ b/contracts/root/Vesting.sol @@ -62,7 +62,7 @@ contract Vesting is Ownable { allocations[addr] = allocation; totalAllocation += allocation; - ISQToken(vtToken).mint(address(this), allocation); + ISQToken(vtToken).mint(addr, allocation); emit VestingAllocated(addr, planId, allocation); } From ef2a6e26ea902f518f9f8aba6df4adb41e62b96a Mon Sep 17 00:00:00 2001 From: mzxyz <8177474+mzxyz@users.noreply.github.com> Date: Tue, 12 Dec 2023 18:11:11 +1300 Subject: [PATCH 05/16] Remove unused revert code --- publish/revertcode.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/publish/revertcode.json b/publish/revertcode.json index 29e2008f..2563bf63 100644 --- a/publish/revertcode.json +++ b/publish/revertcode.json @@ -171,8 +171,6 @@ "V010": "balance not enough for allocation", "V011": "vesting is not set on the account", "V012": "no token available to claim", - "V013": "inconsistent amount between SQT and vtSQT", - "V014": "insufficient vtSQT to widthdraw", "OR001": "invalid asset price", "OR002": "not meet the block number limitation", "OR003": "invalid price size change", From d8910749d497d2b6d05cfb3178f5243a10dd887f Mon Sep 17 00:00:00 2001 From: mzxyz <8177474+mzxyz@users.noreply.github.com> Date: Mon, 18 Dec 2023 20:54:01 +1300 Subject: [PATCH 06/16] Add `vtSQToken` to contract deployment flow --- hardhat.config.ts | 6 + publish/ABI/VTSQToken.json | 428 +++++++++++++++++++++++++++++ scripts/abi.ts | 1 + scripts/config/contracts.config.ts | 2 + scripts/contracts.ts | 6 +- scripts/deployContracts.ts | 10 + src/contracts.ts | 2 + src/types.ts | 2 + 8 files changed, 456 insertions(+), 1 deletion(-) create mode 100644 publish/ABI/VTSQToken.json diff --git a/hardhat.config.ts b/hardhat.config.ts index 01346d79..d3d8eceb 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -91,6 +91,12 @@ task('publishRoot', "verify and publish contracts on etherscan") address: deployment.Vesting.address, constructorArguments: [deployment.SQToken.address], }); + //VTSQToken + console.log(`verify VTSQToken`); + await hre.run("verify:verify", { + address: deployment.VTSQToken.address, + constructorArguments: [constants.AddressZero, ...config.SQToken], + }); //Settings console.log(`verify Settings`); await hre.run("verify:verify", { diff --git a/publish/ABI/VTSQToken.json b/publish/ABI/VTSQToken.json new file mode 100644 index 00000000..21e98a5a --- /dev/null +++ b/publish/ABI/VTSQToken.json @@ -0,0 +1,428 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_minter", + "type": "address" + }, + { + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burnFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getMinter", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "destination", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "minter", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_minter", + "type": "address" + } + ], + "name": "setMinter", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/scripts/abi.ts b/scripts/abi.ts index 5806165f..a18e8475 100644 --- a/scripts/abi.ts +++ b/scripts/abi.ts @@ -33,6 +33,7 @@ const main = async () => { const rootContracts = [ 'SQToken', 'Vesting', + 'VTSQToken', 'InflationController', ] const proxyContracts = [ diff --git a/scripts/config/contracts.config.ts b/scripts/config/contracts.config.ts index 8980873b..14516991 100644 --- a/scripts/config/contracts.config.ts +++ b/scripts/config/contracts.config.ts @@ -4,6 +4,7 @@ export default { mainnet: { InflationController: [10000, '0x34c35136ECe9CBD6DfDf2F896C6e29be01587c0C'], // inflationRate, inflationDestination SQToken: [utils.parseEther("10000000000")], // initial supply 10 billion + VTSQToken: [0], // initial supply 0 Staking: [1209600, 1e3], // lockPeriod, unbondFeeRate Airdropper: ['0x34c35136ECe9CBD6DfDf2F896C6e29be01587c0C'], // settle destination EraManager: [604800], // 7 day @@ -28,6 +29,7 @@ export default { testnet: { InflationController: [10000, '0x4ae8fcdddc859e2984ce0b8f4ef490d61a7a9b7f'], // inflationRate, inflationDestination SQToken: [utils.parseEther("10000000000")], // initial supply 10 billion + VTSQToken: [0], // initial supply 0 Staking: [1000, 1e3], // lockPeriod, unbondFeeRate Airdropper: ['0x4ae8fcdddc859e2984ce0b8f4ef490d61a7a9b7f'], // settle destination EraManager: [3600], // 1 hour diff --git a/scripts/contracts.ts b/scripts/contracts.ts index 98181dd1..a8e302ee 100644 --- a/scripts/contracts.ts +++ b/scripts/contracts.ts @@ -1,6 +1,6 @@ import { Provider } from '@ethersproject/abstract-provider'; import { Wallet } from '@ethersproject/wallet'; -import { BaseContract, BigNumber, ContractFactory, Signer } from 'ethers'; +import { BaseContract, ContractFactory, Signer } from 'ethers'; import CONTRACTS from '../src/contracts'; @@ -64,6 +64,8 @@ import { PolygonDestination, PolygonDestination__factory, ChildERC20__factory, + VTSQToken, + VTSQToken__factory, } from '../src'; export interface FactoryContstructor { @@ -97,6 +99,7 @@ export type Contracts = { permissionedExchange: PermissionedExchange; tokenExchange: TokenExchange; vesting: Vesting; + vtSQToken: VTSQToken; consumerHost: ConsumerHost; disputeManager: DisputeManager; consumerRegistry: ConsumerRegistry; @@ -138,6 +141,7 @@ export const CONTRACT_FACTORY: Record<ContractName, FactoryContstructor> = { VSQToken: VSQToken__factory, Airdropper: Airdropper__factory, Vesting: Vesting__factory, + VTSQToken: VTSQToken__factory, Staking: Staking__factory, StakingManager: StakingManager__factory, EraManager: EraManager__factory, diff --git a/scripts/deployContracts.ts b/scripts/deployContracts.ts index 4ab78832..6fab2ed3 100644 --- a/scripts/deployContracts.ts +++ b/scripts/deployContracts.ts @@ -43,6 +43,7 @@ import { TokenExchange, PolygonDestination, RootChainManager__factory, + VTSQToken, } from '../src'; import { CONTRACT_FACTORY, @@ -223,12 +224,14 @@ export async function deployRootContracts( }); logger?.info('🤞 SQToken'); + // deploy InflationController const inflationController = await deployContract<InflationController>('InflationController', 'root', { initConfig: [settingsAddress], proxyAdmin, }); logger?.info('🤞 InflationController'); + // setup minter let tx = await sqtToken.setMinter(inflationController.address); await tx.wait(confirms); @@ -236,6 +239,12 @@ export async function deployRootContracts( const vesting = await deployContract<Vesting>('Vesting', 'root', { deployConfig: [deployment.root.SQToken.address] }); logger?.info('🤞 Vesting'); + // deploy VTSQToken + const vtSQToken = await deployContract<VTSQToken>('VTSQToken', 'root', { + deployConfig: [vesting.address, ...config['SQToken']], + }); + logger?.info('🤞 VTSQToken'); + //deploy PolygonDestination contract const polygonDestination = await deployContract<PolygonDestination>('PolygonDestination' as any, 'root', { deployConfig: [settingsAddress, constants.AddressZero] }); @@ -268,6 +277,7 @@ export async function deployRootContracts( { inflationController, rootToken: sqtToken, + vtSQToken, proxyAdmin, vesting, polygonDestination, diff --git a/src/contracts.ts b/src/contracts.ts index 90927c9f..ec7af94e 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -29,6 +29,7 @@ import StateChannel from './artifacts/contracts/StateChannel.sol/StateChannel.js import VSQToken from './artifacts/contracts/VSQToken.sol/VSQToken.json'; import ChildERC20 from './artifacts/contracts/polygon/ChildERC20.sol/ChildERC20.json'; import Vesting from './artifacts/contracts/root/Vesting.sol/Vesting.json'; +import VTSQToken from './artifacts/contracts/root/VTSQToken.sol/VTSQToken.json'; import PolygonDestination from './artifacts/contracts/root/PolygonDestination.sol/PolygonDestination.json'; export default { @@ -55,6 +56,7 @@ export default { PermissionedExchange, TokenExchange, Vesting, + VTSQToken, ConsumerHost, DisputeManager, PriceOracle, diff --git a/src/types.ts b/src/types.ts index 8054a552..3ceb68a9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,6 +32,7 @@ import { StateChannel__factory, VSQToken__factory, Vesting__factory, + VTSQToken__factory, ChildERC20__factory, TokenExchange__factory, PolygonDestination__factory, @@ -101,6 +102,7 @@ export const CONTRACT_FACTORY: Record<ContractName, FactoryContstructor> = { VSQToken: VSQToken__factory, Airdropper: Airdropper__factory, Vesting: Vesting__factory, + VTSQToken: VTSQToken__factory, Staking: Staking__factory, StakingManager: StakingManager__factory, EraManager: EraManager__factory, From 7d99654fba9099d1a767e69f87be714dc0d9e258 Mon Sep 17 00:00:00 2001 From: mzxyz <8177474+mzxyz@users.noreply.github.com> Date: Mon, 18 Dec 2023 21:49:07 +1300 Subject: [PATCH 07/16] Resolve Vesting test issues --- scripts/deployContracts.ts | 16 +++++++++++----- test/Vesting.test.ts | 18 +++++++++--------- test/setup.ts | 1 + 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/scripts/deployContracts.ts b/scripts/deployContracts.ts index 6fab2ed3..387a3c2a 100644 --- a/scripts/deployContracts.ts +++ b/scripts/deployContracts.ts @@ -234,17 +234,23 @@ export async function deployRootContracts( // setup minter let tx = await sqtToken.setMinter(inflationController.address); await tx.wait(confirms); - - //deploy vesting contract - const vesting = await deployContract<Vesting>('Vesting', 'root', { deployConfig: [deployment.root.SQToken.address] }); - logger?.info('🤞 Vesting'); + logger?.info('🤞 Set SQToken minter'); // deploy VTSQToken const vtSQToken = await deployContract<VTSQToken>('VTSQToken', 'root', { - deployConfig: [vesting.address, ...config['SQToken']], + deployConfig: [constants.AddressZero, ...config['SQToken']], }); logger?.info('🤞 VTSQToken'); + //deploy vesting contract + const vesting = await deployContract<Vesting>('Vesting', 'root', { deployConfig: [sqtToken.address, vtSQToken.address] }); + logger?.info('🤞 Vesting'); + + // set vesting contract as the minter of vtSQToken + tx = await vtSQToken.setMinter(vesting.address); + await tx.wait(confirms); + logger?.info('🤞 Set VTSQToken minter'); + //deploy PolygonDestination contract const polygonDestination = await deployContract<PolygonDestination>('PolygonDestination' as any, 'root', { deployConfig: [settingsAddress, constants.AddressZero] }); diff --git a/test/Vesting.test.ts b/test/Vesting.test.ts index 0799961c..cff570b9 100644 --- a/test/Vesting.test.ts +++ b/test/Vesting.test.ts @@ -8,18 +8,21 @@ import { ethers, waffle } from 'hardhat'; import { SQToken, Vesting } from '../src'; import { eventFrom } from "./helper"; import { deployRootContracts } from './setup'; +import { VTSQToken } from "build"; describe('Vesting Contract', () => { const mockProvider = waffle.provider; const [wallet, wallet1, wallet2, wallet3, wallet4] = mockProvider.getWallets(); let token: SQToken; + let vtSQToken: VTSQToken; let vestingContract: Vesting; let lockPeriod: number; let vestingPeriod: number; let initialUnlockPercent = 10; async function claimVesting(wallet: Wallet): Promise<{user: string, amount: BigNumber}> { + await vtSQToken.connect(wallet).increaseAllowance(vestingContract.address, parseEther(10000)); const tx = await vestingContract.connect(wallet).claim(); const evt = await eventFrom(tx, vestingContract, 'VestingClaimed(address,uint256)'); return evt as any; @@ -65,6 +68,7 @@ describe('Vesting Contract', () => { const deployment = await waffle.loadFixture(deployer); token = deployment.rootToken; vestingContract = deployment.vesting; + vtSQToken = deployment.vtSQToken; lockPeriod = 86400 * 30; // 2 month vestingPeriod = 86400 * 365; // 1 year @@ -225,6 +229,7 @@ describe('Vesting Contract', () => { describe('Vesting Claim', () => { const wallet1Allocation = parseEther(1000); const wallet2Allocation = parseEther(3000); + beforeEach(async () => { await vestingContract.depositByAdmin(parseEther(4000)); const planId = await createPlan(lockPeriod, vestingPeriod); @@ -233,6 +238,9 @@ describe('Vesting Contract', () => { [wallet1.address, wallet2.address], [wallet1Allocation, wallet2Allocation] ); + + await vtSQToken.connect(wallet1).increaseAllowance(vestingContract.address, parseEther(1000)); + await vtSQToken.connect(wallet2).increaseAllowance(vestingContract.address, parseEther(3000)); }); it('no claimable amount for invalid condition', async () => { @@ -249,15 +257,7 @@ describe('Vesting Contract', () => { expect(await vestingContract.claimableAmount(wallet2.address)).to.equal(0); }); - it('claim before lock period', async () => {// start vesting - await startVesting(); - let claimable = await vestingContract.claimableAmount(wallet1.address); - expect(claimable).to.eq(0); - const evt = await claimVesting(wallet1); - expect(evt.amount).to.eq(0); - }) - - it('claim during vesting period', async () => {// start vesting + it.only('claim during vesting period', async () => {// start vesting await startVesting(); await timeTravel(lockPeriod + 1001); diff --git a/test/setup.ts b/test/setup.ts index d5e9a7e3..841d996f 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -56,6 +56,7 @@ export const deployRootContracts = async (wallet: Wallet, wallet1: Wallet) => { { InflationController: [1000, wallet1.address], SQToken: [etherParse("10000000000").toString()], + VSQToken: [0], } ); From bb3fed02104af5e9b0b577b8d086fc9aa5fc4d67 Mon Sep 17 00:00:00 2001 From: mzxyz <8177474+mzxyz@users.noreply.github.com> Date: Mon, 18 Dec 2023 22:17:00 +1300 Subject: [PATCH 08/16] Add more test --- scripts/config/contracts.config.ts | 1 + scripts/deployContracts.ts | 2 +- test/Vesting.test.ts | 79 ++++++++++++++++++++++++------ test/setup.ts | 2 +- 4 files changed, 68 insertions(+), 16 deletions(-) diff --git a/scripts/config/contracts.config.ts b/scripts/config/contracts.config.ts index 14516991..a636bea9 100644 --- a/scripts/config/contracts.config.ts +++ b/scripts/config/contracts.config.ts @@ -43,6 +43,7 @@ export default { local: { InflationController: [1000, '0x4ae8fcdddc859e2984ce0b8f4ef490d61a7a9b7f'], // inflationRate, inflationDestination SQToken: [utils.parseEther("10000000000")], // initial supply 10 billion + VTSQToken: [utils.parseEther("0")], // initial supply 0 billion Staking: [1000, 1e3], // lockPeriod, unbondFeeRate Airdropper: ['0x4ae8fcdddc859e2984ce0b8f4ef490d61a7a9b7f'], // settle destination EraManager: [60 * 60], // 1 hour diff --git a/scripts/deployContracts.ts b/scripts/deployContracts.ts index 387a3c2a..786781d6 100644 --- a/scripts/deployContracts.ts +++ b/scripts/deployContracts.ts @@ -238,7 +238,7 @@ export async function deployRootContracts( // deploy VTSQToken const vtSQToken = await deployContract<VTSQToken>('VTSQToken', 'root', { - deployConfig: [constants.AddressZero, ...config['SQToken']], + deployConfig: [constants.AddressZero, ...config['VTSQToken']], }); logger?.info('🤞 VTSQToken'); diff --git a/test/Vesting.test.ts b/test/Vesting.test.ts index cff570b9..a8fb9cbb 100644 --- a/test/Vesting.test.ts +++ b/test/Vesting.test.ts @@ -6,7 +6,7 @@ import { expect } from 'chai'; import { BigNumber } from "ethers"; import { ethers, waffle } from 'hardhat'; import { SQToken, Vesting } from '../src'; -import { eventFrom } from "./helper"; +import { etherParse, eventFrom } from "./helper"; import { deployRootContracts } from './setup'; import { VTSQToken } from "build"; @@ -14,7 +14,7 @@ describe('Vesting Contract', () => { const mockProvider = waffle.provider; const [wallet, wallet1, wallet2, wallet3, wallet4] = mockProvider.getWallets(); - let token: SQToken; + let SQToken: SQToken; let vtSQToken: VTSQToken; let vestingContract: Vesting; let lockPeriod: number; @@ -66,13 +66,13 @@ describe('Vesting Contract', () => { beforeEach(async () => { const deployment = await waffle.loadFixture(deployer); - token = deployment.rootToken; + SQToken = deployment.rootToken; vestingContract = deployment.vesting; vtSQToken = deployment.vtSQToken; lockPeriod = 86400 * 30; // 2 month vestingPeriod = 86400 * 365; // 1 year - await token.approve(vestingContract.address, parseEther(4000)); + await SQToken.approve(vestingContract.address, parseEther(4000)); }); describe('Vesting Plan', () => { @@ -165,10 +165,10 @@ describe('Vesting Contract', () => { describe('Token Manangement By Admin', () => { it('deposit and widthdraw all by admin should work', async () => { await vestingContract.depositByAdmin(1000); - expect(await token.balanceOf(vestingContract.address)).to.eq(1000); + expect(await SQToken.balanceOf(vestingContract.address)).to.eq(1000); await vestingContract.withdrawAllByAdmin(); - expect(await token.balanceOf(vestingContract.address)).to.eq(parseEther(0)); + expect(await SQToken.balanceOf(vestingContract.address)).to.eq(parseEther(0)); }); it('deposit and widthdraw without owner should fail', async () => { @@ -192,6 +192,12 @@ describe('Vesting Contract', () => { ); }); + it('mint vtSQToken should work', async () => { + expect(await vtSQToken.totalSupply()).to.equal(parseEther(4000)); + expect(await vtSQToken.balanceOf(wallet1.address)).to.equal(parseEther(1000)); + expect(await vtSQToken.balanceOf(wallet2.address)).to.equal(parseEther(3000)); + }); + it('set incorrect vesting date should fail', async () => { const latestBlock = await mockProvider.getBlock('latest'); await expect(vestingContract.startVesting(latestBlock.timestamp)).to.be.revertedWith( @@ -200,7 +206,7 @@ describe('Vesting Contract', () => { }); it('start vesting without enough balance should fail', async () => { - expect(await token.balanceOf(vestingContract.address)).to.equal(parseEther(0)); + expect(await SQToken.balanceOf(vestingContract.address)).to.equal(parseEther(0)); const latestBlock = await mockProvider.getBlock('latest'); await expect(vestingContract.startVesting(latestBlock.timestamp + 1000)).to.be.revertedWith( 'V010' @@ -257,7 +263,7 @@ describe('Vesting Contract', () => { expect(await vestingContract.claimableAmount(wallet2.address)).to.equal(0); }); - it.only('claim during vesting period', async () => {// start vesting + it('claim during vesting period', async () => {// start vesting await startVesting(); await timeTravel(lockPeriod + 1001); @@ -279,7 +285,7 @@ describe('Vesting Contract', () => { evt = await claimVesting(wallet1); expect(evt.amount).to.gte(claimable); expect(evt.amount.sub(claimable)).to.lt(errorTolerance); - }) + }); it('claim all together in once', async () => {// start vesting await startVesting(); @@ -307,7 +313,7 @@ describe('Vesting Contract', () => { // wallet1 claim await vestingContract.connect(wallet1).claim(); - const balance1 = await token.balanceOf(wallet1.address); + const balance1 = await SQToken.balanceOf(wallet1.address); expect(balance1).to.gt(claimable1); expect(balance1).to.lt(claimable1.add(parseEther(0.001))); // claim after half vesting period @@ -316,18 +322,63 @@ describe('Vesting Contract', () => { expect(claimable1).to.gte(parseEther(450)); // wallet1 claim await vestingContract.connect(wallet1).claim(); - expect(await token.balanceOf(wallet1.address)).to.gt(balance1.add(claimable1)); - expect(await token.balanceOf(wallet1.address)).to.lt(balance1.add(claimable1).add(parseEther(0.001))); + expect(await SQToken.balanceOf(wallet1.address)).to.gt(balance1.add(claimable1)); + expect(await SQToken.balanceOf(wallet1.address)).to.lt(balance1.add(claimable1).add(parseEther(0.001))); // claim after vesting period await timeTravel(vestingPeriod / 2); await vestingContract.connect(wallet1).claim(); - expect(await token.balanceOf(wallet1.address)).to.eq(parseEther(1000)); + expect(await SQToken.balanceOf(wallet1.address)).to.eq(parseEther(1000)); + }); + + it('should burn equal amount of vtSQToken for claimed SQT', async () => { + // start vesting + await startVesting(); + await timeTravel(lockPeriod + 1001); + // wallet1 claim + expect(await SQToken.balanceOf(wallet1.address)).to.eq(0); + expect(await vtSQToken.balanceOf(wallet1.address)).to.eq(parseEther(1000)); + await vestingContract.connect(wallet1).claim(); + const sqtBalance = await SQToken.balanceOf(wallet1.address); + expect(await vtSQToken.balanceOf(wallet1.address)).to.eq(parseEther(1000).sub(sqtBalance)); }); - it('claim on non-vesting account should fail', async () => { + it('should only claim max amount of VTSQToken', async () => { + // start vesting + await startVesting(); + await timeTravel(lockPeriod + 1001); + // wallet1 + expect(await SQToken.balanceOf(wallet1.address)).to.eq(0); + const unlockAmount = await vestingContract.unlockedAmount(wallet1.address); + // transfer VTSQToken to wallet2 + await vtSQToken.connect(wallet1).transfer(wallet2.address, etherParse('999')); + // unlockAmount > 1 SQT, vtSQToken balance = 1 vtSQT + expect(unlockAmount.gt(etherParse('1'))).to.be.true; + const claimableAmount = etherParse('1'); + expect(await vestingContract.claimableAmount(wallet1.address)).to.eq(claimableAmount); + + // check SQT and VTSQT balance + await vestingContract.connect(wallet1).claim(); + expect(await SQToken.balanceOf(wallet1.address)).to.eq(claimableAmount); + expect(await vtSQToken.balanceOf(wallet1.address)).to.eq(0); + }); + + it('claim with invalid condition should fail', async () => { + // claim on non-vesting account should fail await expect(vestingContract.connect(wallet3).claim()).to.be.revertedWith( 'V011' ); + // claim with zero claimable amount should fail + // # case 1 (not start vesting) + await expect(vestingContract.connect(wallet1).claim()).to.be.revertedWith( + 'V012' + ); + // # case 2 (not enough vtSQT) + await startVesting(); + await timeTravel(lockPeriod + 1001); + await vtSQToken.connect(wallet1).transfer(wallet2.address, etherParse('1000')); + await expect(vestingContract.connect(wallet1).claim()).to.be.revertedWith( + 'V012' + ); }); }); }); diff --git a/test/setup.ts b/test/setup.ts index 841d996f..1c5bf7bd 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -56,7 +56,7 @@ export const deployRootContracts = async (wallet: Wallet, wallet1: Wallet) => { { InflationController: [1000, wallet1.address], SQToken: [etherParse("10000000000").toString()], - VSQToken: [0], + VTSQToken: [0], } ); From 808e6dd001804d050f40c802adc88c0d5bbbd3df Mon Sep 17 00:00:00 2001 From: mzxyz <8177474+mzxyz@users.noreply.github.com> Date: Mon, 18 Dec 2023 22:17:00 +1300 Subject: [PATCH 09/16] Add clean cache to PR pipeline --- .github/workflows/pr.yml | 2 + package.json | 1 + scripts/config/contracts.config.ts | 1 + scripts/deployContracts.ts | 2 +- test/Vesting.test.ts | 79 ++++++++++++++++++++++++------ test/setup.ts | 2 +- 6 files changed, 71 insertions(+), 16 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index be98d8cf..386c3688 100755 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -14,6 +14,8 @@ jobs: with: node-version: 18 - run: yarn + - name: clean cache + run: yarn clean - name: build run: yarn build - name: lint diff --git a/package.json b/package.json index 41285663..43ee9b58 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build:ts": "scripts/build.sh", "build:abi": "ts-node --transpileOnly scripts/abi.ts", "build": "yarn build:contract && yarn build:ts && yarn build:abi", + "clean": "rm -rf build artifacts", "lint": "solhint contracts/**/*.sol --fix", "test": "hardhat test", "test:all": "hardhat test ./test/*.test.ts", diff --git a/scripts/config/contracts.config.ts b/scripts/config/contracts.config.ts index 14516991..a636bea9 100644 --- a/scripts/config/contracts.config.ts +++ b/scripts/config/contracts.config.ts @@ -43,6 +43,7 @@ export default { local: { InflationController: [1000, '0x4ae8fcdddc859e2984ce0b8f4ef490d61a7a9b7f'], // inflationRate, inflationDestination SQToken: [utils.parseEther("10000000000")], // initial supply 10 billion + VTSQToken: [utils.parseEther("0")], // initial supply 0 billion Staking: [1000, 1e3], // lockPeriod, unbondFeeRate Airdropper: ['0x4ae8fcdddc859e2984ce0b8f4ef490d61a7a9b7f'], // settle destination EraManager: [60 * 60], // 1 hour diff --git a/scripts/deployContracts.ts b/scripts/deployContracts.ts index 387a3c2a..786781d6 100644 --- a/scripts/deployContracts.ts +++ b/scripts/deployContracts.ts @@ -238,7 +238,7 @@ export async function deployRootContracts( // deploy VTSQToken const vtSQToken = await deployContract<VTSQToken>('VTSQToken', 'root', { - deployConfig: [constants.AddressZero, ...config['SQToken']], + deployConfig: [constants.AddressZero, ...config['VTSQToken']], }); logger?.info('🤞 VTSQToken'); diff --git a/test/Vesting.test.ts b/test/Vesting.test.ts index cff570b9..a8fb9cbb 100644 --- a/test/Vesting.test.ts +++ b/test/Vesting.test.ts @@ -6,7 +6,7 @@ import { expect } from 'chai'; import { BigNumber } from "ethers"; import { ethers, waffle } from 'hardhat'; import { SQToken, Vesting } from '../src'; -import { eventFrom } from "./helper"; +import { etherParse, eventFrom } from "./helper"; import { deployRootContracts } from './setup'; import { VTSQToken } from "build"; @@ -14,7 +14,7 @@ describe('Vesting Contract', () => { const mockProvider = waffle.provider; const [wallet, wallet1, wallet2, wallet3, wallet4] = mockProvider.getWallets(); - let token: SQToken; + let SQToken: SQToken; let vtSQToken: VTSQToken; let vestingContract: Vesting; let lockPeriod: number; @@ -66,13 +66,13 @@ describe('Vesting Contract', () => { beforeEach(async () => { const deployment = await waffle.loadFixture(deployer); - token = deployment.rootToken; + SQToken = deployment.rootToken; vestingContract = deployment.vesting; vtSQToken = deployment.vtSQToken; lockPeriod = 86400 * 30; // 2 month vestingPeriod = 86400 * 365; // 1 year - await token.approve(vestingContract.address, parseEther(4000)); + await SQToken.approve(vestingContract.address, parseEther(4000)); }); describe('Vesting Plan', () => { @@ -165,10 +165,10 @@ describe('Vesting Contract', () => { describe('Token Manangement By Admin', () => { it('deposit and widthdraw all by admin should work', async () => { await vestingContract.depositByAdmin(1000); - expect(await token.balanceOf(vestingContract.address)).to.eq(1000); + expect(await SQToken.balanceOf(vestingContract.address)).to.eq(1000); await vestingContract.withdrawAllByAdmin(); - expect(await token.balanceOf(vestingContract.address)).to.eq(parseEther(0)); + expect(await SQToken.balanceOf(vestingContract.address)).to.eq(parseEther(0)); }); it('deposit and widthdraw without owner should fail', async () => { @@ -192,6 +192,12 @@ describe('Vesting Contract', () => { ); }); + it('mint vtSQToken should work', async () => { + expect(await vtSQToken.totalSupply()).to.equal(parseEther(4000)); + expect(await vtSQToken.balanceOf(wallet1.address)).to.equal(parseEther(1000)); + expect(await vtSQToken.balanceOf(wallet2.address)).to.equal(parseEther(3000)); + }); + it('set incorrect vesting date should fail', async () => { const latestBlock = await mockProvider.getBlock('latest'); await expect(vestingContract.startVesting(latestBlock.timestamp)).to.be.revertedWith( @@ -200,7 +206,7 @@ describe('Vesting Contract', () => { }); it('start vesting without enough balance should fail', async () => { - expect(await token.balanceOf(vestingContract.address)).to.equal(parseEther(0)); + expect(await SQToken.balanceOf(vestingContract.address)).to.equal(parseEther(0)); const latestBlock = await mockProvider.getBlock('latest'); await expect(vestingContract.startVesting(latestBlock.timestamp + 1000)).to.be.revertedWith( 'V010' @@ -257,7 +263,7 @@ describe('Vesting Contract', () => { expect(await vestingContract.claimableAmount(wallet2.address)).to.equal(0); }); - it.only('claim during vesting period', async () => {// start vesting + it('claim during vesting period', async () => {// start vesting await startVesting(); await timeTravel(lockPeriod + 1001); @@ -279,7 +285,7 @@ describe('Vesting Contract', () => { evt = await claimVesting(wallet1); expect(evt.amount).to.gte(claimable); expect(evt.amount.sub(claimable)).to.lt(errorTolerance); - }) + }); it('claim all together in once', async () => {// start vesting await startVesting(); @@ -307,7 +313,7 @@ describe('Vesting Contract', () => { // wallet1 claim await vestingContract.connect(wallet1).claim(); - const balance1 = await token.balanceOf(wallet1.address); + const balance1 = await SQToken.balanceOf(wallet1.address); expect(balance1).to.gt(claimable1); expect(balance1).to.lt(claimable1.add(parseEther(0.001))); // claim after half vesting period @@ -316,18 +322,63 @@ describe('Vesting Contract', () => { expect(claimable1).to.gte(parseEther(450)); // wallet1 claim await vestingContract.connect(wallet1).claim(); - expect(await token.balanceOf(wallet1.address)).to.gt(balance1.add(claimable1)); - expect(await token.balanceOf(wallet1.address)).to.lt(balance1.add(claimable1).add(parseEther(0.001))); + expect(await SQToken.balanceOf(wallet1.address)).to.gt(balance1.add(claimable1)); + expect(await SQToken.balanceOf(wallet1.address)).to.lt(balance1.add(claimable1).add(parseEther(0.001))); // claim after vesting period await timeTravel(vestingPeriod / 2); await vestingContract.connect(wallet1).claim(); - expect(await token.balanceOf(wallet1.address)).to.eq(parseEther(1000)); + expect(await SQToken.balanceOf(wallet1.address)).to.eq(parseEther(1000)); + }); + + it('should burn equal amount of vtSQToken for claimed SQT', async () => { + // start vesting + await startVesting(); + await timeTravel(lockPeriod + 1001); + // wallet1 claim + expect(await SQToken.balanceOf(wallet1.address)).to.eq(0); + expect(await vtSQToken.balanceOf(wallet1.address)).to.eq(parseEther(1000)); + await vestingContract.connect(wallet1).claim(); + const sqtBalance = await SQToken.balanceOf(wallet1.address); + expect(await vtSQToken.balanceOf(wallet1.address)).to.eq(parseEther(1000).sub(sqtBalance)); }); - it('claim on non-vesting account should fail', async () => { + it('should only claim max amount of VTSQToken', async () => { + // start vesting + await startVesting(); + await timeTravel(lockPeriod + 1001); + // wallet1 + expect(await SQToken.balanceOf(wallet1.address)).to.eq(0); + const unlockAmount = await vestingContract.unlockedAmount(wallet1.address); + // transfer VTSQToken to wallet2 + await vtSQToken.connect(wallet1).transfer(wallet2.address, etherParse('999')); + // unlockAmount > 1 SQT, vtSQToken balance = 1 vtSQT + expect(unlockAmount.gt(etherParse('1'))).to.be.true; + const claimableAmount = etherParse('1'); + expect(await vestingContract.claimableAmount(wallet1.address)).to.eq(claimableAmount); + + // check SQT and VTSQT balance + await vestingContract.connect(wallet1).claim(); + expect(await SQToken.balanceOf(wallet1.address)).to.eq(claimableAmount); + expect(await vtSQToken.balanceOf(wallet1.address)).to.eq(0); + }); + + it('claim with invalid condition should fail', async () => { + // claim on non-vesting account should fail await expect(vestingContract.connect(wallet3).claim()).to.be.revertedWith( 'V011' ); + // claim with zero claimable amount should fail + // # case 1 (not start vesting) + await expect(vestingContract.connect(wallet1).claim()).to.be.revertedWith( + 'V012' + ); + // # case 2 (not enough vtSQT) + await startVesting(); + await timeTravel(lockPeriod + 1001); + await vtSQToken.connect(wallet1).transfer(wallet2.address, etherParse('1000')); + await expect(vestingContract.connect(wallet1).claim()).to.be.revertedWith( + 'V012' + ); }); }); }); diff --git a/test/setup.ts b/test/setup.ts index 841d996f..1c5bf7bd 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -56,7 +56,7 @@ export const deployRootContracts = async (wallet: Wallet, wallet1: Wallet) => { { InflationController: [1000, wallet1.address], SQToken: [etherParse("10000000000").toString()], - VSQToken: [0], + VTSQToken: [0], } ); From ff8b31c27d3cb73c2c991a458394db1eaa7a5151 Mon Sep 17 00:00:00 2001 From: mzxyz <8177474+mzxyz@users.noreply.github.com> Date: Tue, 19 Dec 2023 13:01:32 +1300 Subject: [PATCH 10/16] Fix contract file name --- contracts/root/VTSQToken.sol | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 contracts/root/VTSQToken.sol diff --git a/contracts/root/VTSQToken.sol b/contracts/root/VTSQToken.sol new file mode 100644 index 00000000..907c5264 --- /dev/null +++ b/contracts/root/VTSQToken.sol @@ -0,0 +1,38 @@ +// Copyright (C) 2020-2023 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity 0.8.15; + +import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; +import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import '@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol'; +import '@openzeppelin/contracts/access/Ownable.sol'; + +contract VTSQToken is ERC20, Ownable, ERC20Burnable { + using SafeERC20 for IERC20; + address public minter; + + modifier isMinter() { + require(minter == msg.sender, 'Not minter'); + _; + } + + constructor(address _minter, uint256 totalSupply) ERC20('VTSubQueryToken', 'vtSQT') Ownable() { + minter = _minter; + _mint(msg.sender, totalSupply); + } + + function mint(address destination, uint256 amount) external isMinter { + _mint(destination, amount); + } + + /// #if_succeeds {:msg "minter should be set"} minter == _minter; + /// #if_succeeds {:msg "owner functionality"} old(msg.sender == address(owner)); + function setMinter(address _minter) external onlyOwner { + minter = _minter; + } + + function getMinter() external view returns (address) { + return minter; + } +} From 520bcf2c67faef3c17894d396de5d8081ba4abec Mon Sep 17 00:00:00 2001 From: mzxyz <8177474+mzxyz@users.noreply.github.com> Date: Tue, 19 Dec 2023 13:23:33 +1300 Subject: [PATCH 11/16] Delete contracts/root/VTSQtoken.sol --- contracts/root/VTSQtoken.sol | 38 ------------------------------------ 1 file changed, 38 deletions(-) delete mode 100644 contracts/root/VTSQtoken.sol diff --git a/contracts/root/VTSQtoken.sol b/contracts/root/VTSQtoken.sol deleted file mode 100644 index 907c5264..00000000 --- a/contracts/root/VTSQtoken.sol +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2020-2023 SubQuery Pte Ltd authors & contributors -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity 0.8.15; - -import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; -import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; -import '@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol'; -import '@openzeppelin/contracts/access/Ownable.sol'; - -contract VTSQToken is ERC20, Ownable, ERC20Burnable { - using SafeERC20 for IERC20; - address public minter; - - modifier isMinter() { - require(minter == msg.sender, 'Not minter'); - _; - } - - constructor(address _minter, uint256 totalSupply) ERC20('VTSubQueryToken', 'vtSQT') Ownable() { - minter = _minter; - _mint(msg.sender, totalSupply); - } - - function mint(address destination, uint256 amount) external isMinter { - _mint(destination, amount); - } - - /// #if_succeeds {:msg "minter should be set"} minter == _minter; - /// #if_succeeds {:msg "owner functionality"} old(msg.sender == address(owner)); - function setMinter(address _minter) external onlyOwner { - minter = _minter; - } - - function getMinter() external view returns (address) { - return minter; - } -} From 3bf1018cfa759a98a4773fc0c08d52ec8f369077 Mon Sep 17 00:00:00 2001 From: mzxyz <8177474+mzxyz@users.noreply.github.com> Date: Tue, 19 Dec 2023 13:26:05 +1300 Subject: [PATCH 12/16] Remove `mint` when deposit token --- contracts/root/Vesting.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/root/Vesting.sol b/contracts/root/Vesting.sol index dde86d0d..f204ee76 100644 --- a/contracts/root/Vesting.sol +++ b/contracts/root/Vesting.sol @@ -78,7 +78,6 @@ contract Vesting is Ownable { function depositByAdmin(uint256 amount) external onlyOwner { require(amount > 0, "V007"); - ISQToken(vtToken).mint(address(this), amount); require(IERC20(token).transferFrom(msg.sender, address(this), amount), "V008"); } From c5fc78af648340eef988d527387e15cf0ce322ff Mon Sep 17 00:00:00 2001 From: mzxyz <8177474+mzxyz@users.noreply.github.com> Date: Tue, 19 Dec 2023 13:33:50 +1300 Subject: [PATCH 13/16] Add vtSQToken balance check for deposit and withdraw action --- test/Vesting.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Vesting.test.ts b/test/Vesting.test.ts index a8fb9cbb..5a0d5074 100644 --- a/test/Vesting.test.ts +++ b/test/Vesting.test.ts @@ -166,9 +166,11 @@ describe('Vesting Contract', () => { it('deposit and widthdraw all by admin should work', async () => { await vestingContract.depositByAdmin(1000); expect(await SQToken.balanceOf(vestingContract.address)).to.eq(1000); + expect(await vtSQToken.totalSupply()).to.eq(0); await vestingContract.withdrawAllByAdmin(); expect(await SQToken.balanceOf(vestingContract.address)).to.eq(parseEther(0)); + expect(await vtSQToken.totalSupply()).to.eq(0); }); it('deposit and widthdraw without owner should fail', async () => { From a183895dc3ff32484d7603039ce9c76c0fef9d30 Mon Sep 17 00:00:00 2001 From: Ian He <39037239+ianhe8x@users.noreply.github.com> Date: Tue, 19 Dec 2023 17:15:55 +1300 Subject: [PATCH 14/16] add rootSdk to support vesting --- src/index.ts | 1 + src/rootSdk.ts | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 src/rootSdk.ts diff --git a/src/index.ts b/src/index.ts index 7763f910..cae52949 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later export * from './sdk'; +export * from './rootSdk'; export * from './polygonSDK'; export * from './typechain'; export * from './types'; diff --git a/src/rootSdk.ts b/src/rootSdk.ts new file mode 100644 index 00000000..b63ff103 --- /dev/null +++ b/src/rootSdk.ts @@ -0,0 +1,60 @@ +// Copyright (C) 2020-2023 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +import type {Provider as AbstractProvider} from '@ethersproject/abstract-provider'; +import {Signer} from 'ethers'; +import {DEPLOYMENT_DETAILS} from './deployments'; +import {ERC20, SQToken, Vesting} from './typechain'; +import {CONTRACT_FACTORY, ContractDeploymentInner, ContractName, FactoryContstructor, SdkOptions} from './types'; +import assert from "assert"; + +// HOTFIX: Contract names are not consistent between deployments and privous var names +const contractNameConversion: Record<string, string> = { + sQToken: 'sqToken', + vTSQToken: 'vtSQToken', +}; + +const ROOT_CONTRACTS = ['SQToken', 'Vesting', 'VTSQToken']; + + +export class RootContractSDK { + private _contractDeployments: ContractDeploymentInner; + + readonly sqToken!: SQToken; + readonly vtSQToken!: ERC20; + readonly vesting!: Vesting; + + constructor(private readonly signerOrProvider: AbstractProvider | Signer, public readonly options: SdkOptions) { + assert(this.options.deploymentDetails || DEPLOYMENT_DETAILS[options.network], ' missing contract deployment info'); + this._contractDeployments = this.options.deploymentDetails ?? DEPLOYMENT_DETAILS[options.network]!.root; + this._init(); + } + + static create(signerOrProvider: AbstractProvider | Signer, options: SdkOptions) { + return new RootContractSDK(signerOrProvider, options); + } + + private async _init() { + const contracts = Object.entries(this._contractDeployments).filter( ([name]) => + ROOT_CONTRACTS.includes(name) + ).map(([name, contract]) => ({ + address: contract.address, + factory: CONTRACT_FACTORY[name as ContractName] as FactoryContstructor, + name: name as ContractName, + })); + + for (const {name, factory, address} of contracts) { + if (!factory) continue; + const contractInstance = factory.connect(address, this.signerOrProvider); + if (contractInstance) { + const key = name.charAt(0).toLowerCase() + name.slice(1); + const contractName = contractNameConversion[key] ?? key; + Object.defineProperty(this, contractName, { + get: () => contractInstance, + }); + } else { + throw new Error(`${name} contract not found`); + } + } + } +} From 919dad15e0361f77540d779aaad4535c76ca7024 Mon Sep 17 00:00:00 2001 From: Ian He <39037239+ianhe8x@users.noreply.github.com> Date: Tue, 19 Dec 2023 18:18:54 +1300 Subject: [PATCH 15/16] remove totalSupply and publish --- contracts/root/VTSQToken.sol | 3 +-- hardhat.config.ts | 3 ++- publish/ABI/VTSQToken.json | 5 ----- publish/testnet.json | 6 ++++++ scripts/config/contracts.config.ts | 6 +++--- scripts/deployContracts.ts | 2 +- test/Vesting.test.ts | 8 +++++++- 7 files changed, 20 insertions(+), 13 deletions(-) diff --git a/contracts/root/VTSQToken.sol b/contracts/root/VTSQToken.sol index 907c5264..16942973 100644 --- a/contracts/root/VTSQToken.sol +++ b/contracts/root/VTSQToken.sol @@ -17,9 +17,8 @@ contract VTSQToken is ERC20, Ownable, ERC20Burnable { _; } - constructor(address _minter, uint256 totalSupply) ERC20('VTSubQueryToken', 'vtSQT') Ownable() { + constructor(address _minter) ERC20('VTSubQueryToken', 'vtSQT') Ownable() { minter = _minter; - _mint(msg.sender, totalSupply); } function mint(address destination, uint256 amount) external isMinter { diff --git a/hardhat.config.ts b/hardhat.config.ts index d3d8eceb..98c8b074 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -95,7 +95,8 @@ task('publishRoot', "verify and publish contracts on etherscan") console.log(`verify VTSQToken`); await hre.run("verify:verify", { address: deployment.VTSQToken.address, - constructorArguments: [constants.AddressZero, ...config.SQToken], + contract: 'contracts/root/VTSQToken.sol:VTSQToken', + constructorArguments: [constants.AddressZero], }); //Settings console.log(`verify Settings`); diff --git a/publish/ABI/VTSQToken.json b/publish/ABI/VTSQToken.json index 21e98a5a..8078486c 100644 --- a/publish/ABI/VTSQToken.json +++ b/publish/ABI/VTSQToken.json @@ -5,11 +5,6 @@ "internalType": "address", "name": "_minter", "type": "address" - }, - { - "internalType": "uint256", - "name": "totalSupply", - "type": "uint256" } ], "stateMutability": "nonpayable", diff --git a/publish/testnet.json b/publish/testnet.json index 6076603f..56bb1ec7 100644 --- a/publish/testnet.json +++ b/publish/testnet.json @@ -41,6 +41,12 @@ "address": "0x3519c8939b73EAA440A5b626D6090275add4bD69", "bytecodeHash": "895a73f782b2930ee15d1ae0ccbfa751df049bd647ed5a23b7f42d22c441ba8b", "lastUpdate": "Fri, 15 Dec 2023 00:26:14 GMT" + }, + "VTSQToken": { + "innerAddress": "", + "address": "0x0D5A4266573975222292601686f2C3CF02E2120A", + "bytecodeHash": "1a3fdb466834f7139e72a865a759a82189da9dd342ec1fbfbbc9d62397bd109f", + "lastUpdate": "Tue, 19 Dec 2023 05:12:39 GMT" } }, "child": { diff --git a/scripts/config/contracts.config.ts b/scripts/config/contracts.config.ts index a636bea9..2027ac3d 100644 --- a/scripts/config/contracts.config.ts +++ b/scripts/config/contracts.config.ts @@ -4,7 +4,7 @@ export default { mainnet: { InflationController: [10000, '0x34c35136ECe9CBD6DfDf2F896C6e29be01587c0C'], // inflationRate, inflationDestination SQToken: [utils.parseEther("10000000000")], // initial supply 10 billion - VTSQToken: [0], // initial supply 0 + VTSQToken: [], // initial supply 0 Staking: [1209600, 1e3], // lockPeriod, unbondFeeRate Airdropper: ['0x34c35136ECe9CBD6DfDf2F896C6e29be01587c0C'], // settle destination EraManager: [604800], // 7 day @@ -29,7 +29,7 @@ export default { testnet: { InflationController: [10000, '0x4ae8fcdddc859e2984ce0b8f4ef490d61a7a9b7f'], // inflationRate, inflationDestination SQToken: [utils.parseEther("10000000000")], // initial supply 10 billion - VTSQToken: [0], // initial supply 0 + VTSQToken: [], // initial supply 0 Staking: [1000, 1e3], // lockPeriod, unbondFeeRate Airdropper: ['0x4ae8fcdddc859e2984ce0b8f4ef490d61a7a9b7f'], // settle destination EraManager: [3600], // 1 hour @@ -43,7 +43,7 @@ export default { local: { InflationController: [1000, '0x4ae8fcdddc859e2984ce0b8f4ef490d61a7a9b7f'], // inflationRate, inflationDestination SQToken: [utils.parseEther("10000000000")], // initial supply 10 billion - VTSQToken: [utils.parseEther("0")], // initial supply 0 billion + VTSQToken: [], // initial supply 0 billion Staking: [1000, 1e3], // lockPeriod, unbondFeeRate Airdropper: ['0x4ae8fcdddc859e2984ce0b8f4ef490d61a7a9b7f'], // settle destination EraManager: [60 * 60], // 1 hour diff --git a/scripts/deployContracts.ts b/scripts/deployContracts.ts index 786781d6..c0b54fdf 100644 --- a/scripts/deployContracts.ts +++ b/scripts/deployContracts.ts @@ -238,7 +238,7 @@ export async function deployRootContracts( // deploy VTSQToken const vtSQToken = await deployContract<VTSQToken>('VTSQToken', 'root', { - deployConfig: [constants.AddressZero, ...config['VTSQToken']], + deployConfig: [constants.AddressZero], }); logger?.info('🤞 VTSQToken'); diff --git a/test/Vesting.test.ts b/test/Vesting.test.ts index 5a0d5074..da6bcda6 100644 --- a/test/Vesting.test.ts +++ b/test/Vesting.test.ts @@ -60,6 +60,7 @@ describe('Vesting Contract', () => { const checkAllocation = async (planId: number, user: string, allocation: number) => { expect(await vestingContract.userPlanId(user)).to.equal(planId); expect(await vestingContract.allocations(user)).to.equal(parseEther(allocation)); + expect(await vtSQToken.balanceOf(user)).to.equal(parseEther(allocation)); }; const deployer = ()=>deployRootContracts(wallet, wallet1); @@ -100,9 +101,14 @@ describe('Vesting Contract', () => { 'V001' ); }); + + it('non admin should fail', async () => { + await expect(vestingContract.connect(wallet1).addVestingPlan(lockPeriod, vestingPeriod, 0)) + .to.revertedWith('Ownable: caller is not the owner'); + }); }); - describe('Allocate Vestring', () => { + describe('Allocate Vesting', () => { beforeEach(async () => { await vestingContract.addVestingPlan(lockPeriod, vestingPeriod, 10); }); From f2085dd5893f2887982115b641a5256e898f3fd0 Mon Sep 17 00:00:00 2001 From: Ian He <39037239+ianhe8x@users.noreply.github.com> Date: Wed, 20 Dec 2023 10:28:25 +1300 Subject: [PATCH 16/16] improve tests --- test/Vesting.test.ts | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/test/Vesting.test.ts b/test/Vesting.test.ts index da6bcda6..ffeb31f3 100644 --- a/test/Vesting.test.ts +++ b/test/Vesting.test.ts @@ -14,7 +14,7 @@ describe('Vesting Contract', () => { const mockProvider = waffle.provider; const [wallet, wallet1, wallet2, wallet3, wallet4] = mockProvider.getWallets(); - let SQToken: SQToken; + let sqToken: SQToken; let vtSQToken: VTSQToken; let vestingContract: Vesting; let lockPeriod: number; @@ -67,13 +67,13 @@ describe('Vesting Contract', () => { beforeEach(async () => { const deployment = await waffle.loadFixture(deployer); - SQToken = deployment.rootToken; + sqToken = deployment.rootToken; vestingContract = deployment.vesting; vtSQToken = deployment.vtSQToken; lockPeriod = 86400 * 30; // 2 month vestingPeriod = 86400 * 365; // 1 year - await SQToken.approve(vestingContract.address, parseEther(4000)); + await sqToken.approve(vestingContract.address, parseEther(4000)); }); describe('Vesting Plan', () => { @@ -105,6 +105,10 @@ describe('Vesting Contract', () => { it('non admin should fail', async () => { await expect(vestingContract.connect(wallet1).addVestingPlan(lockPeriod, vestingPeriod, 0)) .to.revertedWith('Ownable: caller is not the owner'); + await vestingContract.renounceOwnership(); + + await expect(vestingContract.addVestingPlan(lockPeriod, vestingPeriod, 0)) + .to.revertedWith('Ownable: caller is not the owner'); }); }); @@ -171,11 +175,11 @@ describe('Vesting Contract', () => { describe('Token Manangement By Admin', () => { it('deposit and widthdraw all by admin should work', async () => { await vestingContract.depositByAdmin(1000); - expect(await SQToken.balanceOf(vestingContract.address)).to.eq(1000); + expect(await sqToken.balanceOf(vestingContract.address)).to.eq(1000); expect(await vtSQToken.totalSupply()).to.eq(0); await vestingContract.withdrawAllByAdmin(); - expect(await SQToken.balanceOf(vestingContract.address)).to.eq(parseEther(0)); + expect(await sqToken.balanceOf(vestingContract.address)).to.eq(parseEther(0)); expect(await vtSQToken.totalSupply()).to.eq(0); }); @@ -214,7 +218,7 @@ describe('Vesting Contract', () => { }); it('start vesting without enough balance should fail', async () => { - expect(await SQToken.balanceOf(vestingContract.address)).to.equal(parseEther(0)); + expect(await sqToken.balanceOf(vestingContract.address)).to.equal(parseEther(0)); const latestBlock = await mockProvider.getBlock('latest'); await expect(vestingContract.startVesting(latestBlock.timestamp + 1000)).to.be.revertedWith( 'V010' @@ -293,6 +297,14 @@ describe('Vesting Contract', () => { evt = await claimVesting(wallet1); expect(evt.amount).to.gte(claimable); expect(evt.amount.sub(claimable)).to.lt(errorTolerance); + for (let i=0;i<9;i++) { + await timeTravel(vestingPeriod/10); + await claimVesting(wallet1); + } + claimable = await vestingContract.claimableAmount(wallet1.address); + expect(claimable).to.eq(0); + const claimed = await sqToken.balanceOf(wallet1.address); + expect(claimed).to.eq(wallet1Allocation); }); it('claim all together in once', async () => {// start vesting @@ -321,7 +333,7 @@ describe('Vesting Contract', () => { // wallet1 claim await vestingContract.connect(wallet1).claim(); - const balance1 = await SQToken.balanceOf(wallet1.address); + const balance1 = await sqToken.balanceOf(wallet1.address); expect(balance1).to.gt(claimable1); expect(balance1).to.lt(claimable1.add(parseEther(0.001))); // claim after half vesting period @@ -330,12 +342,12 @@ describe('Vesting Contract', () => { expect(claimable1).to.gte(parseEther(450)); // wallet1 claim await vestingContract.connect(wallet1).claim(); - expect(await SQToken.balanceOf(wallet1.address)).to.gt(balance1.add(claimable1)); - expect(await SQToken.balanceOf(wallet1.address)).to.lt(balance1.add(claimable1).add(parseEther(0.001))); + expect(await sqToken.balanceOf(wallet1.address)).to.gt(balance1.add(claimable1)); + expect(await sqToken.balanceOf(wallet1.address)).to.lt(balance1.add(claimable1).add(parseEther(0.001))); // claim after vesting period await timeTravel(vestingPeriod / 2); await vestingContract.connect(wallet1).claim(); - expect(await SQToken.balanceOf(wallet1.address)).to.eq(parseEther(1000)); + expect(await sqToken.balanceOf(wallet1.address)).to.eq(parseEther(1000)); }); it('should burn equal amount of vtSQToken for claimed SQT', async () => { @@ -343,10 +355,10 @@ describe('Vesting Contract', () => { await startVesting(); await timeTravel(lockPeriod + 1001); // wallet1 claim - expect(await SQToken.balanceOf(wallet1.address)).to.eq(0); + expect(await sqToken.balanceOf(wallet1.address)).to.eq(0); expect(await vtSQToken.balanceOf(wallet1.address)).to.eq(parseEther(1000)); await vestingContract.connect(wallet1).claim(); - const sqtBalance = await SQToken.balanceOf(wallet1.address); + const sqtBalance = await sqToken.balanceOf(wallet1.address); expect(await vtSQToken.balanceOf(wallet1.address)).to.eq(parseEther(1000).sub(sqtBalance)); }); @@ -355,7 +367,7 @@ describe('Vesting Contract', () => { await startVesting(); await timeTravel(lockPeriod + 1001); // wallet1 - expect(await SQToken.balanceOf(wallet1.address)).to.eq(0); + expect(await sqToken.balanceOf(wallet1.address)).to.eq(0); const unlockAmount = await vestingContract.unlockedAmount(wallet1.address); // transfer VTSQToken to wallet2 await vtSQToken.connect(wallet1).transfer(wallet2.address, etherParse('999')); @@ -366,7 +378,7 @@ describe('Vesting Contract', () => { // check SQT and VTSQT balance await vestingContract.connect(wallet1).claim(); - expect(await SQToken.balanceOf(wallet1.address)).to.eq(claimableAmount); + expect(await sqToken.balanceOf(wallet1.address)).to.eq(claimableAmount); expect(await vtSQToken.balanceOf(wallet1.address)).to.eq(0); });