diff --git a/docs/olympix/bugpocer/BugPoCer_Scan_Report_CMTAT.pdf b/docs/olympix/bugpocer/BugPoCer_Scan_Report_CMTAT.pdf new file mode 100644 index 00000000..68c96035 Binary files /dev/null and b/docs/olympix/bugpocer/BugPoCer_Scan_Report_CMTAT.pdf differ diff --git a/docs/olympix/fuzz_testing/Fuzz_Test_Report_f37cd9fe-cb1e-4732-821a-03c6811416dc_20260716_182652.pdf b/docs/olympix/fuzz_testing/Fuzz_Test_Report_f37cd9fe-cb1e-4732-821a-03c6811416dc_20260716_182652.pdf new file mode 100644 index 00000000..56ba46aa Binary files /dev/null and b/docs/olympix/fuzz_testing/Fuzz_Test_Report_f37cd9fe-cb1e-4732-821a-03c6811416dc_20260716_182652.pdf differ diff --git a/docs/olympix/mutation_testing/mutation-results.csv b/docs/olympix/mutation_testing/mutation-results.csv new file mode 100644 index 00000000..4b55a2e9 --- /dev/null +++ b/docs/olympix/mutation_testing/mutation-results.csv @@ -0,0 +1,94 @@ +File;Line Number;Original Code;Mutated Code;Was Mutation Killed?;Killer Tests +contracts/modules/wrapper/core/ERC20BaseModule.sol;28;"0x9bd8d607565c0370ae5f91651ca67fd26d4438022bf72037316600e29e6a3a00";"0x0";False;"" +contracts/modules/wrapper/core/ERC20BaseModule.sol;54;") internal virtual onlyInitializing";") internal virtual";False;"" +contracts/modules/wrapper/core/ERC20BaseModule.sol;55;"ERC20BaseModuleStorage storage $";"ERC20BaseModuleStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/wrapper/core/ERC20BaseModule.sol;80;"result";"!result";True;"testTransferByAnotherAccountWithTheRightAllowance" +contracts/modules/wrapper/core/ERC20BaseModule.sol;83;"emit Spend(from, _msgSender(), value);";"";True;"testTransferByAnotherAccountWithTheRightAllowance" +contracts/modules/wrapper/core/ERC20BaseModule.sol;96;"ERC20BaseModuleStorage storage $";"ERC20BaseModuleStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/wrapper/core/ERC20BaseModule.sol;104;"ERC20BaseModuleStorage storage $";"ERC20BaseModuleStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/wrapper/core/ERC20BaseModule.sol;113;"ERC20BaseModuleStorage storage $";"ERC20BaseModuleStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/wrapper/core/ERC20BaseModule.sol;124;"function setName(string calldata name_) public virtual override(IERC3643ERC20Base) onlyERC20AttributeManager";"function setName(string calldata name_) public virtual override(IERC3643ERC20Base)";True;"testCannotNonAdminUpdateName" +contracts/modules/wrapper/core/ERC20BaseModule.sol;125;"ERC20BaseModuleStorage storage $";"ERC20BaseModuleStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/wrapper/core/ERC20BaseModule.sol;127;"emit Name(name_, name_);";"";True;"testAdminCanUpdateName, testCMTATIntegration" +contracts/modules/wrapper/core/ERC20BaseModule.sol;133;"function setSymbol(string calldata symbol_) public virtual override(IERC3643ERC20Base) onlyERC20AttributeManager";"function setSymbol(string calldata symbol_) public virtual override(IERC3643ERC20Base)";True;"testCannotNonAdminUpdateName" +contracts/modules/wrapper/core/ERC20BaseModule.sol;134;"ERC20BaseModuleStorage storage $";"ERC20BaseModuleStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/wrapper/core/ERC20BaseModule.sol;136;"emit Symbol(symbol_, symbol_);";"";True;"testAdminCanUpdateSymbol" +contracts/modules/wrapper/core/PauseModule.sol;32;"0xab1527b6135145d8da1edcbd6b7b270624e17f2b41c74a8c746ff388ad454700";"0x0";True;"reverts when trying to reschedule a snapshot in the past" +contracts/modules/wrapper/core/PauseModule.sol;59;"function pause() public virtual override(IERC3643Pause, IERC7551Pause) onlyPauseManager";"function pause() public virtual override(IERC3643Pause, IERC7551Pause)";True;"testCannotBePausedByNonPauser, reverts when trying to schedule a snapshot in the past, testCannotBeTakenControlByAttacker" +contracts/modules/wrapper/core/PauseModule.sol;68;"function unpause() public virtual override(IERC3643Pause, IERC7551Pause) onlyPauseManager";"function unpause() public virtual override(IERC3643Pause, IERC7551Pause)";True;"testCannotBeUnpausedByNonPauser" +contracts/modules/wrapper/core/PauseModule.sol;69;"PauseModuleStorage storage $";"PauseModuleStorage memory $";False;"" +contracts/modules/wrapper/core/PauseModule.sol;83;"onlyDeactivateContractManager";"";True;"testCannotBeDeactivatedByNonAdmin" +contracts/modules/wrapper/core/PauseModule.sol;87;"PauseModuleStorage storage $";"PauseModuleStorage memory $";True;"testCannotBeBurnIfContractIsDeactivated, testCannotBeBurnAndMintIfContractIsDeactivated, testCannotBeBatchBurnIfContractIsDeactivated, testCannotBeMintedIfContractIsDeactivated, testCannotBeBatchMintedIfContractIsDeactivated, testCanDeactivatedByAdminIfContractIsPaused, testCannotMintIfContractIsDeactivated" +contracts/modules/wrapper/core/PauseModule.sol;89;"emit Deactivated(_msgSender());";"";True;"testCanDeactivatedByAdminIfContractIsPaused" +contracts/modules/wrapper/core/PauseModule.sol;104;"PauseModuleStorage storage $";"PauseModuleStorage memory $";False;"" +contracts/modules/4_CMTATBaseERC20CrossChain.sol;113;"function _authorizeCCIPSetAdmin() internal virtual override(CCIPModule) onlyRole(DEFAULT_ADMIN_ROLE)";"function _authorizeCCIPSetAdmin() internal virtual override(CCIPModule)";True;"testCannotNonAdminSetCCIPAdmin" +contracts/modules/4_CMTATBaseERC20CrossChain.sol;124;"function _checkTokenBridge(address caller) internal virtual override(ERC20CrossChainModule) whenNotPaused";"function _checkTokenBridge(address caller) internal virtual override(ERC20CrossChainModule)";True;"testCannotBeBurnIfContractIsDeactivated, testCannotBeBurnIfContractIsPaused, testCannotBeMintedIfContractIsPaused, testCannotBeMintedIfContractIsDeactivated" +contracts/modules/4_CMTATBaseERC20CrossChain.sol;133;"function _authorizeBurnFrom() internal virtual override(ERC20CrossChainModule) onlyRole(BURNER_FROM_ROLE) whenNotPaused";"function _authorizeBurnFrom() internal virtual override(ERC20CrossChainModule) onlyRole(BURNER_FROM_ROLE)";True;"testCannotBeBurnFromIfContractIsPaused, testCannotBeBurnFromIfContractIsDeactivated" +contracts/modules/4_CMTATBaseERC20CrossChain.sol;133;"function _authorizeBurnFrom() internal virtual override(ERC20CrossChainModule) onlyRole(BURNER_FROM_ROLE) whenNotPaused";"function _authorizeBurnFrom() internal virtual override(ERC20CrossChainModule) whenNotPaused";True;"testCannotBeBurntWithoutBurnerFromRole" +contracts/modules/4_CMTATBaseERC20CrossChain.sol;140;"function _authorizeSelfBurn() internal virtual override(ERC20CrossChainModule) onlyRole(BURNER_SELF_ROLE) whenNotPaused";"function _authorizeSelfBurn() internal virtual override(ERC20CrossChainModule) onlyRole(BURNER_SELF_ROLE)";True;"testCannotBeMBurnIfContractIsDeactivated, testCanBeBurnEvenIfContractIsPaused" +contracts/modules/4_CMTATBaseERC20CrossChain.sol;140;"function _authorizeSelfBurn() internal virtual override(ERC20CrossChainModule) onlyRole(BURNER_SELF_ROLE) whenNotPaused";"function _authorizeSelfBurn() internal virtual override(ERC20CrossChainModule) whenNotPaused";True;"testCannotBeBurntWithoutBurnerRole" +contracts/modules/wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol;48;") public virtual onlyRuleEngineManager";") public virtual";True;"testCannotBeSetByNonAdmin" +contracts/modules/wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol;49;"address(ruleEngine_) != address(ruleEngine())";"address(ruleEngine_) == address(ruleEngine())";True;"testCanCanTransferWithoutRuleEngine, testCanCanTransferFromWithoutRuleEngine, testCanCanMintWithoutRuleEngine, testCanBeSetByAdmin, testCanNotBeSetByAdminWithTheSameValue" +contracts/modules/wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol;87;"!ValidationModuleCore.canTransfer(from, to, value)";"ValidationModuleCore.canTransfer(from, to, value)";True;"testAdminCanFreezeAddress, testAdminCanBatchFreezeAddress, testAdminCanBatchPartialFreezeAddress, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddress, testEnforcerRoleCanUnfreezeAddress, testCannotTransferTokenWhenPausedWithTransfer, testCannotTransferTokenWhenPausedWithTransferFrom, testCanDeactivatedByAdminIfContractIsPaused, testAdminCanFreezeAddressTwice, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testAdminCanFreezeAddressReason, testEnforcerRoleCanFreezeAddressReason, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason, testCanTransferTokenIfActiveBalanceIsEnough, testAdminCanFreezeAddressWithTokenFrozenDifferentLess, testAdminCanFreezeAddressWithTokenFrozenDifferentMore, testCanCanTransferWithoutRuleEngine, testCanDetectTransferRestrictionValidTransfer, testCanTransferAllowedByRule, testCanCanMintWithoutRuleEngine, testCanMintAllowedByRule, testCannotMintIfNotAllowedByRule, testCanTransferReturnsFalseForMintToFrozenAddress, testCanTransferReturnsFalseForBurnFromFrozenAddress, testCanTransferAllowed, testCanTransfer" +contracts/modules/wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol;100;"!ValidationModuleCore.canTransferFrom(spender, from, to, value)";"ValidationModuleCore.canTransferFrom(spender, from, to, value)";True;"testCannotTransferTokenWhenSpenderIsFrozenWithTransferFrom, testCannotTransferTokenWhenPausedWithTransfer, testCannotTransferTokenWhenPausedWithTransferFrom, testCanDeactivatedByAdminIfContractIsPaused, testCanTransferTokenIfActiveBalanceIsEnough, testCanTransferFrom, testCanCanTransferFromWithoutRuleEngine, testCanApproveAllowedByRuleEngine" +contracts/modules/wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol;114;"address(ruleEngine_) != address(0)";"address(ruleEngine_) == address(0)";True;"testCannotTransferTokenWhenPausedWithTransferFrom, testCanTransferTokenIfActiveBalanceIsEnough, testCannotTransferIfNotAllowedByRule, testCanTransferFrom, testCanCanTransferFromWithoutRuleEngine, testCannotTransferFromIfNotAllowedByRuleEngine" +contracts/modules/wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol;126;"address(ruleEngine_) != address(0)";"address(ruleEngine_) == address(0)";True;"testAdminCanBatchPartialFreezeAddress, testAdminCanUnfreezeAddress, testEnforcerRoleCanUnfreezeAddress, testCannotTransferTokenWhenPausedWithTransferFrom, testAdminCanFreezeAddress, testAdminCanFreezeAddressTwice, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testAdminCanFreezeAddressReason, testEnforcerRoleCanFreezeAddressReason, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason, testCanTransferTokenIfActiveBalanceIsEnough, testAdminCanFreezeAddressWithTokenFrozenDifferentLess, testAdminCanFreezeAddressWithTokenFrozenDifferentMore, testCanCanTransferWithoutRuleEngine, testCanDetectTransferRestrictionWithAmountTooHigh, testCannotTransferIfNotAllowedByRule, testCannotTransferFromIfNotAllowedByRuleEngine, testCanCanMintWithoutRuleEngine, testCanDetectTransferRestrictionWitMintAmountTooHigh, testCannotMintIfNotAllowedByRule, testCanDetectTransferRestrictionValidTransfer, testCanTransferAllowed, testCanTransfer" +contracts/modules/wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol;140;"address(ruleEngine_) != address(0)";"address(ruleEngine_) == address(0)";True;"testCanSendTokenToReceiverContract, testCannotSendTokenToEOAWithERC1363Functions, testCanBeMintedByAdmin, testCanMintByANewMinter, testCanBeMintedWithReasonByAdmin, testCanMintWithReasonByANewMinter, testCanBeMintedEvenIfContractIsPaused, testCanBeMintedBatchByAdmin, testCanBeMintBatchdByANewMinter, testCanBeMintedBatchEvenIfContractIsPaused, testCannotTransferIfNotAllowedByRule, testCannotBatchTransferIfNotAllowedByRule, testCannotTransferFromIfNotAllowedByRuleEngine, testCannotMintIfNotAllowedByRule, testCannotBatchMintIfNotAllowedByRule, testKeepStorageForTokens, testAdminCanTransmitAdminship, testCMTATIntegration" +contracts/modules/wrapper/extensions/ValidationModule/ValidationModuleRuleEngine.sol;141;"spender != address(0)";"spender == address(0)";False;"" +contracts/modules/internal/ERC20BurnModuleInternal.sol;30;"accounts.length != 0";"accounts.length == 0";True;"testCanBeBurntBatchByAdmin, testCanBeBurntBatchByAdminWithoutReason, testCanBeBurntBatchByBurnerRoleWithoutReason, testCanBeBurntBatchByBurnerRole, testCannotBeBurntBatchIfOneBalanceExceeds, testCannotBatchBurnIfLengthMismatchMissingAddresses, testCannotBatchBurnIfLengthMismatchTooManyAddresses, testCannotBatchBurnIfAccountsIsEmpty, testCanBeBatchBurnEvenIfContractIsPaused, testCannotBeBatchBurnIfContractIsDeactivated, testCannotBeBatchBurnIfToIsFrozen, testCannotBatchBurnFromNoWhitelistedAddress" +contracts/modules/internal/ERC20BurnModuleInternal.sol;33;"accounts.length == values.length";"accounts.length != values.length";True;"testCanBeBurntBatchByAdmin, testCanBeBurntBatchByAdminWithoutReason, testCanBeBurntBatchByBurnerRoleWithoutReason, testCanBeBurntBatchByBurnerRole, testCannotBeBurntBatchIfOneBalanceExceeds, testCannotBatchBurnIfLengthMismatchMissingAddresses, testCannotBatchBurnIfLengthMismatchTooManyAddresses, testCanBeBatchBurnEvenIfContractIsPaused, testCannotBeBatchBurnIfContractIsDeactivated, testCannotBeBatchBurnIfToIsFrozen, testCannotBatchBurnFromNoWhitelistedAddress" +contracts/modules/wrapper/controllers/ValidationModule.sol;43;"from == address(0)";"from != address(0)";True;"testAdminCanFreezeAddress, testAdminCanBatchFreezeAddress, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddress, testEnforcerRoleCanUnfreezeAddress, testCannotTransferTokenWhenSpenderIsFrozenWithTransferFrom, testCannotTransferTokenWhenPausedWithTransfer, testCannotTransferTokenWhenPausedWithTransferFrom, testCanTransferReturnsFalseForBurnFromFrozenAddress, testCannotBurnFromUnWhitelistedAddress, testCannotTransferTokenWhenSpenderIsNotAllowlistWithTransferFrom" +contracts/modules/wrapper/controllers/ValidationModule.sol;60;"from == address(0)";"from != address(0)";True;"testCannotBeBurnIfAddressIsFrozen, testCanBeBurnAndMintEvenIFContractIsPaused, testCannotBeBatchBurnIfToIsFrozen, testCanBeMintedEvenIfContractIsPaused, testCannotBeMintedIfContractIsDeactivated, testCanBeMintedBatchEvenIfContractIsPaused, testCannotBeBatchMintedIfContractIsDeactivated, testCannotBeBatchTransferIfContractIsPaused, testCannotBeBatchMTransferIfContractIsDeactivated, testCannotTransferWhenFromIsFrozenWithTransfer, testCannotTransferTokenWhenSpenderIsFrozenWithTransferFrom, testCanBePausedByAdmin, testCanBePausedByPauserRole, testCannotTransferTokenWhenPausedWithTransfer, testCannotTransferTokenWhenPausedWithTransferFrom, testCanDeactivatedByAdminIfContractIsPaused, testCannotBeBurnFromIfAccountIsFrozen, testCanBeMintedByAdmin, testCanMintByANewMinter, testCanBeMintedWithReasonByAdmin, testCanMintWithReasonByANewMinter, testCanBeMintedBatchByAdmin, testCanBeMintBatchdByANewMinter, testCannotBeBatchMintedIfToIsFrozen, testCannotTransferWhenFromIsNotAllowlistWithTransfer, testCannotBatchBurnFromNoWhitelistedAddress, testCannotBurnFromUnWhitelistedAddress, testCannotTransferTokenWhenSpenderIsNotAllowlistWithTransferFrom, testCannotTransferIfContractIsPaused" +contracts/modules/wrapper/controllers/ValidationModule.sol;79;"PauseModule.deactivated() || EnforcementModule.isFrozen(target)";"PauseModule.deactivated() && EnforcementModule.isFrozen(target)";True;"testCanTransferReturnsFalseForMintToFrozenAddress, testCanTransferReturnsFalseForBurnFromFrozenAddress" +contracts/modules/wrapper/controllers/ValidationModule.sol;115;"EnforcementModule.isFrozen(spender) || EnforcementModule.isFrozen(from) || EnforcementModule.isFrozen(to)";"EnforcementModule.isFrozen(spender) && EnforcementModule.isFrozen(from) && EnforcementModule.isFrozen(to)";True;"testAdminCanFreezeAddress, testAdminCanBatchFreezeAddress, testAdminCanBatchPartialFreezeAddress, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddress, testEnforcerRoleCanUnfreezeAddress, testCannotTransferTokenWhenSpenderIsFrozenWithTransferFrom" +contracts/modules/wrapper/controllers/ValidationModule.sol;147;"_canTransferisFrozen(spender, from, to) || PauseModule.paused()";"_canTransferisFrozen(spender, from, to) && PauseModule.paused()";True;"testAdminCanFreezeAddress, testAdminCanBatchFreezeAddress, testAdminCanBatchPartialFreezeAddress, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddress, testEnforcerRoleCanUnfreezeAddress, testCannotTransferTokenWhenSpenderIsFrozenWithTransferFrom, testCannotTransferTokenWhenPausedWithTransfer, testCannotTransferTokenWhenPausedWithTransferFrom, testCanDeactivatedByAdminIfContractIsPaused" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;24;"0x9d8059a24cb596f1948a937c2c163cf14465c2a24abfd3cd009eec4ac4c39800";"0x0";False;"" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;34;"ERC20EnforcementModuleStorage storage $";"ERC20EnforcementModuleStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;37;"value < frozenTokensLocal";"value > frozenTokensLocal";True;"testAdminCanFreezeAddress, testAdminCanFreezeAddressWithTokenFrozenDifferentLess, testAdminCanFreezeAddressWithTokenFrozenDifferentMore, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddress, testEnforcerRoleCanUnfreezeAddress, testCannotTransferMoreThanActiveBalance, testCanTransferTokenIfActiveBalanceIsEnough, testCannotTransferFromTokenIfActiveBalanceIsNotEnough" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;38;"frozenTokensLocal - value";"frozenTokensLocal + value";True;"testAdminCanFreezeAddressWithTokenFrozenDifferentLess, testAdminCanUnfreezeAddress, testEnforcerRoleCanUnfreezeAddress" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;43;"value - frozenTokensLocal";"value + frozenTokensLocal";True;"testAdminCanFreezeAddressWithTokenFrozenDifferentMore" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;53;"ERC20EnforcementModuleStorage storage $";"ERC20EnforcementModuleStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;56;"$._frozenTokens[account] + value";"$._frozenTokens[account] - value";True;"testCanForceTransferFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferFromAddress1ToAddress2AsAdminWithoutUnfreezeTokens, testCanForceTransferBurnFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testAdminCanFreezeAddress, testAdminCanFreezeAddressTwice, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testEnforcerRoleCanUnfreezeAddress, testAdminCanFreezeAddressReason, testEnforcerRoleCanFreezeAddressReason, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason, testCannotNonEnforcerUnfreezeAddress, testCannotTransferMoreThanActiveBalance, testCanTransferTokenIfActiveBalanceIsEnough, testCannotTransferFromTokenIfActiveBalanceIsNotEnough, testAdminCanFreezeAddressWithTokenFrozen, testAdminCanFreezeAddressWithTokenFrozenDifferentLess, testAdminCanFreezeAddressWithTokenFrozenDifferentMore" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;58;"balance >= frozenTokensLocal";"balance < frozenTokensLocal";True;"testCanForceTransferFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferFromAddress1ToAddress2AsAdminWithoutUnfreezeTokens, testCanForceTransferBurnFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testAdminCanFreezeAddress, testAdminCanFreezeAddressTwice, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testEnforcerRoleCanUnfreezeAddress, testAdminCanFreezeAddressReason, testEnforcerRoleCanFreezeAddressReason, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason, testCannotNonEnforcerUnfreezeAddress, testCannotTransferMoreThanActiveBalance, testCanTransferTokenIfActiveBalanceIsEnough, testCannotTransferFromTokenIfActiveBalanceIsNotEnough, testAdminCanFreezeAddressWithTokenFrozen, testAdminCanFreezeAddressWithTokenFrozenDifferentLess, testAdminCanFreezeAddressWithTokenFrozenDifferentMore" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;65;"ERC20EnforcementModuleStorage storage $";"ERC20EnforcementModuleStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;66;"$._frozenTokens[account] >= value";"$._frozenTokens[account] < value";True;"testAdminCanUnfreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testEnforcerRoleCanUnfreezeAddress, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;68;"$._frozenTokens[account] - value";"$._frozenTokens[account] + value";True;"testAdminCanUnfreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testEnforcerRoleCanUnfreezeAddress, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;78;"value > balance";"value < balance";True;"testCanForceTransferFromAddress1ToAddress2AsAdmin, testCanForceTransferFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferFromAddress1ToAddress2AsAdminWithoutUnfreezeTokens, testCanForceTransferBurnFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowanceToZero, testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowance, testCanForceBurnWithForceTransferAsAdmin, testCanForceTransferFromAddress1ToAddress2AsAdminWithoutReason, testCannotForceTransferFromAddress1ToAddress2IfBalanceNotEnough" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;81;"ERC20EnforcementModuleStorage storage $";"ERC20EnforcementModuleStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;83;"balance - $._frozenTokens[account]";"balance + $._frozenTokens[account]";True;"testCanForceTransferFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferBurnFromAddress1ToAddress2AsAdminAndUnfreezeTokens" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;84;"value > activeBalance";"value < activeBalance";True;"testCanForceTransferFromAddress1ToAddress2AsAdmin, testCanForceTransferFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferFromAddress1ToAddress2AsAdminWithoutUnfreezeTokens, testCanForceTransferBurnFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowanceToZero, testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowance, testCanForceBurnWithForceTransferAsAdmin, testCanForceTransferFromAddress1ToAddress2AsAdminWithoutReason" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;85;"value - activeBalance";"value + activeBalance";True;"testCanForceTransferFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferBurnFromAddress1ToAddress2AsAdminAndUnfreezeTokens" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;86;"$._frozenTokens[account] - tokensToUnfreeze";"$._frozenTokens[account] + tokensToUnfreeze";True;"testCanForceTransferFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferBurnFromAddress1ToAddress2AsAdminAndUnfreezeTokens" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;94;"to == address(0)";"to != address(0)";True;"testCanForceTransferFromAddress1ToAddress2AsAdmin, testCanForceTransferFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferFromAddress1ToAddress2AsAdminWithoutUnfreezeTokens, testCanForceTransferBurnFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowanceToZero, testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowance, testCanForceBurnWithForceTransferAsAdmin, testCanForceTransferFromAddress1ToAddress2AsAdminWithoutReason" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;100;"currentAllowance > 0 && currentAllowance < type(uint256).max";"currentAllowance > 0 || currentAllowance < type(uint256).max";False;"" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;100;"currentAllowance < type(uint256).max";"currentAllowance > type(uint256).max";True;"testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowanceToZero, testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowance, reverts when trying to schedule a snapshot in the past" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;100;"currentAllowance > 0";"currentAllowance < 0";True;"testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowanceToZero, testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowance" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;101;"currentAllowance < value";"currentAllowance > value";True;"testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowanceToZero, testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowance" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;107;"currentAllowance - value";"currentAllowance + value";True;"testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowance" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;114;"emit Enforcement(_msgSender(), from, value, data);";"";True;"testCanForceTransferFromAddress1ToAddress2AsAdmin, testCanForceTransferFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferFromAddress1ToAddress2AsAdminWithoutUnfreezeTokens, testCanForceTransferBurnFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowanceToZero, testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowance, testCanForceBurnWithForceTransferAsAdmin, testCanForceTransferFromAddress1ToAddress2AsAdminWithoutReason" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;115;"emit ForcedTransfer(from, to, value);";"";True;"testCanForceTransferFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferFromAddress1ToAddress2AsAdminWithoutUnfreezeTokens, testCanForceTransferBurnFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowanceToZero, testCanForceTransferFromAddress1ToAddress2AsAdminAndReduceAllowance, testCanForceBurnWithForceTransferAsAdmin, testCanForceTransferFromAddress1ToAddress2AsAdminWithoutReason" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;119;"emit TokensFrozen(account, difference, data);";"";True;"testAdminCanFreezeAddress, testAdminCanFreezeAddressTwice, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testEnforcerRoleCanUnfreezeAddress, testAdminCanFreezeAddressReason, testEnforcerRoleCanFreezeAddressReason, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason, testAdminCanFreezeAddressWithTokenFrozenDifferentMore" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;120;"emit IERC7943FungibleEnforcementEventAndError.Frozen(account, frozenTokens);";"";True;"testAdminCanFreezeAddress, testAdminCanFreezeAddressTwice, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testEnforcerRoleCanUnfreezeAddress, testAdminCanFreezeAddressReason, testEnforcerRoleCanFreezeAddressReason, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason, testAdminCanFreezeAddressWithTokenFrozenDifferentMore" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;124;"emit TokensUnfrozen(account, difference, data);";"";True;"testCanForceTransferFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferBurnFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testAdminCanUnfreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testEnforcerRoleCanUnfreezeAddress, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason, testAdminCanFreezeAddressWithTokenFrozenDifferentLess" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;125;"emit IERC7943FungibleEnforcementEventAndError.Frozen(account, frozenTokens );";"";True;"testCanForceTransferFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferBurnFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testAdminCanUnfreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testEnforcerRoleCanUnfreezeAddress, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason, testAdminCanFreezeAddressWithTokenFrozenDifferentLess" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;141;"frozenTokensLocal > 0";"frozenTokensLocal < 0";True;"testAdminCanFreezeAddress, testAdminCanFreezeAddressTwice, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testEnforcerRoleCanUnfreezeAddress, testAdminCanFreezeAddressReason, testEnforcerRoleCanFreezeAddressReason, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason, testCannotTransferMoreThanActiveBalance, testCannotTransferFromTokenIfActiveBalanceIsNotEnough, testAdminCanFreezeAddressWithTokenFrozenDifferentLess, testAdminCanFreezeAddressWithTokenFrozenDifferentMore" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;143;"ERC20Upgradeable.balanceOf(from) - frozenTokensLocal";"ERC20Upgradeable.balanceOf(from) + frozenTokensLocal";True;"testAdminCanFreezeAddress, testAdminCanFreezeAddressTwice, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testEnforcerRoleCanUnfreezeAddress, testAdminCanFreezeAddressReason, testEnforcerRoleCanFreezeAddressReason, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason, testCannotTransferMoreThanActiveBalance, testCannotTransferFromTokenIfActiveBalanceIsNotEnough, testAdminCanFreezeAddressWithTokenFrozenDifferentLess, testAdminCanFreezeAddressWithTokenFrozenDifferentMore" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;144;"value > activeBalance";"value < activeBalance";True;"testAdminCanFreezeAddress, testAdminCanFreezeAddressTwice, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testEnforcerRoleCanUnfreezeAddress, testAdminCanFreezeAddressReason, testEnforcerRoleCanFreezeAddressReason, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason, testCannotTransferMoreThanActiveBalance, testCannotTransferFromTokenIfActiveBalanceIsNotEnough, testAdminCanFreezeAddressWithTokenFrozenDifferentLess, testAdminCanFreezeAddressWithTokenFrozenDifferentMore" +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;154;"ERC20EnforcementModuleStorage storage $";"ERC20EnforcementModuleStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;159;"ERC20EnforcementModuleStorage storage $";"ERC20EnforcementModuleStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/internal/ERC20EnforcementModuleInternal.sol;160;"ERC20Upgradeable.balanceOf(account) - $._frozenTokens[account]";"ERC20Upgradeable.balanceOf(account) + $._frozenTokens[account]";True;"testCanForceTransferFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testCanForceTransferFromAddress1ToAddress2AsAdminWithoutUnfreezeTokens, testCanForceTransferBurnFromAddress1ToAddress2AsAdminAndUnfreezeTokens, testAdminCanFreezeAddress, testAdminCanFreezeAddressTwice, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testEnforcerRoleCanUnfreezeAddress, testAdminCanFreezeAddressReason, testEnforcerRoleCanFreezeAddressReason, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason, testAdminCanFreezeAddressWithTokenFrozenDifferentLess, testAdminCanFreezeAddressWithTokenFrozenDifferentMore" +contracts/modules/2_CMTATBaseAllowlist.sol;53;") public virtual initializer";") public virtual";False;"" +contracts/modules/2_CMTATBaseAllowlist.sol;69;") internal virtual onlyInitializing";") internal virtual";False;"" +contracts/modules/2_CMTATBaseAllowlist.sol;87;"function __CMTAT_openzeppelin_init_unchained(ICMTATConstructor.ERC20Attributes memory ERC20Attributes_) internal virtual onlyInitializing";"function __CMTAT_openzeppelin_init_unchained(ICMTATConstructor.ERC20Attributes memory ERC20Attributes_) internal virtual";False;"" +contracts/modules/2_CMTATBaseAllowlist.sol;98;"function __CMTAT_modules_init_unchained(address admin, ICMTATConstructor.ERC20Attributes memory ERC20Attributes_, ICMTATConstructor.ExtraInformationAttributes memory ExtraInformationAttributes_ ) internal virtual onlyInitializing";"function __CMTAT_modules_init_unchained(address admin, ICMTATConstructor.ERC20Attributes memory ERC20Attributes_, ICMTATConstructor.ExtraInformationAttributes memory ExtraInformationAttributes_ ) internal virtual";False;"" +contracts/modules/2_CMTATBaseAllowlist.sol;119;"function approve(address spender, uint256 value) public virtual override(ERC20Upgradeable) whenNotPaused returns (bool)";"function approve(address spender, uint256 value) public virtual override(ERC20Upgradeable) returns (bool)";True;"testCannotApproveIfPaused" +contracts/modules/2_CMTATBaseAllowlist.sol;132;"!isValid";"isValid";True;"testCannotTransferTokenWhenPausedWithTransferFrom, testAdminCanBatchPartialFreezeAddress, testAdminCanUnfreezeAddress, testEnforcerRoleCanUnfreezeAddress, testCanCanTransferWithoutRuleEngine, testCanDetectTransferRestrictionValidTransfer, testCanTransferAllowed, testCanTransfer, testAdminCanFreezeAddress, testAdminCanFreezeAddressTwice, testEnforcerRoleCanFreezeAddress, testAdminCanUnfreezeAddressAndTransferMoreActiveBalance, testAdminCanUnfreezeAddressTwice, testAdminCanFreezeAddressReason, testEnforcerRoleCanFreezeAddressReason, testAdminCanUnfreezeAddressReason, testEnforcerRoleCanUnfreezeAddressReason, testCanTransferTokenIfActiveBalanceIsEnough, testCannotTransferFromTokenIfActiveBalanceIsNotEnough, testAdminCanFreezeAddressWithTokenFrozenDifferentLess, testAdminCanFreezeAddressWithTokenFrozenDifferentMore, testAdminCanAllowlistAddress, testAdminCanBatchAllowlistAddress, testAdminCanBatchPartialAllowlistAddress, testEnforcerRoleCanAllowlistAddress, testCanBurnFromWhitelistedAddress, testCannotBurnFromUnWhitelistedAddress" +contracts/modules/2_CMTATBaseAllowlist.sol;150;"!isValid";"isValid";True;"testCannotTransferTokenWhenPausedWithTransferFrom, testCanTransferTokenIfActiveBalanceIsEnough, testCannotTransferFromTokenIfActiveBalanceIsNotEnough, testCanTransferTokenWithTransferFrom" +contracts/modules/2_CMTATBaseAllowlist.sol;162;"function _authorizePause() internal virtual override(PauseModule) onlyRole(PAUSER_ROLE)";"function _authorizePause() internal virtual override(PauseModule)";True;"testCannotBePausedByNonPauser, testCannotBeUnpausedByNonPauser" +contracts/modules/2_CMTATBaseAllowlist.sol;163;"function _authorizeDeactivate() internal virtual override(PauseModule) onlyRole(DEFAULT_ADMIN_ROLE)";"function _authorizeDeactivate() internal virtual override(PauseModule)";True;"testCannotBeDeactivatedByNonAdmin" +contracts/modules/2_CMTATBaseAllowlist.sol;165;"function _authorizeFreeze() internal virtual override(EnforcementModule) onlyRole(ENFORCER_ROLE)";"function _authorizeFreeze() internal virtual override(EnforcementModule)";True;"testCannotNonEnforcerFreezeAddress, testCannotNonEnforcerBatchFreezeAddress, testCannotNonEnforcerUnfreezeAddress" +contracts/modules/2_CMTATBaseAllowlist.sol;167;"function _authorizeAllowlistManagement() internal virtual override(AllowlistModule) onlyRole(ALLOWLIST_ROLE)";"function _authorizeAllowlistManagement() internal virtual override(AllowlistModule)";True;"testCannotNonEnforcerAllowlistAddress, testCannotNonEnforcerBatchAllowlistAddress, testCannotNonEnforcerUnWhitelistAddress" +contracts/modules/internal/EnforcementModuleInternal.sol;21;"0x0c7bc8a17be064111d299d7669f49519cb26c58611b72d9f6ccc40a1e1184e00";"0x0";False;"" +contracts/modules/internal/EnforcementModuleInternal.sol;33;"EnforcementModuleInternalStorage storage $";"EnforcementModuleInternalStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/internal/EnforcementModuleInternal.sol;43;"EnforcementModuleInternalStorage storage $";"EnforcementModuleInternalStorage memory $";True;"Failed to compile after introducing mutation." +contracts/modules/internal/EnforcementModuleInternal.sol;54;"EnforcementModuleInternalStorage storage $";"EnforcementModuleInternalStorage memory $";True;"Failed to compile after introducing mutation." \ No newline at end of file diff --git a/docs/olympix/report.md b/docs/olympix/report.md new file mode 100644 index 00000000..abb28164 --- /dev/null +++ b/docs/olympix/report.md @@ -0,0 +1,149 @@ +# Olympix Security Verification — CMTAT + +**Target:** [`CMTA/CMTAT`](https://github.com/CMTA/CMTAT) · security-token / RWA tokenization framework +**Commit:** `49544f4d` · **Branch:** `master` · **Date:** 16–17 July 2026 UTC +**Toolchain:** Olympix BugPoCer · Mutation Testing · Symbolic-execution Fuzzing + +> This page is a presentation-format demo: it collects three independent Olympix runs against CMTAT into a single reviewable surface. Every number links back to a shipped artifact (findings JSON, mutation CSV, runnable PoC tests). + +--- + +## At a glance + +| Tool | What it measured | Headline result | +|------|------------------|-----------------| +| **BugPoCer** | End-to-end exploit generation across the codebase | **21 true-positive findings** — 2 High · 7 Medium · 12 Low | +| **Mutation testing** | Does the existing test suite actually catch bugs? | **87.1% killed** (81 / 93 mutants) — 10 of 12 survivors are equivalent mutants | +| **Fuzzing (symbolic)** | Adversarial reachability across the core surface | **35,188 paths → 27,252 feasible**, 0 exploitable path found in this run | + +--- + +## BugPoCer — exploit-driven findings + +BugPoCer decomposes the codebase into units, explores reachable paths, and for each candidate issue attempts to write a **runnable Foundry/Hardhat PoC that fails on the vulnerable code**. Only confirmed true positives are listed below. + +**Severity breakdown:** 2 High · 7 Medium · 12 Low — **21 total** + +| # | Sev | Finding | Location | PoC | +|---|-----|---------|----------|-----| +| 1 | High | Missing deactivation guard on `forcedBurn` | `modules/0_CMTATBaseCore.sol` | [runnable](./tests/olympix/pocs/missing_deactivation_guard.t.sol) | +| 2 | High | Unchecked over-frozen balance underflow | `modules/wrapper/extensions/ERC20EnforcementModule.sol` | documented | +| 3 | Med | Burn operator-context bypass | `modules/wrapper/options/ERC20CrossChainModule.sol` | [runnable](./tests/olympix/pocs/burn_operator_context_bypass.t.sol) | +| 4 | Med | Incomplete mint validation | `modules/0_CMTATBaseCommon.sol` | [runnable](./tests/olympix/pocs/incomplete_mint_validation.t.sol) | +| 5 | Med | Missing zero-address validation (batch freeze) | `modules/internal/common/EnforcementModuleLibrary.sol` | [runnable](./tests/olympix/pocs/missing_zero_address_validation.t.sol) | +| 6 | Med | Over-frozen balance transfer DoS | `modules/6_CMTATBaseERC1363.sol` | documented | +| 7 | Med | Snapshot hook runs after state update | `modules/0_CMTATBaseCommon.sol` | documented | +| 8 | Med | Unchecked frozen-token cap | `modules/wrapper/core/EnforcementModule.sol` | documented | +| 9 | Med | Unchecked over-frozen balance | `modules/wrapper/core/EnforcementModule.sol` | documented | +| 10 | Low | `approve` missing pause protection | `modules/0_CMTATBaseCore.sol` | [runnable](./tests/olympix/pocs/approve_missing_pause_protection.t.sol) | +| 11 | Low | Over-frozen balance accounting corruption | `modules/wrapper/extensions/ERC20EnforcementModule.sol` | [runnable](./tests/olympix/pocs/overfrozen_balance_accounting_corruption.t.sol) | +| 12 | Low | Inconsistent compliance view | `modules/wrapper/controllers/ValidationModule.sol` | [runnable](./tests/olympix/pocs/inconsistent_compliance_view.t.sol) | +| 13 | Low | Zero-address transfer validation mismatch | `modules/wrapper/core/ValidationModuleCore.sol` | [runnable](./tests/olympix/pocs/zero_address_transfer_validation_mismatch.t.sol) | +| 14 | Low | View / validation mismatch | `modules/wrapper/controllers/ValidationModule.sol` | documented | +| 15 | Low | Incomplete predicate view | `modules/wrapper/controllers/ValidationModule.sol` | documented | +| 16 | Low | External-call DoS | `modules/0_CMTATBaseCommon.sol` | documented | +| 17 | Low | Cross-interface event inconsistency | `modules/wrapper/extensions/ExtraInformationModule.sol` | documented | +| 18 | Low | Rule-engine setter invariant mismatch | `modules/internal/ValidationModuleRuleEngineInternal.sol` | documented | +| 19 | Low | Misleading burn-authority surface | `modules/wrapper/core/ERC20BurnModule.sol` | documented | +| 20 | Low | Misleading access-control docs (CCIP) | `modules/wrapper/options/ERC20CrossChainModule.sol` | documented | +| 21 | Low | Misleading access-control docs (enforcer) | `modules/wrapper/core/EnforcementModule.sol` | documented | + +
+High #1 — Missing deactivation guard on forcedBurn + +**Location:** `contracts/modules/0_CMTATBaseCore.sol` — `forcedBurn()` + +`forcedBurn` can still reduce balances and `totalSupply` **after the token has been permanently deactivated**. Normal burn paths route through `_burnOverride()`, which calls the validation module and rejects a deactivated contract. `forcedBurn` deliberately skips that path and calls `ERC20Upgradeable._burn()` directly after only checking that the account is frozen. After `pause()` + `deactivateContract()`, a `DEFAULT_ADMIN_ROLE` holder can freeze an account and destroy its tokens — despite the deactivation guarantee that burn operations stop permanently. + +**PoC:** [`pocs/missing_deactivation_guard.t.sol`](./tests/olympix/pocs/missing_deactivation_guard.t.sol) — mints to a holder, pauses, deactivates, confirms the normal `burn()` reverts with `EnforcedDeactivation`, then shows `forcedBurn()` still succeeds (assertion fails on the vulnerable code). +
+ +
+High #2 — Unchecked over-frozen balance underflow + +**Location:** `contracts/modules/internal/ERC20EnforcementModuleInternal.sol` — `_checkActiveBalance()` / `_setFrozenTokens()` + +Active-balance logic assumes `frozenTokens(account) <= balanceOf(account)` and subtracts directly, but the absolute `setFrozenTokens` / `_setFrozenTokens` path can write an arbitrary frozen amount without enforcing that cap. An over-frozen account makes `_checkActiveBalance`, `getActiveBalanceOf`, transfer/burn validation, forced-transfer recovery, and `canTransfer` reads revert via arithmetic underflow until a privileged correction. Setting a nonzero frozen amount for `address(0)` is especially damaging: mint validation treats `address(0)` as the source, so ordinary and cross-chain minting can be **globally bricked**. + +**Cross-signal:** this same enforcement-arithmetic surface is the most heavily mutated file (see mutation section) — 33/35 mutants killed, confirming the tests exercise it, while BugPoCer shows the *absolute-set* path is the one that escapes the cap invariant. +
+ +**Findings source:** `.opix/agent/24c6f02a-…/findings.json` · **Session:** `24c6f02a-cca3-43b5-853f-d84b2bcef93c` + +--- + +## Mutation testing + +Mutation testing measures whether the existing test suite actually *detects* bugs by injecting small faults and checking that a test fails ("kills" the mutant). + +**Overall: 81 / 93 killed — `87.1%`** · Session `6b2c6237-f5d3-42fb-addb-f505c5bb78c2` + +| File | Killed / Total | Score | +|------|----------------|-------| +| `4_CMTATBaseERC20CrossChain.sol` | 6 / 6 | 100% | +| `ERC20BurnModuleInternal.sol` | 2 / 2 | 100% | +| `ValidationModule.sol` | 5 / 5 | 100% | +| `ERC20EnforcementModuleInternal.sol` | 33 / 35 | 94.3% | +| `ValidationModuleRuleEngine.sol` | 7 / 8 | 87.5% | +| `ERC20BaseModule.sol` | 12 / 14 | 85.7% | +| `PauseModule.sol` | 6 / 8 | 75.0% | +| `EnforcementModuleInternal.sol` | 3 / 4 | 75.0% | +| `2_CMTATBaseAllowlist.sol` | 7 / 11 | 63.6% | + +
+Surviving mutants (12) — 10 equivalent, 2 worth a look + +Most survivors are **equivalent mutants** (semantically identical — cannot be killed by any test): + +| Location | Mutation | Why it survives | +|----------|----------|-----------------| +| `ERC20BaseModule.sol:28`, `ERC20EnforcementModuleInternal.sol:24`, `EnforcementModuleInternal.sol:21` | ERC-7201 storage-slot constant → `0x0` | Slot constant; value never asserted in tests | +| `ERC20BaseModule.sol:54`, `2_CMTATBaseAllowlist.sol:53/69/87/98` | remove `initializer`/`onlyInitializing` | Init guards; tests never double-initialize | +| `PauseModule.sol:69/104` | `storage $` → `memory $` | Read-only access; no observable difference | + +**Worth a look (2):** + +| Location | Mutation | Note | +|----------|----------|------| +| `ValidationModuleRuleEngine.sol:141` | `spender != address(0)` → `== address(0)` | Zero-address branch not covered — **corroborates BugPoCer #5 (missing zero-address validation)** | +| `ERC20EnforcementModuleInternal.sol:100` | `&&` → `\|\|` in allowance-cap check | `currentAllowance > 0 && < max` boundary not asserted | + +
+ +**Raw data:** [`mutation-results.csv`](./docs/olympix/mutation_testing/mutation-results.csv) + +--- + +## Fuzzing — symbolic-execution-driven + +The engine performs whole-contract symbolic execution, keeps only SMT-feasible paths, and (per attack strategy) turns them into concrete adversarial Foundry tests. + +**Session `f37cd9fe-cb1e-4732-821a-03c6811416dc` · 6 min wall-clock** + +| Contract | Paths | Feasible | Exploits | +|----------|-------|----------|----------| +| `CMTATStandalone` | 19,974 | 15,518 | 0 | +| `CMTATUpgradeable` | 8,138 | 5,910 | 0 | +| `CMTATStandaloneAllowlist` | 7,076 | 5,824 | 0 | +| **Total** | **35,188** | **27,252** | **0** | + +The engine enumerated 35,188 execution paths across the 3 highest-value deployment variants and kept the 27,252 SMT-feasible ones. Guarded entry points were unreachable to an unauthorized caller — a positive signal, not a coverage gap. + +> **Maturity note (honest framing):** this run exercised **0 adversarial attack strategies** — that is the fuzz engine's *current* concrete-test-generation set (it is under active development; more strategies and deeper multi-call sequences land each release). So the "0 exploits" result here is a **floor on the path-reachability signal, not a proof of security**. Treat it as reachability coverage, not negative assurance, until the strategy set matures. + +--- + +## Artifacts & reproducibility + +| Artifact | Source | +|----------|--------| +| BugPoCer report | [`BugPoCer_Scan_Report_CMTAT.pdf`](./docs/olympix/bugpocer/BugPoCer_Scan_Report_CMTAT.pdf) (43 pp) | +| Mutation results | [`mutation-results.csv`](./docs/olympix/mutation_testing/mutation-results.csv) (93 mutants) | +| Fuzz report | [`Fuzz_Test_Report.pdf`](./docs/olympix/fuzz_testing/Fuzz_Test_Report_f37cd9fe-cb1e-4732-821a-03c6811416dc_20260716_182652.pdf) (7 pp) | +| Runnable PoCs | [`tests/olympix/pocs/`](./tests/olympix/pocs/) — 8 confirmed exploit tests | + +| Run | Session ID | +|-----|-----------| +| BugPoCer | `24c6f02a-cca3-43b5-853f-d84b2bcef93c` | +| Mutation | `6b2c6237-f5d3-42fb-addb-f505c5bb78c2` | +| Fuzz | `f37cd9fe-cb1e-4732-821a-03c6811416dc` | diff --git a/tests/olympix/pocs/approve_missing_pause_protection.t.sol b/tests/olympix/pocs/approve_missing_pause_protection.t.sol new file mode 100644 index 00000000..2a7f93dd --- /dev/null +++ b/tests/olympix/pocs/approve_missing_pause_protection.t.sol @@ -0,0 +1,25 @@ +const { expect } = require('chai') +const { ethers } = require('hardhat') + +const TERMS = [ + 'doc1', + 'https://example.com/doc1', + '0x6a12eff2f559a5e529ca2c563c53194f6463ed5c61d1ae8f8731137467ab0279' +] +const ZERO = ethers.ZeroAddress + +/* + * Vulnerability: approve missing pause protection (CMTATBaseCore / Light approve()). + * The Light deployment does not gate approve with whenNotPaused, so allowances can be + * created while the token is paused, unlike the full variants. + */ +describe('PoC: Light approve() is callable while paused', function () { + it('approve must revert while the contract is paused', async function () { + const [deployer, admin, holder, spender] = await ethers.getSigners() + const cmtat = await ethers.deployContract('CMTATStandaloneLight', [admin.address, ['CMTA Token', 'CMTAT', 0]]) + await cmtat.waitForDeployment() + await cmtat.connect(admin).pause() + // FIXED: approve reverts while paused. VULNERABLE: it succeeds -> assertion FAILS. + await expect(cmtat.connect(holder).approve(spender.address, 100n)).to.be.reverted + }) +}) diff --git a/tests/olympix/pocs/burn_operator_context_bypass.t.sol b/tests/olympix/pocs/burn_operator_context_bypass.t.sol new file mode 100644 index 00000000..683c54b1 --- /dev/null +++ b/tests/olympix/pocs/burn_operator_context_bypass.t.sol @@ -0,0 +1,101 @@ +const { expect } = require('chai') +const { ethers } = require('hardhat') +const { + deployCMTATStandalone, + fixture, + loadFixture +} = require('../deploymentUtils') + +const VALUE = 10n + +describe('PoC - burnFrom drops operator context before RuleEngine validation', function () { + async function deployFixture () { + const accounts = await loadFixture(fixture) + const cmtat = await deployCMTATStandalone( + accounts._.address, + accounts.admin.address, + accounts.deployerAddress.address + ) + + const holder = accounts.address2 + const recipient = accounts.address3 + const authorizedSpender = accounts.address1 + const unauthorizedBurner = accounts.attacker + + // RuleEngineMock rejects canTransferFrom/transferred(spender, ...) for every + // non-zero spender except `authorizedSpender`, while allowing the + // no-operator canTransfer/transferred(from, ...) path for VALUE. + const RuleEngineMock = await ethers.getContractFactory('RuleEngineMock') + const ruleEngine = await RuleEngineMock.deploy(authorizedSpender.address) + await ruleEngine.waitForDeployment() + + await cmtat.connect(accounts.admin).setRuleEngine(await ruleEngine.getAddress()) + + const burnerFromRole = await cmtat.BURNER_FROM_ROLE() + await cmtat.connect(accounts.admin).grantRole(burnerFromRole, unauthorizedBurner.address) + + await cmtat.connect(accounts.admin).mint(holder.address, VALUE) + await cmtat.connect(holder).approve(unauthorizedBurner.address, VALUE) + + return { + cmtat, + ruleEngine, + holder, + recipient, + authorizedSpender, + unauthorizedBurner + } + } + + it('should reject burnFrom when the RuleEngine rejects the actual burner/spender', async function () { + const { + cmtat, + ruleEngine, + holder, + recipient, + unauthorizedBurner + } = await loadFixture(deployFixture) + + // Sanity check: the configured RuleEngine explicitly rejects this operator. + expect( + await ruleEngine.canTransferFrom( + unauthorizedBurner.address, + holder.address, + ethers.ZeroAddress, + VALUE + ) + ).to.equal(false) + + // The token's spender-aware pre-check also reports that this burn-from + // should be non-compliant. + expect( + await cmtat.canTransferFrom( + unauthorizedBurner.address, + holder.address, + ethers.ZeroAddress, + VALUE + ) + ).to.equal(false) + + // Sibling path check: ordinary transferFrom carries _msgSender() into the + // validation pipeline, so the same unauthorized operator is rejected. + await expect( + cmtat + .connect(unauthorizedBurner) + .transferFrom(holder.address, recipient.address, VALUE) + ) + .to.be.revertedWithCustomError(ruleEngine, 'RuleEngine_InvalidTransfer') + .withArgs(holder.address, recipient.address, VALUE) + + // Security expectation: burnFrom must be validated with the real spender as + // well. On vulnerable code this DOES NOT revert: burnFrom spends the + // allowance and CMTATBaseCommon._burnOverride calls _checkTransferred with + // spender = address(0), selecting RuleEngine.transferred(from, to, value) + // instead of transferred(spender, from, to, value). + await expect( + cmtat.connect(unauthorizedBurner).burnFrom(holder.address, VALUE) + ) + .to.be.revertedWithCustomError(ruleEngine, 'RuleEngine_InvalidTransfer') + .withArgs(holder.address, ethers.ZeroAddress, VALUE) + }) +}) diff --git a/tests/olympix/pocs/incomplete_mint_validation.t.sol b/tests/olympix/pocs/incomplete_mint_validation.t.sol new file mode 100644 index 00000000..ce60445d --- /dev/null +++ b/tests/olympix/pocs/incomplete_mint_validation.t.sol @@ -0,0 +1,27 @@ +const { expect } = require('chai') +const { ethers } = require('hardhat') + +const TERMS = [ + 'doc1', + 'https://example.com/doc1', + '0x6a12eff2f559a5e529ca2c563c53194f6463ed5c61d1ae8f8731137467ab0279' +] +const ZERO = ethers.ZeroAddress + +/* + * Vulnerability: incomplete mint validation via base _checkTransferred (CMTATBaseCommon._mintOverride). + * Mint validation runs _checkTransferred(address(0), address(0), to, value); over-freezing the + * address(0) sentinel makes the active-balance check underflow and bricks every mint path. + */ +describe('PoC: freezing address(0) bricks minting through base validation', function () { + it('mint must still succeed when a benign frozen amount is set on address(0)', async function () { + const [deployer, admin, forwarder, holder] = await ethers.getSigners() + const cmtat = await ethers.deployContract('CMTATStandalone', [ + forwarder.address, admin.address, ['CMTA Token', 'CMTAT', 0], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], [ZERO] + ]) + await cmtat.waitForDeployment() + await cmtat.connect(admin).setFrozenTokens(ZERO, 1n) + // FIXED: mint succeeds (sentinel ignored). VULNERABLE: mint reverts -> assertion FAILS. + await expect(cmtat.connect(admin).mint(holder.address, 10n)).to.not.be.reverted + }) +}) diff --git a/tests/olympix/pocs/inconsistent_compliance_view.t.sol b/tests/olympix/pocs/inconsistent_compliance_view.t.sol new file mode 100644 index 00000000..99722e64 --- /dev/null +++ b/tests/olympix/pocs/inconsistent_compliance_view.t.sol @@ -0,0 +1,26 @@ +const { expect } = require('chai') +const { ethers } = require('hardhat') + +const TERMS = [ + 'doc1', + 'https://example.com/doc1', + '0x6a12eff2f559a5e529ca2c563c53194f6463ed5c61d1ae8f8731137467ab0279' +] +const ZERO = ethers.ZeroAddress + +/* + * Vulnerability: inconsistent compliance view (ValidationModule._canTransferGenericByModule). + * A zero-to-zero request is classified as a mint and can return true even though it is not a + * valid transfer, so canTransfer reports an impossible movement as compliant. + */ +describe('PoC: canTransfer reports a zero-to-zero movement as valid', function () { + it('canTransfer(address(0), address(0), 1) must be false', async function () { + const [deployer, admin, forwarder] = await ethers.getSigners() + const cmtat = await ethers.deployContract('CMTATStandalone', [ + forwarder.address, admin.address, ['CMTA Token', 'CMTAT', 0], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], [ZERO] + ]) + await cmtat.waitForDeployment() + // FIXED: false. VULNERABLE: true -> assertion FAILS. + expect(await cmtat.canTransfer(ZERO, ZERO, 1n)).to.equal(false) + }) +}) diff --git a/tests/olympix/pocs/missing_deactivation_guard.t.sol b/tests/olympix/pocs/missing_deactivation_guard.t.sol new file mode 100644 index 00000000..b0c1a718 --- /dev/null +++ b/tests/olympix/pocs/missing_deactivation_guard.t.sol @@ -0,0 +1,60 @@ +const { expect } = require('chai') +const { ethers } = require('hardhat') +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers') +const { deployCMTATLightStandalone } = require('../deploymentUtils') + +// PoC for contracts/modules/0_CMTATBaseCore.sol:CMTATBaseCore.forcedBurn() +// CMTATStandaloneLight is the concrete deployment artifact that inherits CMTATBaseCore +// and exposes forcedBurn(). +describe('CMTATBaseCore forcedBurn deactivation guard PoC', function () { + const INITIAL_SUPPLY = 50n + const BURN_AMOUNT = 20n + const REASON = '0x' + + async function deployFixture () { + const [deployer, admin, holder] = await ethers.getSigners() + const cmtat = await deployCMTATLightStandalone( + admin.address, + deployer.address + ) + await cmtat.waitForDeployment() + + return { cmtat, admin, holder } + } + + it('should not allow forcedBurn to reduce balances or totalSupply after deactivateContract()', async function () { + const { cmtat, admin, holder } = await loadFixture(deployFixture) + + // Arrange: mint tokens to a holder, then permanently deactivate the token. + await cmtat.connect(admin).mint(holder.address, INITIAL_SUPPLY) + expect(await cmtat.balanceOf(holder.address)).to.equal(INITIAL_SUPPLY) + expect(await cmtat.totalSupply()).to.equal(INITIAL_SUPPLY) + + await cmtat.connect(admin).pause() + await cmtat.connect(admin).deactivateContract() + expect(await cmtat.paused()).to.equal(true) + expect(await cmtat.deactivated()).to.equal(true) + + // Sanity check: the normal burn path routes through _burnOverride(), which + // calls ValidationModule._canMintBurnByModuleAndRevert() and rejects a + // deactivated contract. + await expect(cmtat.connect(admin).burn(holder.address, 1n)) + .to.be.revertedWithCustomError(cmtat, 'EnforcedDeactivation') + + // The holder can still be frozen after deactivation; forcedBurn only checks + // this frozen flag and then calls ERC20Upgradeable._burn() directly. + await cmtat.connect(admin).setAddressFrozen(holder.address, true) + expect(await cmtat.isFrozen(holder.address)).to.equal(true) + + // Security expectation: deactivation guarantees burn operations stop + // permanently, so forcedBurn should also revert with EnforcedDeactivation. + // On the vulnerable implementation this transaction succeeds, reducing both + // holder balance and totalSupply, so this assertion fails and proves the bug. + await expect(cmtat.connect(admin).forcedBurn(holder.address, BURN_AMOUNT, REASON)) + .to.be.revertedWithCustomError(cmtat, 'EnforcedDeactivation') + + // These post-conditions are reachable only on a fixed implementation. + expect(await cmtat.balanceOf(holder.address)).to.equal(INITIAL_SUPPLY) + expect(await cmtat.totalSupply()).to.equal(INITIAL_SUPPLY) + }) +}) diff --git a/tests/olympix/pocs/missing_zero_address_validation.t.sol b/tests/olympix/pocs/missing_zero_address_validation.t.sol new file mode 100644 index 00000000..f4895fb0 --- /dev/null +++ b/tests/olympix/pocs/missing_zero_address_validation.t.sol @@ -0,0 +1,66 @@ +const { expect } = require('chai') +const { ethers } = require('hardhat') +const { + deployCMTATLightStandalone, + fixture, + loadFixture +} = require('../deploymentUtils') + +// PoC for contracts/modules/internal/common/EnforcementModuleLibrary.sol:_checkInput(). +// The vulnerable batch freeze path accepts address(0). In CMTATBaseCore.transfer(), +// direct ERC20 transfers validate with address(0) as the spender sentinel, so a frozen +// zero address makes every direct transfer fail even though the token is not paused. +describe('PoC - batch freeze of address(0) disables direct transfers', function () { + async function deployFixture () { + const ctx = await fixture() + const cmtat = await deployCMTATLightStandalone( + ctx.admin.address, + ctx.deployerAddress.address + ) + return { ...ctx, cmtat } + } + + it('does not let ENFORCER_ROLE globally block direct transfers by freezing address(0)', async function () { + const { cmtat, admin, address1, address2 } = await loadFixture(deployFixture) + + // Arrange: admin is the default admin and is implicitly authorized to mint/freeze. + // A normal holder-to-holder direct transfer succeeds before the zero-address freeze. + await cmtat.connect(admin).mint(address1.address, 100n) + await expect(cmtat.connect(address1).transfer(address2.address, 1n)) + .to.emit(cmtat, 'Transfer') + .withArgs(address1.address, address2.address, 1n) + + // Act: attempt the exact vulnerable batch path. A fixed implementation should + // reject address(0) here; if so, direct transfers remain available and the test passes. + let zeroAddressFreezeAccepted = false + try { + await cmtat + .connect(admin) + .batchSetAddressFrozen([ethers.ZeroAddress], [true]) + zeroAddressFreezeAccepted = true + } catch (e) { + zeroAddressFreezeAccepted = false + } + + if (!zeroAddressFreezeAccepted) { + expect(await cmtat.isFrozen(ethers.ZeroAddress)).to.equal(false) + await expect(cmtat.connect(address1).transfer(address2.address, 1n)) + .to.emit(cmtat, 'Transfer') + .withArgs(address1.address, address2.address, 1n) + return + } + + // Vulnerable state: address(0) was written into the frozen-address mapping/list, + // while the global pause switch remains off. This should not be enough to stop + // ordinary direct ERC20 transfers; if it is, the ENFORCER_ROLE has bypassed PAUSER_ROLE. + expect(await cmtat.isFrozen(ethers.ZeroAddress)).to.equal(true) + expect(await cmtat.paused()).to.equal(false) + + // Secure expectation: targeted account freezing must not globally disable all direct + // transfers. On the vulnerable code this assertion fails because transfer() uses + // address(0) as the spender sentinel and ValidationModule rejects frozen spenders. + await expect(cmtat.connect(address1).transfer(address2.address, 1n)) + .to.emit(cmtat, 'Transfer') + .withArgs(address1.address, address2.address, 1n) + }) +}) diff --git a/tests/olympix/pocs/overfrozen_balance_accounting_corruption.t.sol b/tests/olympix/pocs/overfrozen_balance_accounting_corruption.t.sol new file mode 100644 index 00000000..b607bb6c --- /dev/null +++ b/tests/olympix/pocs/overfrozen_balance_accounting_corruption.t.sol @@ -0,0 +1,28 @@ +const { expect } = require('chai') +const { ethers } = require('hardhat') + +const TERMS = [ + 'doc1', + 'https://example.com/doc1', + '0x6a12eff2f559a5e529ca2c563c53194f6463ed5c61d1ae8f8731137467ab0279' +] +const ZERO = ethers.ZeroAddress + +/* + * Vulnerability: overfrozen balance accounting corruption (ERC20EnforcementModule.setFrozenTokens). + * setFrozenTokens writes the ABSOLUTE frozen amount without the `value <= balanceOf(account)` + * cap that _freezePartialTokens enforces, so getActiveBalanceOf underflows (Panic 0x11). + */ +describe('PoC: setFrozenTokens above balance corrupts active-balance accounting', function () { + it('a frozen amount greater than the balance must not brick getActiveBalanceOf', async function () { + const [deployer, admin, forwarder, holder] = await ethers.getSigners() + const cmtat = await ethers.deployContract('CMTATStandalone', [ + forwarder.address, admin.address, ['CMTA Token', 'CMTAT', 0], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], [ZERO] + ]) + await cmtat.waitForDeployment() + await cmtat.connect(admin).mint(holder.address, 50n) + await cmtat.connect(admin).setFrozenTokens(holder.address, 100n) + // FIXED: getActiveBalanceOf returns a bounded value. VULNERABLE: 50-100 underflows -> assertion FAILS. + await expect(cmtat.getActiveBalanceOf(holder.address)).to.not.be.reverted + }) +}) diff --git a/tests/olympix/pocs/zero_address_transfer_validation_mismatch.t.sol b/tests/olympix/pocs/zero_address_transfer_validation_mismatch.t.sol new file mode 100644 index 00000000..3bcdf150 --- /dev/null +++ b/tests/olympix/pocs/zero_address_transfer_validation_mismatch.t.sol @@ -0,0 +1,26 @@ +const { expect } = require('chai') +const { ethers } = require('hardhat') + +const TERMS = [ + 'doc1', + 'https://example.com/doc1', + '0x6a12eff2f559a5e529ca2c563c53194f6463ed5c61d1ae8f8731137467ab0279' +] +const ZERO = ethers.ZeroAddress + +/* + * Vulnerability: zero-address transfer validation mismatch (ValidationModuleCore.canTransfer). + * canTransfer does not reject zero-address endpoints; a zero `from` is treated as a mint and + * can return true for an impossible ERC-20 transfer. + */ +describe('PoC: canTransfer reports a zero-from transfer as valid', function () { + it('canTransfer(address(0), holder, 1) must be false', async function () { + const [deployer, admin, forwarder, holder] = await ethers.getSigners() + const cmtat = await ethers.deployContract('CMTATStandalone', [ + forwarder.address, admin.address, ['CMTA Token', 'CMTAT', 0], ['CMTAT_ISIN', TERMS, 'CMTAT_info'], [ZERO] + ]) + await cmtat.waitForDeployment() + // FIXED: false (invalid endpoint). VULNERABLE: true -> assertion FAILS. + expect(await cmtat.canTransfer(ZERO, holder.address, 1n)).to.equal(false) + }) +})