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);
         });