mirror of
https://github.com/luxfi/erc-3643.git
synced 2026-10-30 16:58:28 +00:00
* ✨ integrate foundry alongside hardhat for dual testing framework - Add foundry.toml configuration - Reorganize test structure: test/forge/ and test/hardhat/ - Add Foundry scripts to package.json (build:forge, test:forge, coverage:forge) - Update hardhat.config.ts with evmVersion for compatibility - Add GitHub workflows and coverage scripts from template - Add Husky hooks (pre-commit, post-checkout, post-merge) - Add .vscode/settings.json for better IDE support - Disable coverage checks until tests are migrated (commented with TODO) This migration is based on TokenySolutions/contracts-template and enables gradual migration of tests from Hardhat (TypeScript) to Foundry (Solidity). Related to: BT-1418 * ✅ trex factory tests * ⚰️ remove trex suit * feat(forge): reorganize Foundry test structure and sync GitHub workflows - Move TREXFactorySetup to helpers folder - Rename TREXSuite.t.sol to TREXFactory.t.sol - Update fixture to inherit from Test for proper Foundry pattern - Transfer ownership to deployer address to match Hardhat behavior - Sync GitHub workflows with package.json scripts - Update test.yml to use npm run build and npm run test - Update push_checking.yml to use unified npm run lint - Update forge_coverage.sh to use npm run coverage * fix(ci): install Foundry and skip husky in push_checking workflow - Add Foundry installation step before npm ci - Set HUSKY=0 to skip husky setup in CI environment - Fixes 'forge: not found' error during npm prepare script * 🔧 keep hardhat settings * ✅ create addressess directly * ✅ removed test_StandardAddressesSet * ✅ fix lint * 💡 removed title natspecs * ✅ migrated trex factort tests * ✅ added test module multicall * ✅ migrate agent role test * ✅ migrate claim topics registry * ✅ migrated trusted issuer registry * ✅ migrate identity registry storage * ✅ migrated identity registry tests * ✅ migrated implementation authority test * ✅ migrated token agen restrictions * ✅ migrated token information test * ✅ migrated token recover * ✅ migrated token permit * ✅ migrated token transfer * ✅ migrated utilities upgrade * ✅ migrated freeze check * ✅ migrated compliance check in utilities test * ✅ migrated eligibility check * ✅ migrated transfer check in utilities * ✅ migrated compliance test * ✅ migrated gateway tests * 🔥 removed already defined events * 🗑️ imported Initializable error from openzeppelin * ➕ added already defined events and errors * ✅ added base test token * ✅ used idFactory * ♻️ refactored compliance test * ⚰️ remoced useless vm.prank * 🔧 deleted hardhat configurations * 💚 fixed ci * refactor: remove 100% coverage requirement from forge_coverage.sh - Remove strict 100% coverage checks that were causing workflow failures - Script now only generates and displays coverage report without failing - Aligns with previous behavior where coverage was informational only * ✅ resolve getVerifiedDetails * ✅ applied _getEligibility and remove via-ir from foundry.toml * ✅ cover tokena and token permit * ✅ cover compliance * ✅ cover identity registry * ✅ increased trex gateway coverage * ✅ increase coverage for trex factory * ✅ increased AbstractModuleUpgradeable.sol coverage * ⚰️ removed abstract module * 🔧 updted generate coverage * ✅ coverage 100% identity registry * ✅ coverage 100% trex gate way * ✅ coverage trex factory * ✅ coverage 100% AbstractModuleUpgradeable by test module * ✅ coverage IAFactory.sol * ✅ coverage 100% for implementation authority * ✅ coverage 1005 abstract proxy * ✅ coverage 100% claim topic registry * ✅ coverage 100% identity registry storage * ✅ coverage 100% modular compliance proxy * ✅ coverage trusted issuer registry proxy * ✅ coverage 100% identity registry * ✅ coverage 100% token proxy * 🚨 resolve linter in token information * ✅ coverage 100% roles * 🚨 resolve linter * 🔧 added no coverage for test, _testContracts and utils * ✅ coverage 100% for token.sol * ✅ coverage 100% for modular compliance * ✅ coverage 100% trusted issuer registry * 🔧 restore package.json * ✅ coverage 100% for utlity checker * 🚨 fix linter
865 lines
33 KiB
Solidity
865 lines
33 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity 0.8.30;
|
|
|
|
import { IIdentity } from "@onchain-id/solidity/contracts/interface/IIdentity.sol";
|
|
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
|
|
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
|
import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
|
|
import {
|
|
AddressFrozen,
|
|
Paused,
|
|
TokensFrozen,
|
|
TokensUnfrozen,
|
|
Unpaused,
|
|
UpdatedTokenInformation
|
|
} from "contracts/ERC-3643/IERC3643.sol";
|
|
import { IERC3643IdentityRegistry } from "contracts/ERC-3643/IERC3643IdentityRegistry.sol";
|
|
import { MockContract } from "contracts/_testContracts/MockContract.sol";
|
|
import { TestTokenInternal } from "contracts/_testContracts/TestTokenInternal.sol";
|
|
import {
|
|
ERC20InvalidReceiver,
|
|
ERC20InvalidSender,
|
|
ERC20InvalidSpender,
|
|
InitializationFailed
|
|
} from "contracts/errors/CommonErrors.sol";
|
|
import { DecimalsOutOfRange, EmptyString, ZeroAddress } from "contracts/errors/InvalidArgumentErrors.sol";
|
|
import { ModularComplianceProxy } from "contracts/proxy/ModularComplianceProxy.sol";
|
|
import { TokenProxy } from "contracts/proxy/TokenProxy.sol";
|
|
import { ITREXImplementationAuthority } from "contracts/proxy/authority/ITREXImplementationAuthority.sol";
|
|
import { TREXImplementationAuthority } from "contracts/proxy/authority/TREXImplementationAuthority.sol";
|
|
import { IdentityRegistry } from "contracts/registry/implementation/IdentityRegistry.sol";
|
|
import { AlreadyInitialized } from "contracts/token/Token.sol";
|
|
import {
|
|
AgentNotAuthorized,
|
|
AmountAboveFrozenTokens,
|
|
CallerDoesNotHaveAgentRole,
|
|
ERC20InsufficientBalance,
|
|
EnforcedPause,
|
|
ExpectedPause,
|
|
Token
|
|
} from "contracts/token/Token.sol";
|
|
import { TokenRoles } from "contracts/token/TokenStructs.sol";
|
|
import { InterfaceIdCalculator } from "contracts/utils/InterfaceIdCalculator.sol";
|
|
import { TokenTestBase } from "test/token/TokenTestBase.sol";
|
|
|
|
contract TokenInformationTest is TokenTestBase {
|
|
|
|
// Token suite components
|
|
IdentityRegistry public identityRegistry;
|
|
|
|
function setUp() public override {
|
|
super.setUp();
|
|
|
|
// Get IdentityRegistry
|
|
IERC3643IdentityRegistry ir = token.identityRegistry();
|
|
identityRegistry = IdentityRegistry(address(ir));
|
|
|
|
// Add tokenAgent as an agent
|
|
vm.startPrank(deployer);
|
|
token.addAgent(tokenAgent);
|
|
identityRegistry.addAgent(tokenAgent);
|
|
vm.stopPrank();
|
|
|
|
// Register alice and bob in IdentityRegistry
|
|
vm.startPrank(tokenAgent);
|
|
identityRegistry.registerIdentity(alice, aliceIdentity, 42);
|
|
identityRegistry.registerIdentity(bob, bobIdentity, 666);
|
|
vm.stopPrank();
|
|
|
|
// Unpause token
|
|
vm.prank(tokenAgent);
|
|
token.unpause();
|
|
}
|
|
|
|
// ============ setName() Tests ============
|
|
|
|
/// @notice Should revert when called by not owner
|
|
function test_setName_RevertWhen_NotOwner() public {
|
|
vm.prank(another);
|
|
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, another));
|
|
token.setName("My Token");
|
|
}
|
|
|
|
/// @notice Should revert when the name is empty
|
|
function test_setName_RevertWhen_EmptyString() public {
|
|
vm.prank(deployer);
|
|
vm.expectRevert(EmptyString.selector);
|
|
token.setName("");
|
|
}
|
|
|
|
/// @notice Should set the name
|
|
function test_setName_Success() public {
|
|
string memory newName = "Updated Test Token";
|
|
string memory currentSymbol = token.symbol();
|
|
uint8 currentDecimals = token.decimals();
|
|
string memory currentVersion = token.version();
|
|
address currentOnchainID = token.onchainID();
|
|
|
|
vm.prank(deployer);
|
|
token.setName(newName);
|
|
|
|
assertEq(keccak256(bytes(token.name())), keccak256(bytes(newName)), "Token name should match");
|
|
}
|
|
|
|
// ============ setSymbol() Tests ============
|
|
|
|
/// @notice Should revert when called by not owner
|
|
function test_setSymbol_RevertWhen_NotOwner() public {
|
|
vm.prank(another);
|
|
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, another));
|
|
token.setSymbol("UpdtTK");
|
|
}
|
|
|
|
/// @notice Should revert when the symbol is empty
|
|
function test_setSymbol_RevertWhen_EmptyString() public {
|
|
vm.prank(deployer);
|
|
vm.expectRevert(EmptyString.selector);
|
|
token.setSymbol("");
|
|
}
|
|
|
|
/// @notice Should set the symbol
|
|
function test_setSymbol_Success() public {
|
|
string memory newSymbol = "UpdtTK";
|
|
|
|
vm.prank(deployer);
|
|
token.setSymbol(newSymbol);
|
|
|
|
assertEq(keccak256(bytes(token.symbol())), keccak256(bytes(newSymbol)), "Token symbol should match");
|
|
}
|
|
|
|
// ============ setOnchainID() Tests ============
|
|
|
|
/// @notice Should revert when called by not owner
|
|
function test_setOnchainID_RevertWhen_NotOwner() public {
|
|
vm.prank(another);
|
|
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, another));
|
|
token.setOnchainID(address(0));
|
|
}
|
|
|
|
/// @notice Should set the onchainID
|
|
function test_setOnchainID_Success() public {
|
|
// create an identity using IdFactory
|
|
vm.startPrank(deployer);
|
|
IIdentity newIdentity = IIdentity(onchainidSetup.idFactory.createIdentity(deployer, "deployer-salt"));
|
|
token.setOnchainID(address(newIdentity));
|
|
vm.stopPrank();
|
|
|
|
assertEq(token.onchainID(), address(newIdentity));
|
|
}
|
|
|
|
// ============ setIdentityRegistry() Tests ============
|
|
|
|
/// @notice Should revert when called by not owner
|
|
function test_setIdentityRegistry_RevertWhen_NotOwner() public {
|
|
vm.prank(another);
|
|
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, another));
|
|
token.setIdentityRegistry(address(0));
|
|
}
|
|
|
|
// ============ totalSupply() Tests ============
|
|
|
|
/// @notice Should return the total supply
|
|
function test_totalSupply_ReturnsTotalSupply() public view {
|
|
// Token starts with zero total supply
|
|
assertEq(token.totalSupply(), 0);
|
|
}
|
|
|
|
// ============ setCompliance() Tests ============
|
|
|
|
/// @notice Should revert when called by not owner
|
|
function test_setCompliance_RevertWhen_NotOwner() public {
|
|
vm.prank(another);
|
|
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, another));
|
|
token.setCompliance(address(0));
|
|
}
|
|
|
|
// ============ compliance() Tests ============
|
|
|
|
/// @notice Should return the compliance address
|
|
function test_compliance_ReturnsComplianceAddress() public {
|
|
// Deploy ModularCompliance proxy (similar to deploySuiteWithModularCompliancesFixture)
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
// Transfer ownership to deployer (compliance is owned by test contract after deployment)
|
|
Ownable(address(complianceProxy)).transferOwnership(deployer);
|
|
|
|
// Set compliance
|
|
vm.prank(deployer);
|
|
token.setCompliance(address(complianceProxy));
|
|
|
|
assertEq(address(token.compliance()), address(complianceProxy));
|
|
}
|
|
|
|
/// @notice Should unbind existing compliance when setting new compliance
|
|
function test_setCompliance_UnbindsExistingCompliance() public {
|
|
// Deploy first compliance
|
|
ModularComplianceProxy complianceProxy1 = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
Ownable(address(complianceProxy1)).transferOwnership(deployer);
|
|
|
|
// Deploy second compliance
|
|
ModularComplianceProxy complianceProxy2 = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
Ownable(address(complianceProxy2)).transferOwnership(deployer);
|
|
|
|
// Set first compliance
|
|
vm.prank(deployer);
|
|
token.setCompliance(address(complianceProxy1));
|
|
assertEq(address(token.compliance()), address(complianceProxy1));
|
|
|
|
// Set second compliance (should unbind first)
|
|
vm.prank(deployer);
|
|
token.setCompliance(address(complianceProxy2));
|
|
assertEq(address(token.compliance()), address(complianceProxy2));
|
|
}
|
|
|
|
// ============ pause() Tests ============
|
|
|
|
/// @notice Should revert when the caller is not an agent
|
|
function test_pause_RevertWhen_NotAgent() public {
|
|
vm.prank(another);
|
|
vm.expectRevert(CallerDoesNotHaveAgentRole.selector);
|
|
token.pause();
|
|
}
|
|
|
|
/// @notice Should revert when agent permission is restricted
|
|
function test_pause_RevertWhen_AgentRestricted() public {
|
|
TokenRoles memory restrictions = TokenRoles({
|
|
disableMint: false,
|
|
disableBurn: false,
|
|
disablePartialFreeze: false,
|
|
disableAddressFreeze: false,
|
|
disableRecovery: false,
|
|
disableForceTransfer: false,
|
|
disablePause: true
|
|
});
|
|
|
|
vm.prank(deployer);
|
|
token.setAgentRestrictions(tokenAgent, restrictions);
|
|
|
|
vm.prank(tokenAgent);
|
|
vm.expectRevert(abi.encodeWithSelector(AgentNotAuthorized.selector, tokenAgent, "pause disabled"));
|
|
token.pause();
|
|
}
|
|
|
|
/// @notice Should pause the token when not paused
|
|
function test_pause_Success() public {
|
|
vm.prank(tokenAgent);
|
|
token.pause();
|
|
|
|
assertTrue(token.paused());
|
|
}
|
|
|
|
/// @notice Should revert when the token is already paused
|
|
function test_pause_RevertWhen_AlreadyPaused() public {
|
|
// First pause
|
|
vm.prank(tokenAgent);
|
|
token.pause();
|
|
|
|
// Try to pause again
|
|
vm.prank(tokenAgent);
|
|
vm.expectRevert(EnforcedPause.selector);
|
|
token.pause();
|
|
}
|
|
|
|
// ============ unpause() Tests ============
|
|
|
|
/// @notice Should revert when the caller is not an agent
|
|
function test_unpause_RevertWhen_NotAgent() public {
|
|
vm.prank(another);
|
|
vm.expectRevert(CallerDoesNotHaveAgentRole.selector);
|
|
token.unpause();
|
|
}
|
|
|
|
/// @notice Should revert when agent permission is restricted
|
|
function test_unpause_RevertWhen_AgentRestricted() public {
|
|
// First pause
|
|
vm.prank(tokenAgent);
|
|
token.pause();
|
|
|
|
// Set restrictions
|
|
TokenRoles memory restrictions = TokenRoles({
|
|
disableMint: false,
|
|
disableBurn: false,
|
|
disablePartialFreeze: false,
|
|
disableAddressFreeze: false,
|
|
disableRecovery: false,
|
|
disableForceTransfer: false,
|
|
disablePause: true
|
|
});
|
|
|
|
vm.prank(deployer);
|
|
token.setAgentRestrictions(tokenAgent, restrictions);
|
|
|
|
vm.prank(tokenAgent);
|
|
vm.expectRevert(abi.encodeWithSelector(AgentNotAuthorized.selector, tokenAgent, "pause disabled"));
|
|
token.unpause();
|
|
}
|
|
|
|
/// @notice Should unpause the token when paused
|
|
function test_unpause_Success() public {
|
|
// First pause
|
|
vm.prank(tokenAgent);
|
|
token.pause();
|
|
|
|
// Unpause
|
|
vm.prank(tokenAgent);
|
|
token.unpause();
|
|
|
|
assertFalse(token.paused());
|
|
}
|
|
|
|
/// @notice Should revert when the token is not paused
|
|
function test_unpause_RevertWhen_NotPaused() public {
|
|
vm.prank(tokenAgent);
|
|
vm.expectRevert(ExpectedPause.selector);
|
|
token.unpause();
|
|
}
|
|
|
|
// ============ setAddressFrozen() Tests ============
|
|
|
|
/// @notice Should revert when sender is not an agent
|
|
function test_setAddressFrozen_RevertWhen_NotAgent() public {
|
|
vm.prank(another);
|
|
vm.expectRevert(CallerDoesNotHaveAgentRole.selector);
|
|
token.setAddressFrozen(another, true);
|
|
}
|
|
|
|
/// @notice Should revert when agent permission is restricted
|
|
function test_setAddressFrozen_RevertWhen_AgentRestricted() public {
|
|
TokenRoles memory restrictions = TokenRoles({
|
|
disableMint: false,
|
|
disableBurn: false,
|
|
disablePartialFreeze: false,
|
|
disableAddressFreeze: true,
|
|
disableRecovery: false,
|
|
disableForceTransfer: false,
|
|
disablePause: false
|
|
});
|
|
|
|
vm.prank(deployer);
|
|
token.setAgentRestrictions(tokenAgent, restrictions);
|
|
|
|
vm.prank(tokenAgent);
|
|
vm.expectRevert(abi.encodeWithSelector(AgentNotAuthorized.selector, tokenAgent, "address freeze disabled"));
|
|
token.setAddressFrozen(alice, true);
|
|
}
|
|
|
|
/// @notice Should freeze address successfully
|
|
function test_setAddressFrozen_Success() public {
|
|
vm.prank(tokenAgent);
|
|
vm.expectEmit(true, true, true, false, address(token));
|
|
emit AddressFrozen(alice, true, tokenAgent);
|
|
token.setAddressFrozen(alice, true);
|
|
|
|
assertTrue(token.isFrozen(alice));
|
|
}
|
|
|
|
/// @notice Should unfreeze address successfully
|
|
function test_setAddressFrozen_UnfreezeSuccess() public {
|
|
vm.prank(tokenAgent);
|
|
token.setAddressFrozen(alice, true);
|
|
|
|
vm.prank(tokenAgent);
|
|
vm.expectEmit(true, true, true, false, address(token));
|
|
emit AddressFrozen(alice, false, tokenAgent);
|
|
token.setAddressFrozen(alice, false);
|
|
|
|
assertFalse(token.isFrozen(alice));
|
|
}
|
|
|
|
// ============ freezePartialTokens() Tests ============
|
|
|
|
/// @notice Should revert when amounts exceed current balance
|
|
function test_freezePartialTokens_RevertWhen_AmountExceedsBalance() public {
|
|
vm.prank(tokenAgent);
|
|
vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientBalance.selector, another, 0, 1));
|
|
token.freezePartialTokens(another, 1);
|
|
}
|
|
|
|
// ============ supportsInterface() Tests ============
|
|
|
|
/// @notice Should return false for unsupported interfaces
|
|
function test_supportsInterface_ReturnsFalse_ForUnsupported() public view {
|
|
bytes4 unsupportedInterfaceId = 0x12345678;
|
|
assertFalse(token.supportsInterface(unsupportedInterfaceId));
|
|
}
|
|
|
|
/// @notice Should correctly identify the IERC20 interface ID
|
|
function test_supportsInterface_ReturnsTrue_ForIERC20() public {
|
|
InterfaceIdCalculator calculator = new InterfaceIdCalculator();
|
|
bytes4 interfaceId = calculator.getIERC20InterfaceId();
|
|
assertTrue(token.supportsInterface(interfaceId));
|
|
}
|
|
|
|
/// @notice Should correctly identify the IToken interface ID
|
|
function test_supportsInterface_ReturnsTrue_ForIToken() public {
|
|
InterfaceIdCalculator calculator = new InterfaceIdCalculator();
|
|
bytes4 interfaceId = calculator.getITokenInterfaceId();
|
|
assertTrue(token.supportsInterface(interfaceId));
|
|
}
|
|
|
|
/// @notice Should correctly identify the IERC3643 interface ID
|
|
function test_supportsInterface_ReturnsTrue_ForIERC3643() public {
|
|
InterfaceIdCalculator calculator = new InterfaceIdCalculator();
|
|
bytes4 interfaceId = calculator.getIERC3643InterfaceId();
|
|
assertTrue(token.supportsInterface(interfaceId));
|
|
}
|
|
|
|
/// @notice Should correctly identify the IERC173 interface ID
|
|
function test_supportsInterface_ReturnsTrue_ForIERC173() public {
|
|
InterfaceIdCalculator calculator = new InterfaceIdCalculator();
|
|
bytes4 interfaceId = calculator.getIERC173InterfaceId();
|
|
assertTrue(token.supportsInterface(interfaceId));
|
|
}
|
|
|
|
/// @notice Should correctly identify the IERC165 interface ID
|
|
function test_supportsInterface_ReturnsTrue_ForIERC165() public {
|
|
InterfaceIdCalculator calculator = new InterfaceIdCalculator();
|
|
bytes4 interfaceId = calculator.getIERC165InterfaceId();
|
|
assertTrue(token.supportsInterface(interfaceId));
|
|
}
|
|
|
|
/// @notice Should correctly identify the IERC20Permit interface ID
|
|
function test_supportsInterface_ReturnsTrue_ForIERC20Permit() public {
|
|
InterfaceIdCalculator calculator = new InterfaceIdCalculator();
|
|
bytes4 interfaceId = calculator.getIERC20PermitInterfaceId();
|
|
assertTrue(token.supportsInterface(interfaceId));
|
|
}
|
|
|
|
// ============ batchSetAddressFrozen() Tests ============
|
|
|
|
/// @notice Should perform batch address freezing
|
|
function test_batchSetAddressFrozen_Success() public {
|
|
address[] memory userAddresses = new address[](2);
|
|
userAddresses[0] = alice;
|
|
userAddresses[1] = bob;
|
|
bool[] memory freeze = new bool[](2);
|
|
freeze[0] = true;
|
|
freeze[1] = true;
|
|
|
|
vm.prank(tokenAgent);
|
|
vm.expectEmit(true, true, true, false, address(token));
|
|
emit AddressFrozen(alice, true, tokenAgent);
|
|
vm.expectEmit(true, true, true, false, address(token));
|
|
emit AddressFrozen(bob, true, tokenAgent);
|
|
token.batchSetAddressFrozen(userAddresses, freeze);
|
|
|
|
assertTrue(token.isFrozen(alice));
|
|
assertTrue(token.isFrozen(bob));
|
|
}
|
|
|
|
// ============ batchFreezePartialTokens() Tests ============
|
|
|
|
/// @notice Should perform batch partial token freezing
|
|
function test_batchFreezePartialTokens_Success() public {
|
|
// Ensure users have balances
|
|
vm.prank(tokenAgent);
|
|
token.mint(alice, 1000);
|
|
vm.prank(tokenAgent);
|
|
token.mint(bob, 500);
|
|
|
|
address[] memory userAddresses = new address[](2);
|
|
userAddresses[0] = alice;
|
|
userAddresses[1] = bob;
|
|
uint256[] memory amounts = new uint256[](2);
|
|
amounts[0] = 100;
|
|
amounts[1] = 200;
|
|
|
|
vm.prank(tokenAgent);
|
|
token.batchFreezePartialTokens(userAddresses, amounts);
|
|
|
|
assertEq(token.getFrozenTokens(alice), 100);
|
|
assertEq(token.getFrozenTokens(bob), 200);
|
|
}
|
|
|
|
// ============ batchUnfreezePartialTokens() Tests ============
|
|
|
|
/// @notice Should perform batch partial token unfreezing
|
|
function test_batchUnfreezePartialTokens_Success() public {
|
|
// Ensure users have balances
|
|
vm.prank(tokenAgent);
|
|
token.mint(alice, 1000);
|
|
vm.prank(tokenAgent);
|
|
token.mint(bob, 500);
|
|
|
|
// First freeze tokens
|
|
vm.prank(tokenAgent);
|
|
token.freezePartialTokens(alice, 200);
|
|
vm.prank(tokenAgent);
|
|
token.freezePartialTokens(bob, 300);
|
|
|
|
address[] memory userAddresses = new address[](2);
|
|
userAddresses[0] = alice;
|
|
userAddresses[1] = bob;
|
|
uint256[] memory amounts = new uint256[](2);
|
|
amounts[0] = 100;
|
|
amounts[1] = 200;
|
|
|
|
vm.prank(tokenAgent);
|
|
token.batchUnfreezePartialTokens(userAddresses, amounts);
|
|
|
|
assertEq(token.getFrozenTokens(alice), 100);
|
|
assertEq(token.getFrozenTokens(bob), 100);
|
|
}
|
|
|
|
// ============ Constructor Tests ============
|
|
|
|
/// @notice Should prevent direct initialization of Token implementation
|
|
function test_constructor_CallsDisableInitializers() public {
|
|
Token tokenImplementation = new Token();
|
|
assertTrue(address(tokenImplementation) != address(0));
|
|
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
Ownable(address(complianceProxy)).transferOwnership(deployer);
|
|
|
|
vm.expectRevert(Initializable.InvalidInitialization.selector);
|
|
tokenImplementation.init(
|
|
address(identityRegistry), address(complianceProxy), "Test Token", "TEST", 18, address(0)
|
|
);
|
|
}
|
|
|
|
/// @notice Should revert when implementation authority is zero address
|
|
function test_TokenProxy_constructor_RevertWhen_ImplementationAuthorityZeroAddress() public {
|
|
address randomAddress = vm.addr(999);
|
|
vm.expectRevert(ZeroAddress.selector);
|
|
new TokenProxy(address(0), randomAddress, randomAddress, "Test", "TST", 18, address(0));
|
|
}
|
|
|
|
/// @notice Should revert when identity registry is zero address
|
|
function test_TokenProxy_constructor_RevertWhen_IdentityRegistryZeroAddress() public {
|
|
address randomAddress = vm.addr(999);
|
|
vm.expectRevert(ZeroAddress.selector);
|
|
new TokenProxy(
|
|
address(getTREXImplementationAuthority()), address(0), randomAddress, "Test", "TST", 18, address(0)
|
|
);
|
|
}
|
|
|
|
/// @notice Should revert when compliance is zero address
|
|
function test_TokenProxy_constructor_RevertWhen_ComplianceZeroAddress() public {
|
|
address randomAddress = vm.addr(999);
|
|
vm.expectRevert(ZeroAddress.selector);
|
|
new TokenProxy(
|
|
address(getTREXImplementationAuthority()), randomAddress, address(0), "Test", "TST", 18, address(0)
|
|
);
|
|
}
|
|
|
|
/// @notice Should revert when name is empty string
|
|
function test_TokenProxy_constructor_RevertWhen_NameEmpty() public {
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
address randomAddress = vm.addr(999);
|
|
vm.expectRevert(EmptyString.selector);
|
|
new TokenProxy(
|
|
address(getTREXImplementationAuthority()),
|
|
randomAddress,
|
|
address(complianceProxy),
|
|
"",
|
|
"TST",
|
|
18,
|
|
address(0)
|
|
);
|
|
}
|
|
|
|
/// @notice Should revert when symbol is empty string
|
|
function test_TokenProxy_constructor_RevertWhen_SymbolEmpty() public {
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
address randomAddress = vm.addr(999);
|
|
vm.expectRevert(EmptyString.selector);
|
|
new TokenProxy(
|
|
address(getTREXImplementationAuthority()),
|
|
randomAddress,
|
|
address(complianceProxy),
|
|
"Test",
|
|
"",
|
|
18,
|
|
address(0)
|
|
);
|
|
}
|
|
|
|
/// @notice Should revert when decimals is greater than 18
|
|
function test_TokenProxy_constructor_RevertWhen_DecimalsGreaterThan18() public {
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
address randomAddress = vm.addr(999);
|
|
vm.expectRevert(abi.encodeWithSelector(DecimalsOutOfRange.selector, 19));
|
|
new TokenProxy(
|
|
address(getTREXImplementationAuthority()),
|
|
randomAddress,
|
|
address(complianceProxy),
|
|
"Test",
|
|
"TST",
|
|
19,
|
|
address(0)
|
|
);
|
|
}
|
|
|
|
/// @notice Should revert when initialization fails (invalid implementation)
|
|
function test_TokenProxy_constructor_RevertWhen_InitializationFails() public {
|
|
// Deploy a mock contract that doesn't have init() function
|
|
MockContract mockImpl = new MockContract();
|
|
|
|
// Deploy an IA and manually set an invalid Token implementation
|
|
TREXImplementationAuthority incompleteIA = new TREXImplementationAuthority(true, address(0), address(0));
|
|
|
|
// Create a version with invalid Token implementation (mock contract without init())
|
|
ITREXImplementationAuthority.Version memory version =
|
|
ITREXImplementationAuthority.Version({ major: 4, minor: 0, patch: 0 });
|
|
|
|
ITREXImplementationAuthority.TREXContracts memory contracts = ITREXImplementationAuthority.TREXContracts({
|
|
tokenImplementation: address(mockImpl), // Invalid - doesn't have init() function
|
|
ctrImplementation: address(mockImpl), // Invalid
|
|
irImplementation: address(mockImpl), // Invalid
|
|
irsImplementation: address(mockImpl), // Invalid
|
|
tirImplementation: address(mockImpl), // Invalid
|
|
mcImplementation: address(mockImpl) // Invalid
|
|
});
|
|
|
|
// Add version to IA (need to be owner)
|
|
Ownable(address(incompleteIA)).transferOwnership(deployer);
|
|
vm.prank(deployer);
|
|
incompleteIA.addAndUseTREXVersion(version, contracts);
|
|
|
|
// Now try to deploy proxy - delegatecall to mockImpl.init() will fail
|
|
// because MockContract doesn't have init() function, causing InitializationFailed() revert
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
address randomAddress = vm.addr(999);
|
|
vm.expectRevert(InitializationFailed.selector);
|
|
new TokenProxy(address(incompleteIA), randomAddress, address(complianceProxy), "Test", "TST", 18, address(0));
|
|
}
|
|
|
|
// ============ Token.init() Tests ============
|
|
|
|
function test_init_RevertWhen_IdentityRegistryZeroAddress_DirectCall() public {
|
|
// Deploy new implementation
|
|
Token implementation = new Token();
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
Ownable(address(complianceProxy)).transferOwnership(deployer);
|
|
|
|
vm.expectRevert(ZeroAddress.selector);
|
|
new ERC1967Proxy(
|
|
address(implementation),
|
|
abi.encodeWithSelector(
|
|
Token.init.selector,
|
|
address(0), // Zero address for Identity Registry
|
|
address(complianceProxy),
|
|
"Test Token",
|
|
"TEST",
|
|
18,
|
|
address(0)
|
|
)
|
|
);
|
|
}
|
|
|
|
function test_init_RevertWhen_ComplianceZeroAddress_DirectCall() public {
|
|
// Deploy new implementation
|
|
Token implementation = new Token();
|
|
address randomAddress = vm.addr(999);
|
|
|
|
vm.expectRevert(ZeroAddress.selector);
|
|
new ERC1967Proxy(
|
|
address(implementation),
|
|
abi.encodeWithSelector(
|
|
Token.init.selector,
|
|
randomAddress,
|
|
address(0), // Zero address for Compliance
|
|
"Test Token",
|
|
"TEST",
|
|
18,
|
|
address(0)
|
|
)
|
|
);
|
|
}
|
|
|
|
function test_init_RevertWhen_NameEmpty_DirectCall() public {
|
|
// Deploy new implementation
|
|
Token implementation = new Token();
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
Ownable(address(complianceProxy)).transferOwnership(deployer);
|
|
address randomAddress = vm.addr(999);
|
|
|
|
vm.expectRevert(EmptyString.selector);
|
|
new ERC1967Proxy(
|
|
address(implementation),
|
|
abi.encodeWithSelector(
|
|
Token.init.selector,
|
|
randomAddress,
|
|
address(complianceProxy),
|
|
"", // Empty name
|
|
"TEST",
|
|
18,
|
|
address(0)
|
|
)
|
|
);
|
|
}
|
|
|
|
function test_init_RevertWhen_SymbolEmpty_DirectCall() public {
|
|
// Deploy new implementation
|
|
Token implementation = new Token();
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
Ownable(address(complianceProxy)).transferOwnership(deployer);
|
|
address randomAddress = vm.addr(999);
|
|
|
|
vm.expectRevert(EmptyString.selector);
|
|
new ERC1967Proxy(
|
|
address(implementation),
|
|
abi.encodeWithSelector(
|
|
Token.init.selector,
|
|
randomAddress,
|
|
address(complianceProxy),
|
|
"Test Token",
|
|
"", // Empty symbol
|
|
18,
|
|
address(0)
|
|
)
|
|
);
|
|
}
|
|
|
|
function test_init_RevertWhen_DecimalsGreaterThan18_DirectCall() public {
|
|
// Deploy new implementation
|
|
Token implementation = new Token();
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
Ownable(address(complianceProxy)).transferOwnership(deployer);
|
|
address randomAddress = vm.addr(999);
|
|
|
|
vm.expectRevert(abi.encodeWithSelector(DecimalsOutOfRange.selector, 19));
|
|
new ERC1967Proxy(
|
|
address(implementation),
|
|
abi.encodeWithSelector(
|
|
Token.init.selector,
|
|
randomAddress,
|
|
address(complianceProxy),
|
|
"Test Token",
|
|
"TEST",
|
|
19, // Decimals > 18
|
|
address(0)
|
|
)
|
|
);
|
|
}
|
|
|
|
function test_init_RevertWhen_AlreadyInitialized_OwnerNotZero() public {
|
|
Token implementation = new Token();
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
Ownable(address(complianceProxy)).transferOwnership(deployer);
|
|
address randomAddress = vm.addr(999);
|
|
|
|
// Deploy proxy without initialization
|
|
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), "");
|
|
Token proxyToken = Token(address(proxy));
|
|
|
|
// Simulate legacy contract state where owner is already set (defensive check at line 174)
|
|
// Owner slot is at position 0 in OwnableOnceNext2StepUpgradeable
|
|
vm.store(address(proxy), bytes32(uint256(0)), bytes32(uint256(uint160(vm.addr(999)))));
|
|
|
|
vm.expectRevert(AlreadyInitialized.selector);
|
|
proxyToken.init(randomAddress, address(complianceProxy), "Test Token", "TEST", 18, address(0));
|
|
}
|
|
|
|
// ============ Internal Function Zero Address Tests ============
|
|
|
|
function test_internalTransfer_RevertWhen_FromZeroAddress() public {
|
|
TestTokenInternal implementation = new TestTokenInternal();
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
Ownable(address(complianceProxy)).transferOwnership(deployer);
|
|
|
|
bytes memory initData = abi.encodeWithSelector(
|
|
Token.init.selector,
|
|
address(identityRegistry),
|
|
address(complianceProxy),
|
|
"Test Token",
|
|
"TEST",
|
|
18,
|
|
address(0)
|
|
);
|
|
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData);
|
|
TestTokenInternal testToken = TestTokenInternal(address(proxy));
|
|
|
|
vm.expectRevert(abi.encodeWithSelector(ERC20InvalidSpender.selector, address(0)));
|
|
testToken.exposeTransfer(address(0), bob, 100);
|
|
}
|
|
|
|
function test_internalTransfer_RevertWhen_ToZeroAddress() public {
|
|
TestTokenInternal implementation = new TestTokenInternal();
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
Ownable(address(complianceProxy)).transferOwnership(deployer);
|
|
|
|
bytes memory initData = abi.encodeWithSelector(
|
|
Token.init.selector,
|
|
address(identityRegistry),
|
|
address(complianceProxy),
|
|
"Test Token",
|
|
"TEST",
|
|
18,
|
|
address(0)
|
|
);
|
|
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData);
|
|
TestTokenInternal testToken = TestTokenInternal(address(proxy));
|
|
|
|
testToken.addAgent(tokenAgent);
|
|
vm.prank(tokenAgent);
|
|
testToken.mint(alice, 1000);
|
|
|
|
vm.expectRevert(abi.encodeWithSelector(ERC20InvalidReceiver.selector, address(0)));
|
|
testToken.exposeTransfer(alice, address(0), 100);
|
|
}
|
|
|
|
function test_internalMint_RevertWhen_UserAddressZero() public {
|
|
TestTokenInternal implementation = new TestTokenInternal();
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
Ownable(address(complianceProxy)).transferOwnership(deployer);
|
|
|
|
bytes memory initData = abi.encodeWithSelector(
|
|
Token.init.selector,
|
|
address(identityRegistry),
|
|
address(complianceProxy),
|
|
"Test Token",
|
|
"TEST",
|
|
18,
|
|
address(0)
|
|
);
|
|
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData);
|
|
TestTokenInternal testToken = TestTokenInternal(address(proxy));
|
|
|
|
vm.expectRevert(abi.encodeWithSelector(ERC20InvalidReceiver.selector, address(0)));
|
|
testToken.exposeMint(address(0), 100);
|
|
}
|
|
|
|
function test_internalBurn_RevertWhen_UserAddressZero() public {
|
|
TestTokenInternal implementation = new TestTokenInternal();
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
Ownable(address(complianceProxy)).transferOwnership(deployer);
|
|
|
|
bytes memory initData = abi.encodeWithSelector(
|
|
Token.init.selector,
|
|
address(identityRegistry),
|
|
address(complianceProxy),
|
|
"Test Token",
|
|
"TEST",
|
|
18,
|
|
address(0)
|
|
);
|
|
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData);
|
|
TestTokenInternal testToken = TestTokenInternal(address(proxy));
|
|
|
|
vm.expectRevert(abi.encodeWithSelector(ERC20InvalidSpender.selector, address(0)));
|
|
testToken.exposeBurn(address(0), 100);
|
|
}
|
|
|
|
function test_internalApprove_RevertWhen_OwnerZeroAddress() public {
|
|
TestTokenInternal implementation = new TestTokenInternal();
|
|
ModularComplianceProxy complianceProxy = new ModularComplianceProxy(address(getTREXImplementationAuthority()));
|
|
Ownable(address(complianceProxy)).transferOwnership(deployer);
|
|
|
|
bytes memory initData = abi.encodeWithSelector(
|
|
Token.init.selector,
|
|
address(identityRegistry),
|
|
address(complianceProxy),
|
|
"Test Token",
|
|
"TEST",
|
|
18,
|
|
address(0)
|
|
);
|
|
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData);
|
|
TestTokenInternal testToken = TestTokenInternal(address(proxy));
|
|
|
|
vm.expectRevert(abi.encodeWithSelector(ERC20InvalidSender.selector, address(0)));
|
|
testToken.exposeApprove(address(0), bob, 100);
|
|
}
|
|
|
|
}
|