HIGH severity: Token name update breaks EIP-712 Domain Separator for permit functionality. **Description:** The EIP-712 domain separator is initialized once during contract deployment in `__ERC20PermitMixin_init()` with the initial token name:

However, `StandardToken` allows the Master role to update the token name via `updateNameAndSymbol`:

**Impact:** When the token name is changed,.....

Vulnerable code:
function __ERC20PermitMixin_init(string memory name_) internal onlyInitializing {
    __EIP712_init(name_, "1");  // Domain separator set with initial name
    __Nonces_init();
}

function updateNameAndSymbol(string calldata _name, string calldata _symbol) external onlyMaster {
    // ...
    name = _name;  // Name updated but EIP-712 domain separator NOT updated
    // ...
}

import { expect } from 'chai';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import hre from 'hardhat';
import {
  deployDSTokenRegulated,
  INVESTORS,
} from './utils/fixture';
import { buildPermitSignature, registerInvestor } from './utils/test-helper';

describe('M-01: Token Name Update Breaks Permit Functionality - Proof of Concept', function() {

  describe('Demonstrating the Vulnerability', function() {

    it('CRITICAL: Permit fails after name update - All new permits become invalid', async function() {
      const [owner, spender] = await hre.ethers.getSigners();
      const { dsToken } = await loadFixture(deployDSTokenRegulated);

      // Initial state: Token name is "Token Example 1"
      const originalName = await dsToken.name();
      expect(originalName).to.equal('Token Example 1');

      // ✅ STEP 1: Permit works BEFORE name change
      console.log('\n--- BEFORE NAME CHANGE ---');
      const deadline1 = BigInt(Math.floor(Date.now() / 1000) + 3600);
      const value = 100;
      const message1 = {
        owner: owner.address,
        spender: spender.address,
        value,
        nonce: await dsToken.nonces(owner.address),
        deadline: deadline1,
      };

      // User signs with original name "Token Example 1"
      const sig1 = await buildPermitSignature(
        owner,
        message1,
        originalName,  // Uses "Token Example 1"
        await dsToken.getAddress()
      );

      // Permit succeeds with original name
      await dsToken.permit(owner.address, spender.address, value, deadline1, sig1.v, sig1.r, sig1.s);
      console.log('✅ Permit with original name: SUCCESS');
      expect(await dsToken.allowance(owner.address, spender.address)).to.equal(value);

      // ⚠️ STEP 2: Master updates token name
      console.log('\n--- NAME CHANGE ---');
      const newName = 'Token Example 2 - Updated';
      await dsToken.updateNameAndSymbol(newName, 'TX2');
      expect(await dsToken.name()).to.equal(newName);
      console.log(`Token name updated: "${originalName}" → "${newName}"`);

      // ❌ STEP 3: Permit FAILS after name change
      console.log('\n--- AFTER NAME CHANGE ---');
      const deadline2 = BigInt(Math.floor(Date.now() / 1000) + 3600);
      const message2 = {
        owner: owner.address,
        spender: spender.address,
        value: 200,
        nonce: await dsToken.nonces(owner.address),
        deadline: deadline2,
      };

      // User's wallet fetches current name and generates signature
      const currentName = await dsToken.name(); // Returns "Token Example 2 - Updated"
      console.log(`User's wallet uses current name: "${currentName}"`);

      const sig2 = await buildPermitSignature(
        owner,
        message2,
        currentName,  // Uses NEW name "Token Example 2 - Updated"
        await dsToken.getAddress()
      );

      // 🚨 PERMIT FAILS - Domain separator mismatch!
      await expect(
        dsToken.permit(owner.address, spender.address, 200, deadline2, sig2.v, sig2.r, sig2.s)
      ).to.be.revertedWith('Permit: invalid signature');

      console.log('❌ Permit with new name: FAILED - "Permit: invalid signature"');
      console.log('\n🚨 VULNERABILITY CONFIRMED: All new permits fail after name change!');
    });

    it('IMPACT: Old permits continue working while new ones fail - Inconsistent behavior', async function() {
      const [owner, spender] = await hre.ethers.getSigners();
      const { dsToken } = await loadFixture(deployDSTokenRegulated);

      const originalName = await dsToken.name();

      // Generate permit signature BEFORE name change
      const deadline1 = BigInt(Math.floor(Date.now() / 1000) + 3600);
      const message1 = {
        owner: owner.address,
        spender: spender.address,
        value: 100,
        nonce: await dsToken.nonces(owner.address),
        deadline: deadline1,
      };

      const oldSignature = await buildPermitSignature(
        owner,
        message1,
        originalName,
        await dsToken.getAddress()
      );

      // Master changes the name
      await dsToken.updateNameAndSymbol('Token Example 2', 'TX2');
      const newName = await dsToken.name();

      // ✅ OLD permit (signed before name change) still works!
      await dsToken.permit(
        owner.address,
        spender.address,
        100,
        deadline1,
        oldSignature.v,
        oldSignature.r,
        oldSignature.s
      );
      console.log('✅ Old permit (signed before name change): SUCCESS');

      // ❌ NEW permit (signed after name change) fails!
      const deadline2 = BigInt(Math.floor(Date.now() / 1000) + 3600);
      const message2 = {
        owner: owner.address,
        spender: spender.address,
        value: 200,
        nonce: await dsToken.nonces(owner.address),
        deadline: deadline2,
      };

      const newSignature = await buildPermitSignature(
        owner,
        message2,
        newName,  // Uses new name
        await dsToken.getAddress()
      );

      await expect(
        dsToken.permit(owner.address, spender.address, 200, deadline2, newSignature.v, newSignature.r, newSignature.s)
      ).to.be.revertedWith('Permit: invalid signature');

      console.log('❌ New permit (signed after name change): FAILED');
      console.log('\n🚨 INCONSISTENT STATE: Split behavior based on signature timing!');
    });

    it('IMPACT: DApp integrations break silently', async function() {
      const [owner, spender] = await hre.ethers.getSigners();
      const { dsToken } = await loadFixture(deployDSTokenRegulated);

      // Simulate a DApp that dynamically fetches token name
      async function dAppGeneratePermitSignature(tokenContract, ownerSigner, spenderAddress, value, deadline) {
        // Standard DApp implementation: fetch name dynamically
        const tokenName = await tokenContract.name();
        const tokenAddress = await tokenContract.getAddress();

        const message = {
          owner: ownerSigner.address,
          spender: spenderAddress,
          value,
          nonce: await tokenContract.nonces(ownerSigner.address),
          deadline,
        };

        return await buildPermitSignature(ownerSigner, message, tokenName, tokenAddress);
      }

      // ✅ DApp works fine initially
      const deadline1 = BigInt(Math.floor(Date.now() / 1000) + 3600);
      const sig1 = await dAppGeneratePermitSignature(dsToken, owner, spender.address, 100, deadline1);

      await dsToken.permit(owner.address, spender.address, 100, deadline1, sig1.v, sig1.r, sig1.s);
      console.log('✅ DApp integration BEFORE name change: SUCCESS');

      // Master updates name
      await dsToken.updateNameAndSymbol('Token Example 2', 'TX2');
      console.log('\n⚠️  Token name updated to "Token Example 2"');

      // ❌ DApp breaks - it fetches the NEW name but contract validates against OLD name
      const deadline2 = BigInt(Math.floor(Date.now() / 1000) + 3600);
      const sig2 = await dAppGeneratePermitSignature(dsToken, owner, spender.address, 200, deadline2);

      await expect(
        dsToken.permit(owner.address, spender.address, 200, deadline2, sig2.v, sig2.r, sig2.s)
      ).to.be.revertedWith('Permit: invalid signature');

      console.log('❌ DApp integration AFTER name change: FAILED');
      console.log('🚨 DApp has NO way to detect this issue programmatically!');
    });

    it('WORKAROUND: Permit succeeds if user manually uses ORIGINAL name (terrible UX)', async function() {
      const [owner, spender] = await hre.ethers.getSigners();
      const { dsToken } = await loadFixture(deployDSTokenRegulated);

      const originalName = await dsToken.name(); // "Token Example 1"

      // Master updates name
      await dsToken.updateNameAndSymbol('Token Example 2', 'TX2');

      // ❌ Using current name fails
      const deadline1 = BigInt(Math.floor(Date.now() / 1000) + 3600);
      const message1 = {
        owner: owner.address,
        spender: spender.address,
        value: 100,
        nonce: await dsToken.nonces(owner.address),
        deadline: deadline1,
      };

      const sigWithNewName = await buildPermitSignature(
        owner,
        message1,
        await dsToken.name(),  // "Token Example 2" (current)
        await dsToken.getAddress()
      );

      await expect(
        dsToken.permit(owner.address, spender.address, 100, deadline1, sigWithNewName.v, sigWithNewName.r, sigWithNewName.s)
      ).to.be.revertedWith('Permit: invalid signature');
      console.log('❌ Permit with current name "Token Example 2": FAILED');

      // ✅ Using ORIGINAL name works (but terrible UX)
      const deadline2 = BigInt(Math.floor(Date.now() / 1000) + 3600);
      const message2 = {
        owner: owner.address,
        spender: spender.address,
        value: 200,
        nonce: await dsToken.nonces(owner.address),
        deadline: deadline2,
      };

      const sigWithOriginalName = await buildPermitSignature(
        owner,
        message2,
        originalName,  // "Token Example 1" (original)
        await dsToken.getAddress()
      );

      await dsToken.permit(
        owner.address,
        spender.address,
        200,
        deadline2,
        sigWithOriginalName.v,
        sigWithOriginalName.r,
        sigWithOriginalName.s
      );
      console.log('✅ Permit with ORIGINAL name "Token Example 1": SUCCESS');
      console.log('\n🚨 WORKAROUND: Users must use deprecated name - breaks EIP-2612 expectations!');
    });

    it('VERIFICATION: DOMAIN_SEPARATOR remains unchanged after name update', async function() {
      const { dsToken } = await loadFixture(deployDSTokenRegulated);

      const originalName = 'Token Example 1';

      // Get domain separator before name change
      const domainSeparatorBefore = await dsToken.DOMAIN_SEPARATOR();
      console.log('Domain separator BEFORE name change:', domainSeparatorBefore);

      // Compute expected domain separator with original name
      const expectedDomainBefore = hre.ethers.TypedDataEncoder.hashDomain({
        version: '1',
        name: originalName,
        verifyingContract: await dsToken.getAddress(),
        chainId: (await hre.ethers.provider.getNetwork()).chainId,
      });

      expect(domainSeparatorBefore).to.equal(expectedDomainBefore);

      // Update name
      await dsToken.updateNameAndSymbol('Token Example 2', 'TX2');
      const newName = await dsToken.name();
      console.log(`\nName updated: "${originalName}" → "${newName}"`);

      // Get domain separator after name change
      const domainSeparatorAfter = await dsToken.DOMAIN_SEPARATOR();
      console.log('Domain separator AFTER name change:', domainSeparatorAfter);

      // 🚨 DOMAIN SEPARATOR UNCHANGED!
      expect(domainSeparatorAfter).to.equal(domainSeparatorBefore);
      console.log('\n🚨 VERIFIED: Domain separator did NOT update with new name!');

      // Compute what the domain separator SHOULD be with new name
      const expectedDomainWithNewName = hre.ethers.TypedDataEncoder.hashDomain({
        version: '1',
        name: newName,
        verifyingContract: await dsToken.getAddress(),
        chainId: (await hre.ethers.provider.getNetwork()).chainId,
      });

      console.log('\nExpected domain with NEW name:', expectedDomainWithNewName);
      console.log('Actual domain separator:      ', domainSeparatorAfter);
      console.log('Match:', domainSeparatorAfter === expectedDomainWithNewName ? '✅' : '❌');

      // They don't match - this is the root cause
      expect(domainSeparatorAfter).to.not.equal(expectedDomainWithNewName);
    });
  });

  describe('Real-World Attack Scenarios', function() {

    it('SCENARIO 1: Protocol rebranding breaks all user permits', async function() {
      const [owner, user1, user2, dex] = await hre.ethers.getSigners();
      const { dsToken, registryService } = await loadFixture(deployDSTokenRegulated);

      // Setup investors
      await registerInvestor(INVESTORS.INVESTOR_ID.INVESTOR_ID_1, user1, registryService);
      await registerInvestor(INVESTORS.INVESTOR_ID.INVESTOR_ID_2, user2, registryService);
      await dsToken.issueTokens(user1, 1000);

      console.log('\n📊 SCENARIO: Token rebranding from "Token Example 1" to "Acme Securities Token"');

      // Before rebrand: User1 can use permit to approve DEX
      const deadline1 = BigInt(Math.floor(Date.now() / 1000) + 3600);
      const message1 = {
        owner: user1.address,
        spender: dex.address,
        value: 500,
        nonce: await dsToken.nonces(user1.address),
        deadline: deadline1,
      };

      const sig1 = await buildPermitSignature(
        user1,
        message1,
        await dsToken.name(),
        await dsToken.getAddress()
      );

      await dsToken.permit(user1.address, dex.address, 500, deadline1, sig1.v, sig1.r, sig1.s);
      console.log('✅ User1 successfully approved DEX using permit (before rebrand)');

      // 🏢 PROTOCOL REBRANDS
      await dsToken.updateNameAndSymbol('Acme Securities Token', 'AST');
      console.log('\n🏢 Protocol rebrands to "Acme Securities Token"');

      // After rebrand: All new permits fail
      const deadline2 = BigInt(Math.floor(Date.now() / 1000) + 3600);
      const message2 = {
        owner: user2.address,
        spender: dex.address,
        value: 500,
        nonce: await dsToken.nonces(user2.address),
        deadline: deadline2,
      };

      await dsToken.issueTokens(user2, 1000);

      const sig2 = await buildPermitSignature(
        user2,
        message2,
        await dsToken.name(), // Uses new name
        await dsToken.getAddress()
      );

      await expect(
        dsToken.permit(user2.address, dex.address, 500, deadline2, sig2.v, sig2.r, sig2.s)
      ).to.be.revertedWith('Permit: invalid signature');

      console.log('❌ User2 permit FAILS after rebrand');
      console.log('🚨 Impact: 100% of new users cannot use gasless approvals!');
      console.log('📞 Result: Support tickets flood in, users confused');
    });

    it('SCENARIO 2: Front-end integration breaks without warning', async function() {
      const [owner, user, spender] = await hre.ethers.getSigners();
      const { dsToken, registryService } = await loadFixture(deployDSTokenRegulated);

      await registerInvestor(INVESTORS.INVESTOR_ID.INVESTOR_ID_1, user, registryService);
      await dsToken.issueTokens(user, 1000);

      console.log('\n🌐 SCENARIO: Frontend dApp integration');

      // Simulate frontend code
      const frontendPermitFlow = async (token, fromUser, toSpender, amount) => {
        // Standard EIP-2612 implementation in frontend
        const name = await token.name(); // Fetch current name dynamically
        const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);
        const nonce = await token.nonces(fromUser.address);

        const message = {
          owner: fromUser.address,
          spender: toSpender,
          value: amount,
          nonce,
          deadline,
        };

        const signature = await buildPermitSignature(
          fromUser,
          message,
          name,
          await token.getAddress()
        );

        return { deadline, signature };
      };

      // ✅ Frontend works initially
      const { deadline: d1, signature: s1 } = await frontendPermitFlow(dsToken, user, spender.address, 100);
      await dsToken.permit(user.address, spender.address, 100, d1, s1.v, s1.r, s1.s);
      console.log('✅ Frontend permit flow: SUCCESS (initial deployment)');

      // Master updates name (e.g., for compliance reasons)
      await dsToken.updateNameAndSymbol('Compliant Token v2', 'CTv2');
      console.log('\n⚠️  Master updates name for compliance');

      // ❌ Frontend breaks silently
      const { deadline: d2, signature: s2 } = await frontendPermitFlow(dsToken, user, spender.address, 200);
      await expect(
        dsToken.permit(user.address, spender.address, 200, d2, s2.v, s2.r, s2.s)
      ).to.be.revertedWith('Permit: invalid signature');

      console.log('❌ Frontend permit flow: BROKEN (after name update)');
      console.log('🚨 Error message gives NO hint about name mismatch');
      console.log('😰 Users see "Invalid signature" and blame wallet/frontend');
    });
  });
});