Skip to content

Commit 2a39c03

Browse files
authored
Merge pull request #2 from KoxyG/review
Add Upgrade Testing Infrastructure and Code Optimization
2 parents 3e75bd2 + 9a737d1 commit 2a39c03

File tree

7 files changed

+384
-46
lines changed

7 files changed

+384
-46
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ ttn-token/
1717
├── scripts/ # Deployment and upgrade scripts
1818
├── deployments/ # Deployment artifacts
1919
├── .env.example # Example environment configuration
20+
├── test # Test files
2021
├── hardhat.config.ts # Hardhat configuration
2122
└── README.md # Project documentation
2223
```
@@ -36,9 +37,9 @@ The XXX token system implements the following features:
3637

3738
- **Flexible Vesting and Locking**: Custom unlock patterns including cliff periods, linear releases, and milestone-based unlocks
3839

39-
- **Airdrop Functionality**: Batch-allocate tokens to multiple addresses for airdrops
40+
- **Airdrop Functionality**: Batch-allocate tokens to multiple addresses for airdrops.
4041

41-
- **Manual Unlocking**: Perform early unlocks when needed (e.g., exchange listings)
42+
- **Manual Unlocking**: Perform early unlocks when needed (e.g., exchange listings).
4243

4344
- **Allocation Revocation**: Revoke part or all of locked allocations when needed
4445

contracts/XXXTokenVault.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ contract TokenVault is Initializable,
5555

5656
// Allocation tracking
5757
struct Allocation {
58-
address beneficiary;
5958
uint256 amount;
59+
address beneficiary;
6060
bool revoked;
6161
}
6262

contracts/XXXVestingManager.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ contract VestingManager is Initializable,
3737

3838
// Vesting schedule structure
3939
struct VestingSchedule {
40-
address beneficiary;
4140
uint256 totalAmount;
4241
uint256 startTime;
4342
uint256 cliffDuration;
4443
uint256 duration;
4544
uint256 releasedAmount;
46-
bool revoked;
4745
uint256 createdAt;
4846
uint256 allocationId;
47+
address beneficiary;
48+
bool revoked;
4949
}
5050

5151
// Vesting schedule counter

contracts/upgrades/XXXTokenV2.sol

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import "../XXXToken.sol";
5+
6+
/**
7+
* @title XXXTokenV2
8+
* @dev This is a demonstration contract for testing the upgradeability of XXXToken.
9+
* It serves as a simple example of how to implement contract upgrades using the UUPS pattern.
10+
*
11+
* Key features:
12+
* - Inherits all functionality from XXXToken
13+
* - Adds a version number to demonstrate upgrade success
14+
* - Uses reinitializer to safely initialize new state variables
15+
*
16+
* This contract is primarily used for testing purposes to verify that:
17+
* 1. The upgrade process works correctly
18+
* 2. All existing functionality is preserved
19+
* 3. New functionality can be added safely
20+
*
21+
* @custom:oz-upgrades-validate-as-initializer
22+
*/
23+
contract XXXTokenV2 is XXXToken {
24+
/// @notice The version number of this contract implementation
25+
uint256 public version;
26+
27+
/**
28+
* @dev Initializes the V2 contract with a version number.
29+
* This function is called after the upgrade to set up the new state variables.
30+
* The reinitializer modifier ensures this can only be called once after the upgrade.
31+
*/
32+
function initializeV2() public reinitializer(2) {
33+
version = 2;
34+
}
35+
}

test/TokenVault.test.ts

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import { expect } from "chai";
2+
import { ethers, upgrades } from "hardhat";
3+
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
4+
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
5+
import { TokenVault, XXXToken } from "../typechain-types";
6+
7+
describe("TokenVault", function () {
8+
// Role identifiers
9+
const ALLOCATOR_ROLE = ethers.keccak256(ethers.toUtf8Bytes("ALLOCATOR_ROLE"));
10+
const AIRDROP_ROLE = ethers.keccak256(ethers.toUtf8Bytes("AIRDROP_ROLE"));
11+
const UPGRADER_ROLE = ethers.keccak256(ethers.toUtf8Bytes("UPGRADER_ROLE"));
12+
13+
// Test accounts
14+
let owner: SignerWithAddress;
15+
let allocator: SignerWithAddress;
16+
let airdropper: SignerWithAddress;
17+
let vestingManager: SignerWithAddress;
18+
let beneficiary1: SignerWithAddress;
19+
let beneficiary2: SignerWithAddress;
20+
let user: SignerWithAddress;
21+
22+
// Contract instances
23+
let token: XXXToken;
24+
let vault: TokenVault;
25+
26+
async function deployVaultFixture() {
27+
[owner, allocator, airdropper, vestingManager, beneficiary1, beneficiary2, user] = await ethers.getSigners();
28+
29+
// Deploy token
30+
const XXXToken = await ethers.getContractFactory("XXXToken");
31+
const token = await upgrades.deployProxy(XXXToken, [], { initializer: 'initialize' });
32+
await token.waitForDeployment();
33+
34+
// Deploy vault
35+
const TokenVault = await ethers.getContractFactory("TokenVault");
36+
const vault = await upgrades.deployProxy(TokenVault, [await token.getAddress()], { initializer: 'initialize' });
37+
await vault.waitForDeployment();
38+
39+
// Grant all roles to the owner (admin)
40+
const MINTER_ROLE = ethers.keccak256(ethers.toUtf8Bytes("MINTER_ROLE"));
41+
const ALLOCATOR_ROLE = ethers.keccak256(ethers.toUtf8Bytes("ALLOCATOR_ROLE"));
42+
const AIRDROP_ROLE = ethers.keccak256(ethers.toUtf8Bytes("AIRDROP_ROLE"));
43+
const UPGRADER_ROLE = ethers.keccak256(ethers.toUtf8Bytes("UPGRADER_ROLE"));
44+
const DEFAULT_ADMIN_ROLE = "0x0000000000000000000000000000000000000000000000000000000000000000";
45+
46+
// Grant MINTER_ROLE to the vault contract
47+
await token.grantRole(MINTER_ROLE, await vault.getAddress());
48+
49+
// Grant all other roles to the owner
50+
await vault.grantRole(DEFAULT_ADMIN_ROLE, owner.address);
51+
await vault.grantRole(ALLOCATOR_ROLE, owner.address);
52+
await vault.grantRole(AIRDROP_ROLE, owner.address);
53+
await vault.grantRole(UPGRADER_ROLE, owner.address);
54+
55+
return { token, vault, owner, allocator, airdropper, vestingManager, beneficiary1, beneficiary2, user };
56+
}
57+
58+
beforeEach(async function () {
59+
({ token, vault, owner, allocator, airdropper, vestingManager, beneficiary1, beneficiary2, user } =
60+
await loadFixture(deployVaultFixture));
61+
});
62+
63+
describe("Deployment", function () {
64+
it("Should set the correct token address", async function () {
65+
expect(await vault.ttnToken()).to.equal(await token.getAddress());
66+
});
67+
68+
it("Should assign the correct roles", async function () {
69+
expect(await vault.hasRole(ALLOCATOR_ROLE, owner.address)).to.be.true;
70+
expect(await vault.hasRole(AIRDROP_ROLE, owner.address)).to.be.true;
71+
expect(await vault.hasRole(UPGRADER_ROLE, owner.address)).to.be.true;
72+
});
73+
74+
it("Should initialize counters to zero", async function () {
75+
expect(await vault.getAllocationsForBeneficiary(beneficiary1.address)).to.be.empty;
76+
});
77+
});
78+
79+
describe("Vesting Manager", function () {
80+
it("Should allow admin to set vesting manager", async function () {
81+
await vault.setVestingManager(vestingManager.address);
82+
expect(await vault.vestingManager()).to.equal(vestingManager.address);
83+
});
84+
85+
it("Should not allow non-admin to set vesting manager", async function () {
86+
await expect(
87+
vault.connect(user).setVestingManager(vestingManager.address)
88+
).to.be.revertedWithCustomError(vault, "AccessControlUnauthorizedAccount");
89+
});
90+
91+
it("Should not allow setting zero address as vesting manager", async function () {
92+
await expect(
93+
vault.setVestingManager(ethers.ZeroAddress)
94+
).to.be.revertedWithCustomError(vault, "ZeroAddress");
95+
});
96+
});
97+
98+
describe("Allocations", function () {
99+
it("Should allow allocator to create allocation", async function () {
100+
const amount = ethers.parseEther("1000");
101+
await vault.connect(owner).createAllocation(beneficiary1.address, amount);
102+
103+
const allocations = await vault.getAllocationsForBeneficiary(beneficiary1.address);
104+
expect(allocations.length).to.equal(1);
105+
106+
const allocation = await vault.allocations(allocations[0]);
107+
expect(allocation.amount).to.equal(amount);
108+
expect(allocation.beneficiary).to.equal(beneficiary1.address);
109+
expect(allocation.revoked).to.be.false;
110+
});
111+
112+
it("Should not allow non-allocator to create allocation", async function () {
113+
await expect(
114+
vault.connect(user).createAllocation(beneficiary1.address, ethers.parseEther("1000"))
115+
).to.be.revertedWithCustomError(vault, "AccessControlUnauthorizedAccount");
116+
});
117+
118+
it("Should not allow creating allocation with zero amount", async function () {
119+
await expect(
120+
vault.connect(owner).createAllocation(beneficiary1.address, 0)
121+
).to.be.revertedWithCustomError(vault, "InvalidAmount");
122+
});
123+
124+
it("Should not allow creating allocation for zero address", async function () {
125+
await expect(
126+
vault.connect(owner).createAllocation(ethers.ZeroAddress, ethers.parseEther("1000"))
127+
).to.be.revertedWithCustomError(vault, "InvalidBeneficiary");
128+
});
129+
130+
it("Should allow allocator to revoke allocation", async function () {
131+
const amount = ethers.parseEther("1000");
132+
await vault.connect(owner).createAllocation(beneficiary1.address, amount);
133+
134+
const allocations = await vault.getAllocationsForBeneficiary(beneficiary1.address);
135+
await vault.connect(owner).revokeAllocation(allocations[0]);
136+
137+
const allocation = await vault.allocations(allocations[0]);
138+
expect(allocation.revoked).to.be.true;
139+
});
140+
141+
it("Should not allow revoking non-existent allocation", async function () {
142+
await expect(
143+
vault.connect(owner).revokeAllocation(999)
144+
).to.be.revertedWithCustomError(vault, "InvalidAllocationId");
145+
});
146+
147+
it("Should not allow revoking already revoked allocation", async function () {
148+
const amount = ethers.parseEther("1000");
149+
await vault.connect(owner).createAllocation(beneficiary1.address, amount);
150+
151+
const allocations = await vault.getAllocationsForBeneficiary(beneficiary1.address);
152+
await vault.connect(owner).revokeAllocation(allocations[0]);
153+
154+
await expect(
155+
vault.connect(owner).revokeAllocation(allocations[0])
156+
).to.be.revertedWithCustomError(vault, "AllocationAlreadyRevoked");
157+
});
158+
});
159+
160+
describe("Airdrops", function () {
161+
it("Should allow airdropper to execute airdrop", async function () {
162+
const beneficiaries = [beneficiary1.address, beneficiary2.address];
163+
const amounts = [ethers.parseEther("1000"), ethers.parseEther("2000")];
164+
165+
await vault.connect(owner).executeAirdrop(beneficiaries, amounts);
166+
167+
const allocations1 = await vault.getAllocationsForBeneficiary(beneficiary1.address);
168+
const allocations2 = await vault.getAllocationsForBeneficiary(beneficiary2.address);
169+
170+
expect(allocations1.length).to.equal(1);
171+
expect(allocations2.length).to.equal(1);
172+
173+
const allocation1 = await vault.allocations(allocations1[0]);
174+
const allocation2 = await vault.allocations(allocations2[0]);
175+
176+
expect(allocation1.amount).to.equal(amounts[0]);
177+
expect(allocation2.amount).to.equal(amounts[1]);
178+
});
179+
180+
it("Should not allow non-airdropper to execute airdrop", async function () {
181+
const beneficiaries = [beneficiary1.address];
182+
const amounts = [ethers.parseEther("1000")];
183+
184+
await expect(
185+
vault.connect(user).executeAirdrop(beneficiaries, amounts)
186+
).to.be.revertedWithCustomError(vault, "AccessControlUnauthorizedAccount");
187+
});
188+
189+
it("Should not allow airdrop with empty beneficiaries list", async function () {
190+
await expect(
191+
vault.connect(owner).executeAirdrop([], [])
192+
).to.be.revertedWithCustomError(vault, "EmptyBeneficiariesList");
193+
});
194+
195+
it("Should not allow airdrop with mismatched arrays", async function () {
196+
const beneficiaries = [beneficiary1.address, beneficiary2.address];
197+
const amounts = [ethers.parseEther("1000")];
198+
199+
await expect(
200+
vault.connect(owner).executeAirdrop(beneficiaries, amounts)
201+
).to.be.revertedWithCustomError(vault, "ArraysLengthMismatch");
202+
});
203+
});
204+
205+
describe("Pausing", function () {
206+
it("Should allow admin to pause and unpause", async function () {
207+
await vault.pause();
208+
expect(await vault.paused()).to.be.true;
209+
210+
await vault.unpause();
211+
expect(await vault.paused()).to.be.false;
212+
});
213+
214+
it("Should not allow non-admin to pause", async function () {
215+
await expect(
216+
vault.connect(user).pause()
217+
).to.be.revertedWithCustomError(vault, "AccessControlUnauthorizedAccount");
218+
});
219+
220+
it("Should not allow allocations when paused", async function () {
221+
await vault.pause();
222+
223+
await expect(
224+
vault.connect(owner).createAllocation(beneficiary1.address, ethers.parseEther("1000"))
225+
).to.be.revertedWithCustomError(vault, "EnforcedPause");
226+
});
227+
228+
it("Should not allow airdrops when paused", async function () {
229+
await vault.pause();
230+
231+
const beneficiaries = [beneficiary1.address];
232+
const amounts = [ethers.parseEther("1000")];
233+
234+
await expect(
235+
vault.connect(owner).executeAirdrop(beneficiaries, amounts)
236+
).to.be.revertedWithCustomError(vault, "EnforcedPause");
237+
});
238+
});
239+
});

0 commit comments

Comments
 (0)