| RuleSanctionsList.sol |
diff --git a/doc/coverage/lcov.info b/doc/coverage/lcov.info
index 4b819914..fb45efa5 100644
--- a/doc/coverage/lcov.info
+++ b/doc/coverage/lcov.info
@@ -1,708 +1,20 @@
TN:
-SF:script/DeployCMTATWithBlacklist.s.sol
-DA:11,1
-FN:11,DeployCMTATWithBlacklist.deploy
-FNDA:1,DeployCMTATWithBlacklist.deploy
-DA:12,1
-DA:13,1
-DA:14,1
-DA:15,1
-DA:22,1
-DA:24,1
-DA:25,1
-DA:27,1
-DA:29,1
-BRDA:29,0,0,1
-DA:30,1
-DA:31,1
-DA:35,0
-FN:35,DeployCMTATWithBlacklist.run
-FNDA:0,DeployCMTATWithBlacklist.run
-DA:36,0
-DA:37,0
-DA:38,0
-FNF:2
-FNH:1
-LF:16
-LH:12
-BRF:1
-BRH:1
-end_of_record
-TN:
-SF:script/DeployCMTATWithBlacklistAndSanctionsList.s.sol
-DA:27,18
-FN:27,DeployCMTATWithBlacklistAndSanctionsList.deploy
-FNDA:18,DeployCMTATWithBlacklistAndSanctionsList.deploy
-DA:36,18
-DA:37,18
-DA:38,18
-DA:39,18
-DA:46,18
-DA:49,18
-DA:52,18
-DA:53,18
-DA:57,18
-DA:60,18
-DA:61,18
-DA:64,18
-DA:67,18
-BRDA:67,0,0,18
-DA:68,18
-DA:69,18
-DA:70,18
-DA:71,18
-DA:75,0
-FN:75,DeployCMTATWithBlacklistAndSanctionsList.run
-FNDA:0,DeployCMTATWithBlacklistAndSanctionsList.run
-DA:84,0
-DA:87,0
-DA:89,0
-FNF:2
-FNH:1
-LF:22
-LH:18
-BRF:1
-BRH:1
-end_of_record
-TN:
-SF:script/DeployCMTATWithWhitelist.s.sol
-DA:11,1
-FN:11,DeployCMTATWithWhitelist.deploy
-FNDA:1,DeployCMTATWithWhitelist.deploy
-DA:15,1
-DA:16,1
-DA:17,1
-DA:18,1
-DA:25,1
-DA:27,1
-DA:28,1
-DA:30,1
-DA:32,1
-BRDA:32,0,0,1
-DA:33,1
-DA:34,1
-DA:38,0
-FN:38,DeployCMTATWithWhitelist.run
-FNDA:0,DeployCMTATWithWhitelist.run
-DA:39,0
-DA:40,0
-DA:41,0
-FNF:2
-FNH:1
-LF:16
-LH:12
-BRF:1
-BRH:1
-end_of_record
-TN:
-SF:src/mocks/AggregatorV3Mock.sol
-DA:24,601
-FN:24,AggregatorV3Mock.constructor
-FNDA:601,AggregatorV3Mock.constructor
-DA:25,601
-DA:26,601
-DA:27,601
-DA:28,601
-DA:39,264
-FN:39,AggregatorV3Mock.setAnswer
-FNDA:264,AggregatorV3Mock.setAnswer
-DA:40,264
-DA:41,264
-DA:42,264
-DA:49,1
-FN:49,AggregatorV3Mock.setUpdatedAt
-FNDA:1,AggregatorV3Mock.setUpdatedAt
-DA:50,1
-DA:57,7
-FN:57,AggregatorV3Mock.setDecimals
-FNDA:7,AggregatorV3Mock.setDecimals
-DA:58,7
-DA:65,2
-FN:65,AggregatorV3Mock.setRevertOnDecimals
-FNDA:2,AggregatorV3Mock.setRevertOnDecimals
-DA:66,2
-DA:73,2
-FN:73,AggregatorV3Mock.setRevertOnLatestRoundData
-FNDA:2,AggregatorV3Mock.setRevertOnLatestRoundData
-DA:74,2
-DA:80,1861
-FN:80,AggregatorV3Mock.decimals
-FNDA:1861,AggregatorV3Mock.decimals
-DA:81,1861
-BRDA:81,0,0,3
-BRDA:81,0,1,1858
-DA:82,1858
-DA:88,1
-FN:88,AggregatorV3Mock.description
-FNDA:1,AggregatorV3Mock.description
-DA:89,1
-DA:95,1
-FN:95,AggregatorV3Mock.version
-FNDA:1,AggregatorV3Mock.version
-DA:96,1
-DA:102,1
-FN:102,AggregatorV3Mock.getRoundData
-FNDA:1,AggregatorV3Mock.getRoundData
-DA:108,1
-DA:114,1247
-FN:114,AggregatorV3Mock.latestRoundData
-FNDA:1247,AggregatorV3Mock.latestRoundData
-DA:120,1247
-BRDA:120,1,0,2
-BRDA:120,1,1,1245
-DA:121,1245
-FNF:11
-FNH:11
-LF:29
-LH:29
-BRF:4
-BRH:4
-end_of_record
-TN:
-SF:src/mocks/ERC3643TokenMock.sol
-DA:67,27
-FN:67,ERC3643TokenMock.constructor
-FNDA:27,ERC3643TokenMock.constructor
-DA:68,27
-DA:69,27
-DA:73,25
-FN:73,ERC3643TokenMock.onlyAgent
-FNDA:25,ERC3643TokenMock.onlyAgent
-DA:74,25
-BRDA:74,0,0,-
-BRDA:74,0,1,6
-DA:86,0
-FN:86,ERC3643TokenMock.setIdentityRegistry
-FNDA:0,ERC3643TokenMock.setIdentityRegistry
-DA:87,0
-DA:97,13
-FN:97,ERC3643TokenMock.setCompliance
-FNDA:13,ERC3643TokenMock.setCompliance
-DA:98,13
-BRDA:98,1,0,-
-DA:99,0
-DA:101,13
-DA:102,13
-DA:110,0
-FN:110,ERC3643TokenMock.setAgent
-FNDA:0,ERC3643TokenMock.setAgent
-DA:111,0
-DA:122,7
-FN:122,ERC3643TokenMock.transfer
-FNDA:7,ERC3643TokenMock.transfer
-DA:123,7
-BRDA:123,2,0,-
-BRDA:123,2,1,7
-DA:124,7
-BRDA:124,3,0,3
-DA:125,3
-DA:126,3
-DA:127,3
-DA:129,4
-DA:140,4
-FN:140,ERC3643TokenMock.transferFrom
-FNDA:4,ERC3643TokenMock.transferFrom
-DA:141,4
-BRDA:141,4,0,-
-BRDA:141,4,1,4
-DA:142,4
-BRDA:142,5,0,2
-DA:143,2
-DA:144,2
-DA:145,2
-DA:147,2
-DA:158,4
-FN:158,ERC3643TokenMock.forcedTransfer
-FNDA:4,ERC3643TokenMock.forcedTransfer
-DA:159,6
-BRDA:159,6,0,-
-BRDA:159,6,1,6
-DA:163,6
-BRDA:163,7,0,5
-DA:164,5
-DA:165,5
-DA:166,4
-DA:168,1
-DA:177,25
-FN:177,ERC3643TokenMock.mint
-FNDA:25,ERC3643TokenMock.mint
-DA:178,25
-BRDA:178,8,0,2
-BRDA:178,8,1,23
-DA:179,23
-BRDA:179,9,0,1
-BRDA:179,9,1,22
-DA:180,22
-DA:181,22
-DA:182,22
-DA:183,22
-BRDA:183,10,0,10
-DA:184,10
-DA:194,3
-FN:194,ERC3643TokenMock.burn
-FNDA:3,ERC3643TokenMock.burn
-DA:195,3
-BRDA:195,11,0,-
-BRDA:195,11,1,3
-DA:196,3
-DA:197,3
-DA:198,3
-DA:199,3
-BRDA:199,12,0,2
-DA:200,2
-DA:219,4
-FN:219,ERC3643TokenMock.recoveryAddress
-FNDA:4,ERC3643TokenMock.recoveryAddress
-DA:224,4
-BRDA:224,13,0,-
-BRDA:224,13,1,4
-DA:226,4
-DA:227,4
-BRDA:227,14,0,3
-DA:228,3
-DA:229,3
-DA:232,2
-DA:233,2
-DA:234,2
-DA:235,2
-DA:237,1
-DA:251,31
-FN:251,ERC3643TokenMock._canTransfer
-FNDA:31,ERC3643TokenMock._canTransfer
-DA:252,31
-BRDA:252,15,0,15
-DA:253,15
-DA:255,16
-DA:264,10
-FN:264,ERC3643TokenMock._complianceTransferred
-FNDA:10,ERC3643TokenMock._complianceTransferred
-DA:265,10
-BRDA:265,16,0,4
-DA:266,4
-DA:276,10
-FN:276,ERC3643TokenMock._transfer
-FNDA:10,ERC3643TokenMock._transfer
-DA:277,10
-DA:278,10
-DA:279,10
-FNF:14
-FNH:12
-LF:72
-LH:67
-BRF:25
-BRH:18
-end_of_record
-TN:
-SF:src/mocks/IAddressListInterfaceIdHelper.sol
-DA:88,1
-FN:88,IAddressListInterfaceIdHelper.getIAddressListInterfaceId
-FNDA:1,IAddressListInterfaceIdHelper.getIAddressListInterfaceId
-DA:89,1
-DA:96,0
-FN:96,IAddressListInterfaceIdHelper.getIAddressListAllFunctionsInterfaceId
-FNDA:0,IAddressListInterfaceIdHelper.getIAddressListAllFunctionsInterfaceId
-DA:97,0
-DA:104,0
-FN:104,IAddressListInterfaceIdHelper.getAddressListInterfaceIdConstant
-FNDA:0,IAddressListInterfaceIdHelper.getAddressListInterfaceIdConstant
-DA:105,0
-DA:112,1
-FN:112,IAddressListInterfaceIdHelper.getIIdentityRegistryContainsInterfaceId
-FNDA:1,IAddressListInterfaceIdHelper.getIIdentityRegistryContainsInterfaceId
-DA:113,1
-FNF:4
-FNH:2
-LF:8
-LH:4
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:src/mocks/IdentityRegistryMock.sol
-DA:25,35
-FN:25,IdentityRegistryMock.setVerified
-FNDA:35,IdentityRegistryMock.setVerified
-DA:26,35
-DA:34,60
-FN:34,IdentityRegistryMock.isVerified
-FNDA:60,IdentityRegistryMock.isVerified
-DA:35,60
-FNF:2
-FNH:2
-LF:4
-LH:4
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:src/mocks/MockERC20TransferFromFalse.sol
-DA:25,1
-FN:25,MockERC20TransferFromFalse.setAllowance
-FNDA:1,MockERC20TransferFromFalse.setAllowance
-DA:26,1
-DA:35,1
-FN:35,MockERC20TransferFromFalse.allowance
-FNDA:1,MockERC20TransferFromFalse.allowance
-DA:36,1
-DA:43,1
-FN:43,MockERC20TransferFromFalse.transferFrom
-FNDA:1,MockERC20TransferFromFalse.transferFrom
-DA:44,1
-FNF:3
-FNH:3
-LF:6
-LH:6
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:src/mocks/MockERC20WithTransferContext.sol
-DA:37,19
-FN:37,MockERC20WithTransferContext.setRule
-FNDA:19,MockERC20WithTransferContext.setRule
-DA:38,19
-DA:46,19
-FN:46,MockERC20WithTransferContext.mint
-FNDA:19,MockERC20WithTransferContext.mint
-DA:47,19
-DA:58,3
-FN:58,MockERC20WithTransferContext.transferWithContext
-FNDA:3,MockERC20WithTransferContext.transferWithContext
-DA:62,3
-DA:63,2
-BRDA:63,0,0,2
-BRDA:63,0,1,1
-DA:64,2
-DA:66,1
-DA:68,3
-DA:80,3
-FN:80,MockERC20WithTransferContext.transferFromWithContext
-FNDA:3,MockERC20WithTransferContext.transferFromWithContext
-DA:84,3
-DA:85,3
-DA:86,3
-DA:88,2
-BRDA:88,1,0,2
-BRDA:88,1,1,1
-DA:89,2
-DA:91,1
-DA:93,3
-DA:103,3
-FN:103,MockERC20WithTransferContext.transfer
-FNDA:3,MockERC20WithTransferContext.transfer
-DA:104,3
-DA:105,3
-DA:106,2
-DA:112,5
-FN:112,MockERC20WithTransferContext.transferFrom
-FNDA:5,MockERC20WithTransferContext.transferFrom
-DA:113,5
-DA:114,5
-DA:115,5
-DA:116,4
-DA:131,12
-FN:131,MockERC20WithTransferContext._notifyFungible
-FNDA:12,MockERC20WithTransferContext._notifyFungible
-DA:132,12
-BRDA:132,2,0,12
-DA:133,12
-DA:136,12
-DA:144,12
-DA:156,2
-FN:156,MockERC20WithTransferContext._notifyMultiToken
-FNDA:2,MockERC20WithTransferContext._notifyMultiToken
-DA:157,2
-BRDA:157,3,0,2
-DA:158,2
-DA:161,2
-DA:170,2
-FNF:8
-FNH:8
-LF:37
-LH:37
-BRF:6
-BRH:6
-end_of_record
-TN:
-SF:src/mocks/MockERC721WithTransferContext.sol
-DA:37,3
-FN:37,MockERC721WithTransferContext.setRule
-FNDA:3,MockERC721WithTransferContext.setRule
-DA:38,3
-DA:46,3
-FN:46,MockERC721WithTransferContext.mint
-FNDA:3,MockERC721WithTransferContext.mint
-DA:47,3
-DA:57,4
-FN:57,MockERC721WithTransferContext.transferFrom
-FNDA:4,MockERC721WithTransferContext.transferFrom
-DA:58,4
-DA:59,4
-DA:60,4
-DA:75,4
-FN:75,MockERC721WithTransferContext._notifyRule
-FNDA:4,MockERC721WithTransferContext._notifyRule
-DA:76,4
-BRDA:76,0,0,4
-DA:77,4
-DA:80,4
-DA:89,4
-FNF:4
-FNH:4
-LF:13
-LH:13
-BRF:1
-BRH:1
-end_of_record
-TN:
-SF:src/mocks/OnchainIdMock.sol
-DA:25,3
-FN:25,OnchainIdMock.addWalletKey
-FNDA:3,OnchainIdMock.addWalletKey
-DA:26,3
-DA:32,4
-FN:32,OnchainIdMock.keyHasPurpose
-FNDA:4,OnchainIdMock.keyHasPurpose
-DA:33,4
-FNF:2
-FNH:2
-LF:4
-LH:4
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:src/mocks/SanctionListOracle.sol
-DA:24,24
-FN:24,SanctionListOracle.addToSanctionsList
-FNDA:24,SanctionListOracle.addToSanctionsList
-DA:25,24
-DA:32,1
-FN:32,SanctionListOracle.removeFromSanctionsList
-FNDA:1,SanctionListOracle.removeFromSanctionsList
-DA:33,1
-DA:41,267
-FN:41,SanctionListOracle.isSanctioned
-FNDA:267,SanctionListOracle.isSanctioned
-DA:42,267
-FNF:3
-FNH:3
-LF:6
-LH:6
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:src/mocks/TotalSupplyDecimalsMock.sol
-DA:18,593
-FN:18,TotalSupplyDecimalsMock.constructor
-FNDA:593,TotalSupplyDecimalsMock.constructor
-DA:19,593
-DA:30,554
-FN:30,TotalSupplyDecimalsMock.setTotalSupply
-FNDA:554,TotalSupplyDecimalsMock.setTotalSupply
-DA:31,554
-DA:38,2
-FN:38,TotalSupplyDecimalsMock.setRevertOnTotalSupply
-FNDA:2,TotalSupplyDecimalsMock.setRevertOnTotalSupply
-DA:39,2
-DA:46,1033
-FN:46,TotalSupplyDecimalsMock.totalSupply
-FNDA:1033,TotalSupplyDecimalsMock.totalSupply
-DA:47,1033
-BRDA:47,0,0,4
-BRDA:47,0,1,1029
-DA:48,1029
-DA:55,594
-FN:55,TotalSupplyDecimalsMock.decimals
-FNDA:594,TotalSupplyDecimalsMock.decimals
-DA:56,594
-FNF:5
-FNH:5
-LF:11
-LH:11
-BRF:2
-BRH:2
-end_of_record
-TN:
-SF:src/mocks/TotalSupplyMock.sol
-DA:22,781
-FN:22,TotalSupplyMock.setTotalSupply
-FNDA:781,TotalSupplyMock.setTotalSupply
-DA:23,781
-DA:30,1333
-FN:30,TotalSupplyMock.totalSupply
-FNDA:1333,TotalSupplyMock.totalSupply
-DA:31,1333
-FNF:2
-FNH:2
-LF:4
-LH:4
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:src/mocks/harness/DeploymentCoverageHarnesses.sol
-DA:38,1
-FN:38,RuleBlacklistHarness.exposedMsgDataLength.0
-FNDA:1,RuleBlacklistHarness.exposedMsgDataLength.0
-DA:39,1
-DA:70,1
-FN:70,RuleWhitelistHarness.exposedMsgDataLength.1
-FNDA:1,RuleWhitelistHarness.exposedMsgDataLength.1
-DA:71,1
-DA:102,1
-FN:102,RuleWhitelistWrapperHarness.exposedMsgDataLength.2
-FNDA:1,RuleWhitelistWrapperHarness.exposedMsgDataLength.2
-DA:103,1
-DA:133,1
-FN:133,RuleERC2980Harness.exposedMsgDataLength.3
-FNDA:1,RuleERC2980Harness.exposedMsgDataLength.3
-DA:134,1
-DA:164,1
-FN:164,RuleSanctionsListHarness.exposedMsgDataLength.4
-FNDA:1,RuleSanctionsListHarness.exposedMsgDataLength.4
-DA:165,1
-DA:192,1
-FN:192,RuleBlacklistOwnable2StepHarness.exposedMsgDataLength.5
-FNDA:1,RuleBlacklistOwnable2StepHarness.exposedMsgDataLength.5
-DA:193,1
-DA:224,1
-FN:224,RuleWhitelistOwnable2StepHarness.exposedMsgDataLength.6
-FNDA:1,RuleWhitelistOwnable2StepHarness.exposedMsgDataLength.6
-DA:225,1
-DA:256,1
-FN:256,RuleWhitelistWrapperOwnable2StepHarness.exposedMsgDataLength.7
-FNDA:1,RuleWhitelistWrapperOwnable2StepHarness.exposedMsgDataLength.7
-DA:257,1
-DA:287,1
-FN:287,RuleERC2980Ownable2StepHarness.exposedMsgDataLength.8
-FNDA:1,RuleERC2980Ownable2StepHarness.exposedMsgDataLength.8
-DA:288,1
-FNF:9
-FNH:9
-LF:18
-LH:18
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:src/mocks/harness/RuleReceiverWhitelistHarnesses.sol
-DA:32,1
-FN:32,RuleReceiverWhitelistHarness.exposedMsgSender.0
-FNDA:1,RuleReceiverWhitelistHarness.exposedMsgSender.0
-DA:33,1
-DA:40,1
-FN:40,RuleReceiverWhitelistHarness.exposedMsgData.0
-FNDA:1,RuleReceiverWhitelistHarness.exposedMsgData.0
-DA:41,1
-DA:48,1
-FN:48,RuleReceiverWhitelistHarness.exposedContextSuffixLength.0
-FNDA:1,RuleReceiverWhitelistHarness.exposedContextSuffixLength.0
-DA:49,1
-DA:78,1
-FN:78,RuleReceiverWhitelistOwnable2StepHarness.exposedMsgSender.1
-FNDA:1,RuleReceiverWhitelistOwnable2StepHarness.exposedMsgSender.1
-DA:79,1
-DA:86,1
-FN:86,RuleReceiverWhitelistOwnable2StepHarness.exposedMsgData.1
-FNDA:1,RuleReceiverWhitelistOwnable2StepHarness.exposedMsgData.1
-DA:87,1
-DA:94,1
-FN:94,RuleReceiverWhitelistOwnable2StepHarness.exposedContextSuffixLength.1
-FNDA:1,RuleReceiverWhitelistOwnable2StepHarness.exposedContextSuffixLength.1
-DA:95,1
-FNF:6
-FNH:6
-LF:12
-LH:12
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:src/mocks/harness/RuleSanctionsListOwnable2StepHarness.sol
-DA:33,1
-FN:33,RuleSanctionsListOwnable2StepHarness.exposedMsgSender
-FNDA:1,RuleSanctionsListOwnable2StepHarness.exposedMsgSender
-DA:34,1
-DA:41,1
-FN:41,RuleSanctionsListOwnable2StepHarness.exposedMsgData
-FNDA:1,RuleSanctionsListOwnable2StepHarness.exposedMsgData
-DA:42,1
-DA:49,1
-FN:49,RuleSanctionsListOwnable2StepHarness.exposedContextSuffixLength
-FNDA:1,RuleSanctionsListOwnable2StepHarness.exposedContextSuffixLength
-DA:50,1
-FNF:3
-FNH:3
-LF:6
-LH:6
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:src/mocks/harness/RuleSpenderWhitelistHarnesses.sol
-DA:30,1
-FN:30,RuleSpenderWhitelistHarness.exposedMsgSender.0
-FNDA:1,RuleSpenderWhitelistHarness.exposedMsgSender.0
-DA:31,1
-DA:38,1
-FN:38,RuleSpenderWhitelistHarness.exposedMsgData.0
-FNDA:1,RuleSpenderWhitelistHarness.exposedMsgData.0
-DA:39,1
-DA:46,1
-FN:46,RuleSpenderWhitelistHarness.exposedContextSuffixLength.0
-FNDA:1,RuleSpenderWhitelistHarness.exposedContextSuffixLength.0
-DA:47,1
-DA:76,1
-FN:76,RuleSpenderWhitelistOwnable2StepHarness.exposedMsgSender.1
-FNDA:1,RuleSpenderWhitelistOwnable2StepHarness.exposedMsgSender.1
-DA:77,1
-DA:84,1
-FN:84,RuleSpenderWhitelistOwnable2StepHarness.exposedMsgData.1
-FNDA:1,RuleSpenderWhitelistOwnable2StepHarness.exposedMsgData.1
-DA:85,1
-DA:92,1
-FN:92,RuleSpenderWhitelistOwnable2StepHarness.exposedContextSuffixLength.1
-FNDA:1,RuleSpenderWhitelistOwnable2StepHarness.exposedContextSuffixLength.1
-DA:93,1
-FNF:6
-FNH:6
-LF:12
-LH:12
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:src/mocks/harness/RuleWhitelistWrapperHarnessInternal.sol
-DA:36,1
-FN:36,RuleWhitelistWrapperHarnessInternal.exposedTransferredSpenderInternal
-FNDA:1,RuleWhitelistWrapperHarnessInternal.exposedTransferredSpenderInternal
-DA:37,1
-FNF:1
-FNH:1
-LF:2
-LH:2
-BRF:0
-BRH:0
-end_of_record
-TN:
SF:src/modules/AccessControlModuleStandalone.sol
-DA:30,2308
+DA:30,2595
FN:30,AccessControlModuleStandalone.constructor
-FNDA:2308,AccessControlModuleStandalone.constructor
-DA:31,2308
+FNDA:2595,AccessControlModuleStandalone.constructor
+DA:31,2595
BRDA:31,0,0,7
-BRDA:31,0,1,2301
-DA:35,2301
-DA:46,2432
+BRDA:31,0,1,2588
+DA:35,2588
+DA:46,2731
FN:46,AccessControlModuleStandalone.hasRole
-FNDA:2432,AccessControlModuleStandalone.hasRole
-DA:55,22060
-BRDA:55,1,0,4187
-BRDA:55,1,1,17873
-DA:56,4187
-DA:58,17873
+FNDA:2731,AccessControlModuleStandalone.hasRole
+DA:55,23039
+BRDA:55,1,0,4834
+BRDA:55,1,1,18205
+DA:56,4834
+DA:58,18205
FNF:2
FNH:2
LF:7
@@ -712,12 +24,12 @@ BRH:4
end_of_record
TN:
SF:src/modules/Ownable2StepERC165Module.sol
-DA:17,78
+DA:17,81
FN:17,Ownable2StepERC165Module.supportsInterface
-FNDA:78,Ownable2StepERC165Module.supportsInterface
-DA:18,78
-DA:19,66
-DA:20,54
+FNDA:81,Ownable2StepERC165Module.supportsInterface
+DA:18,81
+DA:19,69
+DA:20,57
FNF:1
FNH:1
LF:4
@@ -727,10 +39,10 @@ BRH:0
end_of_record
TN:
SF:src/modules/VersionModule.sol
-DA:23,7
+DA:23,20
FN:23,VersionModule.version
-FNDA:7,VersionModule.version
-DA:24,7
+FNDA:20,VersionModule.version
+DA:24,20
FNF:1
FNH:1
LF:2
@@ -740,9 +52,9 @@ BRH:0
end_of_record
TN:
SF:src/registry/IdentityRegistryWhitelist.sol
-DA:34,85
+DA:34,107
FN:34,IdentityRegistryWhitelist._authorizeIdentityRegistrar
-FNDA:85,IdentityRegistryWhitelist._authorizeIdentityRegistrar
+FNDA:107,IdentityRegistryWhitelist._authorizeIdentityRegistrar
FNF:1
FNH:1
LF:1
@@ -752,48 +64,46 @@ BRH:0
end_of_record
TN:
SF:src/registry/abstract/IdentityRegistryWhitelistBase.sol
-DA:69,78
-FN:69,IdentityRegistryWhitelistBase.registerIdentity
-FNDA:78,IdentityRegistryWhitelistBase.registerIdentity
-DA:81,76
-BRDA:81,0,0,1
-BRDA:81,0,1,75
-DA:82,75
-BRDA:82,1,0,1
-BRDA:82,1,1,74
-DA:83,74
-DA:84,74
-DA:91,7
-FN:91,IdentityRegistryWhitelistBase.deleteIdentity
-FNDA:7,IdentityRegistryWhitelistBase.deleteIdentity
-DA:92,6
-BRDA:92,2,0,1
-BRDA:92,2,1,5
-DA:93,5
-DA:94,5
-DA:103,3
-FN:103,IdentityRegistryWhitelistBase.registeredIdentityCount
+DA:46,96
+FN:46,IdentityRegistryWhitelistBase.registerIdentity
+FNDA:96,IdentityRegistryWhitelistBase.registerIdentity
+DA:58,93
+BRDA:58,0,0,1
+BRDA:58,0,1,92
+DA:59,92
+BRDA:59,1,0,1
+BRDA:59,1,1,91
+DA:60,91
+DA:67,11
+FN:67,IdentityRegistryWhitelistBase.deleteIdentity
+FNDA:11,IdentityRegistryWhitelistBase.deleteIdentity
+DA:68,10
+BRDA:68,2,0,1
+BRDA:68,2,1,9
+DA:69,9
+DA:78,3
+FN:78,IdentityRegistryWhitelistBase.registeredIdentityCount
FNDA:3,IdentityRegistryWhitelistBase.registeredIdentityCount
-DA:104,3
-DA:117,54
-FN:117,IdentityRegistryWhitelistBase.isVerified
-FNDA:54,IdentityRegistryWhitelistBase.isVerified
-DA:118,54
-DA:127,7
-FN:127,IdentityRegistryWhitelistBase.investorCountry
+DA:79,3
+DA:92,84
+FN:92,IdentityRegistryWhitelistBase.isVerified
+FNDA:84,IdentityRegistryWhitelistBase.isVerified
+DA:93,84
+DA:102,7
+FN:102,IdentityRegistryWhitelistBase.investorCountry
FNDA:7,IdentityRegistryWhitelistBase.investorCountry
-DA:136,7
-DA:143,78
-FN:143,IdentityRegistryWhitelistBase.onlyIdentityRegistrar
-FNDA:78,IdentityRegistryWhitelistBase.onlyIdentityRegistrar
-DA:144,78
-DA:152,0
-FN:152,IdentityRegistryWhitelistBase._authorizeIdentityRegistrar
+DA:111,7
+DA:118,96
+FN:118,IdentityRegistryWhitelistBase.onlyIdentityRegistrar
+FNDA:96,IdentityRegistryWhitelistBase.onlyIdentityRegistrar
+DA:119,96
+DA:127,0
+FN:127,IdentityRegistryWhitelistBase._authorizeIdentityRegistrar
FNDA:0,IdentityRegistryWhitelistBase._authorizeIdentityRegistrar
FNF:7
FNH:6
-LF:18
-LH:17
+LF:16
+LH:15
BRF:6
BRH:6
end_of_record
@@ -807,15 +117,15 @@ DA:49,47
DA:50,46
DA:51,30
DA:52,29
-DA:62,58
+DA:62,69
FN:62,RuleConditionalTransferLight._onlyComplianceManager
-FNDA:58,RuleConditionalTransferLight._onlyComplianceManager
-DA:67,7593
+FNDA:69,RuleConditionalTransferLight._onlyComplianceManager
+DA:67,7637
FN:67,RuleConditionalTransferLight._authorizeTransferApproval
-FNDA:7593,RuleConditionalTransferLight._authorizeTransferApproval
+FNDA:7637,RuleConditionalTransferLight._authorizeTransferApproval
DA:72,3
-FN:72,RuleConditionalTransferLight._authorizeComplianceBindingChange
-FNDA:3,RuleConditionalTransferLight._authorizeComplianceBindingChange
+FN:72,RuleConditionalTransferLight._authorizeTokenBindingChange
+FNDA:3,RuleConditionalTransferLight._authorizeTokenBindingChange
FNF:4
FNH:4
LF:9
@@ -833,12 +143,12 @@ DA:41,6
DA:42,6
DA:43,4
DA:44,4
-DA:50,38
+DA:50,52
FN:50,RuleConditionalTransferLightMultiToken._onlyComplianceManager
-FNDA:38,RuleConditionalTransferLightMultiToken._onlyComplianceManager
-DA:55,31
+FNDA:52,RuleConditionalTransferLightMultiToken._onlyComplianceManager
+DA:55,43
FN:55,RuleConditionalTransferLightMultiToken._authorizeTransferApproval
-FNDA:31,RuleConditionalTransferLightMultiToken._authorizeTransferApproval
+FNDA:43,RuleConditionalTransferLightMultiToken._authorizeTransferApproval
FNF:3
FNH:3
LF:8
@@ -856,16 +166,16 @@ DA:40,2
DA:41,2
DA:42,2
DA:43,2
-DA:49,0
+DA:49,6
FN:49,RuleConditionalTransferLightMultiTokenOwnable2Step._onlyComplianceManager
-FNDA:0,RuleConditionalTransferLightMultiTokenOwnable2Step._onlyComplianceManager
-DA:54,0
+FNDA:6,RuleConditionalTransferLightMultiTokenOwnable2Step._onlyComplianceManager
+DA:54,2
FN:54,RuleConditionalTransferLightMultiTokenOwnable2Step._authorizeTransferApproval
-FNDA:0,RuleConditionalTransferLightMultiTokenOwnable2Step._authorizeTransferApproval
+FNDA:2,RuleConditionalTransferLightMultiTokenOwnable2Step._authorizeTransferApproval
FNF:3
-FNH:1
+FNH:3
LF:8
-LH:6
+LH:8
BRF:0
BRH:0
end_of_record
@@ -879,40 +189,40 @@ DA:47,8
DA:48,7
DA:49,6
DA:50,4
-DA:60,3
+DA:60,5
FN:60,RuleConditionalTransferLightOwnable2Step._onlyComplianceManager
-FNDA:3,RuleConditionalTransferLightOwnable2Step._onlyComplianceManager
+FNDA:5,RuleConditionalTransferLightOwnable2Step._onlyComplianceManager
DA:65,4
FN:65,RuleConditionalTransferLightOwnable2Step._authorizeTransferApproval
FNDA:4,RuleConditionalTransferLightOwnable2Step._authorizeTransferApproval
-DA:70,0
-FN:70,RuleConditionalTransferLightOwnable2Step._authorizeComplianceBindingChange
-FNDA:0,RuleConditionalTransferLightOwnable2Step._authorizeComplianceBindingChange
+DA:70,2
+FN:70,RuleConditionalTransferLightOwnable2Step._authorizeTokenBindingChange
+FNDA:2,RuleConditionalTransferLightOwnable2Step._authorizeTokenBindingChange
FNF:4
-FNH:3
+FNH:4
LF:9
-LH:8
+LH:9
BRF:0
BRH:0
end_of_record
TN:
SF:src/rules/operation/RuleMintAllowance.sol
-DA:38,34
+DA:38,37
FN:38,RuleMintAllowance.supportsInterface
-FNDA:34,RuleMintAllowance.supportsInterface
-DA:47,34
-DA:48,33
-DA:49,32
-DA:50,21
-DA:60,312
+FNDA:37,RuleMintAllowance.supportsInterface
+DA:47,37
+DA:48,36
+DA:49,35
+DA:50,23
+DA:60,313
FN:60,RuleMintAllowance._onlyComplianceManager
-FNDA:312,RuleMintAllowance._onlyComplianceManager
-DA:65,10087
+FNDA:313,RuleMintAllowance._onlyComplianceManager
+DA:65,10089
FN:65,RuleMintAllowance._authorizeSetMintAllowance
-FNDA:10087,RuleMintAllowance._authorizeSetMintAllowance
+FNDA:10089,RuleMintAllowance._authorizeSetMintAllowance
DA:70,4
-FN:70,RuleMintAllowance._authorizeComplianceBindingChange
-FNDA:4,RuleMintAllowance._authorizeComplianceBindingChange
+FN:70,RuleMintAllowance._authorizeTokenBindingChange
+FNDA:4,RuleMintAllowance._authorizeTokenBindingChange
FNF:4
FNH:4
LF:8
@@ -936,8 +246,8 @@ DA:62,6
FN:62,RuleMintAllowanceOwnable2Step._authorizeSetMintAllowance
FNDA:6,RuleMintAllowanceOwnable2Step._authorizeSetMintAllowance
DA:67,2
-FN:67,RuleMintAllowanceOwnable2Step._authorizeComplianceBindingChange
-FNDA:2,RuleMintAllowanceOwnable2Step._authorizeComplianceBindingChange
+FN:67,RuleMintAllowanceOwnable2Step._authorizeTokenBindingChange
+FNDA:2,RuleMintAllowanceOwnable2Step._authorizeTokenBindingChange
FNF:4
FNH:4
LF:8
@@ -951,203 +261,208 @@ DA:22,4
FN:22,RuleConditionalTransferLightApprovalBase.onlyTransferApprover
FNDA:4,RuleConditionalTransferLightApprovalBase.onlyTransferApprover
DA:23,4
-DA:27,3
+DA:27,6
FN:27,RuleConditionalTransferLightApprovalBase.onlyTransferExecutor
-FNDA:3,RuleConditionalTransferLightApprovalBase.onlyTransferExecutor
-DA:28,3
-DA:40,3
+FNDA:6,RuleConditionalTransferLightApprovalBase.onlyTransferExecutor
+DA:28,6
+DA:40,6
FN:40,RuleConditionalTransferLightApprovalBase.transferred
-FNDA:3,RuleConditionalTransferLightApprovalBase.transferred
-DA:41,3
-DA:54,6211
+FNDA:6,RuleConditionalTransferLightApprovalBase.transferred
+DA:41,6
+DA:54,6242
FN:54,RuleConditionalTransferLightApprovalBase.approveTransfer
-FNDA:6211,RuleConditionalTransferLightApprovalBase.approveTransfer
-DA:55,6214
-DA:56,6214
-DA:57,6214
-DA:66,1371
-FN:66,RuleConditionalTransferLightApprovalBase.cancelTransferApproval
-FNDA:1371,RuleConditionalTransferLightApprovalBase.cancelTransferApproval
-DA:67,1370
-DA:68,1370
-DA:69,1370
-BRDA:69,0,0,1
-BRDA:69,0,1,1369
-DA:70,1369
-DA:71,1369
-DA:86,4
-FN:86,RuleConditionalTransferLightApprovalBase.resetApproval
+FNDA:6242,RuleConditionalTransferLightApprovalBase.approveTransfer
+DA:55,6249
+DA:56,6249
+DA:57,6249
+DA:58,6249
+DA:67,1376
+FN:67,RuleConditionalTransferLightApprovalBase.cancelTransferApproval
+FNDA:1376,RuleConditionalTransferLightApprovalBase.cancelTransferApproval
+DA:68,1375
+DA:69,1375
+DA:70,1375
+BRDA:70,0,0,1
+BRDA:70,0,1,1374
+DA:71,1374
+DA:72,1374
+DA:87,4
+FN:87,RuleConditionalTransferLightApprovalBase.resetApproval
FNDA:4,RuleConditionalTransferLightApprovalBase.resetApproval
-DA:92,3
DA:93,3
DA:94,3
-BRDA:94,1,0,1
-BRDA:94,1,1,2
-DA:95,2
+DA:95,3
+BRDA:95,1,0,1
+BRDA:95,1,1,2
DA:96,2
-DA:106,9207
-FN:106,RuleConditionalTransferLightApprovalBase.approvedCount
-FNDA:9207,RuleConditionalTransferLightApprovalBase.approvedCount
-DA:107,9207
-DA:108,9207
-DA:119,3
-FN:119,RuleConditionalTransferLightApprovalBase._transferredFromContext
-FNDA:3,RuleConditionalTransferLightApprovalBase._transferredFromContext
-DA:120,3
-DA:130,6258
-FN:130,RuleConditionalTransferLightApprovalBase._transferred
-FNDA:6258,RuleConditionalTransferLightApprovalBase._transferred
-DA:131,6258
-BRDA:131,2,0,6258
-DA:132,6258
-DA:134,2217
-DA:135,2217
-DA:137,2217
-BRDA:137,3,0,5
-BRDA:137,3,1,2212
-DA:139,2212
-DA:140,2212
-DA:150,19020
-FN:150,RuleConditionalTransferLightApprovalBase._transferHash
-FNDA:19020,RuleConditionalTransferLightApprovalBase._transferHash
-DA:153,19020
-DA:154,19020
-DA:155,19020
-DA:156,19020
-DA:157,19020
-DA:164,0
-FN:164,RuleConditionalTransferLightApprovalBase._authorizeTransferApproval
+DA:97,2
+DA:107,9216
+FN:107,RuleConditionalTransferLightApprovalBase.approvedCount
+FNDA:9216,RuleConditionalTransferLightApprovalBase.approvedCount
+DA:108,9231
+DA:109,9231
+DA:120,6
+FN:120,RuleConditionalTransferLightApprovalBase._transferredFromContext
+FNDA:6,RuleConditionalTransferLightApprovalBase._transferredFromContext
+DA:121,6
+DA:131,6291
+FN:131,RuleConditionalTransferLightApprovalBase._transferred
+FNDA:6291,RuleConditionalTransferLightApprovalBase._transferred
+DA:132,6291
+BRDA:132,2,0,6291
+DA:133,6291
+DA:135,2246
+DA:136,2246
+DA:138,2246
+BRDA:138,3,0,5
+BRDA:138,3,1,2241
+DA:140,2241
+DA:141,2241
+DA:163,19113
+FN:163,RuleConditionalTransferLightApprovalBase._transferHash
+FNDA:19113,RuleConditionalTransferLightApprovalBase._transferHash
+DA:168,19113
+DA:169,19113
+DA:170,19113
+DA:171,19113
+DA:172,19113
+DA:179,0
+FN:179,RuleConditionalTransferLightApprovalBase._authorizeTransferApproval
FNDA:0,RuleConditionalTransferLightApprovalBase._authorizeTransferApproval
-DA:169,0
-FN:169,RuleConditionalTransferLightApprovalBase._authorizeTransferExecution
+DA:184,0
+FN:184,RuleConditionalTransferLightApprovalBase._authorizeTransferExecution
FNDA:0,RuleConditionalTransferLightApprovalBase._authorizeTransferExecution
FNF:12
FNH:10
-LF:43
-LH:41
+LF:44
+LH:42
BRF:7
BRH:7
end_of_record
TN:
SF:src/rules/operation/abstract/RuleConditionalTransferLightBase.sol
-DA:59,1
-FN:59,RuleConditionalTransferLightBase.created
+DA:61,1
+FN:61,RuleConditionalTransferLightBase.created
FNDA:1,RuleConditionalTransferLightBase.created
-DA:60,1
-DA:68,1
-FN:68,RuleConditionalTransferLightBase.destroyed
+DA:62,1
+DA:70,1
+FN:70,RuleConditionalTransferLightBase.destroyed
FNDA:1,RuleConditionalTransferLightBase.destroyed
-DA:69,1
-DA:75,1
-FN:75,RuleConditionalTransferLightBase.canReturnTransferRestrictionCode
+DA:71,1
+DA:77,1
+FN:77,RuleConditionalTransferLightBase.canReturnTransferRestrictionCode
FNDA:1,RuleConditionalTransferLightBase.canReturnTransferRestrictionCode
-DA:76,1
-DA:82,2
-FN:82,RuleConditionalTransferLightBase.messageForTransferRestriction
+DA:78,1
+DA:84,2
+FN:84,RuleConditionalTransferLightBase.messageForTransferRestriction
FNDA:2,RuleConditionalTransferLightBase.messageForTransferRestriction
-DA:88,2
-BRDA:88,0,0,1
-DA:89,1
+DA:90,2
+BRDA:90,0,0,1
DA:91,1
-DA:113,6
-FN:113,RuleConditionalTransferLightBase.approveAndTransferIfAllowed
-FNDA:6,RuleConditionalTransferLightBase.approveAndTransferIfAllowed
-DA:118,6
-DA:119,6
-BRDA:119,1,0,1
-BRDA:119,1,1,5
-DA:121,5
-DA:123,5
-DA:124,4
-BRDA:124,2,0,1
-BRDA:124,2,1,3
-DA:126,3
-DA:127,2
-DA:133,6252
-FN:133,RuleConditionalTransferLightBase.transferred.0
-FNDA:6252,RuleConditionalTransferLightBase.transferred.0
-DA:138,6247
-DA:144,7
-FN:144,RuleConditionalTransferLightBase.transferred.1
+DA:93,1
+DA:115,10
+FN:115,RuleConditionalTransferLightBase.approveAndTransferIfAllowed
+FNDA:10,RuleConditionalTransferLightBase.approveAndTransferIfAllowed
+DA:121,10
+DA:122,10
+BRDA:122,1,0,1
+BRDA:122,1,1,9
+DA:124,9
+DA:125,9
+DA:127,9
+DA:128,8
+BRDA:128,2,0,1
+BRDA:128,2,1,7
+DA:130,7
+DA:136,6
+BRDA:136,3,0,1
+BRDA:136,3,1,5
+DA:140,5
+DA:146,6283
+FN:146,RuleConditionalTransferLightBase.transferred.0
+FNDA:6283,RuleConditionalTransferLightBase.transferred.0
+DA:152,6277
+DA:158,7
+FN:158,RuleConditionalTransferLightBase.transferred.1
FNDA:7,RuleConditionalTransferLightBase.transferred.1
-DA:155,6
-DA:177,45
-FN:177,RuleConditionalTransferLightBase.bindToken
-FNDA:45,RuleConditionalTransferLightBase.bindToken
-DA:178,44
-BRDA:178,3,0,1
-BRDA:178,3,1,43
-DA:179,43
-DA:208,13
-FN:208,RuleConditionalTransferLightBase.bindRuleEngine
+DA:170,6
+DA:192,58
+FN:192,RuleConditionalTransferLightBase.bindToken
+FNDA:58,RuleConditionalTransferLightBase.bindToken
+DA:198,57
+BRDA:198,4,0,1
+BRDA:198,4,1,56
+DA:199,56
+DA:218,13
+FN:218,RuleConditionalTransferLightBase.bindRuleEngine
FNDA:13,RuleConditionalTransferLightBase.bindRuleEngine
-DA:209,12
-BRDA:209,4,0,1
-BRDA:209,4,1,11
-DA:210,11
-BRDA:210,5,0,1
-BRDA:210,5,1,10
-DA:211,10
-DA:212,10
-DA:219,3
-FN:219,RuleConditionalTransferLightBase.unbindRuleEngine
+DA:219,12
+BRDA:219,5,0,1
+BRDA:219,5,1,11
+DA:220,11
+BRDA:220,6,0,1
+BRDA:220,6,1,10
+DA:221,10
+DA:222,10
+DA:229,3
+FN:229,RuleConditionalTransferLightBase.unbindRuleEngine
FNDA:3,RuleConditionalTransferLightBase.unbindRuleEngine
-DA:220,2
-DA:221,2
-BRDA:221,6,0,1
-BRDA:221,6,1,1
-DA:222,1
-DA:223,1
-DA:231,8
-FN:231,RuleConditionalTransferLightBase.isTransferExecutor
+DA:230,2
+DA:231,2
+BRDA:231,7,0,1
+BRDA:231,7,1,1
+DA:232,1
+DA:233,1
+DA:241,8
+FN:241,RuleConditionalTransferLightBase.isTransferExecutor
FNDA:8,RuleConditionalTransferLightBase.isTransferExecutor
-DA:232,6270
-DA:238,7
-FN:238,RuleConditionalTransferLightBase.detectTransferRestriction
+DA:242,6301
+DA:248,7
+FN:248,RuleConditionalTransferLightBase.detectTransferRestriction
FNDA:7,RuleConditionalTransferLightBase.detectTransferRestriction
-DA:244,13
-BRDA:244,7,0,4
-DA:245,4
-DA:247,9
-DA:248,9
-BRDA:248,8,0,6
-DA:249,6
-DA:251,3
-DA:257,1
-FN:257,RuleConditionalTransferLightBase.detectTransferRestrictionFrom
+DA:254,13
+BRDA:254,8,0,4
+DA:255,4
+DA:257,9
+DA:258,9
+BRDA:258,9,0,6
+DA:259,6
+DA:261,3
+DA:267,1
+FN:267,RuleConditionalTransferLightBase.detectTransferRestrictionFrom
FNDA:1,RuleConditionalTransferLightBase.detectTransferRestrictionFrom
-DA:269,2
-DA:275,4
-FN:275,RuleConditionalTransferLightBase.canTransfer
+DA:279,2
+DA:285,4
+FN:285,RuleConditionalTransferLightBase.canTransfer
FNDA:4,RuleConditionalTransferLightBase.canTransfer
-DA:281,4
-DA:287,1
-FN:287,RuleConditionalTransferLightBase.canTransferFrom
+DA:291,4
+DA:297,1
+FN:297,RuleConditionalTransferLightBase.canTransferFrom
FNDA:1,RuleConditionalTransferLightBase.canTransferFrom
-DA:293,1
-DA:306,6262
-FN:306,RuleConditionalTransferLightBase._authorizeTransferExecution
-FNDA:6262,RuleConditionalTransferLightBase._authorizeTransferExecution
-DA:307,6262
-BRDA:307,9,0,6
-BRDA:307,9,1,6256
+DA:303,1
+DA:316,6293
+FN:316,RuleConditionalTransferLightBase._authorizeTransferExecution
+FNDA:6293,RuleConditionalTransferLightBase._authorizeTransferExecution
+DA:317,6293
+BRDA:317,10,0,6
+BRDA:317,10,1,6287
FNF:16
FNH:16
-LF:52
-LH:52
-BRF:17
-BRH:17
+LF:54
+LH:54
+BRF:19
+BRH:19
end_of_record
TN:
SF:src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol
-DA:34,24
+DA:34,33
FN:34,RuleConditionalTransferLightMultiTokenBase.onlyTransferApprover
-FNDA:24,RuleConditionalTransferLightMultiTokenBase.onlyTransferApprover
-DA:35,24
-DA:39,3
+FNDA:33,RuleConditionalTransferLightMultiTokenBase.onlyTransferApprover
+DA:35,33
+DA:39,4
FN:39,RuleConditionalTransferLightMultiTokenBase.onlyTransferExecutor
-FNDA:3,RuleConditionalTransferLightMultiTokenBase.onlyTransferExecutor
-DA:40,3
+FNDA:4,RuleConditionalTransferLightMultiTokenBase.onlyTransferExecutor
+DA:40,4
DA:49,2
FN:49,RuleConditionalTransferLightMultiTokenBase.created
FNDA:2,RuleConditionalTransferLightMultiTokenBase.created
@@ -1156,10 +471,10 @@ DA:58,2
FN:58,RuleConditionalTransferLightMultiTokenBase.destroyed
FNDA:2,RuleConditionalTransferLightMultiTokenBase.destroyed
DA:59,1
-DA:66,3
+DA:66,4
FN:66,RuleConditionalTransferLightMultiTokenBase.transferred.0
-FNDA:3,RuleConditionalTransferLightMultiTokenBase.transferred.0
-DA:67,3
+FNDA:4,RuleConditionalTransferLightMultiTokenBase.transferred.0
+DA:67,4
DA:73,2
FN:73,RuleConditionalTransferLightMultiTokenBase.canReturnTransferRestrictionCode
FNDA:2,RuleConditionalTransferLightMultiTokenBase.canReturnTransferRestrictionCode
@@ -1171,260 +486,261 @@ DA:86,2
BRDA:86,0,0,1
DA:87,1
DA:89,1
-DA:99,24
+DA:99,33
FN:99,RuleConditionalTransferLightMultiTokenBase.approveTransfer
-FNDA:24,RuleConditionalTransferLightMultiTokenBase.approveTransfer
-DA:100,24
-DA:110,3
-FN:110,RuleConditionalTransferLightMultiTokenBase.cancelTransferApproval
-FNDA:3,RuleConditionalTransferLightMultiTokenBase.cancelTransferApproval
-DA:114,2
-DA:126,1
-FN:126,RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed
-FNDA:1,RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed
-DA:131,1
-BRDA:131,1,0,-
-BRDA:131,1,1,1
-DA:133,1
-DA:135,1
-DA:136,1
-BRDA:136,2,0,-
-BRDA:136,2,1,1
-DA:140,1
-DA:141,1
-DA:147,4
-FN:147,RuleConditionalTransferLightMultiTokenBase.transferred.1
-FNDA:4,RuleConditionalTransferLightMultiTokenBase.transferred.1
-DA:152,4
-DA:158,6
-FN:158,RuleConditionalTransferLightMultiTokenBase.transferred.2
+FNDA:33,RuleConditionalTransferLightMultiTokenBase.approveTransfer
+DA:104,32
+DA:114,4
+FN:114,RuleConditionalTransferLightMultiTokenBase.cancelTransferApproval
+FNDA:4,RuleConditionalTransferLightMultiTokenBase.cancelTransferApproval
+DA:119,3
+DA:131,5
+FN:131,RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed
+FNDA:5,RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed
+DA:137,5
+BRDA:137,1,0,1
+BRDA:137,1,1,4
+DA:139,4
+DA:140,4
+DA:142,4
+DA:143,4
+BRDA:143,2,0,1
+BRDA:143,2,1,3
+DA:147,3
+DA:152,3
+BRDA:152,3,0,1
+BRDA:152,3,1,2
+DA:156,2
+DA:162,5
+FN:162,RuleConditionalTransferLightMultiTokenBase.transferred.1
+FNDA:5,RuleConditionalTransferLightMultiTokenBase.transferred.1
+DA:168,4
+DA:174,6
+FN:174,RuleConditionalTransferLightMultiTokenBase.transferred.2
FNDA:6,RuleConditionalTransferLightMultiTokenBase.transferred.2
-DA:169,6
-DA:187,3
-FN:187,RuleConditionalTransferLightMultiTokenBase.resetApproval
+DA:186,6
+DA:204,3
+FN:204,RuleConditionalTransferLightMultiTokenBase.resetApproval
FNDA:3,RuleConditionalTransferLightMultiTokenBase.resetApproval
-DA:193,2
-DA:194,2
-DA:195,2
-BRDA:195,3,0,1
-BRDA:195,3,1,1
-DA:196,1
-DA:197,1
-DA:208,16
-FN:208,RuleConditionalTransferLightMultiTokenBase.approvedCount
-FNDA:16,RuleConditionalTransferLightMultiTokenBase.approvedCount
-DA:209,16
-DA:210,16
-DA:222,265
-FN:222,RuleConditionalTransferLightMultiTokenBase.detectTransferRestriction
+DA:210,2
+DA:211,2
+DA:212,2
+BRDA:212,4,0,1
+BRDA:212,4,1,1
+DA:213,1
+DA:214,1
+DA:225,24
+FN:225,RuleConditionalTransferLightMultiTokenBase.approvedCount
+FNDA:24,RuleConditionalTransferLightMultiTokenBase.approvedCount
+DA:226,31
+DA:227,31
+DA:239,265
+FN:239,RuleConditionalTransferLightMultiTokenBase.detectTransferRestriction
FNDA:265,RuleConditionalTransferLightMultiTokenBase.detectTransferRestriction
-DA:228,270
-DA:242,263
-FN:242,RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionForToken
+DA:245,270
+DA:259,263
+FN:259,RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionForToken
FNDA:263,RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionForToken
-DA:248,263
-DA:261,5
-FN:261,RuleConditionalTransferLightMultiTokenBase.canTransferForToken
+DA:265,263
+DA:278,5
+FN:278,RuleConditionalTransferLightMultiTokenBase.canTransferForToken
FNDA:5,RuleConditionalTransferLightMultiTokenBase.canTransferForToken
-DA:267,5
-DA:274,2
-FN:274,RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionFrom
+DA:284,5
+DA:291,2
+FN:291,RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionFrom
FNDA:2,RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionFrom
-DA:286,4
-DA:294,1
-FN:294,RuleConditionalTransferLightMultiTokenBase.canTransfer
+DA:303,4
+DA:311,1
+FN:311,RuleConditionalTransferLightMultiTokenBase.canTransfer
FNDA:1,RuleConditionalTransferLightMultiTokenBase.canTransfer
-DA:300,1
-DA:306,2
-FN:306,RuleConditionalTransferLightMultiTokenBase.canTransferFrom
+DA:317,1
+DA:323,2
+FN:323,RuleConditionalTransferLightMultiTokenBase.canTransferFrom
FNDA:2,RuleConditionalTransferLightMultiTokenBase.canTransferFrom
-DA:312,2
-DA:327,38
-FN:327,RuleConditionalTransferLightMultiTokenBase._authorizeComplianceBindingChange
-FNDA:38,RuleConditionalTransferLightMultiTokenBase._authorizeComplianceBindingChange
-DA:328,38
-DA:338,25
-FN:338,RuleConditionalTransferLightMultiTokenBase._approveTransfer
-FNDA:25,RuleConditionalTransferLightMultiTokenBase._approveTransfer
-DA:339,25
-BRDA:339,4,0,2
-BRDA:339,4,1,23
-DA:340,23
-DA:341,23
-DA:342,23
-DA:352,2
-FN:352,RuleConditionalTransferLightMultiTokenBase._cancelTransferApproval
-FNDA:2,RuleConditionalTransferLightMultiTokenBase._cancelTransferApproval
-DA:353,2
-BRDA:353,5,0,-
-BRDA:353,5,1,2
-DA:354,2
-DA:355,2
+DA:329,2
+DA:340,36
+FN:340,RuleConditionalTransferLightMultiTokenBase._approveTransfer
+FNDA:36,RuleConditionalTransferLightMultiTokenBase._approveTransfer
+DA:341,36
+BRDA:341,5,0,2
+BRDA:341,5,1,34
+DA:342,34
+DA:343,34
+DA:344,34
+DA:345,34
+DA:355,3
+FN:355,RuleConditionalTransferLightMultiTokenBase._cancelTransferApproval
+FNDA:3,RuleConditionalTransferLightMultiTokenBase._cancelTransferApproval
+DA:356,3
+BRDA:356,6,0,1
+BRDA:356,6,1,2
DA:357,2
-BRDA:357,6,0,1
-BRDA:357,6,1,1
-DA:359,1
-DA:360,1
-DA:371,15
-FN:371,RuleConditionalTransferLightMultiTokenBase._transferred
-FNDA:15,RuleConditionalTransferLightMultiTokenBase._transferred
-DA:372,15
-BRDA:372,7,0,15
-DA:373,15
-DA:376,9
-DA:377,9
-DA:379,9
-BRDA:379,8,0,3
-BRDA:379,8,1,6
-DA:381,6
-DA:382,6
-DA:397,538
-FN:397,RuleConditionalTransferLightMultiTokenBase._detectTransferRestrictionForToken
+DA:358,2
+DA:360,2
+BRDA:360,7,0,1
+BRDA:360,7,1,1
+DA:362,1
+DA:363,1
+DA:374,16
+FN:374,RuleConditionalTransferLightMultiTokenBase._transferred
+FNDA:16,RuleConditionalTransferLightMultiTokenBase._transferred
+DA:375,16
+BRDA:375,8,0,16
+DA:376,16
+DA:379,10
+DA:380,10
+DA:382,10
+BRDA:382,9,0,3
+BRDA:382,9,1,7
+DA:384,7
+DA:385,7
+DA:400,538
+FN:400,RuleConditionalTransferLightMultiTokenBase._detectTransferRestrictionForToken
FNDA:538,RuleConditionalTransferLightMultiTokenBase._detectTransferRestrictionForToken
-DA:403,538
-BRDA:403,9,0,2
-DA:404,2
-DA:407,536
-BRDA:407,10,0,7
-DA:408,7
-DA:411,529
-BRDA:411,11,0,519
-DA:412,519
-DA:415,10
-DA:421,13
-FN:421,RuleConditionalTransferLightMultiTokenBase._authorizeTransferExecution
-FNDA:13,RuleConditionalTransferLightMultiTokenBase._authorizeTransferExecution
-DA:422,13
-BRDA:422,12,0,-
-BRDA:422,12,1,13
-DA:431,0
-FN:431,RuleConditionalTransferLightMultiTokenBase._authorizeTransferApproval
+DA:406,538
+BRDA:406,10,0,2
+DA:407,2
+DA:410,536
+BRDA:410,11,0,7
+DA:411,7
+DA:414,529
+BRDA:414,12,0,519
+DA:415,519
+DA:418,10
+DA:424,15
+FN:424,RuleConditionalTransferLightMultiTokenBase._authorizeTransferExecution
+FNDA:15,RuleConditionalTransferLightMultiTokenBase._authorizeTransferExecution
+DA:425,15
+BRDA:425,13,0,1
+BRDA:425,13,1,14
+DA:434,0
+FN:434,RuleConditionalTransferLightMultiTokenBase._authorizeTransferApproval
FNDA:0,RuleConditionalTransferLightMultiTokenBase._authorizeTransferApproval
-DA:441,581
-FN:441,RuleConditionalTransferLightMultiTokenBase._transferHash
-FNDA:581,RuleConditionalTransferLightMultiTokenBase._transferHash
-DA:448,581
-DA:449,581
-DA:450,581
-DA:451,581
-DA:452,581
-DA:453,581
-FNF:28
-FNH:27
-LF:92
-LH:91
-BRF:21
-BRH:17
+DA:450,608
+FN:450,RuleConditionalTransferLightMultiTokenBase._transferHash
+FNDA:608,RuleConditionalTransferLightMultiTokenBase._transferHash
+DA:457,608
+DA:458,608
+DA:459,608
+DA:460,608
+DA:461,608
+DA:462,608
+FNF:27
+FNH:26
+LF:93
+LH:92
+BRF:23
+BRH:23
end_of_record
TN:
SF:src/rules/operation/abstract/RuleMintAllowanceBase.sol
-DA:51,4
-FN:51,RuleMintAllowanceBase.onlyAllowanceOperator
+DA:53,4
+FN:53,RuleMintAllowanceBase.onlyAllowanceOperator
FNDA:4,RuleMintAllowanceBase.onlyAllowanceOperator
-DA:52,4
-DA:63,1
-FN:63,RuleMintAllowanceBase.created
+DA:54,4
+DA:65,1
+FN:65,RuleMintAllowanceBase.created
FNDA:1,RuleMintAllowanceBase.created
-DA:68,1
-FN:68,RuleMintAllowanceBase.destroyed
+DA:70,1
+FN:70,RuleMintAllowanceBase.destroyed
FNDA:1,RuleMintAllowanceBase.destroyed
-DA:73,2
-FN:73,RuleMintAllowanceBase.canReturnTransferRestrictionCode
+DA:75,2
+FN:75,RuleMintAllowanceBase.canReturnTransferRestrictionCode
FNDA:2,RuleMintAllowanceBase.canReturnTransferRestrictionCode
-DA:74,2
-DA:86,3753
-FN:86,RuleMintAllowanceBase.setMintAllowance
+DA:76,2
+DA:88,3753
+FN:88,RuleMintAllowanceBase.setMintAllowance
FNDA:3753,RuleMintAllowanceBase.setMintAllowance
-DA:87,3751
-DA:95,3241
-FN:95,RuleMintAllowanceBase.increaseMintAllowance
+DA:89,3751
+DA:97,3241
+FN:97,RuleMintAllowanceBase.increaseMintAllowance
FNDA:3241,RuleMintAllowanceBase.increaseMintAllowance
-DA:96,3240
-DA:97,3240
DA:98,3240
-DA:107,3095
-FN:107,RuleMintAllowanceBase.decreaseMintAllowance
-FNDA:3095,RuleMintAllowanceBase.decreaseMintAllowance
-DA:108,3094
-DA:109,3094
-BRDA:109,0,0,1
-BRDA:109,0,1,3093
-DA:110,3093
-DA:111,3093
-DA:112,3093
-DA:124,4
-FN:124,RuleMintAllowanceBase.clearMintAllowances
+DA:99,3240
+DA:100,3240
+DA:109,3097
+FN:109,RuleMintAllowanceBase.decreaseMintAllowance
+FNDA:3097,RuleMintAllowanceBase.decreaseMintAllowance
+DA:110,3096
+DA:111,3096
+BRDA:111,0,0,1
+BRDA:111,0,1,3095
+DA:112,3095
+DA:113,3095
+DA:114,3095
+DA:126,4
+FN:126,RuleMintAllowanceBase.clearMintAllowances
FNDA:4,RuleMintAllowanceBase.clearMintAllowances
-DA:125,3
-DA:126,7
-DA:142,323
-FN:142,RuleMintAllowanceBase.bindToken
-FNDA:323,RuleMintAllowanceBase.bindToken
-DA:143,321
-BRDA:143,1,0,2
-BRDA:143,1,1,319
-DA:144,319
-DA:154,3
-FN:154,RuleMintAllowanceBase.transferred.0
+DA:127,3
+DA:128,7
+DA:144,324
+FN:144,RuleMintAllowanceBase.bindToken
+FNDA:324,RuleMintAllowanceBase.bindToken
+DA:150,322
+BRDA:150,1,0,2
+BRDA:150,1,1,320
+DA:151,320
+DA:161,3
+FN:161,RuleMintAllowanceBase.transferred.0
FNDA:3,RuleMintAllowanceBase.transferred.0
-DA:160,2
-DA:171,6849
-FN:171,RuleMintAllowanceBase.transferred.1
-FNDA:6849,RuleMintAllowanceBase.transferred.1
-DA:177,6848
-DA:183,2
-FN:183,RuleMintAllowanceBase.messageForTransferRestriction
+DA:167,2
+DA:178,6837
+FN:178,RuleMintAllowanceBase.transferred.1
+FNDA:6837,RuleMintAllowanceBase.transferred.1
+DA:184,6836
+DA:190,2
+FN:190,RuleMintAllowanceBase.messageForTransferRestriction
FNDA:2,RuleMintAllowanceBase.messageForTransferRestriction
-DA:189,2
-BRDA:189,2,0,1
-DA:190,1
-DA:192,1
-DA:200,5
-FN:200,RuleMintAllowanceBase.detectTransferRestriction
-FNDA:5,RuleMintAllowanceBase.detectTransferRestriction
-DA:207,5
-DA:213,9
-FN:213,RuleMintAllowanceBase.detectTransferRestrictionFrom
-FNDA:9,RuleMintAllowanceBase.detectTransferRestrictionFrom
-DA:220,15
-DA:227,3
-FN:227,RuleMintAllowanceBase.canTransfer
-FNDA:3,RuleMintAllowanceBase.canTransfer
+DA:196,2
+BRDA:196,2,0,1
+DA:197,1
+DA:199,1
+DA:207,9
+FN:207,RuleMintAllowanceBase.detectTransferRestriction
+FNDA:9,RuleMintAllowanceBase.detectTransferRestriction
+DA:214,9
+DA:220,12
+FN:220,RuleMintAllowanceBase.detectTransferRestrictionFrom
+FNDA:12,RuleMintAllowanceBase.detectTransferRestrictionFrom
+DA:227,18
DA:234,3
-DA:240,6
-FN:240,RuleMintAllowanceBase.canTransferFrom
-FNDA:6,RuleMintAllowanceBase.canTransferFrom
+FN:234,RuleMintAllowanceBase.canTransfer
+FNDA:3,RuleMintAllowanceBase.canTransfer
+DA:241,3
DA:247,6
-DA:258,2
-FN:258,RuleMintAllowanceBase._transferred
+FN:247,RuleMintAllowanceBase.canTransferFrom
+FNDA:6,RuleMintAllowanceBase.canTransferFrom
+DA:254,6
+DA:265,2
+FN:265,RuleMintAllowanceBase._transferred
FNDA:2,RuleMintAllowanceBase._transferred
-DA:270,6848
-FN:270,RuleMintAllowanceBase._transferredFrom
-FNDA:6848,RuleMintAllowanceBase._transferredFrom
-DA:271,6848
-BRDA:271,3,0,6848
-DA:272,6848
-DA:274,3577
-DA:275,3577
-BRDA:275,4,0,258
-BRDA:275,4,1,3319
-DA:276,3319
-DA:277,3319
-DA:278,3319
-DA:286,3758
-FN:286,RuleMintAllowanceBase._setMintAllowance
+DA:277,6836
+FN:277,RuleMintAllowanceBase._transferredFrom
+FNDA:6836,RuleMintAllowanceBase._transferredFrom
+DA:278,6836
+BRDA:278,3,0,6836
+DA:279,6836
+DA:281,3565
+DA:282,3565
+BRDA:282,4,0,249
+BRDA:282,4,1,3316
+DA:283,3316
+DA:284,3316
+DA:285,3316
+DA:293,3758
+FN:293,RuleMintAllowanceBase._setMintAllowance
FNDA:3758,RuleMintAllowanceBase._setMintAllowance
-DA:287,3758
-DA:288,3758
-DA:299,15
-FN:299,RuleMintAllowanceBase._detectTransferRestrictionFrom
-FNDA:15,RuleMintAllowanceBase._detectTransferRestrictionFrom
-DA:305,15
-BRDA:305,5,0,7
-DA:306,7
-DA:308,8
-DA:314,0
-FN:314,RuleMintAllowanceBase._authorizeSetMintAllowance
+DA:294,3758
+DA:295,3758
+DA:306,18
+FN:306,RuleMintAllowanceBase._detectTransferRestrictionFrom
+FNDA:18,RuleMintAllowanceBase._detectTransferRestrictionFrom
+DA:312,18
+BRDA:312,5,0,10
+DA:313,10
+DA:315,8
+DA:321,0
+FN:321,RuleMintAllowanceBase._authorizeSetMintAllowance
FNDA:0,RuleMintAllowanceBase._authorizeSetMintAllowance
FNF:21
FNH:20
@@ -1434,556 +750,437 @@ BRF:9
BRH:9
end_of_record
TN:
+SF:src/rules/validation/abstract/RuleAddressSet/AddressSetBatchLib.sol
+DA:39,556
+FN:39,AddressSetBatchLib.addBatch
+FNDA:556,AddressSetBatchLib.addBatch
+DA:44,556
+DA:45,1628
+DA:46,1622
+BRDA:46,0,0,1135
+BRDA:46,0,1,487
+DA:47,1135
+DA:49,487
+DA:63,270
+FN:63,AddressSetBatchLib.removeBatch
+FNDA:270,AddressSetBatchLib.removeBatch
+DA:67,270
+DA:68,794
+BRDA:68,1,0,531
+BRDA:68,1,1,263
+DA:69,531
+DA:71,263
+FNF:2
+FNH:2
+LF:11
+LH:11
+BRF:4
+BRH:4
+end_of_record
+TN:
SF:src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol
-DA:40,279
-FN:40,RuleAddressSet.onlyAddressListAdd
-FNDA:279,RuleAddressSet.onlyAddressListAdd
-DA:41,279
-DA:45,17
-FN:45,RuleAddressSet.onlyAddressListRemove
-FNDA:17,RuleAddressSet.onlyAddressListRemove
-DA:46,17
-DA:61,279
-FN:61,RuleAddressSet.addAddresses
-FNDA:279,RuleAddressSet.addAddresses
-DA:62,278
-DA:63,276
-DA:73,261
-FN:73,RuleAddressSet.removeAddresses
-FNDA:261,RuleAddressSet.removeAddresses
-DA:74,260
-DA:75,260
-DA:85,182
-FN:85,RuleAddressSet.addAddress
-FNDA:182,RuleAddressSet.addAddress
-DA:86,175
-BRDA:86,0,0,3
-BRDA:86,0,1,172
-DA:87,172
-BRDA:87,1,0,1
-BRDA:87,1,1,171
-DA:88,171
-DA:89,171
-DA:99,17
-FN:99,RuleAddressSet.removeAddress
-FNDA:17,RuleAddressSet.removeAddress
-DA:100,10
-BRDA:100,2,0,1
-BRDA:100,2,1,9
-DA:101,9
-DA:102,9
-DA:109,545
-FN:109,RuleAddressSet.listedAddressCount
-FNDA:545,RuleAddressSet.listedAddressCount
-DA:110,545
-DA:118,4
-FN:118,RuleAddressSet.contains
+DA:42,539
+FN:42,RuleAddressSet.onlyAddressListAdd
+FNDA:539,RuleAddressSet.onlyAddressListAdd
+DA:43,539
+DA:47,18
+FN:47,RuleAddressSet.onlyAddressListRemove
+FNDA:18,RuleAddressSet.onlyAddressListRemove
+DA:48,18
+DA:66,539
+FN:66,RuleAddressSet.addAddresses
+FNDA:539,RuleAddressSet.addAddresses
+DA:67,538
+DA:68,536
+DA:78,262
+FN:78,RuleAddressSet.removeAddresses
+FNDA:262,RuleAddressSet.removeAddresses
+DA:79,261
+DA:80,261
+DA:90,422
+FN:90,RuleAddressSet.addAddress
+FNDA:422,RuleAddressSet.addAddress
+DA:91,415
+BRDA:91,0,0,3
+BRDA:91,0,1,412
+DA:92,412
+BRDA:92,1,0,1
+BRDA:92,1,1,411
+DA:93,411
+DA:103,18
+FN:103,RuleAddressSet.removeAddress
+FNDA:18,RuleAddressSet.removeAddress
+DA:104,11
+BRDA:104,2,0,1
+BRDA:104,2,1,10
+DA:105,10
+DA:112,548
+FN:112,RuleAddressSet.listedAddressCount
+FNDA:548,RuleAddressSet.listedAddressCount
+DA:113,548
+DA:121,4
+FN:121,RuleAddressSet.contains
FNDA:4,RuleAddressSet.contains
-DA:119,4
-DA:127,86
-FN:127,RuleAddressSet.isAddressListed
-FNDA:86,RuleAddressSet.isAddressListed
-DA:128,623
-DA:136,157
-FN:136,RuleAddressSet.areAddressesListed
-FNDA:157,RuleAddressSet.areAddressesListed
-DA:137,157
-DA:138,157
-DA:139,349
-DA:150,0
-FN:150,RuleAddressSet._authorizeAddressListAdd
+DA:122,4
+DA:130,89
+FN:130,RuleAddressSet.isAddressListed
+FNDA:89,RuleAddressSet.isAddressListed
+DA:131,876
+DA:139,179
+FN:139,RuleAddressSet.areAddressesListed
+FNDA:179,RuleAddressSet.areAddressesListed
+DA:140,179
+DA:141,179
+DA:142,395
+DA:153,0
+FN:153,RuleAddressSet._authorizeAddressListAdd
FNDA:0,RuleAddressSet._authorizeAddressListAdd
-DA:155,0
-FN:155,RuleAddressSet._authorizeAddressListRemove
+DA:158,0
+FN:158,RuleAddressSet._authorizeAddressListRemove
FNDA:0,RuleAddressSet._authorizeAddressListRemove
-DA:160,1127
-FN:160,RuleAddressSet._msgSender
-FNDA:1127,RuleAddressSet._msgSender
-DA:161,1127
-DA:167,8
-FN:167,RuleAddressSet._msgData
+DA:163,1693
+FN:163,RuleAddressSet._msgSender
+FNDA:1693,RuleAddressSet._msgSender
+DA:164,1693
+DA:170,8
+FN:170,RuleAddressSet._msgData
FNDA:8,RuleAddressSet._msgData
-DA:168,8
-DA:174,1139
-FN:174,RuleAddressSet._contextSuffixLength
-FNDA:1139,RuleAddressSet._contextSuffixLength
-DA:175,1139
+DA:171,8
+DA:177,1705
+FN:177,RuleAddressSet._contextSuffixLength
+FNDA:1705,RuleAddressSet._contextSuffixLength
+DA:178,1705
FNF:15
FNH:13
-LF:37
-LH:35
+LF:35
+LH:33
BRF:6
BRH:6
end_of_record
TN:
SF:src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol
-DA:41,278
-FN:41,RuleAddressSetInternal._addAddresses
-FNDA:278,RuleAddressSetInternal._addAddresses
-DA:42,278
-DA:49,815
-BRDA:49,0,0,2
-BRDA:49,0,1,813
-DA:50,813
-BRDA:50,1,0,553
-BRDA:50,1,1,260
-DA:51,553
-DA:53,260
-DA:67,260
-FN:67,RuleAddressSetInternal._removeAddresses
-FNDA:260,RuleAddressSetInternal._removeAddresses
-DA:71,260
-DA:72,777
-BRDA:72,2,0,520
-BRDA:72,2,1,257
-DA:73,520
-DA:75,257
-DA:84,245
-FN:84,RuleAddressSetInternal._addAddress
-FNDA:245,RuleAddressSetInternal._addAddress
-DA:85,245
-DA:92,14
-FN:92,RuleAddressSetInternal._removeAddress
-FNDA:14,RuleAddressSetInternal._removeAddress
-DA:93,14
-DA:100,548
-FN:100,RuleAddressSetInternal._listedAddressCount
-FNDA:548,RuleAddressSetInternal._listedAddressCount
-DA:101,548
-DA:109,1345
-FN:109,RuleAddressSetInternal._isAddressListed
-FNDA:1345,RuleAddressSetInternal._isAddressListed
-DA:110,1345
-FNF:6
-FNH:6
-LF:19
-LH:19
-BRF:6
-BRH:6
+DA:44,543
+FN:44,RuleAddressSetInternal._addAddresses
+FNDA:543,RuleAddressSetInternal._addAddresses
+DA:49,543
+DA:64,1602
+FN:64,RuleAddressSetInternal._requireNotZeroAddress
+FNDA:1602,RuleAddressSetInternal._requireNotZeroAddress
+DA:65,1602
+BRDA:65,0,0,3
+BRDA:65,0,1,1599
+DA:77,263
+FN:77,RuleAddressSetInternal._removeAddresses
+FNDA:263,RuleAddressSetInternal._removeAddresses
+DA:82,263
+DA:93,513
+FN:93,RuleAddressSetInternal._addAddress
+FNDA:513,RuleAddressSetInternal._addAddress
+DA:94,513
+DA:103,24
+FN:103,RuleAddressSetInternal._removeAddress
+FNDA:24,RuleAddressSetInternal._removeAddress
+DA:104,24
+DA:111,557
+FN:111,RuleAddressSetInternal._listedAddressCount
+FNDA:557,RuleAddressSetInternal._listedAddressCount
+DA:112,557
+DA:120,1467
+FN:120,RuleAddressSetInternal._isAddressListed
+FNDA:1467,RuleAddressSetInternal._isAddressListed
+DA:121,1467
+FNF:7
+FNH:7
+LF:14
+LH:14
+BRF:2
+BRH:2
end_of_record
TN:
SF:src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol
-DA:44,4
-FN:44,RuleERC2980Internal._addWhitelistAddresses
-FNDA:4,RuleERC2980Internal._addWhitelistAddresses
-DA:48,4
-DA:51,6
-BRDA:51,0,0,-
-BRDA:51,0,1,6
-DA:52,6
-BRDA:52,1,0,5
-BRDA:52,1,1,1
-DA:53,5
-DA:55,1
-DA:66,3
-FN:66,RuleERC2980Internal._removeWhitelistAddresses
-FNDA:3,RuleERC2980Internal._removeWhitelistAddresses
-DA:70,3
-DA:71,3
-BRDA:71,2,0,2
-BRDA:71,2,1,1
-DA:72,2
-DA:74,1
-DA:83,44
-FN:83,RuleERC2980Internal._addWhitelistAddress
-FNDA:44,RuleERC2980Internal._addWhitelistAddress
-DA:84,44
-DA:91,4
-FN:91,RuleERC2980Internal._removeWhitelistAddress
-FNDA:4,RuleERC2980Internal._removeWhitelistAddress
-DA:92,4
-DA:105,4
-FN:105,RuleERC2980Internal._addFrozenlistAddresses
-FNDA:4,RuleERC2980Internal._addFrozenlistAddresses
-DA:109,4
-DA:112,6
-BRDA:112,3,0,-
-BRDA:112,3,1,6
-DA:113,6
-BRDA:113,4,0,5
-BRDA:113,4,1,1
-DA:114,5
-DA:116,1
-DA:127,2
-FN:127,RuleERC2980Internal._removeFrozenlistAddresses
-FNDA:2,RuleERC2980Internal._removeFrozenlistAddresses
-DA:131,2
-DA:132,2
-BRDA:132,5,0,1
-BRDA:132,5,1,1
-DA:133,1
-DA:135,1
-DA:144,19
-FN:144,RuleERC2980Internal._addFrozenlistAddress
-FNDA:19,RuleERC2980Internal._addFrozenlistAddress
-DA:145,19
-DA:152,4
-FN:152,RuleERC2980Internal._removeFrozenlistAddress
-FNDA:4,RuleERC2980Internal._removeFrozenlistAddress
-DA:153,4
-DA:165,115
-FN:165,RuleERC2980Internal._isWhitelisted
-FNDA:115,RuleERC2980Internal._isWhitelisted
-DA:166,115
-DA:173,5
-FN:173,RuleERC2980Internal._whitelistCount
+DA:47,7
+FN:47,RuleERC2980Internal._addWhitelistAddresses
+FNDA:7,RuleERC2980Internal._addWhitelistAddresses
+DA:52,7
+DA:61,4
+FN:61,RuleERC2980Internal._removeWhitelistAddresses
+FNDA:4,RuleERC2980Internal._removeWhitelistAddresses
+DA:66,4
+DA:73,51
+FN:73,RuleERC2980Internal._addWhitelistAddress
+FNDA:51,RuleERC2980Internal._addWhitelistAddress
+DA:74,51
+DA:81,5
+FN:81,RuleERC2980Internal._removeWhitelistAddress
+FNDA:5,RuleERC2980Internal._removeWhitelistAddress
+DA:82,5
+DA:96,6
+FN:96,RuleERC2980Internal._addFrozenlistAddresses
+FNDA:6,RuleERC2980Internal._addFrozenlistAddresses
+DA:101,6
+DA:110,3
+FN:110,RuleERC2980Internal._removeFrozenlistAddresses
+FNDA:3,RuleERC2980Internal._removeFrozenlistAddresses
+DA:115,3
+DA:122,22
+FN:122,RuleERC2980Internal._addFrozenlistAddress
+FNDA:22,RuleERC2980Internal._addFrozenlistAddress
+DA:123,22
+DA:130,5
+FN:130,RuleERC2980Internal._removeFrozenlistAddress
+FNDA:5,RuleERC2980Internal._removeFrozenlistAddress
+DA:131,5
+DA:142,25
+FN:142,RuleERC2980Internal._requireNotZeroAddress
+FNDA:25,RuleERC2980Internal._requireNotZeroAddress
+DA:143,25
+BRDA:143,0,0,2
+BRDA:143,0,1,23
+DA:155,80
+FN:155,RuleERC2980Internal._isWhitelisted
+FNDA:80,RuleERC2980Internal._isWhitelisted
+DA:156,80
+DA:163,5
+FN:163,RuleERC2980Internal._whitelistCount
FNDA:5,RuleERC2980Internal._whitelistCount
-DA:174,5
-DA:182,163
-FN:182,RuleERC2980Internal._isFrozen
-FNDA:163,RuleERC2980Internal._isFrozen
-DA:183,163
-DA:190,4
-FN:190,RuleERC2980Internal._frozenlistCount
+DA:164,5
+DA:172,165
+FN:172,RuleERC2980Internal._isFrozen
+FNDA:165,RuleERC2980Internal._isFrozen
+DA:173,165
+DA:180,4
+FN:180,RuleERC2980Internal._frozenlistCount
FNDA:4,RuleERC2980Internal._frozenlistCount
-DA:191,4
-FNF:12
-FNH:12
-LF:38
-LH:38
-BRF:12
-BRH:10
+DA:181,4
+FNF:13
+FNH:13
+LF:26
+LH:26
+BRF:2
+BRH:2
end_of_record
TN:
SF:src/rules/validation/abstract/base/RuleBlacklistBase.sol
-DA:37,18
-FN:37,RuleBlacklistBase.transferred.0
-FNDA:18,RuleBlacklistBase.transferred.0
-DA:43,18
-DA:50,46
-FN:50,RuleBlacklistBase.transferred.1
-FNDA:46,RuleBlacklistBase.transferred.1
-DA:56,46
-DA:62,4
-FN:62,RuleBlacklistBase.canReturnTransferRestrictionCode
-FNDA:4,RuleBlacklistBase.canReturnTransferRestrictionCode
-DA:69,4
-DA:70,1
-DA:76,12
-FN:76,RuleBlacklistBase.messageForTransferRestriction
-FNDA:12,RuleBlacklistBase.messageForTransferRestriction
-DA:83,12
-BRDA:83,0,0,5
-BRDA:83,0,1,3
-DA:84,5
-DA:85,7
-BRDA:85,1,0,3
-BRDA:85,1,1,3
-DA:86,3
-DA:87,4
-BRDA:87,2,0,1
-BRDA:87,2,1,3
-DA:88,1
-DA:90,3
-DA:97,65
-FN:97,RuleBlacklistBase.supportsInterface
-FNDA:65,RuleBlacklistBase.supportsInterface
-DA:100,65
-DA:101,63
-DA:114,140
-FN:114,RuleBlacklistBase._detectTransferRestriction
-FNDA:140,RuleBlacklistBase._detectTransferRestriction
-DA:124,140
-BRDA:124,3,0,40
-BRDA:124,3,1,81
-DA:125,40
-DA:126,100
-BRDA:126,4,0,19
-DA:127,19
-DA:129,81
-DA:140,80
-FN:140,RuleBlacklistBase._detectTransferRestrictionFrom
-FNDA:80,RuleBlacklistBase._detectTransferRestrictionFrom
-DA:146,80
-BRDA:146,5,0,8
-DA:147,8
-DA:149,72
-DA:158,29
-FN:158,RuleBlacklistBase._transferred
-FNDA:29,RuleBlacklistBase._transferred
-DA:159,29
-DA:160,29
-BRDA:160,6,0,17
-BRDA:160,6,1,12
-DA:173,54
-FN:173,RuleBlacklistBase._transferredFrom
-FNDA:54,RuleBlacklistBase._transferredFrom
-DA:174,54
-DA:175,54
-BRDA:175,7,0,9
-BRDA:175,7,1,45
-FNF:9
-FNH:9
-LF:34
-LH:34
+DA:43,29
+FN:43,RuleBlacklistBase.transferred.0
+FNDA:29,RuleBlacklistBase.transferred.0
+DA:49,29
+DA:56,94
+FN:56,RuleBlacklistBase.transferred.1
+FNDA:94,RuleBlacklistBase.transferred.1
+DA:62,94
+DA:68,7
+FN:68,RuleBlacklistBase.canReturnTransferRestrictionCode
+FNDA:7,RuleBlacklistBase.canReturnTransferRestrictionCode
+DA:75,7
+DA:76,3
+DA:82,13
+FN:82,RuleBlacklistBase.messageForTransferRestriction
+FNDA:13,RuleBlacklistBase.messageForTransferRestriction
+DA:89,13
+BRDA:89,0,0,6
+BRDA:89,0,1,3
+DA:90,6
+DA:91,7
+BRDA:91,1,0,3
+BRDA:91,1,1,3
+DA:92,3
+DA:93,4
+BRDA:93,2,0,1
+BRDA:93,2,1,3
+DA:94,1
+DA:96,3
+DA:103,109
+FN:103,RuleBlacklistBase.supportsInterface
+FNDA:109,RuleBlacklistBase.supportsInterface
+DA:106,109
+DA:107,107
+DA:108,105
+DA:109,103
+DA:116,3
+FN:116,RuleBlacklistBase.isAllowList
+FNDA:3,RuleBlacklistBase.isAllowList
+DA:117,3
+DA:130,230
+FN:130,RuleBlacklistBase._detectTransferRestriction
+FNDA:230,RuleBlacklistBase._detectTransferRestriction
+DA:141,230
+BRDA:141,3,0,54
+BRDA:141,3,1,153
+DA:142,54
+DA:143,176
+BRDA:143,4,0,23
+DA:144,23
+DA:146,153
+DA:157,134
+FN:157,RuleBlacklistBase._detectTransferRestrictionFrom
+FNDA:134,RuleBlacklistBase._detectTransferRestrictionFrom
+DA:164,134
+BRDA:164,5,0,8
+DA:165,8
+DA:167,126
+DA:176,46
+FN:176,RuleBlacklistBase._transferred
+FNDA:46,RuleBlacklistBase._transferred
+DA:177,46
+DA:178,46
+BRDA:178,6,0,25
+BRDA:178,6,1,21
+DA:191,102
+FN:191,RuleBlacklistBase._transferredFrom
+FNDA:102,RuleBlacklistBase._transferredFrom
+DA:192,102
+DA:193,102
+BRDA:193,7,0,10
+BRDA:193,7,1,92
+FNF:10
+FNH:10
+LF:38
+LH:38
BRF:14
BRH:14
end_of_record
TN:
SF:src/rules/validation/abstract/base/RuleChainlinkPoRBase.sol
-DA:74,600
-FN:74,RuleChainlinkPoRBase.constructor
-FNDA:600,RuleChainlinkPoRBase.constructor
-DA:80,600
-DA:81,598
-DA:82,596
-DA:94,8
-FN:94,RuleChainlinkPoRBase.canReturnTransferRestrictionCode
-FNDA:8,RuleChainlinkPoRBase.canReturnTransferRestrictionCode
-DA:95,8
-DA:96,4
-DA:109,8
-FN:109,RuleChainlinkPoRBase.setReservesFeed
-FNDA:8,RuleChainlinkPoRBase.setReservesFeed
-DA:110,6
-DA:119,12
-FN:119,RuleChainlinkPoRBase.setTokenMetadata
-FNDA:12,RuleChainlinkPoRBase.setTokenMetadata
-DA:120,10
-DA:127,5
-FN:127,RuleChainlinkPoRBase.setMaxStalenessSeconds
-FNDA:5,RuleChainlinkPoRBase.setMaxStalenessSeconds
-DA:128,3
+DA:42,624
+FN:42,RuleChainlinkPoRBase.constructor
+FNDA:624,RuleChainlinkPoRBase.constructor
+DA:48,624
+DA:49,622
+DA:50,620
+DA:62,9
+FN:62,RuleChainlinkPoRBase.canReturnTransferRestrictionCode
+FNDA:9,RuleChainlinkPoRBase.canReturnTransferRestrictionCode
+DA:63,9
+DA:64,5
+DA:65,2
+DA:75,16
+FN:75,RuleChainlinkPoRBase.transferred.0
+FNDA:16,RuleChainlinkPoRBase.transferred.0
+DA:76,16
+DA:82,16
+FN:82,RuleChainlinkPoRBase.transferred.1
+FNDA:16,RuleChainlinkPoRBase.transferred.1
+DA:83,16
+DA:89,9
+FN:89,RuleChainlinkPoRBase.messageForTransferRestriction
+FNDA:9,RuleChainlinkPoRBase.messageForTransferRestriction
+DA:95,9
+BRDA:95,0,0,2
+BRDA:95,0,1,1
+DA:96,2
+DA:97,7
+BRDA:97,1,0,2
+BRDA:97,1,1,1
+DA:98,2
+DA:99,5
+BRDA:99,2,0,2
+BRDA:99,2,1,1
+DA:100,2
+DA:101,3
+BRDA:101,3,0,1
+BRDA:101,3,1,1
+DA:102,1
+DA:103,2
+BRDA:103,4,0,1
+DA:104,1
+DA:106,1
+DA:116,629
+FN:116,RuleChainlinkPoRBase._detectTransferRestriction
+FNDA:629,RuleChainlinkPoRBase._detectTransferRestriction
+DA:129,629
+BRDA:129,5,0,9
+DA:130,9
+DA:132,620
+DA:133,620
+BRDA:133,6,0,151
+DA:134,151
+DA:136,469
+DA:137,469
+BRDA:137,7,0,4
DA:138,4
-FN:138,RuleChainlinkPoRBase.feedDecimals
-FNDA:4,RuleChainlinkPoRBase.feedDecimals
-DA:139,4
-DA:152,657
-FN:152,RuleChainlinkPoRBase.maxBackedSupply
-FNDA:657,RuleChainlinkPoRBase.maxBackedSupply
-DA:153,657
-DA:159,4
-FN:159,RuleChainlinkPoRBase.transferred.0
-FNDA:4,RuleChainlinkPoRBase.transferred.0
-DA:160,4
-DA:166,13
-FN:166,RuleChainlinkPoRBase.transferred.1
-FNDA:13,RuleChainlinkPoRBase.transferred.1
-DA:167,13
-DA:173,8
-FN:173,RuleChainlinkPoRBase.messageForTransferRestriction
-FNDA:8,RuleChainlinkPoRBase.messageForTransferRestriction
-DA:179,8
-BRDA:179,0,0,2
-BRDA:179,0,1,1
-DA:180,2
-DA:181,6
-BRDA:181,1,0,2
-BRDA:181,1,1,1
-DA:182,2
-DA:183,4
-BRDA:183,2,0,2
-BRDA:183,2,1,1
-DA:184,2
-DA:185,2
-BRDA:185,3,0,1
-DA:186,1
-DA:188,1
-DA:195,8
-FN:195,RuleChainlinkPoRBase.onlyChainlinkPoRManager
-FNDA:8,RuleChainlinkPoRBase.onlyChainlinkPoRManager
-DA:196,8
-DA:204,0
-FN:204,RuleChainlinkPoRBase._authorizeChainlinkPoRManager
-FNDA:0,RuleChainlinkPoRBase._authorizeChainlinkPoRManager
-DA:218,606
-FN:218,RuleChainlinkPoRBase._setReservesFeed
-FNDA:606,RuleChainlinkPoRBase._setReservesFeed
-DA:219,606
-DA:220,606
-BRDA:220,4,0,1
-BRDA:220,4,1,605
-DA:221,605
-BRDA:221,5,0,1
-BRDA:221,5,1,604
-DA:222,604
-DA:223,604
-BRDA:223,6,0,604
-DA:224,603
-DA:225,1
-BRDA:225,6,1,1
-DA:226,1
-DA:228,603
-BRDA:228,7,0,1
-BRDA:228,7,1,602
-DA:229,602
-DA:230,602
-DA:241,608
-FN:241,RuleChainlinkPoRBase._setTokenMetadata
-FNDA:608,RuleChainlinkPoRBase._setTokenMetadata
-DA:242,608
-BRDA:242,8,0,2
-BRDA:242,8,1,606
-DA:246,606
-BRDA:246,9,0,2
-BRDA:246,9,1,604
-DA:247,604
-BRDA:247,10,0,1
-BRDA:247,10,1,603
-DA:248,603
-BRDA:248,11,0,603
-DA:249,602
-BRDA:249,12,0,1
-BRDA:249,12,1,601
-DA:258,602
-BRDA:258,13,0,602
-DA:259,1
-BRDA:259,13,1,1
-DA:260,1
-DA:262,601
-DA:263,601
-DA:264,601
-DA:271,599
-FN:271,RuleChainlinkPoRBase._setMaxStalenessSeconds
-FNDA:599,RuleChainlinkPoRBase._setMaxStalenessSeconds
-DA:272,599
-DA:273,599
-DA:282,1252
-FN:282,RuleChainlinkPoRBase._maxBackedSupply
-FNDA:1252,RuleChainlinkPoRBase._maxBackedSupply
-DA:283,1252
-DA:286,1252
-DA:287,1252
-BRDA:287,14,0,1252
-DA:288,1250
-DA:289,2
-BRDA:289,14,1,2
-DA:290,2
-DA:294,1250
-BRDA:294,15,0,3
-DA:295,3
-DA:297,1247
-BRDA:297,16,0,1247
-DA:299,1245
-BRDA:299,17,0,135
-DA:300,135
-DA:302,1110
-DA:303,1110
-BRDA:303,18,0,3
-DA:304,3
-DA:308,1107
-DA:309,1107
-DA:310,2
-BRDA:310,16,1,2
-DA:311,2
-DA:325,453
-FN:325,RuleChainlinkPoRBase._currentSupply
-FNDA:453,RuleChainlinkPoRBase._currentSupply
-DA:326,453
-DA:327,453
-BRDA:327,19,0,453
-DA:328,449
-DA:329,4
-BRDA:329,19,1,4
-DA:330,4
-DA:343,1107
-FN:343,RuleChainlinkPoRBase._scaleReserve
-FNDA:1107,RuleChainlinkPoRBase._scaleReserve
-DA:344,1107
-DA:345,1107
-BRDA:345,20,0,58
-DA:346,58
-DA:348,1049
-BRDA:348,21,0,639
-DA:350,639
-DA:351,639
-BRDA:351,22,0,32
-DA:352,32
-DA:354,607
-DA:357,410
-DA:363,601
-FN:363,RuleChainlinkPoRBase._detectTransferRestriction
-FNDA:601,RuleChainlinkPoRBase._detectTransferRestriction
-DA:375,601
-BRDA:375,23,0,6
-DA:376,6
-DA:378,595
-DA:379,595
-BRDA:379,24,0,142
-DA:380,142
-DA:382,453
-DA:383,453
-BRDA:383,25,0,4
-DA:384,4
-DA:388,449
-BRDA:388,26,0,224
-DA:389,224
-DA:391,225
-DA:397,18
-FN:397,RuleChainlinkPoRBase._detectTransferRestrictionFrom
-FNDA:18,RuleChainlinkPoRBase._detectTransferRestrictionFrom
-DA:403,18
-DA:412,4
-FN:412,RuleChainlinkPoRBase._transferred
-FNDA:4,RuleChainlinkPoRBase._transferred
-DA:413,4
-DA:414,4
-BRDA:414,27,0,2
-BRDA:414,27,1,2
-DA:427,13
-FN:427,RuleChainlinkPoRBase._transferredFrom
-FNDA:13,RuleChainlinkPoRBase._transferredFrom
-DA:428,13
-DA:429,13
-BRDA:429,28,0,5
-BRDA:429,28,1,8
-FNF:22
-FNH:21
-LF:116
-LH:115
-BRF:46
-BRH:46
+DA:142,465
+BRDA:142,8,0,251
+DA:143,251
+DA:145,214
+DA:151,5
+FN:151,RuleChainlinkPoRBase._detectTransferRestrictionFrom
+FNDA:5,RuleChainlinkPoRBase._detectTransferRestrictionFrom
+DA:158,5
+DA:175,19
+FN:175,RuleChainlinkPoRBase._detectTransferRestrictionOnNotify
+FNDA:19,RuleChainlinkPoRBase._detectTransferRestrictionOnNotify
+DA:181,19
+DA:190,16
+FN:190,RuleChainlinkPoRBase._transferred
+FNDA:16,RuleChainlinkPoRBase._transferred
+DA:191,16
+DA:192,16
+BRDA:192,9,0,7
+BRDA:192,9,1,9
+DA:205,16
+FN:205,RuleChainlinkPoRBase._transferredFrom
+FNDA:16,RuleChainlinkPoRBase._transferredFrom
+DA:206,16
+DA:207,16
+BRDA:207,10,0,6
+BRDA:207,10,1,10
+FNF:10
+FNH:10
+LF:46
+LH:46
+BRF:17
+BRH:17
end_of_record
TN:
SF:src/rules/validation/abstract/base/RuleERC2980Base.sol
-DA:64,75
+DA:64,87
FN:64,RuleERC2980Base.constructor
-FNDA:75,RuleERC2980Base.constructor
-DA:65,75
-DA:66,75
-DA:67,75
-DA:68,75
+FNDA:87,RuleERC2980Base.constructor
+DA:65,87
+DA:66,87
+DA:67,87
+DA:68,87
DA:75,5
FN:75,RuleERC2980Base.onlyMintBurnManager
FNDA:5,RuleERC2980Base.onlyMintBurnManager
DA:76,5
-DA:80,6
+DA:80,9
FN:80,RuleERC2980Base.onlyWhitelistAdd
-FNDA:6,RuleERC2980Base.onlyWhitelistAdd
-DA:81,6
+FNDA:9,RuleERC2980Base.onlyWhitelistAdd
+DA:81,9
DA:85,7
FN:85,RuleERC2980Base.onlyWhitelistRemove
FNDA:7,RuleERC2980Base.onlyWhitelistRemove
DA:86,7
-DA:90,6
+DA:90,8
FN:90,RuleERC2980Base.onlyFrozenlistAdd
-FNDA:6,RuleERC2980Base.onlyFrozenlistAdd
-DA:91,6
-DA:95,2
+FNDA:8,RuleERC2980Base.onlyFrozenlistAdd
+DA:91,8
+DA:95,3
FN:95,RuleERC2980Base.onlyFrozenlistRemove
-FNDA:2,RuleERC2980Base.onlyFrozenlistRemove
-DA:96,2
-DA:109,6
-FN:109,RuleERC2980Base.addWhitelistAddresses
-FNDA:6,RuleERC2980Base.addWhitelistAddresses
-DA:110,4
-DA:111,4
-DA:119,4
-FN:119,RuleERC2980Base.removeWhitelistAddresses
-FNDA:4,RuleERC2980Base.removeWhitelistAddresses
-DA:120,3
-DA:121,3
-DA:133,49
-FN:133,RuleERC2980Base.addWhitelistAddress
-FNDA:49,RuleERC2980Base.addWhitelistAddress
-DA:134,46
-BRDA:134,0,0,1
-BRDA:134,0,1,45
-DA:135,45
-BRDA:135,1,0,1
-BRDA:135,1,1,44
-DA:136,44
-DA:137,44
+FNDA:3,RuleERC2980Base.onlyFrozenlistRemove
+DA:96,3
+DA:110,9
+FN:110,RuleERC2980Base.addWhitelistAddresses
+FNDA:9,RuleERC2980Base.addWhitelistAddresses
+DA:111,7
+DA:112,6
+DA:120,5
+FN:120,RuleERC2980Base.removeWhitelistAddresses
+FNDA:5,RuleERC2980Base.removeWhitelistAddresses
+DA:121,4
+DA:122,4
+DA:134,55
+FN:134,RuleERC2980Base.addWhitelistAddress
+FNDA:55,RuleERC2980Base.addWhitelistAddress
+DA:135,52
+BRDA:135,0,0,1
+BRDA:135,0,1,51
+DA:136,51
+BRDA:136,1,0,1
+BRDA:136,1,1,50
+DA:137,50
DA:149,7
FN:149,RuleERC2980Base.removeWhitelistAddress
FNDA:7,RuleERC2980Base.removeWhitelistAddress
@@ -1991,467 +1188,507 @@ DA:150,5
BRDA:150,2,0,1
BRDA:150,2,1,4
DA:151,4
-DA:152,4
-DA:164,6
+DA:164,8
FN:164,RuleERC2980Base.addFrozenlistAddresses
-FNDA:6,RuleERC2980Base.addFrozenlistAddresses
-DA:165,4
-DA:166,4
-DA:174,2
+FNDA:8,RuleERC2980Base.addFrozenlistAddresses
+DA:165,6
+DA:166,5
+DA:174,3
FN:174,RuleERC2980Base.removeFrozenlistAddresses
-FNDA:2,RuleERC2980Base.removeFrozenlistAddresses
-DA:175,2
-DA:176,2
-DA:188,24
+FNDA:3,RuleERC2980Base.removeFrozenlistAddresses
+DA:175,3
+DA:176,3
+DA:188,26
FN:188,RuleERC2980Base.addFrozenlistAddress
-FNDA:24,RuleERC2980Base.addFrozenlistAddress
-DA:189,21
+FNDA:26,RuleERC2980Base.addFrozenlistAddress
+DA:189,23
BRDA:189,3,0,1
-BRDA:189,3,1,20
-DA:190,20
+BRDA:189,3,1,22
+DA:190,22
BRDA:190,4,0,1
-BRDA:190,4,1,19
-DA:191,19
-DA:192,19
-DA:204,7
-FN:204,RuleERC2980Base.removeFrozenlistAddress
+BRDA:190,4,1,21
+DA:191,21
+DA:203,7
+FN:203,RuleERC2980Base.removeFrozenlistAddress
FNDA:7,RuleERC2980Base.removeFrozenlistAddress
-DA:205,5
-BRDA:205,5,0,1
-BRDA:205,5,1,4
-DA:206,4
-DA:207,4
-DA:218,5
-FN:218,RuleERC2980Base.setAllowMint
+DA:204,5
+BRDA:204,5,0,1
+BRDA:204,5,1,4
+DA:205,4
+DA:216,5
+FN:216,RuleERC2980Base.setAllowMint
FNDA:5,RuleERC2980Base.setAllowMint
-DA:219,3
-DA:220,3
-DA:227,3
-FN:227,RuleERC2980Base.setAllowBurn
+DA:217,3
+DA:218,3
+DA:225,3
+FN:225,RuleERC2980Base.setAllowBurn
FNDA:3,RuleERC2980Base.setAllowBurn
-DA:228,2
-DA:229,2
-DA:235,6
-FN:235,RuleERC2980Base.transferred.0
-FNDA:6,RuleERC2980Base.transferred.0
-DA:241,6
-DA:247,4
-FN:247,RuleERC2980Base.transferred.1
+DA:226,2
+DA:227,2
+DA:233,8
+FN:233,RuleERC2980Base.transferred.0
+FNDA:8,RuleERC2980Base.transferred.0
+DA:239,8
+DA:245,4
+FN:245,RuleERC2980Base.transferred.1
FNDA:4,RuleERC2980Base.transferred.1
-DA:253,4
-DA:259,5
-FN:259,RuleERC2980Base.canReturnTransferRestrictionCode
+DA:251,4
+DA:257,5
+FN:257,RuleERC2980Base.canReturnTransferRestrictionCode
FNDA:5,RuleERC2980Base.canReturnTransferRestrictionCode
-DA:266,5
-DA:267,3
-DA:268,1
-DA:274,7
-FN:274,RuleERC2980Base.messageForTransferRestriction
+DA:264,5
+DA:265,3
+DA:266,1
+DA:272,7
+FN:272,RuleERC2980Base.messageForTransferRestriction
FNDA:7,RuleERC2980Base.messageForTransferRestriction
-DA:281,7
-BRDA:281,6,0,1
-BRDA:281,6,1,1
+DA:279,7
+BRDA:279,6,0,1
+BRDA:279,6,1,1
+DA:280,1
+DA:281,6
+BRDA:281,7,0,1
+BRDA:281,7,1,1
DA:282,1
-DA:283,6
-BRDA:283,7,0,1
-BRDA:283,7,1,1
+DA:283,5
+BRDA:283,8,0,1
+BRDA:283,8,1,1
DA:284,1
-DA:285,5
-BRDA:285,8,0,1
-BRDA:285,8,1,1
+DA:285,4
+BRDA:285,9,0,1
+BRDA:285,9,1,1
DA:286,1
-DA:287,4
-BRDA:287,9,0,1
-BRDA:287,9,1,1
+DA:287,3
+BRDA:287,10,0,1
+BRDA:287,10,1,1
DA:288,1
-DA:289,3
-BRDA:289,10,0,1
-BRDA:289,10,1,1
+DA:289,2
+BRDA:289,11,0,1
+BRDA:289,11,1,1
DA:290,1
-DA:291,2
-BRDA:291,11,0,1
-BRDA:291,11,1,1
DA:292,1
-DA:294,1
-DA:301,3
-FN:301,RuleERC2980Base.supportsInterface
+DA:299,3
+FN:299,RuleERC2980Base.supportsInterface
FNDA:3,RuleERC2980Base.supportsInterface
-DA:302,3
-DA:309,5
-FN:309,RuleERC2980Base.whitelistAddressCount
+DA:300,3
+DA:307,5
+FN:307,RuleERC2980Base.whitelistAddressCount
FNDA:5,RuleERC2980Base.whitelistAddressCount
-DA:310,5
-DA:318,15
-FN:318,RuleERC2980Base.isWhitelisted
-FNDA:15,RuleERC2980Base.isWhitelisted
-DA:319,15
-DA:327,11
-FN:327,RuleERC2980Base.whitelist
-FNDA:11,RuleERC2980Base.whitelist
-DA:328,11
-DA:338,5
-FN:338,RuleERC2980Base.isVerified
+DA:308,5
+DA:316,16
+FN:316,RuleERC2980Base.isWhitelisted
+FNDA:16,RuleERC2980Base.isWhitelisted
+DA:317,16
+DA:325,17
+FN:325,RuleERC2980Base.whitelist
+FNDA:17,RuleERC2980Base.whitelist
+DA:326,17
+DA:336,5
+FN:336,RuleERC2980Base.isVerified
FNDA:5,RuleERC2980Base.isVerified
-DA:339,5
-DA:347,1
-FN:347,RuleERC2980Base.areWhitelisted
+DA:337,5
+DA:345,1
+FN:345,RuleERC2980Base.areWhitelisted
FNDA:1,RuleERC2980Base.areWhitelisted
-DA:348,1
-DA:349,1
-DA:350,2
-DA:358,4
-FN:358,RuleERC2980Base.frozenlistAddressCount
+DA:346,1
+DA:347,1
+DA:348,2
+DA:356,4
+FN:356,RuleERC2980Base.frozenlistAddressCount
FNDA:4,RuleERC2980Base.frozenlistAddressCount
-DA:359,4
-DA:367,12
-FN:367,RuleERC2980Base.isFrozen
+DA:357,4
+DA:365,12
+FN:365,RuleERC2980Base.isFrozen
FNDA:12,RuleERC2980Base.isFrozen
-DA:368,12
-DA:376,7
-FN:376,RuleERC2980Base.frozenlist
-FNDA:7,RuleERC2980Base.frozenlist
-DA:377,7
-DA:385,1
-FN:385,RuleERC2980Base.areFrozen
+DA:366,12
+DA:374,10
+FN:374,RuleERC2980Base.frozenlist
+FNDA:10,RuleERC2980Base.frozenlist
+DA:375,10
+DA:383,1
+FN:383,RuleERC2980Base.areFrozen
FNDA:1,RuleERC2980Base.areFrozen
-DA:386,1
-DA:387,1
-DA:388,2
-DA:399,0
-FN:399,RuleERC2980Base._authorizeMintBurnManager
+DA:384,1
+DA:385,1
+DA:386,2
+DA:397,0
+FN:397,RuleERC2980Base._authorizeMintBurnManager
FNDA:0,RuleERC2980Base._authorizeMintBurnManager
-DA:404,0
-FN:404,RuleERC2980Base._authorizeWhitelistAdd
+DA:402,0
+FN:402,RuleERC2980Base._authorizeWhitelistAdd
FNDA:0,RuleERC2980Base._authorizeWhitelistAdd
-DA:408,0
-FN:408,RuleERC2980Base._authorizeWhitelistRemove
+DA:406,0
+FN:406,RuleERC2980Base._authorizeWhitelistRemove
FNDA:0,RuleERC2980Base._authorizeWhitelistRemove
-DA:412,0
-FN:412,RuleERC2980Base._authorizeFrozenlistAdd
+DA:410,0
+FN:410,RuleERC2980Base._authorizeFrozenlistAdd
FNDA:0,RuleERC2980Base._authorizeFrozenlistAdd
-DA:416,0
-FN:416,RuleERC2980Base._authorizeFrozenlistRemove
+DA:414,0
+FN:414,RuleERC2980Base._authorizeFrozenlistRemove
FNDA:0,RuleERC2980Base._authorizeFrozenlistRemove
-DA:421,62
-FN:421,RuleERC2980Base._detectTransferRestriction
-FNDA:62,RuleERC2980Base._detectTransferRestriction
-DA:432,62
-DA:433,62
-DA:436,62
-BRDA:436,12,0,1
-DA:437,1
-DA:439,61
-BRDA:439,13,0,2
-DA:440,2
-DA:444,59
-BRDA:444,14,0,20
-DA:445,20
-DA:447,39
-BRDA:447,15,0,4
-DA:448,4
-DA:451,35
-BRDA:451,16,0,5
-DA:452,5
-DA:454,30
-DA:460,24
-FN:460,RuleERC2980Base._detectTransferRestrictionFrom
+DA:419,78
+FN:419,RuleERC2980Base._detectTransferRestriction
+FNDA:78,RuleERC2980Base._detectTransferRestriction
+DA:430,78
+DA:431,78
+DA:434,78
+BRDA:434,12,0,1
+DA:435,1
+DA:437,77
+BRDA:437,13,0,2
+DA:438,2
+DA:442,75
+BRDA:442,14,0,28
+DA:443,28
+DA:445,47
+BRDA:445,15,0,4
+DA:446,4
+DA:449,43
+BRDA:449,16,0,5
+DA:450,5
+DA:452,38
+DA:458,24
+FN:458,RuleERC2980Base._detectTransferRestrictionFrom
FNDA:24,RuleERC2980Base._detectTransferRestrictionFrom
-DA:467,24
-BRDA:467,17,0,4
-DA:468,4
-DA:470,20
-DA:476,13
-FN:476,RuleERC2980Base._transferred
-FNDA:13,RuleERC2980Base._transferred
-DA:477,13
-DA:478,13
-BRDA:478,18,0,7
-BRDA:478,18,1,6
-DA:487,11
-FN:487,RuleERC2980Base._transferredFrom
+DA:465,24
+BRDA:465,17,0,4
+DA:466,4
+DA:468,20
+DA:474,21
+FN:474,RuleERC2980Base._transferred
+FNDA:21,RuleERC2980Base._transferred
+DA:475,21
+DA:476,21
+BRDA:476,18,0,11
+BRDA:476,18,1,10
+DA:485,11
+FN:485,RuleERC2980Base._transferredFrom
FNDA:11,RuleERC2980Base._transferredFrom
-DA:488,11
-DA:489,11
-BRDA:489,19,0,5
-BRDA:489,19,1,6
-DA:498,293
-FN:498,RuleERC2980Base._msgSender
-FNDA:293,RuleERC2980Base._msgSender
-DA:499,293
-DA:505,2
-FN:505,RuleERC2980Base._msgData
+DA:486,11
+DA:487,11
+BRDA:487,19,0,5
+BRDA:487,19,1,6
+DA:496,320
+FN:496,RuleERC2980Base._msgSender
+FNDA:320,RuleERC2980Base._msgSender
+DA:497,320
+DA:503,2
+FN:503,RuleERC2980Base._msgData
FNDA:2,RuleERC2980Base._msgData
-DA:506,2
-DA:512,295
-FN:512,RuleERC2980Base._contextSuffixLength
-FNDA:295,RuleERC2980Base._contextSuffixLength
-DA:513,295
+DA:504,2
+DA:510,322
+FN:510,RuleERC2980Base._contextSuffixLength
+FNDA:322,RuleERC2980Base._contextSuffixLength
+DA:511,322
FNF:42
FNH:37
-LF:132
-LH:127
+LF:128
+LH:123
BRF:34
BRH:34
end_of_record
TN:
SF:src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol
-DA:63,39
-FN:63,RuleIdentityRegistryBase.constructor
-FNDA:39,RuleIdentityRegistryBase.constructor
-DA:64,39
-BRDA:64,0,0,37
-DA:65,37
-DA:67,39
-DA:68,39
-DA:69,39
-DA:70,39
-DA:77,5
-FN:77,RuleIdentityRegistryBase.onlyIdentityRegistryManager
+DA:55,64
+FN:55,RuleIdentityRegistryBase.constructor
+FNDA:64,RuleIdentityRegistryBase.constructor
+DA:60,64
+BRDA:60,0,0,57
+DA:61,57
+DA:62,57
+DA:64,64
+DA:65,64
+DA:66,64
+DA:67,64
+DA:74,5
+FN:74,RuleIdentityRegistryBase.onlyIdentityRegistryManager
FNDA:5,RuleIdentityRegistryBase.onlyIdentityRegistryManager
-DA:78,5
-DA:91,4
-FN:91,RuleIdentityRegistryBase.canReturnTransferRestrictionCode
+DA:75,5
+DA:88,4
+FN:88,RuleIdentityRegistryBase.canReturnTransferRestrictionCode
FNDA:4,RuleIdentityRegistryBase.canReturnTransferRestrictionCode
-DA:92,4
-DA:93,2
-DA:104,4
-FN:104,RuleIdentityRegistryBase.setIdentityRegistry
+DA:89,4
+DA:90,2
+DA:101,4
+FN:101,RuleIdentityRegistryBase.setIdentityRegistry
FNDA:4,RuleIdentityRegistryBase.setIdentityRegistry
-DA:105,2
-BRDA:105,1,0,1
-BRDA:105,1,1,1
-DA:106,1
-DA:107,1
-DA:116,2
-FN:116,RuleIdentityRegistryBase.setCheckSender
-FNDA:2,RuleIdentityRegistryBase.setCheckSender
-DA:117,2
-DA:118,2
-DA:126,5
-FN:126,RuleIdentityRegistryBase.setCheckSpender
-FNDA:5,RuleIdentityRegistryBase.setCheckSpender
-DA:127,5
-DA:128,5
-DA:134,5
-FN:134,RuleIdentityRegistryBase.clearIdentityRegistry
+DA:102,2
+BRDA:102,1,0,1
+BRDA:102,1,1,1
+DA:103,1
+DA:104,1
+DA:113,3
+FN:113,RuleIdentityRegistryBase.setCheckSender
+FNDA:3,RuleIdentityRegistryBase.setCheckSender
+DA:114,3
+DA:115,3
+DA:123,6
+FN:123,RuleIdentityRegistryBase.setCheckSpender
+FNDA:6,RuleIdentityRegistryBase.setCheckSpender
+DA:124,6
+DA:125,6
+DA:131,5
+FN:131,RuleIdentityRegistryBase.clearIdentityRegistry
FNDA:5,RuleIdentityRegistryBase.clearIdentityRegistry
-DA:135,3
-DA:136,3
-DA:142,5
-FN:142,RuleIdentityRegistryBase.transferred.0
-FNDA:5,RuleIdentityRegistryBase.transferred.0
-DA:143,5
-DA:149,7
-FN:149,RuleIdentityRegistryBase.transferred.1
-FNDA:7,RuleIdentityRegistryBase.transferred.1
-DA:150,7
-DA:156,4
-FN:156,RuleIdentityRegistryBase.messageForTransferRestriction
+DA:132,3
+DA:133,3
+DA:139,14
+FN:139,RuleIdentityRegistryBase.transferred.0
+FNDA:14,RuleIdentityRegistryBase.transferred.0
+DA:140,14
+DA:146,23
+FN:146,RuleIdentityRegistryBase.transferred.1
+FNDA:23,RuleIdentityRegistryBase.transferred.1
+DA:147,23
+DA:153,4
+FN:153,RuleIdentityRegistryBase.messageForTransferRestriction
FNDA:4,RuleIdentityRegistryBase.messageForTransferRestriction
-DA:162,4
-BRDA:162,2,0,1
-BRDA:162,2,1,1
-DA:163,1
-DA:164,3
-BRDA:164,3,0,1
-BRDA:164,3,1,1
-DA:165,1
-DA:166,2
-BRDA:166,4,0,1
-DA:167,1
-DA:169,1
-DA:179,0
-FN:179,RuleIdentityRegistryBase._authorizeIdentityRegistryManager
+DA:159,4
+BRDA:159,2,0,1
+BRDA:159,2,1,1
+DA:160,1
+DA:161,3
+BRDA:161,3,0,1
+BRDA:161,3,1,1
+DA:162,1
+DA:163,2
+BRDA:163,4,0,1
+DA:164,1
+DA:166,1
+DA:176,0
+FN:176,RuleIdentityRegistryBase._authorizeIdentityRegistryManager
FNDA:0,RuleIdentityRegistryBase._authorizeIdentityRegistryManager
-DA:187,62
-FN:187,RuleIdentityRegistryBase._detectTransferRestriction
-FNDA:62,RuleIdentityRegistryBase._detectTransferRestriction
-DA:197,62
-BRDA:197,5,0,3
-DA:198,3
-DA:201,59
-BRDA:201,6,0,3
-DA:202,3
-DA:206,56
-BRDA:206,7,0,1
-DA:207,1
-DA:212,55
-BRDA:212,8,0,6
-DA:213,6
-DA:215,49
-DA:226,31
-FN:226,RuleIdentityRegistryBase._detectTransferRestrictionFrom
-FNDA:31,RuleIdentityRegistryBase._detectTransferRestrictionFrom
-DA:232,31
-BRDA:232,9,0,1
-DA:233,1
-DA:236,30
-BRDA:236,10,0,2
-DA:237,2
-DA:245,5
-DA:246,4
-DA:247,3
-BRDA:247,11,0,3
-DA:248,3
-DA:250,25
-DA:256,11
-FN:256,RuleIdentityRegistryBase._transferred
-FNDA:11,RuleIdentityRegistryBase._transferred
-DA:257,11
-DA:258,11
-BRDA:258,12,0,1
-BRDA:258,12,1,10
-DA:267,13
-FN:267,RuleIdentityRegistryBase._transferredFrom
-FNDA:13,RuleIdentityRegistryBase._transferredFrom
-DA:268,13
-DA:269,13
-BRDA:269,13,0,2
-BRDA:269,13,1,11
+DA:184,121
+FN:184,RuleIdentityRegistryBase._detectTransferRestriction
+FNDA:121,RuleIdentityRegistryBase._detectTransferRestriction
+DA:197,121
+DA:198,121
+BRDA:198,5,0,10
+DA:199,10
+DA:202,111
+BRDA:202,6,0,10
+DA:203,10
+DA:207,101
+BRDA:207,7,0,2
+DA:208,2
+DA:213,99
+BRDA:213,8,0,13
+DA:214,13
+DA:216,86
+DA:227,57
+FN:227,RuleIdentityRegistryBase._detectTransferRestrictionFrom
+FNDA:57,RuleIdentityRegistryBase._detectTransferRestrictionFrom
+DA:234,57
+DA:239,57
+BRDA:239,9,0,11
+DA:240,11
+DA:249,46
+BRDA:249,10,0,5
+DA:250,5
+DA:252,41
+DA:258,26
+FN:258,RuleIdentityRegistryBase._transferred
+FNDA:26,RuleIdentityRegistryBase._transferred
+DA:259,26
+DA:260,26
+BRDA:260,11,0,5
+BRDA:260,11,1,21
+DA:269,29
+FN:269,RuleIdentityRegistryBase._transferredFrom
+FNDA:29,RuleIdentityRegistryBase._transferredFrom
+DA:270,29
+DA:271,29
+BRDA:271,12,0,4
+BRDA:271,12,1,25
FNF:15
FNH:14
-LF:64
-LH:63
-BRF:19
-BRH:19
+LF:63
+LH:62
+BRF:18
+BRH:18
end_of_record
TN:
-SF:src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol
-DA:36,550
-FN:36,RuleMaxTotalSupplyBase.constructor
-FNDA:550,RuleMaxTotalSupplyBase.constructor
-DA:37,550
-DA:38,547
-DA:39,547
-DA:51,3
-FN:51,RuleMaxTotalSupplyBase.canReturnTransferRestrictionCode
-FNDA:3,RuleMaxTotalSupplyBase.canReturnTransferRestrictionCode
-DA:52,3
-DA:63,260
-FN:63,RuleMaxTotalSupplyBase.setMaxTotalSupply
-FNDA:260,RuleMaxTotalSupplyBase.setMaxTotalSupply
-DA:64,258
-DA:65,258
-DA:72,8
-FN:72,RuleMaxTotalSupplyBase.setTokenContract
-FNDA:8,RuleMaxTotalSupplyBase.setTokenContract
-DA:73,6
-DA:74,3
-DA:75,3
-DA:81,3
-FN:81,RuleMaxTotalSupplyBase.transferred.0
-FNDA:3,RuleMaxTotalSupplyBase.transferred.0
-DA:82,3
-DA:88,2
-FN:88,RuleMaxTotalSupplyBase.transferred.1
-FNDA:2,RuleMaxTotalSupplyBase.transferred.1
-DA:89,2
-DA:95,3
-FN:95,RuleMaxTotalSupplyBase.messageForTransferRestriction
-FNDA:3,RuleMaxTotalSupplyBase.messageForTransferRestriction
+SF:src/rules/validation/abstract/base/RuleMaxBalanceBase.sol
+DA:42,63
+FN:42,RuleMaxBalanceBase.constructor
+FNDA:63,RuleMaxBalanceBase.constructor
+DA:43,63
+DA:44,60
+DA:56,3
+FN:56,RuleMaxBalanceBase.canReturnTransferRestrictionCode
+FNDA:3,RuleMaxBalanceBase.canReturnTransferRestrictionCode
+DA:57,3
+DA:76,5
+FN:76,RuleMaxBalanceBase.remainingCapacity
+FNDA:5,RuleMaxBalanceBase.remainingCapacity
+DA:77,5
+DA:78,5
+BRDA:78,0,0,1
+DA:79,1
+DA:81,4
+DA:87,5
+FN:87,RuleMaxBalanceBase.transferred.0
+FNDA:5,RuleMaxBalanceBase.transferred.0
+DA:88,5
+DA:94,15
+FN:94,RuleMaxBalanceBase.transferred.1
+FNDA:15,RuleMaxBalanceBase.transferred.1
+DA:95,15
DA:101,3
-BRDA:101,0,0,1
-BRDA:101,0,1,1
-DA:102,1
-DA:103,2
-BRDA:103,1,0,1
-DA:104,1
-DA:106,1
-DA:113,260
-FN:113,RuleMaxTotalSupplyBase.onlyMaxTotalSupplyManager
-FNDA:260,RuleMaxTotalSupplyBase.onlyMaxTotalSupplyManager
-DA:114,260
-DA:121,0
-FN:121,RuleMaxTotalSupplyBase._authorizeMaxTotalSupplyManager
-FNDA:0,RuleMaxTotalSupplyBase._authorizeMaxTotalSupplyManager
-DA:135,556
-FN:135,RuleMaxTotalSupplyBase._validateTokenContract
-FNDA:556,RuleMaxTotalSupplyBase._validateTokenContract
-DA:136,556
-BRDA:136,2,0,2
-BRDA:136,2,1,554
-DA:137,554
-BRDA:137,3,0,2
-BRDA:137,3,1,552
-DA:138,552
-BRDA:138,4,0,552
-DA:139,2
-BRDA:139,4,1,2
-DA:140,2
-DA:157,788
-FN:157,RuleMaxTotalSupplyBase._currentSupply
-FNDA:788,RuleMaxTotalSupplyBase._currentSupply
-DA:158,788
-DA:159,788
-BRDA:159,5,0,788
-DA:160,784
-DA:161,4
-BRDA:161,5,1,4
-DA:162,4
-DA:169,793
-FN:169,RuleMaxTotalSupplyBase._detectTransferRestriction
-FNDA:793,RuleMaxTotalSupplyBase._detectTransferRestriction
-DA:180,793
-BRDA:180,6,0,788
-DA:181,788
-DA:182,788
-BRDA:182,7,0,4
-DA:183,4
-DA:187,784
-BRDA:187,8,0,452
-DA:188,452
-DA:191,337
-DA:197,4
-FN:197,RuleMaxTotalSupplyBase._detectTransferRestrictionFrom
+FN:101,RuleMaxBalanceBase.messageForTransferRestriction
+FNDA:3,RuleMaxBalanceBase.messageForTransferRestriction
+DA:107,3
+BRDA:107,1,0,1
+BRDA:107,1,1,1
+DA:108,1
+DA:109,2
+BRDA:109,2,0,1
+DA:110,1
+DA:112,1
+DA:122,46
+FN:122,RuleMaxBalanceBase._detectTransferRestriction
+FNDA:46,RuleMaxBalanceBase._detectTransferRestriction
+DA:134,46
+DA:135,46
+BRDA:135,3,0,2
+DA:136,2
+DA:138,17
+BRDA:138,4,0,17
+DA:139,17
+DA:141,27
+DA:149,2
+FN:149,RuleMaxBalanceBase._detectTransferRestrictionFrom
+FNDA:2,RuleMaxBalanceBase._detectTransferRestrictionFrom
+DA:156,2
+DA:173,18
+FN:173,RuleMaxBalanceBase._detectTransferRestrictionOnNotify
+FNDA:18,RuleMaxBalanceBase._detectTransferRestrictionOnNotify
+DA:179,18
+DA:188,5
+FN:188,RuleMaxBalanceBase._transferred
+FNDA:5,RuleMaxBalanceBase._transferred
+DA:189,5
+DA:190,5
+BRDA:190,5,0,3
+BRDA:190,5,1,2
+DA:203,15
+FN:203,RuleMaxBalanceBase._transferredFrom
+FNDA:15,RuleMaxBalanceBase._transferredFrom
+DA:204,15
+DA:205,15
+BRDA:205,6,0,3
+BRDA:205,6,1,12
+FNF:11
+FNH:11
+LF:37
+LH:37
+BRF:10
+BRH:10
+end_of_record
+TN:
+SF:src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol
+DA:28,588
+FN:28,RuleMaxTotalSupplyBase.constructor
+FNDA:588,RuleMaxTotalSupplyBase.constructor
+DA:29,588
+DA:30,585
+DA:42,4
+FN:42,RuleMaxTotalSupplyBase.canReturnTransferRestrictionCode
+FNDA:4,RuleMaxTotalSupplyBase.canReturnTransferRestrictionCode
+DA:43,4
+DA:53,18
+FN:53,RuleMaxTotalSupplyBase.transferred.0
+FNDA:18,RuleMaxTotalSupplyBase.transferred.0
+DA:54,18
+DA:60,50
+FN:60,RuleMaxTotalSupplyBase.transferred.1
+FNDA:50,RuleMaxTotalSupplyBase.transferred.1
+DA:61,50
+DA:67,4
+FN:67,RuleMaxTotalSupplyBase.messageForTransferRestriction
+FNDA:4,RuleMaxTotalSupplyBase.messageForTransferRestriction
+DA:73,4
+BRDA:73,0,0,2
+BRDA:73,0,1,1
+DA:74,2
+DA:75,2
+BRDA:75,1,0,1
+DA:76,1
+DA:78,1
+DA:88,868
+FN:88,RuleMaxTotalSupplyBase._detectTransferRestriction
+FNDA:868,RuleMaxTotalSupplyBase._detectTransferRestriction
+DA:100,868
+BRDA:100,2,0,856
+DA:101,856
+DA:102,856
+BRDA:102,3,0,4
+DA:103,4
+DA:105,470
+BRDA:105,4,0,470
+DA:106,470
+DA:109,394
+DA:115,4
+FN:115,RuleMaxTotalSupplyBase._detectTransferRestrictionFrom
FNDA:4,RuleMaxTotalSupplyBase._detectTransferRestrictionFrom
-DA:203,4
-DA:212,3
-FN:212,RuleMaxTotalSupplyBase._transferred
-FNDA:3,RuleMaxTotalSupplyBase._transferred
-DA:213,3
-DA:214,3
-BRDA:214,9,0,2
-BRDA:214,9,1,1
-DA:227,2
-FN:227,RuleMaxTotalSupplyBase._transferredFrom
-FNDA:2,RuleMaxTotalSupplyBase._transferredFrom
-DA:228,2
-DA:229,2
-BRDA:229,10,0,1
-BRDA:229,10,1,1
-FNF:15
-FNH:14
-LF:54
-LH:53
-BRF:18
-BRH:18
+DA:122,4
+DA:139,54
+FN:139,RuleMaxTotalSupplyBase._detectTransferRestrictionOnNotify
+FNDA:54,RuleMaxTotalSupplyBase._detectTransferRestrictionOnNotify
+DA:145,54
+DA:154,18
+FN:154,RuleMaxTotalSupplyBase._transferred
+FNDA:18,RuleMaxTotalSupplyBase._transferred
+DA:155,18
+DA:156,18
+BRDA:156,5,0,7
+BRDA:156,5,1,11
+DA:169,50
+FN:169,RuleMaxTotalSupplyBase._transferredFrom
+FNDA:50,RuleMaxTotalSupplyBase._transferredFrom
+DA:170,50
+DA:171,50
+BRDA:171,6,0,4
+BRDA:171,6,1,46
+FNF:10
+FNH:10
+LF:33
+LH:33
+BRF:10
+BRH:10
end_of_record
TN:
SF:src/rules/validation/abstract/base/RuleReceiverWhitelistBase.sol
-DA:68,2
-FN:68,RuleReceiverWhitelistBase.canReturnTransferRestrictionCode
+DA:58,2
+FN:58,RuleReceiverWhitelistBase.canReturnTransferRestrictionCode
FNDA:2,RuleReceiverWhitelistBase.canReturnTransferRestrictionCode
-DA:69,2
-DA:79,4
-FN:79,RuleReceiverWhitelistBase.transferred.0
+DA:59,2
+DA:69,4
+FN:69,RuleReceiverWhitelistBase.transferred.0
FNDA:4,RuleReceiverWhitelistBase.transferred.0
-DA:80,4
-DA:86,2
-FN:86,RuleReceiverWhitelistBase.transferred.1
+DA:70,4
+DA:76,2
+FN:76,RuleReceiverWhitelistBase.transferred.1
FNDA:2,RuleReceiverWhitelistBase.transferred.1
-DA:87,2
-DA:93,2
-FN:93,RuleReceiverWhitelistBase.messageForTransferRestriction
+DA:77,2
+DA:83,2
+FN:83,RuleReceiverWhitelistBase.messageForTransferRestriction
FNDA:2,RuleReceiverWhitelistBase.messageForTransferRestriction
-DA:99,2
-BRDA:99,0,0,1
-DA:100,1
-DA:102,1
-DA:108,7
-FN:108,RuleReceiverWhitelistBase.supportsInterface
-FNDA:7,RuleReceiverWhitelistBase.supportsInterface
-DA:111,7
-DA:112,6
+DA:89,2
+BRDA:89,0,0,1
+DA:90,1
+DA:92,1
+DA:98,8
+FN:98,RuleReceiverWhitelistBase.supportsInterface
+FNDA:8,RuleReceiverWhitelistBase.supportsInterface
+DA:101,8
+DA:102,7
+DA:103,7
+DA:104,6
+DA:111,1
+FN:111,RuleReceiverWhitelistBase.isAllowList
+FNDA:1,RuleReceiverWhitelistBase.isAllowList
+DA:112,1
DA:125,21
FN:125,RuleReceiverWhitelistBase._detectTransferRestriction
FNDA:21,RuleReceiverWhitelistBase._detectTransferRestriction
@@ -2477,26 +1714,26 @@ DA:175,2
DA:176,2
BRDA:176,3,0,1
BRDA:176,3,1,1
-FNF:9
-FNH:9
-LF:25
-LH:25
+FNF:10
+FNH:10
+LF:29
+LH:29
BRF:6
BRH:6
end_of_record
TN:
SF:src/rules/validation/abstract/base/RuleSanctionsListBase.sol
-DA:32,49
+DA:32,82
FN:32,RuleSanctionsListBase.constructor
-FNDA:49,RuleSanctionsListBase.constructor
-DA:35,48
-BRDA:35,0,0,21
-DA:36,21
-DA:47,3
+FNDA:82,RuleSanctionsListBase.constructor
+DA:35,81
+BRDA:35,0,0,51
+DA:36,51
+DA:47,5
FN:47,RuleSanctionsListBase.canReturnTransferRestrictionCode
-FNDA:3,RuleSanctionsListBase.canReturnTransferRestrictionCode
-DA:48,3
-DA:49,1
+FNDA:5,RuleSanctionsListBase.canReturnTransferRestrictionCode
+DA:48,5
+DA:49,2
DA:61,18
FN:61,RuleSanctionsListBase.setSanctionListOracle
FNDA:18,RuleSanctionsListBase.setSanctionListOracle
@@ -2508,21 +1745,21 @@ DA:70,3
FN:70,RuleSanctionsListBase.clearSanctionListOracle
FNDA:3,RuleSanctionsListBase.clearSanctionListOracle
DA:71,3
-DA:77,9
+DA:77,18
FN:77,RuleSanctionsListBase.transferred.0
-FNDA:9,RuleSanctionsListBase.transferred.0
-DA:78,9
-DA:84,41
+FNDA:18,RuleSanctionsListBase.transferred.0
+DA:78,18
+DA:84,86
FN:84,RuleSanctionsListBase.transferred.1
-FNDA:41,RuleSanctionsListBase.transferred.1
-DA:85,41
-DA:91,4
+FNDA:86,RuleSanctionsListBase.transferred.1
+DA:85,86
+DA:91,5
FN:91,RuleSanctionsListBase.messageForTransferRestriction
-FNDA:4,RuleSanctionsListBase.messageForTransferRestriction
-DA:97,4
-BRDA:97,2,0,1
+FNDA:5,RuleSanctionsListBase.messageForTransferRestriction
+DA:97,5
+BRDA:97,2,0,2
BRDA:97,2,1,1
-DA:98,1
+DA:98,2
DA:99,3
BRDA:99,3,0,1
BRDA:99,3,1,1
@@ -2535,407 +1772,669 @@ DA:111,3
FN:111,RuleSanctionsListBase.onlySanctionListManager
FNDA:3,RuleSanctionsListBase.onlySanctionListManager
DA:112,3
-DA:124,39
+DA:124,69
FN:124,RuleSanctionsListBase._setSanctionListOracle
-FNDA:39,RuleSanctionsListBase._setSanctionListOracle
-DA:125,39
-DA:126,39
+FNDA:69,RuleSanctionsListBase._setSanctionListOracle
+DA:125,69
+DA:126,69
DA:133,0
FN:133,RuleSanctionsListBase._authorizeSanctionListManager
FNDA:0,RuleSanctionsListBase._authorizeSanctionListManager
-DA:141,119
-FN:141,RuleSanctionsListBase._detectTransferRestriction
-FNDA:119,RuleSanctionsListBase._detectTransferRestriction
-DA:151,119
-BRDA:151,5,0,112
-DA:152,112
-BRDA:152,6,0,27
-BRDA:152,6,1,73
-DA:153,27
-DA:154,85
-BRDA:154,7,0,12
-DA:155,12
-DA:158,80
-DA:169,69
-FN:169,RuleSanctionsListBase._detectTransferRestrictionFrom
-FNDA:69,RuleSanctionsListBase._detectTransferRestrictionFrom
-DA:176,69
-BRDA:176,8,0,68
-DA:177,68
-BRDA:177,9,0,6
-DA:178,6
-DA:180,62
-DA:182,1
-DA:191,19
-FN:191,RuleSanctionsListBase._transferred
-FNDA:19,RuleSanctionsListBase._transferred
-DA:192,19
-DA:193,19
-BRDA:193,10,0,10
-BRDA:193,10,1,9
-DA:206,48
-FN:206,RuleSanctionsListBase._transferredFrom
-FNDA:48,RuleSanctionsListBase._transferredFrom
-DA:207,48
-DA:208,48
-BRDA:208,11,0,6
-BRDA:208,11,1,42
+DA:147,212
+FN:147,RuleSanctionsListBase._detectTransferRestriction
+FNDA:212,RuleSanctionsListBase._detectTransferRestriction
+DA:160,212
+DA:161,212
+BRDA:161,5,0,198
+DA:162,198
+BRDA:162,6,0,40
+BRDA:162,6,1,141
+DA:163,40
+DA:164,158
+BRDA:164,7,0,17
+DA:165,17
+DA:168,155
+DA:179,124
+FN:179,RuleSanctionsListBase._detectTransferRestrictionFrom
+FNDA:124,RuleSanctionsListBase._detectTransferRestrictionFrom
+DA:186,124
+DA:191,124
+BRDA:191,8,0,8
+DA:192,8
+DA:194,116
+DA:203,34
+FN:203,RuleSanctionsListBase._transferred
+FNDA:34,RuleSanctionsListBase._transferred
+DA:204,34
+DA:205,34
+BRDA:205,9,0,16
+BRDA:205,9,1,18
+DA:218,93
+FN:218,RuleSanctionsListBase._transferredFrom
+FNDA:93,RuleSanctionsListBase._transferredFrom
+DA:219,93
+DA:220,93
+BRDA:220,10,0,6
+BRDA:220,10,1,87
FNF:14
FNH:13
LF:48
LH:47
-BRF:18
-BRH:18
+BRF:17
+BRH:17
end_of_record
TN:
SF:src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol
-DA:38,2
-FN:38,RuleSpenderWhitelistBase.canReturnTransferRestrictionCode
+DA:45,2
+FN:45,RuleSpenderWhitelistBase.canReturnTransferRestrictionCode
FNDA:2,RuleSpenderWhitelistBase.canReturnTransferRestrictionCode
-DA:39,2
-DA:49,3
-FN:49,RuleSpenderWhitelistBase.transferred.0
-FNDA:3,RuleSpenderWhitelistBase.transferred.0
-DA:54,6
-FN:54,RuleSpenderWhitelistBase.transferred.1
+DA:46,2
+DA:56,4
+FN:56,RuleSpenderWhitelistBase.transferred.0
+FNDA:4,RuleSpenderWhitelistBase.transferred.0
+DA:61,6
+FN:61,RuleSpenderWhitelistBase.transferred.1
FNDA:6,RuleSpenderWhitelistBase.transferred.1
-DA:55,6
-DA:61,2
-FN:61,RuleSpenderWhitelistBase.messageForTransferRestriction
+DA:62,6
+DA:68,2
+FN:68,RuleSpenderWhitelistBase.messageForTransferRestriction
FNDA:2,RuleSpenderWhitelistBase.messageForTransferRestriction
-DA:67,2
-BRDA:67,0,0,1
-DA:68,1
-DA:70,1
-DA:76,8
-FN:76,RuleSpenderWhitelistBase.supportsInterface
-FNDA:8,RuleSpenderWhitelistBase.supportsInterface
-DA:79,8
-DA:80,6
-DA:91,12
-FN:91,RuleSpenderWhitelistBase._detectTransferRestriction
-FNDA:12,RuleSpenderWhitelistBase._detectTransferRestriction
-DA:92,12
-DA:102,35
-FN:102,RuleSpenderWhitelistBase._detectTransferRestrictionFrom
-FNDA:35,RuleSpenderWhitelistBase._detectTransferRestrictionFrom
-DA:111,35
-BRDA:111,1,0,13
-DA:112,13
-DA:114,22
-DA:120,9
-FN:120,RuleSpenderWhitelistBase._transferred
-FNDA:9,RuleSpenderWhitelistBase._transferred
-DA:131,17
-FN:131,RuleSpenderWhitelistBase._transferredFrom
-FNDA:17,RuleSpenderWhitelistBase._transferredFrom
-DA:132,17
-DA:133,17
-BRDA:133,2,0,7
-BRDA:133,2,1,10
+DA:74,2
+BRDA:74,0,0,1
+DA:75,1
+DA:77,1
+DA:83,14
+FN:83,RuleSpenderWhitelistBase.supportsInterface
+FNDA:14,RuleSpenderWhitelistBase.supportsInterface
+DA:86,14
+DA:87,12
+DA:88,10
+DA:99,18
+FN:99,RuleSpenderWhitelistBase._detectTransferRestriction
+FNDA:18,RuleSpenderWhitelistBase._detectTransferRestriction
+DA:100,18
+DA:110,38
+FN:110,RuleSpenderWhitelistBase._detectTransferRestrictionFrom
+FNDA:38,RuleSpenderWhitelistBase._detectTransferRestrictionFrom
+DA:119,38
+BRDA:119,1,0,16
+DA:120,16
+DA:122,22
+DA:128,14
+FN:128,RuleSpenderWhitelistBase._transferred
+FNDA:14,RuleSpenderWhitelistBase._transferred
+DA:139,18
+FN:139,RuleSpenderWhitelistBase._transferredFrom
+FNDA:18,RuleSpenderWhitelistBase._transferredFrom
+DA:140,18
+DA:141,18
+BRDA:141,2,0,8
+BRDA:141,2,1,10
FNF:9
FNH:9
-LF:22
-LH:22
+LF:23
+LH:23
+BRF:4
+BRH:4
+end_of_record
+TN:
+SF:src/rules/validation/abstract/base/RuleWhitelistBase.sol
+DA:38,226
+FN:38,RuleWhitelistBase.constructor
+FNDA:226,RuleWhitelistBase.constructor
+DA:41,226
+DA:42,226
+DA:52,6
+FN:52,RuleWhitelistBase.isVerified
+FNDA:6,RuleWhitelistBase.isVerified
+DA:59,6
+DA:65,473
+FN:65,RuleWhitelistBase.supportsInterface
+FNDA:473,RuleWhitelistBase.supportsInterface
+DA:68,473
+DA:69,471
+DA:70,368
+DA:71,265
+DA:78,104
+FN:78,RuleWhitelistBase.isAllowList
+FNDA:104,RuleWhitelistBase.isAllowList
+DA:79,104
+DA:96,155
+FN:96,RuleWhitelistBase._detectTransferRestriction
+FNDA:155,RuleWhitelistBase._detectTransferRestriction
+DA:107,155
+DA:108,155
+DA:111,155
+DA:112,155
+BRDA:112,0,0,11
+DA:113,11
+DA:118,144
+BRDA:118,1,0,38
+DA:119,38
+DA:121,106
+BRDA:121,2,0,16
+DA:122,16
+DA:124,90
+DA:135,43
+FN:135,RuleWhitelistBase._detectTransferRestrictionFrom
+FNDA:43,RuleWhitelistBase._detectTransferRestrictionFrom
+DA:144,43
+BRDA:144,3,0,8
+DA:145,8
+DA:147,35
+FNF:6
+FNH:6
+LF:27
+LH:27
BRF:4
BRH:4
end_of_record
TN:
-SF:src/rules/validation/abstract/base/RuleWhitelistBase.sol
-DA:32,201
-FN:32,RuleWhitelistBase.constructor
-FNDA:201,RuleWhitelistBase.constructor
-DA:35,201
-DA:36,201
-DA:48,3
-FN:48,RuleWhitelistBase.setCheckSpender
-FNDA:3,RuleWhitelistBase.setCheckSpender
-DA:49,2
-DA:50,2
-DA:56,6
-FN:56,RuleWhitelistBase.isVerified
-FNDA:6,RuleWhitelistBase.isVerified
-DA:63,6
-DA:69,61
-FN:69,RuleWhitelistBase.supportsInterface
-FNDA:61,RuleWhitelistBase.supportsInterface
-DA:72,61
-DA:73,59
-DA:80,3
-FN:80,RuleWhitelistBase.onlyCheckSpenderManager
-FNDA:3,RuleWhitelistBase.onlyCheckSpenderManager
-DA:81,3
-DA:93,2
-FN:93,RuleWhitelistBase._setCheckSpender
-FNDA:2,RuleWhitelistBase._setCheckSpender
-DA:94,2
-DA:101,0
-FN:101,RuleWhitelistBase._authorizeCheckSpenderManager
-FNDA:0,RuleWhitelistBase._authorizeCheckSpenderManager
-DA:109,132
-FN:109,RuleWhitelistBase._detectTransferRestriction
-FNDA:132,RuleWhitelistBase._detectTransferRestriction
-DA:120,132
-DA:121,132
-DA:124,132
-DA:125,132
-BRDA:125,0,0,9
-DA:126,9
-DA:131,123
-BRDA:131,1,0,30
-DA:132,30
-DA:134,93
-BRDA:134,2,0,15
-DA:135,15
-DA:137,78
-DA:148,38
-FN:148,RuleWhitelistBase._detectTransferRestrictionFrom
-FNDA:38,RuleWhitelistBase._detectTransferRestrictionFrom
-DA:157,38
-BRDA:157,3,0,8
-DA:158,8
-DA:160,30
-FNF:9
-FNH:8
-LF:31
-LH:30
-BRF:4
-BRH:4
+SF:src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol
+DA:47,64
+FN:47,RuleWhitelistWrapperBase.constructor
+FNDA:64,RuleWhitelistWrapperBase.constructor
+DA:50,64
+DA:51,64
+DA:61,52
+FN:61,RuleWhitelistWrapperBase.supportsInterface
+FNDA:52,RuleWhitelistWrapperBase.supportsInterface
+DA:62,52
+DA:73,9
+FN:73,RuleWhitelistWrapperBase.isVerified
+FNDA:9,RuleWhitelistWrapperBase.isVerified
+DA:74,9
+DA:88,82
+FN:88,RuleWhitelistWrapperBase._detectTransferRestriction
+FNDA:82,RuleWhitelistWrapperBase._detectTransferRestriction
+DA:100,82
+DA:101,82
+BRDA:101,0,0,4
+DA:102,4
+DA:105,78
+DA:106,78
+DA:113,78
+BRDA:113,1,0,2
+DA:114,2
+DA:116,3
+BRDA:116,2,0,3
+DA:117,3
+BRDA:117,3,0,1
+DA:118,1
+DA:120,2
+DA:122,2
+BRDA:122,4,0,2
+DA:123,2
+BRDA:123,5,0,1
+DA:124,1
+DA:126,1
+DA:129,71
+DA:130,71
+DA:131,71
+DA:133,71
+DA:134,71
+BRDA:134,6,0,30
+BRDA:134,6,1,32
+DA:135,30
+DA:136,41
+BRDA:136,7,0,9
+BRDA:136,7,1,32
+DA:137,9
+DA:139,32
+DA:148,14
+FN:148,RuleWhitelistWrapperBase._isListedInAnyChild
+FNDA:14,RuleWhitelistWrapperBase._isListedInAnyChild
+DA:149,14
+DA:150,14
+DA:151,14
+DA:162,38
+FN:162,RuleWhitelistWrapperBase._detectTransferRestrictionFrom
+FNDA:38,RuleWhitelistWrapperBase._detectTransferRestrictionFrom
+DA:171,38
+BRDA:171,8,0,2
+DA:172,2
+DA:175,36
+DA:176,36
+DA:177,36
+DA:178,36
+DA:180,36
+DA:182,36
+BRDA:182,9,0,9
+BRDA:182,9,1,18
+DA:183,9
+DA:184,27
+BRDA:184,10,0,1
+BRDA:184,10,1,18
+DA:185,1
+DA:186,26
+BRDA:186,11,0,8
+BRDA:186,11,1,18
+DA:187,8
+DA:189,18
+DA:201,28
+FN:201,RuleWhitelistWrapperBase._transferred.0
+FNDA:28,RuleWhitelistWrapperBase._transferred.0
+DA:207,28
+DA:217,1
+FN:217,RuleWhitelistWrapperBase._transferred.1
+FNDA:1,RuleWhitelistWrapperBase._transferred.1
+DA:223,1
+DA:243,106
+FN:243,RuleWhitelistWrapperBase._checkRule
+FNDA:106,RuleWhitelistWrapperBase._checkRule
+DA:244,106
+DA:245,106
+BRDA:245,12,0,2
+BRDA:245,12,1,104
+DA:251,104
+BRDA:251,13,0,1
+BRDA:251,13,1,103
+DA:255,103
+BRDA:255,14,0,1
+BRDA:255,14,1,102
+DA:263,121
+FN:263,RuleWhitelistWrapperBase._detectTransferRestrictionForTargets
+FNDA:121,RuleWhitelistWrapperBase._detectTransferRestrictionForTargets
+DA:269,121
+DA:270,121
+DA:271,121
+DA:275,121
+DA:276,121
+DA:279,174
+DA:280,174
+DA:281,386
+BRDA:281,15,0,188
+DA:282,188
+DA:283,188
+DA:288,174
+BRDA:288,16,0,57
+DA:289,57
+DA:292,121
+DA:303,189
+FN:303,RuleWhitelistWrapperBase._msgSender
+FNDA:189,RuleWhitelistWrapperBase._msgSender
+DA:304,189
+DA:311,2
+FN:311,RuleWhitelistWrapperBase._msgData
+FNDA:2,RuleWhitelistWrapperBase._msgData
+DA:312,2
+DA:319,191
+FN:319,RuleWhitelistWrapperBase._contextSuffixLength
+FNDA:191,RuleWhitelistWrapperBase._contextSuffixLength
+DA:320,191
+FNF:13
+FNH:13
+LF:80
+LH:80
+BRF:25
+BRH:25
+end_of_record
+TN:
+SF:src/rules/validation/abstract/core/BalanceCapManager.sol
+DA:43,4
+FN:43,BalanceCapManager.onlyMaxBalanceManager
+FNDA:4,BalanceCapManager.onlyMaxBalanceManager
+DA:44,4
+DA:59,6
+FN:59,BalanceCapManager.setMaxBalance
+FNDA:6,BalanceCapManager.setMaxBalance
+DA:60,4
+DA:67,5
+FN:67,BalanceCapManager.setBalanceToken
+FNDA:5,BalanceCapManager.setBalanceToken
+DA:68,4
+DA:78,12
+FN:78,BalanceCapManager.addExemptAddress
+FNDA:12,BalanceCapManager.addExemptAddress
+DA:79,10
+DA:88,3
+FN:88,BalanceCapManager.removeExemptAddress
+FNDA:3,BalanceCapManager.removeExemptAddress
+DA:89,3
+DA:98,4
+FN:98,BalanceCapManager.addExemptAddresses
+FNDA:4,BalanceCapManager.addExemptAddresses
+DA:99,4
+DA:100,3
+DA:108,2
+FN:108,BalanceCapManager.removeExemptAddresses
+FNDA:2,BalanceCapManager.removeExemptAddresses
+DA:109,2
+DA:110,2
+DA:118,5
+FN:118,BalanceCapManager.isExemptAddress
+FNDA:5,BalanceCapManager.isExemptAddress
+DA:119,5
+DA:126,6
+FN:126,BalanceCapManager.exemptAddressCount
+FNDA:6,BalanceCapManager.exemptAddressCount
+DA:127,6
+DA:146,10
+FN:146,BalanceCapManager._addExemptAddress
+FNDA:10,BalanceCapManager._addExemptAddress
+DA:147,10
+BRDA:147,0,0,1
+BRDA:147,0,1,9
+DA:148,9
+BRDA:148,1,0,1
+BRDA:148,1,1,8
+DA:149,8
+DA:156,3
+FN:156,BalanceCapManager._removeExemptAddress
+FNDA:3,BalanceCapManager._removeExemptAddress
+DA:157,3
+BRDA:157,2,0,1
+BRDA:157,2,1,2
+DA:158,2
+DA:165,64
+FN:165,BalanceCapManager._setMaxBalance
+FNDA:64,BalanceCapManager._setMaxBalance
+DA:166,64
+DA:167,64
+DA:176,67
+FN:176,BalanceCapManager._setBalanceToken
+FNDA:67,BalanceCapManager._setBalanceToken
+DA:177,67
+BRDA:177,3,0,1
+BRDA:177,3,1,66
+DA:180,66
+BRDA:180,4,0,2
+BRDA:180,4,1,64
+DA:181,64
+BRDA:181,5,0,64
+DA:184,2
+BRDA:184,5,1,2
+DA:185,2
+DA:187,62
+DA:188,62
+DA:195,0
+FN:195,BalanceCapManager._authorizeMaxBalanceManager
+FNDA:0,BalanceCapManager._authorizeMaxBalanceManager
+DA:208,5
+FN:208,BalanceCapManager._remainingCapacity
+FNDA:5,BalanceCapManager._remainingCapacity
+DA:209,5
+BRDA:209,6,0,1
+DA:210,1
+DA:212,4
+DA:213,4
+BRDA:213,7,0,1
+DA:214,1
+DA:216,3
+DA:228,44
+FN:228,BalanceCapManager._balanceOf
+FNDA:44,BalanceCapManager._balanceOf
+DA:229,44
+BRDA:229,8,0,44
+DA:230,41
+DA:231,3
+BRDA:231,8,1,3
+DA:232,3
+DA:248,46
+FN:248,BalanceCapManager._capExceeded
+FNDA:46,BalanceCapManager._capExceeded
+DA:256,46
+BRDA:256,9,0,6
+DA:257,6
+DA:259,40
+DA:260,40
+DA:261,40
+BRDA:261,10,0,2
+DA:262,2
+DA:264,38
+FNF:17
+FNH:16
+LF:59
+LH:58
+BRF:18
+BRH:18
+end_of_record
+TN:
+SF:src/rules/validation/abstract/core/CapAccounting.sol
+DA:31,1355
+FN:31,CapAccounting._capExceededBy
+FNDA:1355,CapAccounting._capExceededBy
+DA:33,1355
+BRDA:33,0,0,444
+DA:34,444
+DA:36,911
+DA:45,3
+FN:45,CapAccounting._capHeadroom
+FNDA:3,CapAccounting._capHeadroom
+DA:46,3
+FNF:2
+FNH:2
+LF:6
+LH:6
+BRF:1
+BRH:1
end_of_record
TN:
-SF:src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol
-DA:37,57
-FN:37,RuleWhitelistWrapperBase.constructor
-FNDA:57,RuleWhitelistWrapperBase.constructor
-DA:40,57
-DA:41,57
-DA:48,4
-FN:48,RuleWhitelistWrapperBase.onlyCheckSpenderManager
-FNDA:4,RuleWhitelistWrapperBase.onlyCheckSpenderManager
-DA:49,4
-DA:65,4
-FN:65,RuleWhitelistWrapperBase.setCheckSpender
-FNDA:4,RuleWhitelistWrapperBase.setCheckSpender
-DA:66,3
-DA:67,3
-DA:73,49
-FN:73,RuleWhitelistWrapperBase.supportsInterface
-FNDA:49,RuleWhitelistWrapperBase.supportsInterface
-DA:74,49
-DA:83,7
-FN:83,RuleWhitelistWrapperBase.isVerified
-FNDA:7,RuleWhitelistWrapperBase.isVerified
-DA:84,7
-DA:85,7
-DA:86,7
-DA:87,7
-DA:98,3
-FN:98,RuleWhitelistWrapperBase._setCheckSpender
-FNDA:3,RuleWhitelistWrapperBase._setCheckSpender
-DA:99,3
-DA:108,0
-FN:108,RuleWhitelistWrapperBase._authorizeCheckSpenderManager
-FNDA:0,RuleWhitelistWrapperBase._authorizeCheckSpenderManager
-DA:117,66
-FN:117,RuleWhitelistWrapperBase._detectTransferRestriction
-FNDA:66,RuleWhitelistWrapperBase._detectTransferRestriction
-DA:129,66
-DA:130,66
-BRDA:130,0,0,4
-DA:131,4
-DA:134,62
-DA:135,62
-DA:142,62
-BRDA:142,1,0,2
-DA:143,2
-DA:145,3
-BRDA:145,2,0,3
-DA:146,3
-BRDA:146,3,0,1
-DA:147,1
-DA:149,2
-DA:151,2
-BRDA:151,4,0,2
-DA:152,2
-BRDA:152,5,0,1
-DA:153,1
-DA:155,1
-DA:158,55
-DA:159,55
-DA:160,55
-DA:162,55
-DA:163,54
-BRDA:163,6,0,22
-BRDA:163,6,1,24
-DA:164,22
-DA:165,32
-BRDA:165,7,0,8
-BRDA:165,7,1,24
-DA:166,8
-DA:168,24
-DA:177,5
-FN:177,RuleWhitelistWrapperBase._isListedInAnyChild
-FNDA:5,RuleWhitelistWrapperBase._isListedInAnyChild
-DA:178,5
-DA:179,5
-DA:180,5
-DA:191,37
-FN:191,RuleWhitelistWrapperBase._detectTransferRestrictionFrom
-FNDA:37,RuleWhitelistWrapperBase._detectTransferRestrictionFrom
-DA:200,37
-BRDA:200,8,0,2
-DA:201,2
-DA:204,35
-DA:205,35
-DA:206,35
-DA:207,35
-DA:209,35
-DA:211,35
-BRDA:211,9,0,9
-BRDA:211,9,1,17
-DA:212,9
-DA:213,26
-BRDA:213,10,0,1
-BRDA:213,10,1,17
-DA:214,1
-DA:215,25
-BRDA:215,11,0,8
-BRDA:215,11,1,17
-DA:216,8
-DA:218,17
-DA:230,20
-FN:230,RuleWhitelistWrapperBase._transferred.0
-FNDA:20,RuleWhitelistWrapperBase._transferred.0
-DA:236,20
-DA:246,1
-FN:246,RuleWhitelistWrapperBase._transferred.1
-FNDA:1,RuleWhitelistWrapperBase._transferred.1
-DA:252,1
-DA:260,102
-FN:260,RuleWhitelistWrapperBase._detectTransferRestrictionForTargets
-FNDA:102,RuleWhitelistWrapperBase._detectTransferRestrictionForTargets
-DA:266,102
-DA:267,102
-DA:268,102
-DA:271,153
-DA:272,152
-DA:273,160
-BRDA:273,12,0,160
-DA:274,160
-DA:279,152
-DA:280,152
-DA:281,278
-BRDA:281,13,0,105
-DA:282,105
-DA:283,105
-DA:286,47
-BRDA:286,14,0,47
-DA:287,47
-DA:290,101
-DA:301,175
-FN:301,RuleWhitelistWrapperBase._msgSender
-FNDA:175,RuleWhitelistWrapperBase._msgSender
-DA:302,175
-DA:309,2
-FN:309,RuleWhitelistWrapperBase._msgData
-FNDA:2,RuleWhitelistWrapperBase._msgData
-DA:310,2
-DA:317,177
-FN:317,RuleWhitelistWrapperBase._contextSuffixLength
-FNDA:177,RuleWhitelistWrapperBase._contextSuffixLength
-DA:318,177
-FNF:16
-FNH:15
-LF:88
-LH:87
-BRF:20
-BRH:20
+SF:src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol
+DA:52,8
+FN:52,ChainlinkPoRFeedManager.onlyChainlinkPoRManager
+FNDA:8,ChainlinkPoRFeedManager.onlyChainlinkPoRManager
+DA:53,8
+DA:67,8
+FN:67,ChainlinkPoRFeedManager.setReservesFeed
+FNDA:8,ChainlinkPoRFeedManager.setReservesFeed
+DA:68,6
+DA:77,12
+FN:77,ChainlinkPoRFeedManager.setTokenMetadata
+FNDA:12,ChainlinkPoRFeedManager.setTokenMetadata
+DA:78,10
+DA:85,8
+FN:85,ChainlinkPoRFeedManager.setMaxStalenessSeconds
+FNDA:8,ChainlinkPoRFeedManager.setMaxStalenessSeconds
+DA:86,5
+DA:96,4
+FN:96,ChainlinkPoRFeedManager.feedDecimals
+FNDA:4,ChainlinkPoRFeedManager.feedDecimals
+DA:97,4
+DA:110,658
+FN:110,ChainlinkPoRFeedManager.maxBackedSupply
+FNDA:658,ChainlinkPoRFeedManager.maxBackedSupply
+DA:111,658
+DA:126,630
+FN:126,ChainlinkPoRFeedManager._setReservesFeed
+FNDA:630,ChainlinkPoRFeedManager._setReservesFeed
+DA:127,630
+DA:128,630
+BRDA:128,0,0,1
+BRDA:128,0,1,629
+DA:129,629
+BRDA:129,1,0,1
+BRDA:129,1,1,628
+DA:130,628
+DA:131,628
+BRDA:131,2,0,628
+DA:132,627
+DA:133,1
+BRDA:133,2,1,1
+DA:134,1
+DA:136,627
+BRDA:136,3,0,1
+BRDA:136,3,1,626
+DA:137,626
+DA:138,626
+DA:149,632
+FN:149,ChainlinkPoRFeedManager._setTokenMetadata
+FNDA:632,ChainlinkPoRFeedManager._setTokenMetadata
+DA:150,632
+BRDA:150,4,0,2
+BRDA:150,4,1,630
+DA:154,630
+BRDA:154,5,0,2
+BRDA:154,5,1,628
+DA:155,628
+BRDA:155,6,0,1
+BRDA:155,6,1,627
+DA:156,627
+BRDA:156,7,0,627
+DA:157,611
+BRDA:157,8,0,1
+BRDA:157,8,1,610
+DA:166,626
+BRDA:166,9,0,1
+BRDA:166,9,1,625
+DA:169,625
+DA:170,625
+DA:171,625
+DA:178,625
+FN:178,ChainlinkPoRFeedManager._setMaxStalenessSeconds
+FNDA:625,ChainlinkPoRFeedManager._setMaxStalenessSeconds
+DA:179,625
+DA:180,625
+DA:187,0
+FN:187,ChainlinkPoRFeedManager._authorizeChainlinkPoRManager
+FNDA:0,ChainlinkPoRFeedManager._authorizeChainlinkPoRManager
+DA:195,1278
+FN:195,ChainlinkPoRFeedManager._maxBackedSupply
+FNDA:1278,ChainlinkPoRFeedManager._maxBackedSupply
+DA:196,1278
+DA:199,1278
+DA:200,1278
+BRDA:200,10,0,1278
+DA:201,1276
+DA:202,2
+BRDA:202,10,1,2
+DA:203,2
+DA:207,1276
+BRDA:207,11,0,3
+DA:208,3
+DA:210,1273
+BRDA:210,12,0,1273
+DA:216,1270
+BRDA:216,13,0,144
+DA:217,144
+DA:219,1126
+DA:221,1126
+BRDA:221,14,0,3
+DA:222,3
+DA:226,1123
+DA:227,1123
+DA:228,3
+BRDA:228,12,1,3
+DA:229,3
+DA:236,469
+FN:236,ChainlinkPoRFeedManager._supplyToken
+FNDA:469,ChainlinkPoRFeedManager._supplyToken
+DA:237,469
+DA:249,1123
+FN:249,ChainlinkPoRFeedManager._scaleReserve
+FNDA:1123,ChainlinkPoRFeedManager._scaleReserve
+DA:250,1123
+DA:251,1123
+BRDA:251,15,0,74
+DA:252,74
+DA:254,1049
+BRDA:254,16,0,614
+DA:256,614
+DA:257,614
+BRDA:257,17,0,26
+DA:258,26
+DA:260,588
+DA:263,435
+FNF:13
+FNH:12
+LF:69
+LH:68
+BRF:29
+BRH:29
end_of_record
TN:
SF:src/rules/validation/abstract/core/RuleNFTAdapter.sol
-DA:46,34
-FN:46,RuleNFTAdapter.transferred.0
-FNDA:34,RuleNFTAdapter.transferred.0
-DA:47,34
-BRDA:47,0,0,17
-BRDA:47,0,1,17
-DA:48,17
-DA:50,17
-DA:57,36
-FN:57,RuleNFTAdapter.transferred.1
-FNDA:36,RuleNFTAdapter.transferred.1
-DA:58,36
-BRDA:58,1,0,17
-BRDA:58,1,1,19
-DA:59,17
-DA:61,19
-DA:72,28
-FN:72,RuleNFTAdapter.transferred.2
+DA:56,47
+FN:56,RuleNFTAdapter.transferred.0
+FNDA:47,RuleNFTAdapter.transferred.0
+DA:57,47
+BRDA:57,0,0,17
+BRDA:57,0,1,30
+DA:58,17
+DA:60,30
+DA:67,50
+FN:67,RuleNFTAdapter.transferred.1
+FNDA:50,RuleNFTAdapter.transferred.1
+DA:68,50
+BRDA:68,1,0,17
+BRDA:68,1,1,33
+DA:69,17
+DA:71,33
+DA:82,28
+FN:82,RuleNFTAdapter.transferred.2
FNDA:28,RuleNFTAdapter.transferred.2
-DA:83,28
-DA:89,23
-FN:89,RuleNFTAdapter.transferred.3
-FNDA:23,RuleNFTAdapter.transferred.3
-DA:101,23
-DA:107,31
-FN:107,RuleNFTAdapter.detectTransferRestriction
-FNDA:31,RuleNFTAdapter.detectTransferRestriction
-DA:120,31
-DA:126,27
-FN:126,RuleNFTAdapter.detectTransferRestrictionFrom
-FNDA:27,RuleNFTAdapter.detectTransferRestrictionFrom
-DA:140,27
-DA:146,29
-FN:146,RuleNFTAdapter.canTransfer
+DA:93,28
+DA:99,38
+FN:99,RuleNFTAdapter.transferred.3
+FNDA:38,RuleNFTAdapter.transferred.3
+DA:111,38
+BRDA:111,2,0,24
+BRDA:111,2,1,14
+DA:112,24
+DA:114,14
+DA:121,32
+FN:121,RuleNFTAdapter.detectTransferRestriction
+FNDA:32,RuleNFTAdapter.detectTransferRestriction
+DA:134,32
+DA:140,42
+FN:140,RuleNFTAdapter.detectTransferRestrictionFrom
+FNDA:42,RuleNFTAdapter.detectTransferRestrictionFrom
+DA:154,80
+DA:162,29
+FN:162,RuleNFTAdapter.canTransfer
FNDA:29,RuleNFTAdapter.canTransfer
-DA:158,29
-DA:164,25
-FN:164,RuleNFTAdapter.canTransferFrom
-FNDA:25,RuleNFTAdapter.canTransferFrom
-DA:178,25
-DA:192,0
-FN:192,RuleNFTAdapter._transferred
+DA:175,29
+DA:181,38
+FN:181,RuleNFTAdapter.canTransferFrom
+FNDA:38,RuleNFTAdapter.canTransferFrom
+DA:195,38
+DA:214,215
+FN:214,RuleNFTAdapter._isDelegated
+FNDA:215,RuleNFTAdapter._isDelegated
+DA:215,215
+DA:224,0
+FN:224,RuleNFTAdapter._transferred
FNDA:0,RuleNFTAdapter._transferred
-DA:201,0
-FN:201,RuleNFTAdapter._transferredFrom
+DA:233,0
+FN:233,RuleNFTAdapter._transferredFrom
FNDA:0,RuleNFTAdapter._transferredFrom
-FNF:10
-FNH:8
-LF:22
-LH:20
-BRF:4
-BRH:4
+FNF:11
+FNH:9
+LF:26
+LH:24
+BRF:6
+BRH:6
end_of_record
TN:
SF:src/rules/validation/abstract/core/RuleTransferValidation.sol
-DA:36,1526
+DA:36,1615
FN:36,RuleTransferValidation.detectTransferRestriction
-FNDA:1526,RuleTransferValidation.detectTransferRestriction
-DA:43,1526
-DA:49,66
+FNDA:1615,RuleTransferValidation.detectTransferRestriction
+DA:43,1615
+DA:49,95
FN:49,RuleTransferValidation.detectTransferRestrictionFrom
-FNDA:66,RuleTransferValidation.detectTransferRestrictionFrom
-DA:56,66
-DA:67,43
+FNDA:95,RuleTransferValidation.detectTransferRestrictionFrom
+DA:56,95
+DA:67,64
FN:67,RuleTransferValidation.canTransfer
-FNDA:43,RuleTransferValidation.canTransfer
-DA:73,43
-DA:79,33
-FN:79,RuleTransferValidation.canTransferFrom
-FNDA:33,RuleTransferValidation.canTransferFrom
-DA:86,33
-DA:95,280
-FN:95,RuleTransferValidation.supportsInterface
-FNDA:280,RuleTransferValidation.supportsInterface
-DA:96,280
-DA:97,277
-DA:98,274
-DA:99,157
-DA:113,0
-FN:113,RuleTransferValidation._detectTransferRestriction
+FNDA:64,RuleTransferValidation.canTransfer
+DA:74,64
+DA:80,36
+FN:80,RuleTransferValidation.canTransferFrom
+FNDA:36,RuleTransferValidation.canTransferFrom
+DA:87,36
+DA:96,655
+FN:96,RuleTransferValidation.supportsInterface
+FNDA:655,RuleTransferValidation.supportsInterface
+DA:97,655
+DA:98,652
+DA:99,649
+DA:100,452
+DA:114,0
+FN:114,RuleTransferValidation._detectTransferRestriction
FNDA:0,RuleTransferValidation._detectTransferRestriction
-DA:127,0
-FN:127,RuleTransferValidation._detectTransferRestrictionFrom
+DA:128,0
+FN:128,RuleTransferValidation._detectTransferRestrictionFrom
FNDA:0,RuleTransferValidation._detectTransferRestrictionFrom
FNF:7
FNH:5
@@ -2950,121 +2449,227 @@ DA:46,32
FN:46,RuleWhitelistShared.onlyMintBurnManager
FNDA:32,RuleWhitelistShared.onlyMintBurnManager
DA:47,32
-DA:62,10
-FN:62,RuleWhitelistShared.canReturnTransferRestrictionCode
+DA:51,8
+FN:51,RuleWhitelistShared.onlyCheckSpenderManager
+FNDA:8,RuleWhitelistShared.onlyCheckSpenderManager
+DA:52,8
+DA:67,10
+FN:67,RuleWhitelistShared.canReturnTransferRestrictionCode
FNDA:10,RuleWhitelistShared.canReturnTransferRestrictionCode
-DA:63,10
-DA:64,5
-DA:65,2
-DA:66,2
-DA:76,19
-FN:76,RuleWhitelistShared.messageForTransferRestriction
+DA:68,10
+DA:69,5
+DA:70,2
+DA:71,2
+DA:81,19
+FN:81,RuleWhitelistShared.messageForTransferRestriction
FNDA:19,RuleWhitelistShared.messageForTransferRestriction
-DA:82,19
-BRDA:82,0,0,6
-BRDA:82,0,1,2
-DA:83,6
-DA:84,13
-BRDA:84,1,0,4
-BRDA:84,1,1,2
-DA:85,4
-DA:86,9
-BRDA:86,2,0,2
-BRDA:86,2,1,2
-DA:87,2
-DA:88,7
-BRDA:88,3,0,3
-BRDA:88,3,1,2
-DA:89,3
+DA:87,19
+BRDA:87,0,0,6
+BRDA:87,0,1,2
+DA:88,6
+DA:89,13
+BRDA:89,1,0,4
+BRDA:89,1,1,2
DA:90,4
-BRDA:90,4,0,2
-BRDA:90,4,1,2
-DA:91,2
-DA:93,2
-DA:105,32
-FN:105,RuleWhitelistShared.setAllowMint
+DA:91,9
+BRDA:91,2,0,2
+BRDA:91,2,1,2
+DA:92,2
+DA:93,7
+BRDA:93,3,0,3
+BRDA:93,3,1,2
+DA:94,3
+DA:95,4
+BRDA:95,4,0,2
+BRDA:95,4,1,2
+DA:96,2
+DA:98,2
+DA:111,8
+FN:111,RuleWhitelistShared.setCheckSpender
+FNDA:8,RuleWhitelistShared.setCheckSpender
+DA:112,6
+DA:119,32
+FN:119,RuleWhitelistShared.setAllowMint
FNDA:32,RuleWhitelistShared.setAllowMint
-DA:106,29
-DA:107,29
-DA:114,8
-FN:114,RuleWhitelistShared.setAllowBurn
+DA:120,29
+DA:121,29
+DA:128,8
+FN:128,RuleWhitelistShared.setAllowBurn
FNDA:8,RuleWhitelistShared.setAllowBurn
-DA:115,6
-DA:116,6
-DA:130,35
-FN:130,RuleWhitelistShared.transferred.0
-FNDA:35,RuleWhitelistShared.transferred.0
-DA:131,35
-DA:145,13
-FN:145,RuleWhitelistShared.transferred.1
-FNDA:13,RuleWhitelistShared.transferred.1
-DA:146,13
-DA:158,258
-FN:158,RuleWhitelistShared._setAllowMintBurn
-FNDA:258,RuleWhitelistShared._setAllowMintBurn
-DA:159,258
-DA:160,258
-DA:161,258
-DA:162,258
-DA:172,198
-FN:172,RuleWhitelistShared._detectMintBurnRestriction
-FNDA:198,RuleWhitelistShared._detectMintBurnRestriction
-DA:173,198
-BRDA:173,5,0,9
-DA:174,9
-DA:176,189
-BRDA:176,6,0,4
-DA:177,4
-DA:179,185
-DA:185,0
-FN:185,RuleWhitelistShared._authorizeMintBurnManager
+DA:129,6
+DA:130,6
+DA:144,40
+FN:144,RuleWhitelistShared.transferred.0
+FNDA:40,RuleWhitelistShared.transferred.0
+DA:145,40
+DA:159,18
+FN:159,RuleWhitelistShared.transferred.1
+FNDA:18,RuleWhitelistShared.transferred.1
+DA:160,18
+DA:175,296
+FN:175,RuleWhitelistShared._setCheckSpender
+FNDA:296,RuleWhitelistShared._setCheckSpender
+DA:176,296
+DA:177,296
+DA:185,290
+FN:185,RuleWhitelistShared._setAllowMintBurn
+FNDA:290,RuleWhitelistShared._setAllowMintBurn
+DA:186,290
+DA:187,290
+DA:188,290
+DA:189,290
+DA:199,237
+FN:199,RuleWhitelistShared._detectMintBurnRestriction
+FNDA:237,RuleWhitelistShared._detectMintBurnRestriction
+DA:200,237
+BRDA:200,5,0,11
+DA:201,11
+DA:203,226
+BRDA:203,6,0,4
+DA:204,4
+DA:206,222
+DA:212,0
+FN:212,RuleWhitelistShared._authorizeMintBurnManager
FNDA:0,RuleWhitelistShared._authorizeMintBurnManager
-DA:190,56
-FN:190,RuleWhitelistShared._transferred
-FNDA:56,RuleWhitelistShared._transferred
-DA:191,56
-DA:192,56
-BRDA:192,7,0,24
-BRDA:192,7,1,32
-DA:201,32
-FN:201,RuleWhitelistShared._transferredFrom
-FNDA:32,RuleWhitelistShared._transferredFrom
-DA:202,32
-DA:203,32
-BRDA:203,8,0,12
-BRDA:203,8,1,20
-FNF:12
-FNH:11
-LF:47
-LH:46
+DA:219,0
+FN:219,RuleWhitelistShared._authorizeCheckSpenderManager
+FNDA:0,RuleWhitelistShared._authorizeCheckSpenderManager
+DA:224,73
+FN:224,RuleWhitelistShared._transferred
+FNDA:73,RuleWhitelistShared._transferred
+DA:225,73
+DA:226,73
+BRDA:226,7,0,33
+BRDA:226,7,1,40
+DA:235,37
+FN:235,RuleWhitelistShared._transferredFrom
+FNDA:37,RuleWhitelistShared._transferredFrom
+DA:236,37
+DA:237,37
+BRDA:237,8,0,13
+BRDA:237,8,1,24
+FNF:16
+FNH:14
+LF:55
+LH:53
BRF:16
BRH:16
end_of_record
TN:
+SF:src/rules/validation/abstract/core/TokenSupplyReader.sol
+DA:31,0
+FN:31,TokenSupplyReader._supplyToken
+FNDA:0,TokenSupplyReader._supplyToken
+DA:43,1323
+FN:43,TokenSupplyReader._currentSupply
+FNDA:1323,TokenSupplyReader._currentSupply
+DA:44,1323
+BRDA:44,0,0,1323
+DA:45,1315
+DA:46,8
+BRDA:46,0,1,8
+DA:47,8
+DA:65,1216
+FN:65,TokenSupplyReader._probeTotalSupplyCallable
+FNDA:1216,TokenSupplyReader._probeTotalSupplyCallable
+DA:66,1216
+BRDA:66,1,0,1216
+DA:67,1213
+DA:68,3
+BRDA:68,1,1,3
+DA:69,3
+FNF:3
+FNH:2
+LF:11
+LH:10
+BRF:4
+BRH:4
+end_of_record
+TN:
+SF:src/rules/validation/abstract/core/TotalSupplyCapManager.sol
+DA:38,263
+FN:38,TotalSupplyCapManager.onlyMaxTotalSupplyManager
+FNDA:263,TotalSupplyCapManager.onlyMaxTotalSupplyManager
+DA:39,263
+DA:51,264
+FN:51,TotalSupplyCapManager.setMaxTotalSupply
+FNDA:264,TotalSupplyCapManager.setMaxTotalSupply
+DA:52,261
+DA:59,8
+FN:59,TotalSupplyCapManager.setTokenContract
+FNDA:8,TotalSupplyCapManager.setTokenContract
+DA:60,6
+DA:73,846
+FN:73,TotalSupplyCapManager._setMaxTotalSupply
+FNDA:846,TotalSupplyCapManager._setMaxTotalSupply
+DA:74,846
+DA:75,846
+DA:83,594
+FN:83,TotalSupplyCapManager._setTokenContract
+FNDA:594,TotalSupplyCapManager._setTokenContract
+DA:84,594
+DA:85,588
+DA:86,588
+DA:97,594
+FN:97,TotalSupplyCapManager._validateTokenContract
+FNDA:594,TotalSupplyCapManager._validateTokenContract
+DA:98,594
+BRDA:98,0,0,2
+BRDA:98,0,1,592
+DA:99,592
+BRDA:99,1,0,2
+BRDA:99,1,1,590
+DA:100,590
+BRDA:100,2,0,2
+BRDA:100,2,1,588
+DA:107,0
+FN:107,TotalSupplyCapManager._authorizeMaxTotalSupplyManager
+FNDA:0,TotalSupplyCapManager._authorizeMaxTotalSupplyManager
+DA:112,854
+FN:112,TotalSupplyCapManager._supplyToken
+FNDA:854,TotalSupplyCapManager._supplyToken
+DA:113,854
+DA:126,856
+FN:126,TotalSupplyCapManager._capExceeded
+FNDA:856,TotalSupplyCapManager._capExceeded
+DA:127,856
+DA:128,856
+DA:129,856
+BRDA:129,3,0,4
+DA:130,4
+DA:132,852
+FNF:9
+FNH:8
+LF:26
+LH:25
+BRF:7
+BRH:7
+end_of_record
+TN:
SF:src/rules/validation/deployment/RuleBlacklist.sol
-DA:37,92
+DA:37,157
FN:37,RuleBlacklist.supportsInterface
-FNDA:92,RuleBlacklist.supportsInterface
-DA:44,92
-DA:45,62
-DA:55,30
+FNDA:157,RuleBlacklist.supportsInterface
+DA:44,157
+DA:45,106
+DA:55,39
FN:55,RuleBlacklist._authorizeAddressListAdd
-FNDA:30,RuleBlacklist._authorizeAddressListAdd
-DA:60,1
+FNDA:39,RuleBlacklist._authorizeAddressListAdd
+DA:60,2
FN:60,RuleBlacklist._authorizeAddressListRemove
-FNDA:1,RuleBlacklist._authorizeAddressListRemove
-DA:70,88
+FNDA:2,RuleBlacklist._authorizeAddressListRemove
+DA:70,128
FN:70,RuleBlacklist._msgSender
-FNDA:88,RuleBlacklist._msgSender
-DA:71,88
+FNDA:128,RuleBlacklist._msgSender
+DA:71,128
DA:78,1
FN:78,RuleBlacklist._msgData
FNDA:1,RuleBlacklist._msgData
DA:79,1
-DA:86,89
+DA:86,129
FN:86,RuleBlacklist._contextSuffixLength
-FNDA:89,RuleBlacklist._contextSuffixLength
-DA:87,89
+FNDA:129,RuleBlacklist._contextSuffixLength
+DA:87,129
FNF:6
FNH:6
LF:11
@@ -3111,9 +2716,9 @@ FN:47,RuleChainlinkPoR.supportsInterface
FNDA:21,RuleChainlinkPoR.supportsInterface
DA:54,21
DA:55,14
-DA:65,19
+DA:65,20
FN:65,RuleChainlinkPoR._authorizeChainlinkPoRManager
-FNDA:19,RuleChainlinkPoR._authorizeChainlinkPoRManager
+FNDA:20,RuleChainlinkPoR._authorizeChainlinkPoRManager
FNF:2
FNH:2
LF:4
@@ -3122,15 +2727,41 @@ BRF:0
BRH:0
end_of_record
TN:
+SF:src/rules/validation/deployment/RuleChainlinkPoRERC3643.sol
+DA:55,9
+FN:55,RuleChainlinkPoRERC3643._detectTransferRestrictionOnNotify
+FNDA:9,RuleChainlinkPoRERC3643._detectTransferRestrictionOnNotify
+DA:66,9
+FNF:1
+FNH:1
+LF:2
+LH:2
+BRF:0
+BRH:0
+end_of_record
+TN:
+SF:src/rules/validation/deployment/RuleChainlinkPoRERC3643Ownable2Step.sol
+DA:55,2
+FN:55,RuleChainlinkPoRERC3643Ownable2Step._detectTransferRestrictionOnNotify
+FNDA:2,RuleChainlinkPoRERC3643Ownable2Step._detectTransferRestrictionOnNotify
+DA:66,2
+FNF:1
+FNH:1
+LF:2
+LH:2
+BRF:0
+BRH:0
+end_of_record
+TN:
SF:src/rules/validation/deployment/RuleChainlinkPoROwnable2Step.sol
DA:44,5
FN:44,RuleChainlinkPoROwnable2Step.supportsInterface
FNDA:5,RuleChainlinkPoROwnable2Step.supportsInterface
DA:51,5
DA:52,4
-DA:62,6
+DA:62,8
FN:62,RuleChainlinkPoROwnable2Step._authorizeChainlinkPoRManager
-FNDA:6,RuleChainlinkPoROwnable2Step._authorizeChainlinkPoRManager
+FNDA:8,RuleChainlinkPoROwnable2Step._authorizeChainlinkPoRManager
FNF:2
FNH:2
LF:4
@@ -3140,37 +2771,37 @@ BRH:0
end_of_record
TN:
SF:src/rules/validation/deployment/RuleERC2980.sol
-DA:56,1
-FN:56,RuleERC2980.supportsInterface
+DA:46,1
+FN:46,RuleERC2980.supportsInterface
FNDA:1,RuleERC2980.supportsInterface
-DA:63,1
-DA:73,5
-FN:73,RuleERC2980._authorizeMintBurnManager
+DA:53,1
+DA:63,5
+FN:63,RuleERC2980._authorizeMintBurnManager
FNDA:5,RuleERC2980._authorizeMintBurnManager
-DA:78,48
-FN:78,RuleERC2980._authorizeWhitelistAdd
-FNDA:48,RuleERC2980._authorizeWhitelistAdd
+DA:68,57
+FN:68,RuleERC2980._authorizeWhitelistAdd
+FNDA:57,RuleERC2980._authorizeWhitelistAdd
+DA:73,9
+FN:73,RuleERC2980._authorizeWhitelistRemove
+FNDA:9,RuleERC2980._authorizeWhitelistRemove
+DA:78,28
+FN:78,RuleERC2980._authorizeFrozenlistAdd
+FNDA:28,RuleERC2980._authorizeFrozenlistAdd
DA:83,8
-FN:83,RuleERC2980._authorizeWhitelistRemove
-FNDA:8,RuleERC2980._authorizeWhitelistRemove
-DA:88,24
-FN:88,RuleERC2980._authorizeFrozenlistAdd
-FNDA:24,RuleERC2980._authorizeFrozenlistAdd
-DA:93,7
-FN:93,RuleERC2980._authorizeFrozenlistRemove
-FNDA:7,RuleERC2980._authorizeFrozenlistRemove
-DA:103,259
-FN:103,RuleERC2980._msgSender
-FNDA:259,RuleERC2980._msgSender
-DA:104,259
-DA:111,1
-FN:111,RuleERC2980._msgData
+FN:83,RuleERC2980._authorizeFrozenlistRemove
+FNDA:8,RuleERC2980._authorizeFrozenlistRemove
+DA:93,286
+FN:93,RuleERC2980._msgSender
+FNDA:286,RuleERC2980._msgSender
+DA:94,286
+DA:101,1
+FN:101,RuleERC2980._msgData
FNDA:1,RuleERC2980._msgData
-DA:112,1
-DA:119,260
-FN:119,RuleERC2980._contextSuffixLength
-FNDA:260,RuleERC2980._contextSuffixLength
-DA:120,260
+DA:102,1
+DA:109,287
+FN:109,RuleERC2980._contextSuffixLength
+FNDA:287,RuleERC2980._contextSuffixLength
+DA:110,287
FNF:9
FNH:9
LF:13
@@ -3220,14 +2851,14 @@ BRH:0
end_of_record
TN:
SF:src/rules/validation/deployment/RuleIdentityRegistry.sol
-DA:45,27
+DA:45,66
FN:45,RuleIdentityRegistry.supportsInterface
-FNDA:27,RuleIdentityRegistry.supportsInterface
-DA:52,27
-DA:53,18
-DA:63,12
+FNDA:66,RuleIdentityRegistry.supportsInterface
+DA:52,66
+DA:53,44
+DA:63,14
FN:63,RuleIdentityRegistry._authorizeIdentityRegistryManager
-FNDA:12,RuleIdentityRegistry._authorizeIdentityRegistryManager
+FNDA:14,RuleIdentityRegistry._authorizeIdentityRegistryManager
FNF:2
FNH:2
LF:4
@@ -3253,15 +2884,49 @@ BRF:0
BRH:0
end_of_record
TN:
+SF:src/rules/validation/deployment/RuleMaxBalance.sol
+DA:40,24
+FN:40,RuleMaxBalance.supportsInterface
+FNDA:24,RuleMaxBalance.supportsInterface
+DA:47,24
+DA:48,16
+DA:58,23
+FN:58,RuleMaxBalance._authorizeMaxBalanceManager
+FNDA:23,RuleMaxBalance._authorizeMaxBalanceManager
+FNF:2
+FNH:2
+LF:4
+LH:4
+BRF:0
+BRH:0
+end_of_record
+TN:
+SF:src/rules/validation/deployment/RuleMaxBalanceOwnable2Step.sol
+DA:41,3
+FN:41,RuleMaxBalanceOwnable2Step.supportsInterface
+FNDA:3,RuleMaxBalanceOwnable2Step.supportsInterface
+DA:48,3
+DA:49,2
+DA:59,9
+FN:59,RuleMaxBalanceOwnable2Step._authorizeMaxBalanceManager
+FNDA:9,RuleMaxBalanceOwnable2Step._authorizeMaxBalanceManager
+FNF:2
+FNH:2
+LF:4
+LH:4
+BRF:0
+BRH:0
+end_of_record
+TN:
SF:src/rules/validation/deployment/RuleMaxTotalSupply.sol
-DA:37,19
+DA:37,79
FN:37,RuleMaxTotalSupply.supportsInterface
-FNDA:19,RuleMaxTotalSupply.supportsInterface
-DA:44,19
-DA:45,13
-DA:55,264
+FNDA:79,RuleMaxTotalSupply.supportsInterface
+DA:44,79
+DA:45,53
+DA:55,266
FN:55,RuleMaxTotalSupply._authorizeMaxTotalSupplyManager
-FNDA:264,RuleMaxTotalSupply._authorizeMaxTotalSupplyManager
+FNDA:266,RuleMaxTotalSupply._authorizeMaxTotalSupplyManager
FNF:2
FNH:2
LF:4
@@ -3270,15 +2935,41 @@ BRF:0
BRH:0
end_of_record
TN:
+SF:src/rules/validation/deployment/RuleMaxTotalSupplyERC3643.sol
+DA:48,10
+FN:48,RuleMaxTotalSupplyERC3643._detectTransferRestrictionOnNotify
+FNDA:10,RuleMaxTotalSupplyERC3643._detectTransferRestrictionOnNotify
+DA:59,10
+FNF:1
+FNH:1
+LF:2
+LH:2
+BRF:0
+BRH:0
+end_of_record
+TN:
+SF:src/rules/validation/deployment/RuleMaxTotalSupplyERC3643Ownable2Step.sol
+DA:48,2
+FN:48,RuleMaxTotalSupplyERC3643Ownable2Step._detectTransferRestrictionOnNotify
+FNDA:2,RuleMaxTotalSupplyERC3643Ownable2Step._detectTransferRestrictionOnNotify
+DA:59,2
+FNF:1
+FNH:1
+LF:2
+LH:2
+BRF:0
+BRH:0
+end_of_record
+TN:
SF:src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol
DA:39,5
FN:39,RuleMaxTotalSupplyOwnable2Step.supportsInterface
FNDA:5,RuleMaxTotalSupplyOwnable2Step.supportsInterface
DA:46,5
DA:47,2
-DA:57,4
+DA:57,6
FN:57,RuleMaxTotalSupplyOwnable2Step._authorizeMaxTotalSupplyManager
-FNDA:4,RuleMaxTotalSupplyOwnable2Step._authorizeMaxTotalSupplyManager
+FNDA:6,RuleMaxTotalSupplyOwnable2Step._authorizeMaxTotalSupplyManager
FNF:2
FNH:2
LF:4
@@ -3288,29 +2979,29 @@ BRH:0
end_of_record
TN:
SF:src/rules/validation/deployment/RuleReceiverWhitelist.sol
-DA:38,6
+DA:38,7
FN:38,RuleReceiverWhitelist.supportsInterface
-FNDA:6,RuleReceiverWhitelist.supportsInterface
-DA:45,6
-DA:46,5
+FNDA:7,RuleReceiverWhitelist.supportsInterface
+DA:45,7
+DA:46,6
DA:56,22
FN:56,RuleReceiverWhitelist._authorizeAddressListAdd
FNDA:22,RuleReceiverWhitelist._authorizeAddressListAdd
DA:61,3
FN:61,RuleReceiverWhitelist._authorizeAddressListRemove
FNDA:3,RuleReceiverWhitelist._authorizeAddressListRemove
-DA:71,62
+DA:71,64
FN:71,RuleReceiverWhitelist._msgSender
-FNDA:62,RuleReceiverWhitelist._msgSender
-DA:72,62
+FNDA:64,RuleReceiverWhitelist._msgSender
+DA:72,64
DA:79,1
FN:79,RuleReceiverWhitelist._msgData
FNDA:1,RuleReceiverWhitelist._msgData
DA:80,1
-DA:87,64
+DA:87,66
FN:87,RuleReceiverWhitelist._contextSuffixLength
-FNDA:64,RuleReceiverWhitelist._contextSuffixLength
-DA:88,64
+FNDA:66,RuleReceiverWhitelist._contextSuffixLength
+DA:88,66
FNF:6
FNH:6
LF:11
@@ -3352,26 +3043,26 @@ BRH:0
end_of_record
TN:
SF:src/rules/validation/deployment/RuleSanctionsList.sol
-DA:40,58
+DA:40,115
FN:40,RuleSanctionsList.supportsInterface
-FNDA:58,RuleSanctionsList.supportsInterface
-DA:47,58
-DA:48,39
+FNDA:115,RuleSanctionsList.supportsInterface
+DA:47,115
+DA:48,77
DA:58,18
FN:58,RuleSanctionsList._authorizeSanctionListManager
FNDA:18,RuleSanctionsList._authorizeSanctionListManager
-DA:68,60
+DA:68,93
FN:68,RuleSanctionsList._msgSender
-FNDA:60,RuleSanctionsList._msgSender
-DA:69,60
+FNDA:93,RuleSanctionsList._msgSender
+DA:69,93
DA:76,1
FN:76,RuleSanctionsList._msgData
FNDA:1,RuleSanctionsList._msgData
DA:77,1
-DA:84,61
+DA:84,94
FN:84,RuleSanctionsList._contextSuffixLength
-FNDA:61,RuleSanctionsList._contextSuffixLength
-DA:85,61
+FNDA:94,RuleSanctionsList._contextSuffixLength
+DA:85,94
FNF:5
FNH:5
LF:10
@@ -3410,29 +3101,29 @@ BRH:0
end_of_record
TN:
SF:src/rules/validation/deployment/RuleSpenderWhitelist.sol
-DA:38,6
+DA:38,14
FN:38,RuleSpenderWhitelist.supportsInterface
-FNDA:6,RuleSpenderWhitelist.supportsInterface
-DA:45,6
-DA:46,5
-DA:56,7
+FNDA:14,RuleSpenderWhitelist.supportsInterface
+DA:45,14
+DA:46,11
+DA:56,9
FN:56,RuleSpenderWhitelist._authorizeAddressListAdd
-FNDA:7,RuleSpenderWhitelist._authorizeAddressListAdd
+FNDA:9,RuleSpenderWhitelist._authorizeAddressListAdd
DA:61,2
FN:61,RuleSpenderWhitelist._authorizeAddressListRemove
FNDA:2,RuleSpenderWhitelist._authorizeAddressListRemove
-DA:71,38
+DA:71,46
FN:71,RuleSpenderWhitelist._msgSender
-FNDA:38,RuleSpenderWhitelist._msgSender
-DA:72,38
+FNDA:46,RuleSpenderWhitelist._msgSender
+DA:72,46
DA:79,1
FN:79,RuleSpenderWhitelist._msgData
FNDA:1,RuleSpenderWhitelist._msgData
DA:80,1
-DA:87,40
+DA:87,48
FN:87,RuleSpenderWhitelist._contextSuffixLength
-FNDA:40,RuleSpenderWhitelist._contextSuffixLength
-DA:88,40
+FNDA:48,RuleSpenderWhitelist._contextSuffixLength
+DA:88,48
FNF:6
FNH:6
LF:11
@@ -3474,35 +3165,35 @@ BRH:0
end_of_record
TN:
SF:src/rules/validation/deployment/RuleWhitelist.sol
-DA:47,86
+DA:47,703
FN:47,RuleWhitelist.supportsInterface
-FNDA:86,RuleWhitelist.supportsInterface
-DA:54,86
-DA:55,58
-DA:65,1
+FNDA:703,RuleWhitelist.supportsInterface
+DA:54,703
+DA:55,470
+DA:65,2
FN:65,RuleWhitelist._authorizeCheckSpenderManager
-FNDA:1,RuleWhitelist._authorizeCheckSpenderManager
+FNDA:2,RuleWhitelist._authorizeCheckSpenderManager
DA:70,30
FN:70,RuleWhitelist._authorizeMintBurnManager
FNDA:30,RuleWhitelist._authorizeMintBurnManager
-DA:75,394
+DA:75,883
FN:75,RuleWhitelist._authorizeAddressListAdd
-FNDA:394,RuleWhitelist._authorizeAddressListAdd
-DA:80,264
+FNDA:883,RuleWhitelist._authorizeAddressListAdd
+DA:80,265
FN:80,RuleWhitelist._authorizeAddressListRemove
-FNDA:264,RuleWhitelist._authorizeAddressListRemove
-DA:90,888
+FNDA:265,RuleWhitelist._authorizeAddressListRemove
+DA:90,1404
FN:90,RuleWhitelist._msgSender
-FNDA:888,RuleWhitelist._msgSender
-DA:91,888
+FNDA:1404,RuleWhitelist._msgSender
+DA:91,1404
DA:98,1
FN:98,RuleWhitelist._msgData
FNDA:1,RuleWhitelist._msgData
DA:99,1
-DA:106,889
+DA:106,1405
FN:106,RuleWhitelist._contextSuffixLength
-FNDA:889,RuleWhitelist._contextSuffixLength
-DA:107,889
+FNDA:1405,RuleWhitelist._contextSuffixLength
+DA:107,1405
FNF:8
FNH:8
LF:13
@@ -3550,19 +3241,19 @@ BRH:0
end_of_record
TN:
SF:src/rules/validation/deployment/RuleWhitelistWrapper.sol
-DA:47,49
+DA:47,56
FN:47,RuleWhitelistWrapper.hasRole
-FNDA:49,RuleWhitelistWrapper.hasRole
-DA:48,159
-DA:56,47
+FNDA:56,RuleWhitelistWrapper.hasRole
+DA:48,173
+DA:56,50
FN:56,RuleWhitelistWrapper.supportsInterface
-FNDA:47,RuleWhitelistWrapper.supportsInterface
-DA:63,47
-DA:64,32
-DA:77,49
+FNDA:50,RuleWhitelistWrapper.supportsInterface
+DA:63,50
+DA:64,35
+DA:77,56
FN:77,RuleWhitelistWrapper._grantRole
-FNDA:49,RuleWhitelistWrapper._grantRole
-DA:78,49
+FNDA:56,RuleWhitelistWrapper._grantRole
+DA:78,56
DA:87,1
FN:87,RuleWhitelistWrapper._revokeRole
FNDA:1,RuleWhitelistWrapper._revokeRole
@@ -3573,24 +3264,24 @@ FNDA:2,RuleWhitelistWrapper._authorizeCheckSpenderManager
DA:103,4
FN:103,RuleWhitelistWrapper._authorizeMintBurnManager
FNDA:4,RuleWhitelistWrapper._authorizeMintBurnManager
-DA:109,98
+DA:109,105
FN:109,RuleWhitelistWrapper._onlyRulesManager
-FNDA:98,RuleWhitelistWrapper._onlyRulesManager
+FNDA:105,RuleWhitelistWrapper._onlyRulesManager
DA:114,2
FN:114,RuleWhitelistWrapper._onlyRulesLimitManager
FNDA:2,RuleWhitelistWrapper._onlyRulesLimitManager
-DA:120,158
+DA:120,172
FN:120,RuleWhitelistWrapper._msgSender
-FNDA:158,RuleWhitelistWrapper._msgSender
-DA:121,158
+FNDA:172,RuleWhitelistWrapper._msgSender
+DA:121,172
DA:128,1
FN:128,RuleWhitelistWrapper._msgData
FNDA:1,RuleWhitelistWrapper._msgData
DA:129,1
-DA:136,159
+DA:136,173
FN:136,RuleWhitelistWrapper._contextSuffixLength
-FNDA:159,RuleWhitelistWrapper._contextSuffixLength
-DA:143,159
+FNDA:173,RuleWhitelistWrapper._contextSuffixLength
+DA:143,173
FNF:11
FNH:11
LF:19
@@ -3636,360 +3327,3 @@ LH:13
BRF:0
BRH:0
end_of_record
-TN:
-SF:test/RuleBlacklist/Ownable/RuleBlacklistOwnable2Step.t.sol
-DA:10,2
-FN:10,RuleBlacklistOwnable2StepTest._deployOwnable2Step
-FNDA:2,RuleBlacklistOwnable2StepTest._deployOwnable2Step
-DA:11,2
-DA:12,2
-DA:13,2
-FNF:1
-FNH:1
-LF:4
-LH:4
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:test/RuleBlacklist/Ownable/RuleBlacklistOwnableAccessControl.t.sol
-DA:9,2
-FN:9,RuleBlacklistOwnable2StepAccessControl._deployAddressList
-FNDA:2,RuleBlacklistOwnable2StepAccessControl._deployAddressList
-DA:10,2
-DA:11,2
-DA:12,2
-FNF:1
-FNH:1
-LF:4
-LH:4
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:test/RuleChainlinkPoR/Ownable/RuleChainlinkPoROwnable2Step.t.sol
-DA:17,2
-FN:17,RuleChainlinkPoROwnable2StepTest._deployOwnable2Step
-FNDA:2,RuleChainlinkPoROwnable2StepTest._deployOwnable2Step
-DA:18,2
-DA:19,2
-DA:20,2
-DA:21,2
-DA:24,2
-FNF:1
-FNH:1
-LF:6
-LH:6
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:test/RuleChainlinkPoR/RuleChainlinkPoRUnit.t.sol
-DA:627,1
-FN:627,DecimalsOnlyMock.decimals
-FNDA:1,DecimalsOnlyMock.decimals
-DA:628,1
-FNF:1
-FNH:1
-LF:2
-LH:2
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:test/RuleERC2980/Ownable/RuleERC2980Ownable2Step.t.sol
-DA:10,2
-FN:10,RuleERC2980Ownable2StepTest._deployOwnable2Step
-FNDA:2,RuleERC2980Ownable2StepTest._deployOwnable2Step
-DA:11,2
-DA:12,2
-DA:13,2
-FNF:1
-FNH:1
-LF:4
-LH:4
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:test/RuleIdentityRegistry/Ownable/RuleIdentityRegistryOwnable2Step.t.sol
-DA:11,2
-FN:11,RuleIdentityRegistryOwnable2StepTest._deployOwnable2Step
-FNDA:2,RuleIdentityRegistryOwnable2StepTest._deployOwnable2Step
-DA:12,2
-DA:13,2
-DA:14,2
-DA:15,2
-DA:16,2
-FNF:1
-FNH:1
-LF:6
-LH:6
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:test/RuleMaxTotalSupply/Ownable/RuleMaxTotalSupplyOwnable2Step.t.sol
-DA:11,2
-FN:11,RuleMaxTotalSupplyOwnable2StepTest._deployOwnable2Step
-FNDA:2,RuleMaxTotalSupplyOwnable2StepTest._deployOwnable2Step
-DA:12,2
-DA:13,2
-DA:14,2
-DA:15,2
-FNF:1
-FNH:1
-LF:5
-LH:5
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:test/RuleMaxTotalSupply/RuleMaxTotalSupplyUnit.t.sol
-DA:211,2
-FN:211,RevertingTotalSupplyMock.setRevertOnTotalSupply
-FNDA:2,RevertingTotalSupplyMock.setRevertOnTotalSupply
-DA:212,2
-DA:215,6
-FN:215,RevertingTotalSupplyMock.totalSupply
-FNDA:6,RevertingTotalSupplyMock.totalSupply
-DA:216,6
-BRDA:216,0,0,4
-BRDA:216,0,1,2
-DA:217,2
-FNF:2
-FNH:2
-LF:5
-LH:5
-BRF:2
-BRH:2
-end_of_record
-TN:
-SF:test/RuleSanctionList/Ownable/RuleSanctionsListOwnable2Step.t.sol
-DA:13,2
-FN:13,RuleSanctionsListOwnable2StepTest._deployOwnable2Step
-FNDA:2,RuleSanctionsListOwnable2StepTest._deployOwnable2Step
-DA:14,2
-DA:15,2
-DA:16,2
-DA:17,2
-FNF:1
-FNH:1
-LF:5
-LH:5
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:test/RuleWhitelist/Ownable/RuleWhitelistOwnable2Step.t.sol
-DA:10,2
-FN:10,RuleWhitelistOwnable2StepTest._deployOwnable2Step
-FNDA:2,RuleWhitelistOwnable2StepTest._deployOwnable2Step
-DA:11,2
-DA:12,2
-DA:13,2
-DA:14,2
-FNF:1
-FNH:1
-LF:5
-LH:5
-BRF:0
-BRH:0
-end_of_record
-TN:
-SF:test/invariant/ConditionalTransferHandler.sol
-DA:61,2
-FN:61,ConditionalTransferHandler.constructor
-FNDA:2,ConditionalTransferHandler.constructor
-DA:62,2
-DA:72,4152
-FN:72,ConditionalTransferHandler.approve
-FNDA:4152,ConditionalTransferHandler.approve
-DA:73,4152
-DA:74,4152
-DA:75,4152
-DA:76,4152
-DA:82,4270
-FN:82,ConditionalTransferHandler.cancel
-FNDA:4270,ConditionalTransferHandler.cancel
-DA:83,4270
-BRDA:83,0,0,4270
-DA:84,4270
-DA:86,4114
-DA:87,4114
-BRDA:87,1,0,2747
-DA:88,2747
-DA:90,1367
-DA:91,1367
-DA:97,4190
-FN:97,ConditionalTransferHandler.execute
-FNDA:4190,ConditionalTransferHandler.execute
-DA:98,4190
-BRDA:98,2,0,4190
-DA:99,4190
-DA:101,4048
-DA:102,4048
-BRDA:102,3,0,2577
-DA:103,2577
-DA:105,1471
-DA:106,1471
-DA:114,4028
-FN:114,ConditionalTransferHandler.executeMintOrBurn
-FNDA:4028,ConditionalTransferHandler.executeMintOrBurn
-DA:115,4028
-DA:116,4028
-DA:117,4028
-BRDA:117,4,0,1998
-BRDA:117,4,1,2030
-DA:118,1998
-DA:120,2030
-DA:122,4028
-DA:132,0
-FN:132,ConditionalTransferHandler.sumApprovalCounts
-FNDA:0,ConditionalTransferHandler.sumApprovalCounts
-DA:133,0
-DA:134,0
-DA:135,0
-DA:142,0
-FN:142,ConditionalTransferHandler.keyCount
-FNDA:0,ConditionalTransferHandler.keyCount
-DA:143,0
-DA:150,4152
-FN:150,ConditionalTransferHandler._tuple
-FNDA:4152,ConditionalTransferHandler._tuple
-DA:156,4152
-DA:157,4152
-DA:158,4152
-DA:161,4152
-FN:161,ConditionalTransferHandler._record
-FNDA:4152,ConditionalTransferHandler._record
-DA:162,4152
-DA:163,4152
-BRDA:163,5,0,2965
-DA:164,2965
-DA:165,2965
-FNF:9
-FNH:7
-LF:45
-LH:39
-BRF:7
-BRH:7
-end_of_record
-TN:
-SF:test/invariant/MintAllowanceHandler.sol
-DA:55,2
-FN:55,MintAllowanceHandler.constructor
-FNDA:2,MintAllowanceHandler.constructor
-DA:56,2
-DA:66,3454
-FN:66,MintAllowanceHandler.setAllowance
-FNDA:3454,MintAllowanceHandler.setAllowance
-DA:67,3454
-DA:68,3454
-DA:69,3454
-DA:70,3454
-DA:71,3454
-DA:77,3236
-FN:77,MintAllowanceHandler.increase
-FNDA:3236,MintAllowanceHandler.increase
-DA:78,3236
-DA:79,3236
-DA:80,3236
-DA:81,3236
-DA:82,3236
-DA:88,3376
-FN:88,MintAllowanceHandler.decrease
-FNDA:3376,MintAllowanceHandler.decrease
-DA:89,3376
-DA:90,3376
-DA:91,3376
-BRDA:91,0,0,287
-DA:92,287
-DA:94,3089
-DA:95,3089
-DA:96,3089
-DA:102,3304
-FN:102,MintAllowanceHandler.mint
-FNDA:3304,MintAllowanceHandler.mint
-DA:103,3304
-DA:104,3304
-DA:105,3304
-BRDA:105,1,0,264
-DA:106,264
-DA:108,3040
-DA:109,3040
-DA:111,3040
-DA:113,3040
-DA:114,3040
-DA:121,3270
-FN:121,MintAllowanceHandler.regularTransfer
-FNDA:3270,MintAllowanceHandler.regularTransfer
-DA:122,3270
-DA:123,3270
-DA:124,3270
-DA:125,3270
-DA:135,0
-FN:135,MintAllowanceHandler.minterAt
-FNDA:0,MintAllowanceHandler.minterAt
-DA:136,0
-DA:137,0
-DA:143,0
-FN:143,MintAllowanceHandler.minterCount
-FNDA:0,MintAllowanceHandler.minterCount
-DA:144,0
-DA:151,16640
-FN:151,MintAllowanceHandler._minter
-FNDA:16640,MintAllowanceHandler._minter
-DA:152,16640
-FNF:9
-FNH:7
-LF:44
-LH:39
-BRF:2
-BRH:2
-end_of_record
-TN:
-SF:test/utils/AccessControlEnumerableTestBase.sol
-DA:22,0
-FN:22,AccessControlEnumerableTestBase._deployAccessControl
-FNDA:0,AccessControlEnumerableTestBase._deployAccessControl
-DA:24,7
-FN:24,AccessControlEnumerableTestBase.setUp
-FNDA:7,AccessControlEnumerableTestBase.setUp
-DA:25,7
-DA:28,14
-FN:28,AccessControlEnumerableTestBase._assertRoleMembers
-FNDA:14,AccessControlEnumerableTestBase._assertRoleMembers
-DA:29,14
-DA:30,14
-DA:31,14
-BRDA:31,0,0,7
-DA:32,7
-DA:34,7
-DA:35,7
-DA:36,7
-FNF:3
-FNH:2
-LF:11
-LH:10
-BRF:1
-BRH:1
-end_of_record
-TN:
-SF:test/utils/CMTATDeployment.sol
-DA:15,78
-FN:15,CMTATDeployment.constructor
-FNDA:78,CMTATDeployment.constructor
-DA:17,78
-DA:18,78
-DA:19,78
-DA:20,78
-DA:27,78
-DA:28,78
-FNF:1
-FNH:1
-LF:7
-LH:7
-BRF:0
-BRH:0
-end_of_record
diff --git a/doc/script/convert_links_for_pdf.sh b/doc/script/convert_links_for_pdf.sh
index 5b2a4f6c..4f0734a5 100755
--- a/doc/script/convert_links_for_pdf.sh
+++ b/doc/script/convert_links_for_pdf.sh
@@ -15,6 +15,23 @@ if [ -z "$1" ]; then
fi
GITHUB_LINK="${1%/}" # Remove trailing slash if present
+
+# Base URL for the *parent* of the input file's directory, used by Step 0.
+# ".../blob/[/doc" -> ".../blob/][". The input file lives in doc/, so its
+# links to repository-root siblings (test/, src/) are written "../path" and can
+# only be rewritten against this. Empty when the base URL has no path segment
+# after the ref: the input file is then the root README, and "../" from there
+# points outside the repository.
+GITHUB_LINK_PARENT=""
+if [[ "$GITHUB_LINK" =~ ^(.*/blob/[^/]+)/(.+)$ ]]; then
+ REF_BASE="${BASH_REMATCH[1]}"
+ DIR_PATH="${BASH_REMATCH[2]}"
+ if [ "$DIR_PATH" = "${DIR_PATH%/*}" ]; then
+ GITHUB_LINK_PARENT="$REF_BASE"
+ else
+ GITHUB_LINK_PARENT="$REF_BASE/${DIR_PATH%/*}"
+ fi
+fi
INPUT_FILE="${2:-../README.md}" # doc/README.md, the full reference (the root README is a short summary)
OUTPUT_FILE="${3:-README_UPDATE.md}"
@@ -29,6 +46,19 @@ cp "$INPUT_FILE" "$TMP_FILE"
# Use a placeholder to avoid sed escaping issues
PLACEHOLDER="__GITHUB_LINK__"
+PLACEHOLDER_PARENT="__GITHUB_LINK_PARENT__"
+
+# Step 0: convert parent-relative links [text](../...) before Step 1, which only
+# recognizes the "./" form and would leave these relative and dead in the PDF.
+if grep -qE '\]\(\.\./[^)]+\)' "$TMP_FILE"; then
+ if [ -z "$GITHUB_LINK_PARENT" ]; then
+ echo "Error: '$INPUT_FILE' contains '../' links, but '$GITHUB_LINK' has no parent directory to resolve them against." >&2
+ echo "Pass a base URL that includes the input file's own directory, e.g. https://github.com/CMTA/Rules/blob//doc" >&2
+ rm -f "$TMP_FILE"
+ exit 1
+ fi
+ sed -i -E "s|\[([^]]+)\]\(\.\./([^)]+)\)|[\1]($PLACEHOLDER_PARENT/\2)|g" "$TMP_FILE"
+fi
# Step 1: Convert ALL relative links [text](./...) to placeholder
sed -i -E "s|\[([^]]+)\]\(\./([^)]+)\)|[\1]($PLACEHOLDER/\2)|g" "$TMP_FILE"
@@ -36,9 +66,11 @@ sed -i -E "s|\[([^]]+)\]\(\./([^)]+)\)|[\1]($PLACEHOLDER/\2)|g" "$TMP_FILE"
# Step 2: Restore image links back to relative (images render inline in PDF)
for ext in png jpg jpeg gif svg ico webp bmp tiff; do
sed -i -E "s|\[([^]]+)\]\($PLACEHOLDER/([^)]+\.$ext)\)|[\1](./\2)|gi" "$TMP_FILE"
+ sed -i -E "s|\[([^]]+)\]\($PLACEHOLDER_PARENT/([^)]+\.$ext)\)|[\1](../\2)|gi" "$TMP_FILE"
done
-# Step 3: Replace placeholder with actual GitHub link
+# Step 3: Replace placeholders with actual GitHub links (parent first)
+sed -i "s|$PLACEHOLDER_PARENT|$GITHUB_LINK_PARENT|g" "$TMP_FILE"
sed -i "s|$PLACEHOLDER|$GITHUB_LINK|g" "$TMP_FILE"
mv "$TMP_FILE" "$OUTPUT_FILE"
diff --git a/doc/security/audits/AUDIT_OVERVIEW.md b/doc/security/audits/AUDIT_OVERVIEW.md
index ebc4bd56..68eeaceb 100644
--- a/doc/security/audits/AUDIT_OVERVIEW.md
+++ b/doc/security/audits/AUDIT_OVERVIEW.md
@@ -3,7 +3,7 @@
> This is a security **overview** (analyses index + triage). It is **not** the vulnerability-reporting policy
> (that belongs in a root `SECURITY.md`).
-**Current package version:** `v0.5.0`
+**Current package version:** `v0.6.0`
**Scope:** production contracts under `src/` — mocks/tests (`src/mocks`, `test/`) and dependencies (`lib/`) are excluded from static-analysis runs unless a run is explicitly marked *mocks included*.
> ⚠️ This project has **not** undergone a formal third-party security audit. The analyses below are automated
@@ -13,6 +13,10 @@
| Date | Type | Tool / Source | Version | Reports |
|---|---|---|---|---|
+| 2026-08-18 | AI-assisted review | Claude Code (Anthropic) | v0.6.0 | [**CLAUDE_ANALYSIS.md**](./tools/v0.6.0/CLAUDE_ANALYSIS.md) (code quality, `src/`) |
+| 2026-08-21 | Static analysis | Slither 0.11.5 | v0.6.0 | [report](./tools/v0.6.0/slither-report.md) · [feedback](./tools/v0.6.0/slither-report-feedback.md) — re-run after the RuleEngine `v3.0.0-rc6` bump, supersedes 2026-08-18 |
+| 2026-08-21 | Static analysis | Aderyn 0.6.5 | v0.6.0 | [report](./tools/v0.6.0/aderyn-report.md) · [feedback](./tools/v0.6.0/aderyn-report-feedback.md) — re-run after the RuleEngine `v3.0.0-rc6` bump, supersedes 2026-08-18 |
+| 2026-08-17 | AI automated scan | [Nethermind AuditAgent (AI)](https://auditagent.nethermind.io/) | v0.5.0 | [report (PDF)](./tools/v0.5.0/nethermind_audit_agent_report_v0.5.0.pdf) · [feedback](./tools/v0.5.0/nethermind_audit_agent_report_v0.5.0-feedback.md) |
| 2026-08-12 | AI-assisted review | Claude Code (Anthropic) | v0.5.0 | [**CLAUDE_ANALYSIS.md**](./tools/v0.5.0/CLAUDE_ANALYSIS.md) (code quality, `src/`) · [**CLAUDE_ANALYSIS_SCRIPT.md**](./tools/v0.5.0/CLAUDE_ANALYSIS_SCRIPT.md) (deployment scripts) |
| 2026-07 | AI-assisted review | Claude (Anthropic) + custom security-audit skills | v0.4.0 | [**CLAUDE_AUDIT.md**](./tools/v0.4.0/claude-audit/CLAUDE_AUDIT.md) |
| 2026-08-11 | Static analysis | Slither 0.11.5 | v0.5.0 | [report](./tools/v0.5.0/slither-report.md) · [feedback](./tools/v0.5.0/slither-report-feedback.md) |
@@ -22,6 +26,60 @@
| 2026-04-16 | Static analysis | Slither / Aderyn | v0.3.0 | [slither](./tools/v0.3.0/slither-report.md) · [aderyn](./tools/v0.3.0/aderyn-report.md) |
| 2026-03-16 | AI-assisted review | Wake Arena (Ackee) | v0.2.0 | [tools/v0.2.0](./tools/v0.2.0/) |
+## Static-analysis results (v0.6.0)
+
+Re-run **2026-08-21** for the `v0.6.0` release, at solc `0.8.36`, with the same tool versions as `v0.5.0` so the
+delta is directly comparable. This supersedes the 2026-08-18 run, which was already one commit stale when it
+was committed. Scope: production contracts only — mocks excluded, vendored dependencies excluded
+via the `lib` filter.
+
+| Tool | High | Medium | Low | Info | Relevant to fix? |
+|---|---|---|---|---|---|
+| Slither 0.11.5 | 2 | 11 | 18 | 15 | **No** — both High-impact results are the long-standing false positive on a permissioned path; see [feedback](./tools/v0.6.0/slither-report-feedback.md) |
+| Aderyn 0.6.5 | 0 | 0 | 9 categories (346 instances) | 0 | **No** — every Low is by design, environmental or cosmetic; see [feedback](./tools/v0.6.0/aderyn-report-feedback.md) |
+
+**Nothing to fix in `v0.6.0`.** Both deltas are small and fully attributed:
+
+- **Slither 44 → 46 (+2).** One `calls-loop` on `RuleWhitelistWrapperBase._checkRule` — the NM-20 polarity guard,
+ bounded by `maxRules` and reachable only from a `RULES_MANAGEMENT_ROLE` configuration call, never a transfer.
+ One `dead-code` on `RuleChainlinkPoRBase._detectTransferRestrictionOnNotify`, which is a **false positive worth
+ reading**: acting on it would delete the seam `RuleChainlinkPoRERC3643` exists to override. It is called twice
+ in the same file, the contract is at 100% function coverage, and the byte-identical seam in
+ `RuleMaxTotalSupplyBase` is not flagged — the detector is unreliable for `internal virtual` functions reached
+ through inheritance.
+- **Aderyn 336 → 346 (+10)** on +203 nSLOC, and the +10 is *exactly* the five new production files appearing once
+ each in `Unspecific Solidity Pragma` and `PUSH0 Opcode`. No new category.
+
+Two non-results are more informative than the totals. **`Centralization Risk` did not move (80 → 80)** despite
+four new deployable contracts: the ERC-3643 variants subclass existing deployables and override one `internal`
+hook, adding no privileged external function. **`Empty Block` did not move (70 → 70)** either, so no new
+access-control hook was introduced.
+
+**Re-run 2026-08-18 → 2026-08-21, after the RuleEngine `v3.0.0-rc6` bump: no detector moved in either tool.**
+Slither holds 46 results across the same nine detectors, Aderyn holds 346 instances across the same nine
+categories, and the only body changes are line numbers plus four renamed snippets. Two commits are covered — the
+NatSpec trim (`c1ebe57`, which is what made the 2026-08-18 reports stale) and the rc6 bump (`f920b07`), which
+renamed `onlyComplianceManager` to `onlyTokenBindingManager`, renamed `_authorizeComplianceBindingChange` to
+`_authorizeTokenBindingChange` and deleted one redundant override. Aderyn's nSLOC moved 4 146 → 4 145. Slither's
+**contract count rose 221 → 225 without any change in `src/`**: rc6 split the binding registry out of
+`ERC3643ComplianceModule` into five new upstream contracts and removed one, all under `lib/` and all filtered out
+of the results — flagged here so a future reader does not mistake it for scope creep. Two stable counts carry
+information: `dead-code` staying at 3 confirms the deleted override was reachable and therefore redundant rather
+than load-bearing, and `Centralization Risk` staying at 80 confirms the rename re-gated nothing.
+
+As in `v0.5.0`: a clean static-analysis report means the tools' pattern sets matched nothing. **None of the seven
+findings fixed in this release was reachable by either analyser** — they came from the Nethermind AuditAgent scan
+and manual review, and are semantic (accounting phase, callback ordering, interface polarity) where these tools
+are syntactic.
+
+Commands used for `v0.6.0` (mocks excluded):
+
+```bash
+slither . --checklist --filter-paths "node_modules,lib,test,forge-std,mocks" \
+ > doc/security/audits/tools/v0.6.0/slither-report.md
+aderyn -x mocks --output doc/security/audits/tools/v0.6.0/aderyn-report.md
+```
+
## Static-analysis results (v0.5.0)
Scope: production contracts only — mocks excluded (`-x mocks` / `mocks` filter) and vendored dependencies
@@ -60,6 +118,73 @@ feed-decimals read that prevents a stale-cache over-mint, and the removal of two
`IdentityRegistryWhitelist` — were found by **manual review, not by either tool**. A clean static-analysis report
means the tools' pattern sets matched nothing; it is not evidence of correctness.
+## AI automated scan results — Nethermind AuditAgent (v0.5.0)
+
+Scan **2026-08-17** (Scan ID `10`, commit `01632da0…951e204c`, 89 contracts / 9 764 LoC) with
+[**Nethermind AuditAgent**](https://auditagent.nethermind.io/).
+
+> ⚠️ **This is an AI-powered automated scan, not a formal human-led audit.** Nethermind's own notice states the
+> report "has been generated entirely by AI… does not constitute a full security audit… must be independently
+> verified", and that it does not authorise describing the project as "audited by Nethermind". The
+> [feedback file](./tools/v0.5.0/nethermind_audit_agent_report_v0.5.0-feedback.md) is that independent
+> verification: every finding was opened against the cited `file:line`.
+
+| Tool | High | Medium | Low | Info | Relevant to fix? |
+|---|---|---|---|---|---|
+| [Nethermind AuditAgent (AI)](https://auditagent.nethermind.io/) | 0 | 13 | 11 | 0 | **7 fixed** (NM-3, 6, 10, 11, 17, 18, 20 — `v0.6.0`), 16 accepted as design, 1 declined; **nothing left open** — see [feedback](./tools/v0.5.0/nethermind_audit_agent_report_v0.5.0-feedback.md) |
+
+**Nothing exploitable, and no contract change required for the CMTAT path.** There are **no false positives** —
+all 24 findings describe real code — but 17 restate positions already reached, documented in-source and recorded
+in `CLAUDE_AUDIT.md` (F-4, F-5, F-7 and the accepted-risk rows for a reverting oracle/registry), and the 24 items
+collapse to roughly **11 distinct claims** (approval/quota scoping is reported six times, cap-rule token binding
+twice, spender-less hooks twice, short ABI return data twice). Every described failure is fail-closed
+(over-restriction, a blocked transfer) or inert (a rule that cannot screen an identity it is never given); none
+of the 13 Medium ratings survives verification at Medium.
+
+**NM-11 — fixed in `v0.6.0` for two of the three cap rules.** The three rules assume the token notifies *before*
+moving the value; ERC-3643 / T-REX notifies *after*, so the observation already includes the amount and the stock
+rule counts it twice, reverting mints that are fully within the cap. `v0.6.0` adds a stateless `CapAccounting`
+primitive and a `_detectTransferRestrictionOnNotify` hook on each cap rule — defaulting to today's CMTAT
+behaviour — then ships **`RuleChainlinkPoRERC3643`** and **`RuleMaxTotalSupplyERC3643`** (each with an
+`Ownable2Step` variant) as one-line overrides of it. Only the write path is re-phased: ERC-3643 calls
+`canTransfer` *before* `_mint` and `created` *after*, both in one transaction, so the read views must keep
+projecting the pending amount. 49 tests, including two suites driving the **genuine** vendored T-REX token and
+four that pin the stock rules failing on it. **`RuleMaxBalance` is deliberately excluded** — a post-update
+variant would revert an agent's `forcedTransfer` and, on T-REX ≤ 4.1 where `recoveryAddress` routes through it,
+brick wallet recovery; that is a policy decision, not a hook override. Write-ups:
+`doc/technical/contracts/RuleChainlinkPoRERC3643.md`, `RuleMaxTotalSupplyERC3643.md`, `RULE_SEMANTICS.md` §5.
+
+A second ERC-3643 hazard surfaced while testing it and is now pinned: T-REX deploys then initialises, and an
+uninitialised `Token` reports `decimals() == 0`, so a PoR rule built before `init` silently caches the wrong
+decimals and mis-scales the reserves. Remedy is deployment order, documented on the contract page.
+
+**Fixed in `v0.6.0` — NM-6.** `RuleNFTAdapter`'s ERC-7943 spender-aware overloads called the delegated hook
+unconditionally, while the `ITransferContext` entrypoints normalised `sender == from` to the direct hook. The
+three interfaces signal a direct transfer differently — ERC-7943 documents its `spender` as "the address
+performing the transfer (**owner**/operator)" and `ctx.sender` is the token's `msg.sender`, so on both an owner
+arrives as `spender == from`, whereas CMTAT uses `spender == address(0)` and the 3-arg overload. The adapter now
+normalises on a shared `_isDelegated` predicate; the 4-arg CMTAT path is deliberately left alone, so the primary
+integration path and every existing restriction code are unchanged. The one behavioural correction is
+`RuleSpenderWhitelist`, which had been rejecting owner-initiated ERC-721 `transferFrom` with code 66 despite
+documenting that direct transfers are always allowed; the deny-lists blocked such a transfer before and after and
+only relabelled the code. Pinned by
+[`test/TransferContext/OverloadParity.t.sol`](../../../test/TransferContext/OverloadParity.t.sol) — the suite
+already existed for this property but tested only two of the three input shapes, which is why the gap survived;
+reverting the fix now fails 6 of its 10 tests across 5 rules.
+
+**Four findings carry a specified, unimplemented improvement** — NM-5, NM-17, NM-18 and NM-23/24 —
+each with the code, its cost and its limit. The two cheapest and clearest wins: assert in
+`approveAndTransferIfAllowed` that the approval it created was consumed (NM-17); and ERC-165-check the wrapper's
+children in a `_checkRule` override, the pattern `RuleEngineBase` already uses (NM-18). Two carry hard limits
+worth knowing before planning work: **NM-5 cannot be fully fixed at the rule level** — the compliance hooks carry
+no token identity, so isolating two tokens behind one engine needs an upstream interface change, and only the
+"one instance, two engines" half is reachable — and NM-18's read-time containment hits the same uncatchable-decode
+problem as NM-23, so only its configuration-time layer is recommended.
+
+The scan reached a strictly different class of issue than Slither and Aderyn, which found none of these: the
+static analysers match syntactic patterns, while every AuditAgent finding is semantic — about which hook is
+called, in what order, and with which arguments.
+
## Static-analysis results (v0.4.0)
Both tools were **re-run on 2026-07-14**, after the security remediation landed. Counts below are from that run.
diff --git a/doc/security/audits/tools/v0.5.0/nethermind_audit_agent_report_v0.5.0-feedback.md b/doc/security/audits/tools/v0.5.0/nethermind_audit_agent_report_v0.5.0-feedback.md
new file mode 100644
index 00000000..552b1bca
--- /dev/null
+++ b/doc/security/audits/tools/v0.5.0/nethermind_audit_agent_report_v0.5.0-feedback.md
@@ -0,0 +1,1247 @@
+# Nethermind AuditAgent `v0.5.0` — triage
+
+Tool: **[Nethermind AuditAgent](https://auditagent.nethermind.io/)** — an **AI-powered automated code scanner**.
+
+> ⚠️ **This is not an audit.** The report carries Nethermind's own *Important Notice*: it "has been generated
+> entirely by AI and has not been manually reviewed by Nethermind's security team. It does not constitute a full
+> security audit… All findings, observations, and recommendations may contain errors or omissions and must be
+> independently verified by a qualified human reviewer before being acted upon." Per Nethermind's terms, this
+> scan does **not** authorise anyone to describe the project as "audited by Nethermind". **This document is that
+> independent verification**: every one of the 24 findings was opened against the cited `file:line` before a
+> disposition was assigned.
+
+## Scan metadata
+
+| | |
+|---|---|
+| Scan ID | `10` |
+| Date | 2026-08-17 |
+| Organization / Repository | CMTA / `Rules` |
+| Branch / Commit | `main` @ `01632da0…951e204c` (`01632da`, the v0.5.0 merge commit — same tree as HEAD at triage time) |
+| Contracts scanned | 89 (all of `src/`; mocks, tests and `lib/` out of scope) |
+| Lines of code | 9 764 |
+
+**Tool-reported findings summary — total 24:**
+
+| High | Medium | Low | Info | Best practices |
+|---|---|---|---|---|
+| **0** | **13** | **11** | 0 | 0 |
+
+## Outcome
+
+**Nothing is exploitable, and nothing required a contract change on the CMTAT deployment path.** Seven findings
+were nonetheless fixed in `v0.6.0` — six as hardening and one (NM-11) by shipping ERC-3643 variants of two cap
+rules — because each was cheap, verifiable and left the library better than the accepted-as-design disposition
+would have.
+
+| Disposition | Count | IDs |
+|---|---|---|
+| **Fixed** (in `v0.6.0`) | 7 | **NM-3**, **NM-6**, **NM-10**, **NM-11**, **NM-17**, **NM-18**, **NM-20** |
+| Accepted as design (real behaviour, intentional, already documented) | 16 | NM-1, 2, 4, 5, 7, 8, 9, 12, 13, 14, 15, 16, 21, 22, 23, 24 |
+| Rejected — false positive | 0 | — |
+| Won't do (confirmed, deliberately declined) | 1 | NM-19 — nesting adds no expressive power at multiplicative gas |
+| Fix recommended | 0 | — |
+| **Total** | **24** | |
+
+Two observations about the report as a whole:
+
+1. **No false positives, and no High findings — but heavy duplication.** The 24 items collapse to roughly
+ **11 distinct claims**. Approval/quota-scoping behind a shared RuleEngine is reported five times
+ (NM-2, 4, 7, 8, 12, 15); the cap rules' static token binding twice (NM-5, 13); spender-less 3-arg hooks twice
+ (NM-9, 16); short ABI return data twice (NM-23, 24). Counting each restatement as a separate Medium inflates
+ the Medium column well past what the underlying set of issues warrants.
+2. **The scanner rediscovered, and re-rated as Medium, four positions this project had already reached,
+ documented in-source, and recorded in a prior audit** — F-4 (multi-token approval scoping),
+ F-7 (`canTransfer` non-authoritative for `RuleMintAllowance`), F-5 (the wrapper's unchecked children), and
+ the v0.4.0 accepted-risk row "reverting sanctions oracle / identity registry bricks transfers". It found the
+ right things; it had no way to see that they were already decided. Re-raising F-5 was useful anyway — it had
+ been open since v0.4.0 and this scan is what got it closed (NM-18).
+
+**Eight entries carry an `Improvement` section** — NM-3, NM-5, NM-6, NM-10, NM-11, NM-17, NM-18 and NM-23/24 —
+setting out what could be implemented, the code to do it, what it buys, what it costs, and where the limit is.
+**Six are implemented in `v0.6.0`**; see the `Resolution` block in each. NM-20, originally dispositioned as
+documentation-only, was also fixed once it became clear the marker interface it called for was a single function.
+
+Two limits are worth reading before planning further work: **NM-5** cannot be fully fixed at the rule level at
+all (the compliance hooks carry no token identity, so it needs an upstream interface change), and **NM-18**'s
+read-time containment runs into the same uncatchable-decode problem as NM-23, which is why only its
+configuration-time layer was implemented. The two improvements that were specified and then **declined** are
+**NM-23/24** and **NM-5**; the reasoning is in each entry, and neither is left as an open TODO.
+
+**The one genuinely new and useful signal** is a theme the scanner keeps circling without naming:
+**several rules' guarantees depend on the token's callback shape and ordering, and a real ERC-3643 / T-REX token
+supplies neither.** That is developed under NM-11 and NM-9, and it was the only item that warranted new contracts.
+It has since been acted on: `v0.6.0` ships ERC-3643 variants of the reserve and supply cap rules, with suites
+running against the genuine vendored token.
+
+---
+
+## Per-finding triage
+
+| ID | Severity (tool → ours) | Finding | Disposition |
+|---|---|---|---|
+| NM-1 | Medium → **Info** | Mint quotas unenforced via `created` / 3-arg `transferred` | Accepted as design — documented CMTAT ≥ v3.3 requirement |
+| NM-2 | Medium → **Low** | Mint quotas shared across tokens behind one RuleEngine | Accepted as design — `bindToken` WARNING |
+| NM-3 | Medium → **Info** | Early returns in `_detectTransferRestrictionFrom` skip delegation | ✅ **Fixed** in `v0.6.0` |
+| NM-4 | Medium → **Low** | Approvals + quotas not token-scoped across shared engine / rebinding | Accepted as design — duplicate of NM-2 / NM-7 |
+| NM-5 | Medium → **Low** | Cap rules read a statically configured token's supply | Accepted as design — documented "one token per instance" |
+| NM-6 | Medium → **Info** | `RuleNFTAdapter` context vs ERC-7943 spender handling differ | ✅ **Fixed** in `v0.6.0` |
+| NM-7 | Medium → **Low** | Single-token approvals reusable across tokens behind one engine | Accepted as design — `bindRuleEngine` WARNING |
+| NM-8 | Medium → **Low** | Mint allowances shared across a multi-token engine | Accepted as design — duplicate of NM-2 |
+| NM-9 | Medium → **Info** | 3-arg ERC-3643 hooks carry no spender, so spender rules are inert | Accepted as design — topology requirement; see NM-11 |
+| NM-10 | Medium → **Info** | Future-dated PoR `updatedAt` skips the staleness check | ✅ **Fixed** in `v0.6.0` |
+| NM-11 | Medium → **Low** | Caps double-count when the token notifies **after** moving value | ✅ **Fixed** in `v0.6.0` — ERC-3643 variants for 2 of 3 rules; `RuleMaxBalance` documented as CMTAT-only |
+| NM-12 | Medium → **Low** | Single-token approval consumable by another token | Accepted as design — duplicate of NM-7 |
+| NM-13 | Medium → **Low** | Cap rules never bind to the calling token; setters can repoint | Accepted as design — duplicate of NM-5 |
+| NM-14 | Low → **Low** | Identity-registry failures revert the read path | Accepted as design — trusted dependency (v0.4.0 audit) |
+| NM-15 | Low → **Low** | Conditional approvals not token-scoped | Accepted as design — duplicate of NM-7 |
+| NM-16 | Low → **Info** | `RuleSpenderWhitelist` inert on spender-less hooks | Accepted as design — duplicate of NM-9 |
+| NM-17 | Low → **Low** | `approveAndTransferIfAllowed` leaves a residual approval if no callback | ✅ **Fixed** in `v0.6.0` — approval-consumed post-condition |
+| NM-18 | Low → **Low** | Wrapper bricked by a non-`IAddressList` child | ✅ **Fixed** in `v0.6.0` — ERC-165 guard on a purpose-built sub-interface |
+| NM-19 | Low → **Info** | Wrapper does not implement `IAddressList`, so it cannot nest | 🚫 **Won't do** — DoS half fixed by NM-18; nesting declined |
+| NM-20 | Low → **Info** | Wrapper reads a `RuleBlacklist` child's membership as eligibility | ✅ **Fixed** in `v0.6.0` — polarity marker interface + ERC-165 |
+| NM-21 | Low → **Info** | `RuleMintAllowance` 3-arg pre-flight views fail open | Accepted as design — audit F-7 |
+| NM-22 | Low → **Low** | A misbehaving sanctions oracle reverts the read path | Accepted as design — trusted dependency (v0.4.0 audit) |
+| NM-23 | Low → **Info** | Short successful return data escapes `try/catch` | Accepted as design — documented in-source; the low-level fix declined, see the entry |
+| NM-24 | Low → **Info** | Same, for `balanceOf` / `totalSupply` | Accepted as design — duplicate of NM-23 |
+
+---
+
+### NM-1 — Mint quotas are not enforced via `created` or the 3-arg `transferred`
+
+**Claim (Medium).** `RuleMintAllowanceBase` deducts only in `_transferredFrom`, reached only from
+`transferred(spender, from, to, value)`. `created(address,uint256)` is empty and the 3-arg `transferred` calls an
+empty `_transferred`, so a bound integration reporting mints either way completes them without touching
+`mintAllowance`.
+
+**Verdict — accepted as design, correct as written.** The code is exactly as described
+(`RuleMintAllowanceBase.sol:63`, `:154-161`, `:258-260`), and confirmed one level up:
+`RuleEngineBase.created(to, value)` forwards `_transferred(address(0), to, value)` — the **3-arg** path — so a
+token that reports mints through `created` does indeed reach a no-op.
+
+This is not a gap that can be closed by checking harder: **the 3-arg signature carries no minter identity**, so
+there is no address to debit. The rule states the requirement in its own NatSpec ("The rule tracks mints via the
+4-arg `transferred(spender, from=0, to, value)` path introduced in CMTAT v3.3. The 3-arg path has no minter
+identity and performs no deduction"), in `CLAUDE.md` ("Requires CMTAT ≥ v3.3"), and in
+`RULE_SEMANTICS.md` §1. On the supported CMTAT ≥ v3.3 path, `_mintOverride` calls
+`_checkTransferred(_msgSender(), address(0), to, value)`, the 4-arg overload runs, and the quota is enforced.
+
+*Optional hardening, not applied:* `_transferred` could **fail closed** when `from == address(0)` — reverting a
+mint reported without a minter identity rather than passing it. It would fire only where the quota is silently
+inert today, and would leave plain transfers and burns untouched. Recorded as a deliberate open choice: it turns
+a documented "unsupported topology" into a hard revert, which is the safer default for a quota rule but is a
+breaking change for any pre-v3.3 integration.
+
+### NM-2 / NM-4 / NM-8 — Mint quotas shared across tokens behind one RuleEngine
+
+**Claim (Medium ×3).** `mapping(address minter => uint256 allowance)` has no token dimension. Binding one
+RuleEngine authorises a caller, not a token, and an engine may serve several tokens; a quota granted for token A
+is spendable minting token B.
+
+**Verdict — accepted as design, explicitly documented.** True and known. `bindToken` enforces single-target
+binding (`RuleMintAllowance_TokenAlreadyBound`), and its NatSpec carries the WARNING that `unbindToken` does not
+clear `mintAllowance` and that quotas survive rebinding, with `clearMintAllowances` provided for migration
+(v0.4.0 audit F-9). The residual exposure — an operator binding a **multi-tenant** engine — sits inside the
+trust model: the compliance manager who chooses the engine is the same role that grants the quotas. This is the
+same shape as the documented "one instance protects one token, with no on-chain guard" position taken for
+`RuleMaxTotalSupply` / `RuleChainlinkPoR`, and the reasoning is recorded there: adding a binding guard to a
+stateless validation rule is a library-wide decision, not a per-rule patch.
+
+### NM-3 — Early returns in `_detectTransferRestrictionFrom` skip the delegation — ✅ FIXED (`v0.6.0`)
+
+**Claim (Medium).** `RuleIdentityRegistryBase._detectTransferRestrictionFrom` returns `TRANSFER_OK` directly when
+the registry is unset or `to == address(0)`, instead of delegating to `_detectTransferRestriction`. A subclass
+adding checks to the latter without also overriding the former would have them silently bypassed —
+and `RuleSanctionsListBase` documents having fixed this exact anti-pattern.
+
+**Verdict — informational; valid observation, no current impact.** The code is as described
+(`RuleIdentityRegistryBase.sol:234-241`), and the cross-reference is accurate: `RuleSanctionsListBase.sol:187-190`
+carries precisely that warning. But the two early returns here are **duplicates of the delegate's own first two
+guards** (`:197-204`): delegating would return `TRANSFER_OK` for the same inputs, so behaviour is identical today.
+No subclass of `RuleIdentityRegistryBase` overrides `_detectTransferRestriction` — the only descendants are the
+two deployment variants.
+
+**Improvement — implemented in `v0.6.0`; behaviour-preserving, ~4 lines.** Delegate instead of returning a
+literal, exactly as `RuleSanctionsListBase` was changed to do. In
+`RuleIdentityRegistryBase._detectTransferRestrictionFrom` (`:234-241`), replace the two early returns:
+
+```solidity
+// before
+IIdentityRegistryVerified registry = identityRegistry;
+if (address(registry) == address(0)) {
+ return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+}
+if (to == address(0)) {
+ return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+}
+
+// after — the guards still scope ONLY the spender check; the delegation is unconditional
+IIdentityRegistryVerified registry = identityRegistry;
+if (address(registry) == address(0) || to == address(0)) {
+ return _detectTransferRestriction(from, to, value);
+}
+```
+
+Why this is safe:
+
+- **Identical outputs today.** `_detectTransferRestriction` opens with the same two guards (`:197-204`) and
+ returns `TRANSFER_OK` for both, so no input changes answer. The existing test suite should pass unmodified —
+ if any test moves, the change was not behaviour-preserving and must be re-examined rather than re-baselined.
+- **Burn stays exempt from the spender check.** The delegate never screens a spender, so routing burn through it
+ preserves the property the current comment at `:247-249` protects. That comment ("Burn is exempt too, but by
+ the early return above -- do NOT re-test `to` here") must be rewritten to say the guard now delegates, or it
+ becomes a stale claim about code that no longer exists.
+- **`view` and gas are unchanged** — one extra internal call, no storage access added.
+
+What it buys: a subclass that overrides only `_detectTransferRestriction` (the natural hook to extend) gets its
+check honoured on the `transferFrom`, mint and burn paths instead of silently dropped. That is the trap the
+sibling rule already closed, so closing it here also removes an inconsistency between two rules a reader will
+compare.
+
+**Also reviewed at the same time:** every other rule that overrides both hooks. `RuleMaxBalanceBase` (`:149-157`),
+`RuleMaxTotalSupplyBase` and `RuleChainlinkPoRBase` already delegate unconditionally and needed no change;
+`RuleSpenderWhitelistBase._detectTransferRestrictionFrom` (`:102-115`) deliberately does **not** delegate, because
+its `_detectTransferRestriction` is a hardcoded `TRANSFER_OK` — left as is.
+
+**Resolution — `v0.6.0`.**
+
+*Changed:* `src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol` — the two early returns become one
+guard that delegates, and the `:247-249` comment (which asserted burn was handled "by the early return above")
+was rewritten so it describes the code that now exists rather than the code that was removed.
+
+*Regression tests added:*
+
+- `src/mocks/harness/IdentityRegistryDelegationHarness.sol` — `IdentityRegistryExtraCheckHarness`, a subclass
+ that overrides **only** `_detectTransferRestriction` to add a registry-independent check. This is the shape
+ that exposes the defect, and it mirrors `SanctionsListDelegationHarness` one for one.
+- `test/RuleIdentityRegistry/RuleIdentityRegistryDelegation.t.sol` — 8 tests: the subclass check must reach
+ `transferFrom` with no registry configured and on `burnFrom`; the two entrypoints must agree; burn must stay
+ exempt from the opt-in spender check; the spender check must still short-circuit ahead of the delegated hook;
+ and base ERC-3643 screening (receiver-only, unverified sender and minter allowed) must be unchanged.
+
+*Verified, not assumed.* Reverting the source change and re-running the new suite fails 3 of the 8 tests with
+exactly the predicted symptoms — `transferFrom must reach the same hook as transfer: 0 != 202`,
+`burnFrom must reach the same hook as burn: 0 != 202`, and the two-entrypoint disagreement — and they pass once
+the change is restored. The pre-existing 21 `RuleIdentityRegistry` tests pass unmodified, which is the evidence
+that the change is behaviour-preserving: had any answer moved, one of them would have.
+
+*Suites:* 828 tests pass on the default profile and 31 on `FOUNDRY_PROFILE=erc3643`. Coverage on the changed
+contract: **100% statements, 100% branches**, 98.41% lines — the single uncovered line is the abstract
+`_authorizeIdentityRegistryManager` declaration, which no test can execute because only the override runs.
+
+### NM-5 / NM-13 — Cap rules evaluate a statically configured token
+
+**Claim (Medium ×2).** `RuleMaxTotalSupplyBase`, `RuleChainlinkPoRBase` and `RuleMaxBalanceBase` read
+`totalSupply()` / `balanceOf()` from a configured address and never verify that `msg.sender` is that token. One
+instance behind a shared engine caps the wrong asset; and `setTokenContract` / `setTokenMetadata` /
+`setBalanceToken` can repoint the observation target at any callable contract.
+
+**Verdict — accepted as design, documented verbatim.** Both halves are true and both are already on the record.
+`CLAUDE.md` states it as a standing gotcha: *"they read `totalSupply()` from the configured `tokenContract`, never
+from the token that triggered the check, and behind a RuleEngine they cannot learn that identity. One instance
+added to two RuleEngines evaluates both tokens against the first one's supply and feed… Chainlink's
+`SecureMintPolicy` blocks this with `onInstall`/`PolicyAlreadyBound`; adding an equivalent here would mean making
+a stateless validation rule bindable, which is a library-wide decision. Documented, not fixed."* The repointing
+half was catalogued and dismissed in the v0.4.0 audit ("`RuleMaxTotalSupply.setTokenContract` can repoint the
+supply oracle — trusted role"), and every repoint emits `TokenContractUpdated` / `TokenMetadataUpdated` /
+`MaxBalanceTokenUpdated` for off-chain monitoring.
+
+**Improvement — partially implementable; the complete fix is not available at the rule level.**
+
+*What cannot be done here.* The rule is never told which token triggered a check. `IRule`'s hooks are
+`transferred(from, to, value)` and `transferred(spender, from, to, value)` — **no token parameter** — and behind a
+RuleEngine `msg.sender` is the engine, so the identity is not recoverable from the call either. `IRule` also has
+no `onInstall` hook, so the rule is not even notified when it is added to an engine: `RulesManagementModule._addRule`
+only validates and stores the address. A rule that serves two tokens through one engine therefore *cannot*
+distinguish them, whatever it stores. Closing that half requires a token argument on the compliance hooks — an
+upstream `RuleEngine` / CMTAT interface change, the same conclusion reached for F-4.
+
+*What can be done — opt-in caller binding, closing the "one instance, two engines" half.* This blocks the
+deployment mistake the CLAUDE.md gotcha actually describes, and is `view`-preserving:
+
+```solidity
+// in TotalSupplyCapManager / ChainlinkPoRFeedManager / BalanceCapManager
+/// @notice When set, the only address allowed to notify this rule. Zero = unrestricted (legacy behaviour).
+address public boundCaller;
+
+function bindCaller(address caller) public virtual onlyMaxTotalSupplyManager {
+ require(caller != address(0), RuleMaxTotalSupply_CallerAddressZeroNotAllowed());
+ require(boundCaller == address(0), RuleMaxTotalSupply_CallerAlreadyBound(boundCaller));
+ boundCaller = caller;
+ emit CallerBound(caller);
+}
+
+// in the rule's write hooks only -- never on the ERC-1404 read path
+function _assertBoundCaller() internal view virtual {
+ address bound = boundCaller;
+ require(bound == address(0) || msg.sender == bound, RuleMaxTotalSupply_CallerNotBound(bound, msg.sender));
+}
+```
+
+Design constraints that make this shape the right one:
+
+- **Bind at configuration, not on first use.** Pinning the first caller lazily would need an `SSTORE` inside
+ `_transferred`, which is `internal view` today and whose public `transferred(...)` wrappers are declared `view`.
+ Making them non-`view` changes the published ABI mutability of four deployable contracts and turns read-only
+ validation rules into stateful ones. An explicit one-shot setter keeps every hook `view` and costs one warm
+ `SLOAD` per transfer.
+- **Unset must stay permissive**, or the change is breaking for every existing deployment and for the direct
+ (Topology B) wiring where the token itself calls the rule.
+- **Enforce on the write path only.** Adding the check to `detectTransferRestriction` would make a third party's
+ pre-flight query revert or fail, and those views must not revert.
+- **Add `unbindCaller`**, symmetric with `unbindToken` on the operation rules, or a mis-set binding bricks the
+ rule permanently. Document that unbinding does not reset the observed token.
+
+*What this does and does not buy.* It stops one instance being wired into two RuleEngines — the silent
+over-mint/freeze scenario. It does **not** isolate two tokens served by a single engine; that remains open and
+must stay documented. Given it is a partial remedy for a documented, trusted-role misconfiguration, the honest
+cost/benefit is: worth doing if the cap rules ever ship an upgradeable variant or a deployment script that wires
+engines automatically, and not worth a breaking storage-layout change before then.
+
+*Zero-cost alternative available today:* `tokenContract` / `balanceToken` are already public and every change
+emits an event, so a deployment checklist plus an off-chain assertion that
+`rule.tokenContract() == the token whose engine holds this rule` catches both halves — including the one no
+on-chain guard can reach. That is the currently recommended control and should be stated in the deployment guide.
+
+### NM-6 — `RuleNFTAdapter` handles owner-initiated transfers differently across entrypoints — ✅ FIXED (`v0.6.0`)
+
+**Claim (Medium).** `transferred(FungibleTransferContext)` / `(MultiTokenTransferContext)` normalise
+`ctx.sender == ctx.from` to the direct `_transferred` hook, while the ERC-7943 5-arg
+`transferred(spender, from, to, tokenId, value)` always calls `_transferredFrom`. For `spender == from`,
+`RuleSpenderWhitelist` accepts via the context path and rejects via the ERC-7943 path.
+
+**Verdict — informational; confirmed divergence, not a bypass.** The asymmetry is real
+(`RuleNFTAdapter.sol:46-63` vs `:89-102`). It is not a loosening of policy: **the context path's answer is the
+one that matches the rest of the library.** `RuleSpenderWhitelist`'s documented contract is that *direct*
+transfers are always allowed and only delegated ones are screened; an owner moving their own tokens is a direct
+transfer, and a plain ERC-20 `transfer` produces exactly the same outcome (CMTAT passes `spender == address(0)`,
+taking the 3-arg path). The deviant branch is the ERC-7943 5-arg one, which is **stricter** than intended when a
+caller elects to pass `spender == from`. Nothing is admitted that a plain transfer would not admit, so there is
+no compliance gap — only an inconsistency for an integrator who reaches for both surfaces.
+
+**Improvement — implemented in `v0.6.0`; contained to one file.** Lift the normalisation the context entrypoints
+already perform into a shared helper, and apply it to the ERC-7943 overloads so all six adapter entrypoints agree.
+
+```solidity
+// RuleNFTAdapter -- one predicate, used by every entrypoint that receives a spender
+/**
+ * @notice Returns whether `spender` acts on behalf of `from` rather than as `from` itself.
+ * @dev An owner moving their own tokens is a direct transfer: a plain ERC-20 `transfer` reaches
+ * the 3-arg hook with `spender == address(0)`, and the ITransferContext entrypoints already
+ * normalise `sender == from` the same way.
+ */
+function _isDelegated(address spender, address from) internal pure virtual returns (bool) {
+ return spender != address(0) && spender != from;
+}
+```
+
+Then the two `ITransferContext` entrypoints (`:46-63`) become `if (_isDelegated(ctx.sender, ctx.from))`, and the
+three spender-aware ERC-7943 overloads gain the same branch:
+
+```solidity
+function transferred(address spender, address from, address to, uint256 /* tokenId */, uint256 value)
+ public virtual override(IERC7943NonFungibleComplianceExtend)
+{
+ if (_isDelegated(spender, from)) {
+ _transferredFrom(spender, from, to, value);
+ } else {
+ _transferred(from, to, value);
+ }
+}
+// identically for detectTransferRestrictionFrom(...) and canTransferFrom(...)
+```
+
+Behaviour audit — which rules actually change when `spender == from` on the 5-arg path:
+
+| Rule | Today | After | Net |
+|---|---|---|---|
+| `RuleSpenderWhitelist` | rejects an unlisted owner (code 66) | allows | **Fixed** — matches its documented "direct transfers are always allowed" |
+| `RuleBlacklist`, `RuleSanctionsList`, `RuleERC2980` | blocks via the spender branch | blocks via the `from` branch | none — still blocked, different code |
+| `RuleWhitelist` (`checkSpender`) | needs `from` listed *and* spender listed | needs `from` listed | none — same address |
+| `RuleIdentityRegistry` (`checkSpender` on, `checkSender` off) | rejects an unverified owner (code 57) | allows | **Observable change**, and the ERC-3643-conformant answer: the spec screens the receiver only, and the same holder's plain `transfer` already passes |
+
+So the deny-lists are unaffected, one rule is corrected, and one loosens in the direction the standard requires.
+Cost: one `internal pure` call, no storage. Pin it with a test per affected rule asserting that the 5-arg
+`spender == from` call and the 3-arg call return the same code.
+
+*Rejected alternative — normalise the other way* (make the context entrypoints always call `_transferredFrom`,
+retaining spender semantics). It would screen an owner as their own spender on every plain transfer relayed
+through `ITransferContext`, breaking `RuleSpenderWhitelist`'s documented contract and diverging from what CMTAT
+produces for the same transfer. Consistency achieved at the price of the wrong answer.
+
+*Do not* apply the normalisation inside `_detectTransferRestrictionFrom` / `_transferredFrom` themselves: those
+are the generic 4-arg hooks CMTAT and the RuleEngine call, and rewriting `spender == from` there would silently
+change every rule on the main integration path, not just the ERC-7943 surface.
+
+**Resolution — `v0.6.0`.**
+
+*The principle that fixed the scope.* The three interfaces signal a direct transfer **differently**, and that,
+not the entrypoint count, is what decides the routing:
+
+| Interface | Direct transfer arrives as | Delegated as |
+|---|---|---|
+| CMTAT 3-arg / 4-arg | the 3-arg overload, or `spender == address(0)` | `spender != address(0)` |
+| ERC-7943 `tokenId` overloads | `spender == from` — the interface calls that parameter "the address performing the transfer (**owner**/operator)" | `spender != from` |
+| `ITransferContext` | `sender == from` (the token's `msg.sender`), or `0` | `sender != from` |
+
+The ERC-7943 and `ctx` interfaces share a convention; the CMTAT pair uses a different one that already
+distinguishes the two cases. So the fix normalises the **adapter** entrypoints only, and deliberately leaves the
+4-arg CMTAT path alone — which also means no change to the primary integration path, no restriction-code
+relabelling for existing integrators, and one file touched instead of twelve.
+
+*Changed:* `src/rules/validation/abstract/core/RuleNFTAdapter.sol` — added
+`_isDelegated(spender, from) => spender != address(0) && spender != from`, replaced the duplicated predicate in
+both `ctx` entrypoints with it, and routed the three ERC-7943 spender-aware overloads
+(`transferred`, `detectTransferRestrictionFrom`, `canTransferFrom`) through it. Contract-level NatSpec records
+the table above and warns against "aligning" the 4-arg path.
+
+*Regression tests* in `test/TransferContext/OverloadParity.t.sol` — the suite already existed for exactly this
+property but only ever tested two of the three input shapes (`sender == 0` and `sender != from`), which is why
+the gap survived. Added `_assertSelfSpenderIsDirect`, run for every rule in the suite on both an allowed and a
+blocked pair, plus two targeted tests: `test_NM6_SelfSpenderIsNotScreenedByTheSpenderWhitelist` (the outcome
+that was wrong) and `test_NM6_CmtatFourArgPathKeepsScreeningASelfSpender` (pinning the deliberate asymmetry so
+nobody removes it later). The suite's header comment, which asserted flat parity, now states the per-interface
+conventions — the loose wording is what made the missing case invisible.
+
+*Verified, not assumed.* Reverting the three routings fails **6 of 10** tests across **5 rules**, and the failure
+messages are the impact analysis:
+
+```
+RuleBlacklist [self-spender, blocked]: 38 != 36 ← blocked either way, code relabelled
+RuleERC2980 [self-spender, blocked]: 62 != 60 ← blocked either way, code relabelled
+RuleSanctionsList [self-spender, blocked]: 32 != 30 ← blocked either way, code relabelled
+RuleWhitelist [self-spender, blocked]: 23 != 21 ← blocked either way, code relabelled
+RuleSpenderWhitelist [self-spender]: 66 != 0 ← THE ONLY OUTCOME CHANGE
+```
+
+For the deny-lists the transfer was rejected before and after — only which leg reported it changed, because the
+owner is screened as `from` instead of as `spender`. `RuleSpenderWhitelist` is the one rule where the answer was
+actually wrong: an owner-initiated ERC-721 `transferFrom` was rejected with code 66 despite the rule documenting
+that direct transfers are always allowed. A genuine delegated transfer by the same unlisted address is still
+rejected — the screen was narrowed to what it always claimed to cover, not removed.
+
+*Suites:* 835 tests pass on the default profile, 31 on `FOUNDRY_PROFILE=erc3643`. Coverage on `RuleNFTAdapter`:
+**100% statements, 100% branches**.
+
+*Also updated:* `RULE_SEMANTICS.md` §3, which previously described the parity as flat and is now the reference
+for the per-interface conventions.
+
+### NM-7 / NM-12 / NM-15 — Conditional-transfer approvals are not token-scoped
+
+**Claim (Medium ×2, Low ×1).** `_transferHash(from, to, value)` has no token dimension, while `bindRuleEngine`
+authorises an engine that may relay several tokens. An approval recorded for token A is consumable by an
+identical transfer of token B, and `unbindToken` clears neither `approvalCounts` nor `ruleEngine`.
+
+**Verdict — accepted as design; this is the documented reason the multi-token variant exists.** Verified at
+`RuleConditionalTransferLightApprovalBase.sol:163-174` and `RuleConditionalTransferLightBase.sol:191-206`. The
+`bindRuleEngine` NatSpec states the constraint in the scanner's own terms and then some:
+
+> **bind ONLY an engine that serves this one token.** Approvals here are keyed `(from, to, value)` with **no
+> token dimension**… If the engine serves several tokens, an approval recorded for one is consumable by ANY of
+> them — approve 100 for token A, and a 100 transfer of token B consumes it. That is inherent to the single-token
+> rule and is why `RuleConditionalTransferLightMultiToken` exists.
+
+The stale-state half is covered by the `bindToken` WARNING plus `resetApproval` / `unbindRuleEngine` (v0.4.0
+audit F-9). The scanner's closing point — "enforced solely by NatSpec warnings; nothing in the code prevents an
+administrator from wiring a multi-tenant engine" — is correct and is the accepted position: the compliance
+manager is a trusted role, and the alternative (a per-token approval key) is a different rule that already ships.
+
+### NM-9 / NM-16 — Spender policy is unenforceable through spender-less hooks
+
+**Claim (Medium + Low).** Spender screening lives exclusively on the 4-arg path.
+`RuleSpenderWhitelistBase.transferred(address,address,uint256)` is a no-op and its 3-arg detector returns
+`TRANSFER_OK`; the same context loss disables code 23 (unlisted spender), 38 (blacklisted), 32 (sanctioned) and
+62 (frozen). A token routing `transferFrom` through the 3-arg path lets a restricted spender move tokens.
+
+**Verdict — accepted as design; correct, and a topology requirement rather than a defect.** Verified at
+`RuleSpenderWhitelistBase.sol:49`, `:91-93`, `:102-115`. A rule cannot screen an identity it is never given.
+`RULE_SEMANTICS.md` §1 already scopes the whole spender column to "the 4-arg `transferred(spender, from, to,
+value)` path… (CMTAT v3.3+)".
+
+The scanner is nonetheless pointing at something worth stating more loudly, and it is the same root cause as
+NM-11: **a real ERC-3643 / T-REX token calls `_tokenCompliance.transferred(_from, _to, _amount)` from
+`transferFrom` — three arguments, no spender.** On that integration `RuleSpenderWhitelist` is *silently inert*
+rather than merely unhelpful, and the spender branches of the blacklist / sanctions / ERC-2980 rules never fire.
+Both supply-based cap rules now ship ERC-3643 variants (NM-11), and the per-interface conventions are written up
+in `RULE_SEMANTICS.md` §5; the spender-inertness above remains a documentation matter, since no rule can screen
+an identity it is never given.
+
+### NM-10 — Future-dated PoR timestamps skip the freshness check — ✅ FIXED (`v0.6.0`)
+
+**Claim (Medium).** `ChainlinkPoRFeedManager._maxBackedSupply` flags staleness only when
+`block.timestamp > updatedAt`; it does not reject `updatedAt > block.timestamp`. A feed returning an old answer
+with a future timestamp is treated as fresh until that timestamp plus the staleness window elapses.
+
+**Verdict — informational; confirmed code behaviour, the cheapest hardening in the report.** The guard is exactly
+as quoted (`ChainlinkPoRFeedManager.sol:215`):
+
+```solidity
+if (staleness != 0 && block.timestamp > updatedAt && block.timestamp - updatedAt > staleness) {
+```
+
+The `block.timestamp > updatedAt` term exists to keep the subtraction from underflowing on a MUST-NOT-revert
+path, and it has the side effect of admitting any future timestamp. Reachability is narrow: a Chainlink
+aggregator stamps `updatedAt` with `block.timestamp` at write time on the same chain, so a future value cannot
+arise legitimately — it requires a faulty or compromised feed, and the report's own severity note concedes that
+"a future timestamp alone does not increase mint headroom". A feed able to forge a timestamp can also simply
+overstate `answer`, which the rule trusts by construction.
+
+**Improvement — implemented in `v0.6.0`; one line, no new restriction code, no new storage.** Treat a future `updatedAt`
+as a **malformed answer** rather than as a staleness question. `CODE_RESERVES_ANSWER_INVALID` (77) already means
+"the feed responded but the answer cannot be used: a negative reserve, or an incomplete round", and a round
+stamped in the future is the same class of defect. Fold it into that existing branch
+(`ChainlinkPoRFeedManager.sol:209-217`):
+
+```solidity
+try feed.latestRoundData() returns (uint80, int256 answer, uint256, uint256 updatedAt, uint80) {
+ // A negative reserve is meaningless, `updatedAt == 0` marks a round that never completed, and a
+ // round stamped in the future cannot have been written by an aggregator on this chain -- all three
+ // are malformed answers, not stale ones.
+ if (answer < 0 || updatedAt == 0 || updatedAt > block.timestamp) {
+ return (CODE_RESERVES_ANSWER_INVALID, 0);
+ }
+ uint256 staleness = maxStalenessSeconds;
+ // `updatedAt <= block.timestamp` is guaranteed above, so the subtraction cannot underflow and the
+ // `block.timestamp > updatedAt` guard that used to carry it is no longer needed.
+ if (staleness != 0 && block.timestamp - updatedAt > staleness) {
+ return (CODE_RESERVES_FEED_STALE, 0);
+ }
+ ...
+```
+
+Why this framing beats a second staleness branch:
+
+- **It resolves the `maxStalenessSeconds == 0` ambiguity instead of creating it.** Zero is documented as
+ *disabling the staleness check*; a future-timestamp rejection gated on `staleness != 0` would be surprising,
+ and one that ignores the gate would contradict the documented meaning of zero. As an answer-validity check it
+ is correctly unconditional — a malformed round is malformed whether or not freshness is being policed.
+- **No code-range or ABI change.** 77 already exists, is already returned by `canReturnTransferRestrictionCode`
+ (`RuleChainlinkPoRBase.sol:62-66`), and already has a `messageForTransferRestriction` string. Adding a new code
+ would touch the invariant storage, the message mapping, `canReturnTransferRestrictionCode`, `CLAUDE.md`'s code
+ table and the docs, for no diagnostic gain.
+- **It removes the underflow guard's side effect** rather than layering a second check on top of it, so the
+ reason each comparison exists stays legible.
+- **It cannot break the revert-free invariant**: the change is one comparison on values already in scope.
+
+**Resolution — `v0.6.0`.**
+
+*Changed:*
+
+- `src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol` — `updatedAt > block.timestamp` folded into the
+ malformed-answer branch, and the now-redundant `block.timestamp > updatedAt` term dropped from the staleness
+ comparison (step 3 guarantees the subtraction cannot underflow). The comment states why a future stamp is not
+ treated as staleness.
+- `src/rules/validation/abstract/invariant/RuleChainlinkPoRInvariantStorage.sol` — the
+ `CODE_RESERVES_ANSWER_INVALID` NatSpec now lists all three causes and records the `maxStalenessSeconds == 0`
+ reasoning.
+
+*Regression tests added* — 5 in `test/RuleChainlinkPoR/RuleChainlinkPoRUnit.t.sol`: a future-dated round yields
+77 from `detectTransferRestriction` and `canTransfer`; it is still rejected with `maxStalenessSeconds == 0` (the
+test that pins the design decision); `updatedAt == block.timestamp` still passes (the boundary a just-published
+round sits on); `maxBackedSupply()` previews 77 without reverting; and the write hook reverts the mint.
+
+*Verified, not assumed.* Reverting the source change fails 4 of the 5 with the predicted symptoms —
+`assertion failed: 0 != 77` three times, and `next call did not revert as expected` for the enforcement test. The
+fifth (the `updatedAt == block.timestamp` boundary) passes either way by construction, which is what makes it a
+useful guard against over-correcting into `updatedAt >= block.timestamp`.
+
+*Suites:* 833 tests pass on the default profile, 31 on `FOUNDRY_PROFILE=erc3643`. Coverage on
+`ChainlinkPoRFeedManager`: **100% statements, 100% branches**; `RuleChainlinkPoRBase` 100% across the board.
+
+*Also updated:* `doc/technical/contracts/RuleChainlinkPoR.md` — the restriction-code table, the numbered
+evaluation order, the operator triage table, and the two rows of the Chainlink ACE comparison that described the
+old underflow guard. The ACE comparison now records that this rule rejects a future-dated round where ACE
+underflow-panics on it, and that the rejection is not gated on `maxStalenessSeconds`.
+
+Still hardening rather than a fix in impact terms: reaching the branch needs an aggregator already misbehaving
+badly enough to forge a timestamp, and such an aggregator can overstate `answer` directly. What it buys is that
+the rule no longer has a state in which it treats an impossible timestamp as evidence of freshness.
+
+### NM-11 — Caps double-count when the token notifies **after** moving the value — ✅ FIXED (`v0.6.0`, 2 of 3 rules)
+
+**Claim (Medium).** `BalanceCapManager._capExceeded` and `TotalSupplyCapManager._capExceeded` compare the live
+balance/supply against `value`. That is correct only if the token calls `transferred(...)` *before* mutating
+state. ERC-3643 / T-REX tokens call it *after*, so `balanceOf(to)` already includes `value` and the effective cap
+becomes `balance_before + 2 × value <= maxBalance`. The read path (`canTransfer`) answers pre-update and the
+write path then reverts on the same parameters — legitimate transfers are blocked and the last chunk of headroom
+is unreachable.
+
+**Verdict — CONFIRMED.** Every step checks out:
+
+- The assumption is real and already stated in-source (`RuleMaxBalanceBase.sol:21-23`): *"**Assumes the token
+ calls this BEFORE moving the value**, so `balanceOf(to)` still excludes `value`. CMTAT does; a token notifying
+ afterwards would halve the effective cap."* It is pinned by
+ `testMintExactlyToTheCapProvesPreUpdateAccounting`, and `CLAUDE_ANALYSIS_MAXBALANCE.md` H-1 records the
+ mutation test that proved the guard.
+- The vendored T-REX token calls it afterwards. `lib/ERC-3643/contracts/token/Token.sol` — `transfer` (`:532-533`),
+ `transferFrom` (`:312-313`) and `forcedTransfer` (`:557-558`) each run `_transfer(...)` **then**
+ `_tokenCompliance.transferred(...)`; `mint` reports through `_tokenCompliance.created(_to, _amount)` (`:572`,
+ after `_mint`), which `RuleEngineBase.created` forwards as the 3-arg `transferred(address(0), to, value)` — and
+ both cap rules gate on `from == address(0)`, so the mint path is affected too.
+- **This is a configuration the project supports and tests**, not a hypothetical: `test/ERC3643Real/
+ ERC3643RealTokenRuleEngine.t.sol` wires `real ERC-3643 Token ── compliance slot ──▶ RuleEngine ──▶ Rule` and
+ relies on the rule reverting inside the post-state-change notification as the enforcement mechanism
+ (`testForcedTransfer_StillBlockedByTheRuleViaTransferred`). That suite exercises `RuleWhitelist`, which is
+ order-independent; **no cap rule is covered there**, which is why this was not caught.
+
+Direction of failure is **conservative** — over-restriction, never over-issuance. Nothing can be minted or
+received above the cap; what breaks is that transfers and mints *within* the cap are rejected, and the pre-flight
+view disagrees with enforcement. There is no exploit, and the CMTAT path is unaffected.
+
+**Enabling structure landed in `v0.6.0` (the fix itself is still a deployment choice).** The three rules now
+share [`CapAccounting`](../../../../../src/rules/validation/abstract/core/CapAccounting.sol) and each exposes
+`_detectTransferRestrictionOnNotify`, the hook the **write** path enforces through. It defaults to the pre-flight
+check — today's CMTAT behaviour, unchanged — and an ERC-3643 variant overrides one line:
+
+```solidity
+function _detectTransferRestrictionOnNotify(address from, address to, uint256)
+ internal view override returns (uint8)
+{
+ return _detectTransferRestriction(from, to, 0); // the observation already includes the value
+}
+```
+
+Two design points that came out of building it, both now pinned by tests:
+
+- **Only the write path may be re-phased.** A single "observation includes the value" flag applied to both paths
+ was the first shape tried and is wrong: a pre-flight view always runs *before* the movement on either kind of
+ token, so re-phasing it makes the pre-flight answer disagree with enforcement — the mirror image of this very
+ finding. `testMaxTotalSupply_PreFlightViewStillCountsTheValue` pins that.
+- **`_currentSupply` / `_balanceOf` are the second seam**, letting a rule serve the figure from its own storage
+ instead of the token. Such a rule controls when it records, so it never has to answer the phase question at
+ all. Verified feasible for *supply*; **not** for per-address balances, because how `Token.recoveryAddress`
+ moves a balance changed across T-REX versions — up to 4.1 it routed through the public `forcedTransfer`, which
+ notifies compliance, while the vendored 4.2.0-beta1 calls `_transfer` directly and notifies nobody. A shadow
+ ledger would be correct on one minor version and permanently skewed on the next.
+
+Worked variants of all three rules live in `src/mocks/harness/ERC3643CapHarnesses.sol`, and
+`test/CapAccounting/ERC3643CapSeams.t.sol` reproduces this finding on the stock rules while showing the variants
+are correct. `RULE_SEMANTICS.md` §5 is the write-up.
+
+**Resolution — `v0.6.0`. Shipped for two of the three rules; the third is deliberately left.**
+
+*The arithmetic, concretely.* Reserves 1000, supply 0, an agent mints 1000 on a real T-REX token:
+
+| Step | `_currentSupply()` | Comparison | Result |
+|---|---|---|---|
+| 1. `canTransfer(0, to, 1000)` — **before** `_mint` | `0` | `_capExceededBy(0, 1000, 1000)` → `1000 > 1000-0`? no | allowed |
+| 2. `_mint(to, 1000)` | — | supply becomes 1000 | — |
+| 3a. `created` → **stock rule** | `1000` | `_capExceededBy(1000, 1000, 1000)` → `1000 > 0`? **yes** | **reverts** |
+| 3b. `created` → **ERC-3643 variant** | `1000` | `_capExceededBy(1000, 1000, 0)` → `0 > 0`? no | allowed |
+
+At step 3 the minted amount is already inside `currentSupply`; the stock rule adds the same amount again as
+`value` and asks whether 2000 fits under 1000. Row 1 is why the read path must keep projecting `value`: the token
+consults compliance on **both** sides of the state change inside one transaction.
+
+*Contracts added.*
+
+| Contract | For |
+|---|---|
+| `RuleChainlinkPoRERC3643` / `…Ownable2Step` | Reserve-backed mint cap on ERC-3643 |
+| `RuleMaxTotalSupplyERC3643` / `…Ownable2Step` | Static supply cap on ERC-3643 |
+
+Each is a subclass overriding `_detectTransferRestrictionOnNotify` and nothing else; reserve/cap logic,
+restriction codes, configuration, roles and events are inherited unchanged, and the stock rules are untouched.
+
+*Tests.* 49 added in total:
+
+- `test/ERC3643Real/ERC3643RealTokenChainlinkPoR.t.sol` (12) and
+ `test/ERC3643Real/ERC3643RealTokenMaxTotalSupply.t.sol` (10) drive the **genuine** vendored
+ `lib/ERC-3643/` token, not a mock. Four of them pin the stock rules failing on that same token, so this
+ finding stays executable rather than becoming prose.
+- The supply-cap suite covers **both compositions with the PoR variant** — static cap binding and reserves
+ binding — which is the pairing the documentation prescribes, since PoR has no margin parameter.
+- Unit suites in the default profile for each variant (10 + 10), because `forge coverage` skips
+ `test/ERC3643Real/**` and the deployables would otherwise report 0%.
+- `test/CapAccounting/ERC3643CapSeams.t.sol` (7) covers the seams generically, including `RuleMaxBalance`.
+
+*A second ERC-3643 hazard found while testing this one, and now pinned.* T-REX deploys the token and
+initialises it in two steps, and an uninitialised `Token` reports `decimals() == 0`. `RuleChainlinkPoR`'s
+constructor probes `decimals()` and accepts a matching `0`, so a rule built before `init` is configured for a
+0-decimals token — and `init(..., 18, ...)` then makes it an 18-decimals token while the rule still believes 0.
+Nothing reverts and no event marks it; reserves are scaled by `10 ** 18` too little and every mint is refused.
+The same mistake reversed would authorise unbacked minting. The constructor probe cannot catch it — it genuinely
+succeeded. The remedy is deployment order (build the rule after `init`, or re-sync with `setTokenMetadata`),
+documented on the contract page and pinned by `testRuleBuiltBeforeInitCachesTheWrongDecimals`.
+
+*Documentation.* New pages `doc/technical/contracts/RuleChainlinkPoRERC3643.md` and
+`RuleMaxTotalSupplyERC3643.md`, each leading with the ERC-3643-only warning and a table of what breaks with the
+wrong variant **in either direction** — neither mistake reverts at deployment. `RULE_SEMANTICS.md` §5 carries the
+seam write-up; `CLAUDE.md` / `AGENTS.md` carry the gotcha; both READMEs list the variants.
+
+**`RuleMaxBalance` deliberately has no ERC-3643 variant.** It is not the same one-line change, for three reasons
+that need a policy decision rather than a hook override:
+
+- `balanceOf(to)` is **per-address**, so the rule engages on every transfer rather than only on mints — a far
+ larger interaction surface with T-REX's agent powers than the two supply rules have.
+- **`forcedTransfer` does notify compliance**, so a post-update variant would *revert* an agent's forced transfer
+ that pushes the recipient over the cap. On T-REX ≤ 4.1, where `recoveryAddress` routes through
+ `forcedTransfer`, that **bricks wallet recovery** whenever the destination wallet already holds tokens.
+- On the vendored 4.2.0-beta1 `recoveryAddress` notifies **nobody**, so a recovered wallet can silently sit above
+ the cap. A token-reading rule self-heals — further receipts are blocked — but the invariant is violated in
+ state with no event from the rule.
+
+T-REX's own module library also already ships a `MaxBalanceModule`, so the marginal value is lowest of the three.
+`RuleMaxBalance` is therefore documented as CMTAT-path-only until the forced-transfer exemption question is
+settled.
+
+### NM-14 / NM-22 — A reverting identity registry or sanctions oracle reverts the read path
+
+**Claim (Low ×2).** `setIdentityRegistry` accepts any non-zero address without verifying `isVerified(address)` is
+callable, and `RuleSanctionsListBase` calls `oracle.isSanctioned(...)` with no failure handling. A registry or
+oracle that reverts, is codeless, or returns malformed data makes `detectTransferRestriction` / `canTransfer` —
+which the project documents as never-reverting — revert, and halts every transfer, mint and burn on the bound
+token.
+
+**Verdict — accepted as design; already catalogued and dismissed in the v0.4.0 audit.** The code is as described
+(`RuleIdentityRegistryBase.sol:101-105`, `:213`; `RuleSanctionsListBase.sol:161-167`, `:191`), and
+`CLAUDE_AUDIT.md`'s "observations considered and dismissed" table carries the row verbatim: *"Reverting sanctions
+oracle / identity registry bricks transfers — trusted external dependency; a revert bubbles up with no state
+corruption."* Failure is **closed** (nothing is admitted), the state is intact, and recovery is a single
+privileged `setSanctionListOracle` / `clearSanctionListOracle` / `setIdentityRegistry` call.
+
+Fair caveat the scanner earns: the library's "the ERC-1404 views MUST NOT revert" invariant is enforced for the
+*supply*, *balance* and *PoR feed* reads (guarded by `try/catch` plus configuration probes) but **not** for these
+two. Closing the gap means choosing a fail direction for an unreadable list and minting new restriction codes for
+"registry unavailable" / "oracle unavailable" — a deliberate, breaking addition to the code ranges. Recorded as an
+open, intentional asymmetry rather than a silent one.
+
+### NM-17 — `approveAndTransferIfAllowed` can leave a residual approval — ✅ FIXED (`v0.6.0`)
+
+**Claim (Low).** The helper records the approval *before* `safeTransferFrom` so the callback can consume it, and
+never verifies afterwards that it was consumed. If the token does not call back — a plain ERC-20 bound for the
+helper, or an engine never bound / since unbound — the transfer succeeds and the approval count stays
+incremented, authorising one later unapproved transfer of the same `(from, to, value)`.
+
+**Verdict — accepted as design; the inversion is deliberate, documented, and previously triaged.** Verified at
+`RuleConditionalTransferLightBase.sol:113-129`; the NatSpec states both halves ("This function is only safe for
+tokens that call back `transferred()` during transfer" and "CEI is intentionally inverted so the approval exists
+for the callback"), and `CLAUDE_AUDIT.md` dismisses the CEI inversion as "deliberate and documented; the rule
+custodies no value, and reentrancy could at most consume approvals the operator already granted for the same
+tuple". Reaching the residual state requires the operator to run the helper against a binding they configured
+incorrectly, and the leftover is visible via `approvedCount` and clearable via `resetApproval` /
+`cancelTransferApproval`.
+
+**Improvement — implementable, ~5 lines plus one error, in both variants.** The helper cannot check the callback
+*happened*, but it can check the only thing that matters: that the approval it created was consumed. Snapshot the
+count, and require it back afterwards.
+
+```solidity
+// RuleConditionalTransferLightBase.approveAndTransferIfAllowed
+function approveAndTransferIfAllowed(address from, address to, uint256 value)
+ public virtual onlyTransferApprover returns (bool)
+{
+ address token = getTokenBound();
+ require(token != address(0), RuleConditionalTransferLight_TokenNotBound());
+
+ uint256 approvalsBefore = approvedCount(from, to, value);
+ approveTransfer(from, to, value);
+
+ uint256 allowed = IERC20(token).allowance(from, address(this));
+ require(allowed >= value, RuleConditionalTransferLight_InsufficientAllowance(token, from, allowed, value));
+
+ IERC20(token).safeTransferFrom(from, to, value);
+
+ // The approval above exists ONLY for the token's compliance callback to consume. If the count did
+ // not come back down, no callback reached this rule -- the binding is wrong -- and leaving the
+ // surplus would authorise a later, never-approved transfer of the same tuple.
+ require(
+ approvedCount(from, to, value) == approvalsBefore,
+ RuleConditionalTransferLight_ApprovalNotConsumed(token, from, to, value)
+ );
+ return true;
+}
+```
+
+with one addition to `RuleConditionalTransferLightInvariantStorage`:
+
+```solidity
+error RuleConditionalTransferLight_ApprovalNotConsumed(address token, address from, address to, uint256 value);
+```
+
+The multi-token variant needs the identical change in `RuleConditionalTransferLightMultiTokenBase`
+(`:131`), using its token-keyed accessor `approvedCount(token, from, to, value)` (`:216`) and its own error
+namespace.
+
+Correctness of the post-condition:
+
+- **Holds in both supported topologies.** `_transferred` decrements by exactly one and returns early only when an
+ endpoint is `address(0)` — impossible here, since `safeTransferFrom` would have reverted on a zero `from` and
+ the helper is not a mint/burn path. Direct binding: the token calls `transferred`. Engine binding: the engine
+ relays it. Either way the count returns to `approvalsBefore`.
+- **Fires exactly where the finding is.** A plain ERC-20 bound with `bindToken` but no callback, or an engine
+ never bound / since unbound, now reverts the whole call — including the ERC-20 transfer — instead of completing
+ it and leaving a spendable approval behind. That is a behaviour change worth calling out in the release notes:
+ a deployment relying on the helper against a non-callback token stops working, which is the point.
+- **Reentrancy-safe by construction.** It reads state *after* the external call, so a hostile token can only make
+ the check fail, never pass spuriously. Any path that consumed more than one approval also fails, which is the
+ desired direction.
+- **Cost:** two warm `SLOAD`s (~200 gas) on an operator-only path.
+
+**Resolution — `v0.6.0`. Implemented in both variants**, with the error declared in each rule's own invariant
+storage (`RuleConditionalTransferLight_ApprovalNotConsumed` /
+`RuleConditionalTransferLightMultiToken_ApprovalNotConsumed`).
+
+*Tests — 5 added, and the existing mock made the awkward case easy.* `MockERC20WithTransferContext` is a no-op
+notifier when no rule is set, so leaving `setRule` uncalled produces a token that moves value and tells nobody —
+exactly the shape of the finding, with no new mock needed. Single-token: the silent token reverts with
+`..._ApprovalNotConsumed`, leaving no residual approval and no moved value; an operator's pre-existing approvals
+for the same tuple survive the helper; the ordinary direct-binding flow still consumes exactly one per call.
+Multi-token: the same silent-token case, plus a check that the count stays per-token.
+
+*Verified, not assumed.* Removing the two `require`s makes both silent-token tests fail with *"next call did not
+revert as expected"*, and nothing else moves.
+
+*Note on the pre-existing suite.* All 871 tests passed unchanged the moment the post-condition was added, because
+every existing test uses a token that does call back. That is simultaneously the reassurance that this is not a
+regression and the evidence that the non-callback path had **no coverage at all** before these tests — which is
+how the hole survived.
+
+*Coverage:* `RuleConditionalTransferLightBase` at 100% statements, branches and functions.
+
+*Documented* in both contract pages and in the `CLAUDE.md` / `AGENTS.md` gotchas, including the behaviour change:
+a deployment running the helper against a non-callback token now reverts instead of completing. That is the fix
+rather than a side effect — the transfer was leaving a compliance hole behind.
+
+### NM-18 — The wrapper can be bricked by a non-`IAddressList` child — ✅ FIXED (`v0.6.0`)
+
+**Claim (Low).** `RuleWhitelistWrapperBase._detectTransferRestrictionForTargets` casts every child to
+`IAddressList` without checking. A rules manager can add a valid `IRule` that is not an address list (e.g.
+`RuleMaxTotalSupply`); once it sits before a later whitelist child, any check that has not already resolved every
+target reverts on the blind `areAddressesListed` call — read path *and* `transferred`.
+
+**Verdict — informational; confirmed, and a known open item.** The unchecked cast is at
+`RuleWhitelistWrapperBase.sol:237`, and `CLAUDE.md` lists it as a standing gotcha ("`RuleWhitelistWrapper` does
+not ERC-165-check its child rules… a non-`IAddressList` child bricks the scan"). It is the still-open half of
+v0.4.0 audit **F-5**, recorded there as "partially fixed — `IAddressList` now advertised; the wrapper guard
+remains open". The scanner's detail about the short-circuit is accurate and matches the documented behaviour of
+`_detectTransferRestrictionForTargets` (early exit once every target resolves), which is exactly why the failure
+is *order-dependent* and can appear only for some address pairs.
+
+**Improvement — implementable now; two complementary layers, and only the first is cheap.**
+
+*Layer 1 — reject at configuration (recommended).* Override `_checkRule`, **not** `addRule`: it is
+`internal view virtual` and both public entrypoints route through it (`addRule` → `_addRule` → `_checkRule`, and
+`setRules` → `_addRule` → `_checkRule`), so one override covers every path and stays `view`. **`RuleEngineBase`
+already does exactly this** for its own children (`RuleEngineBase.sol:228-233`), so the pattern is established
+in the dependency the wrapper inherits from:
+
+```solidity
+// RuleWhitelistWrapperBase -- mirrors RuleEngineBase._checkRule
+import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
+
+function _checkRule(address rule_) internal view virtual override {
+ RulesManagementModule._checkRule(rule_); // zero-address and duplicate checks
+ require(
+ ERC165Checker.supportsInterface(rule_, AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID),
+ RuleWhitelistWrapper_ChildIsNotAnAddressList(rule_)
+ );
+}
+```
+
+`ERC165Checker.supportsInterface` is itself non-reverting — it uses a bounded, gas-capped `staticcall` and
+returns `false` for a codeless address, a missing selector or malformed return data — so a hostile candidate
+cannot brick the setter it is being screened by. The four intended children already advertise the ID
+(`0x5d10e182`): verified on `RuleWhitelistBase:62`, `RuleReceiverWhitelistBase:95`, `RuleSpenderWhitelistBase:79`
+and `RuleBlacklistBase:100`, so no legitimate configuration is rejected.
+
+What layer 1 buys and what it misses:
+
+- ✅ Closes **NM-18** — the `RuleMaxTotalSupply`-as-child case is rejected at `addRule` instead of bricking
+ transfers later.
+- ✅ Closes **NM-19**'s failure mode — a nested wrapper is refused up front with a named error rather than
+ bricking every transfer through the parent. It does not *enable* nesting: that needs the wrapper to implement
+ and advertise `IAddressList` — declined under NM-19, because an OR nested in an OR is algebraically flat.
+- ❌ Does **not** close **NM-20** — `RuleBlacklist` advertises the same interface ID, because `IAddressList`
+ expresses *membership*, not *polarity*. Detecting that needs a separate marker interface (e.g. an `IAllowList`
+ advertised only by the whitelist rules) or documentation; see NM-20.
+- ❌ Does not help a child that is valid at add time and breaks later. EIP-6780 means a deployed child cannot
+ become codeless, but a child behind a proxy can still be upgraded into something that reverts.
+
+*Layer 2 — contain at read time (optional, and genuinely harder than it looks).* To stop an already-installed bad
+child from reverting the MUST-NOT-revert views, the blind call at `:237` would have to tolerate failure and treat
+the child as listing nobody — which is fail-closed for an OR-composition of whitelists. The obstacle is that
+`areAddressesListed` returns a **dynamic `bool[]`**, and `abi.decode` of malformed return data reverts *in this
+frame*, outside any `catch` — the same uncatchable-decode problem documented in `TokenSupplyReader` and raised by
+NM-23. The workable technique is to push the decode into a callee frame so the failure becomes catchable:
+
+```solidity
+function decodeListed(bytes calldata data, uint256 n) external pure returns (bool[] memory listed) {
+ listed = abi.decode(data, (bool[]));
+ require(listed.length == n, ...);
+}
+
+// in the scan loop
+(bool ok, bytes memory data) = rule(i).staticcall(
+ abi.encodeCall(IAddressList.areAddressesListed, (targetAddress))
+);
+bool[] memory isListed = new bool[](targetsLength); // default: lists nobody
+if (ok) {
+ try this.decodeListed(data, targetsLength) returns (bool[] memory decoded) { isListed = decoded; }
+ catch { /* keep the all-false default */ }
+}
+```
+
+This adds a public helper to the ABI, an external self-call per child per check, and a silent-degradation path
+where a broken child stops contributing without any signal. That is a real cost against a scenario layer 1
+already prevents at configuration, so **layer 1 alone is the recommendation**; layer 2 only earns its place if
+the wrapper is ever expected to hold children it does not control.
+
+**Resolution — `v0.6.0`.** Layer 1 implemented; layer 2 deliberately not.
+
+*The interface question, answered.* The wrapper calls **one** function on its children —
+`areAddressesListed(address[])`, at `RuleWhitelistWrapperBase.sol:245`. `IAddressList` declares **eight**
+(four writes, three reads, plus `contains` inherited from `IIdentityRegistryContains`). Guarding on the full id
+would demand seven functions the wrapper never touches, including every write function, and reject a read-only
+child that works perfectly. So the guard asks for a purpose-built sub-interface instead:
+
+```solidity
+interface IAddressListBatchQuery {
+ function areAddressesListed(address[] memory targetAddresses) external view returns (bool[] memory results);
+}
+
+interface IAddressList is IIdentityRegistryContains, IAddressListBatchQuery { /* the other seven */ }
+```
+
+| Constant | Value | Covers |
+|---|---|---|
+| `IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID` | `0x20e8e17a` | the one selector the wrapper requires |
+| `IADDRESS_LIST_INTERFACE_ID` | `0x5d10e182` | the full eight-selector hierarchy, unchanged |
+
+Factoring the selector into a parent left the **flattened set unchanged**, so `0x5d10e182` keeps its value and
+no rule's advertised id moves; all four address-list rules now advertise both. The sub-interface id is safe to
+state as a literal — it declares one function and inherits nothing, so the omitted-parent trap that forces the
+flattened-helper pattern for `IAddressList` does not apply. Asserted in
+`test/InterfaceId/AddressListInterfaceId.t.sol`.
+
+*The guard.* `_checkRule` is overridden exactly as `RuleEngineBase` does it, so one override covers both
+`addRule` and `setRules` and stays `view`:
+
+```solidity
+function _checkRule(address rule_) internal view virtual override {
+ RulesManagementModule._checkRule(rule_);
+ require(
+ ERC165Checker.supportsInterface(rule_, AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID),
+ RuleWhitelistWrapper_ChildIsNotAnAddressList(rule_)
+ );
+}
+```
+
+*Tests.* The WW-2 proof-of-concept in `test/ThreatModel/ThreatModelTests.t.sol` was named
+`..._CurrentBehaviour` precisely because it asserted the broken behaviour, and the fix duly made it fail — the
+signal the project's convention describes. It is renamed `test_WW2_NonAddressListChildRuleIsRejectedAtAddRule`
+and now asserts the rejection, that the wrapper is left intact, and that the bad child was never added. Two
+tests were added beside it: a **nested wrapper** is also refused (it does not implement `areAddressesListed`, so
+it would have bricked the parent — the NM-19 failure mode, though nesting itself remains unsupported), and
+`test_WW2_GuardCannotRejectAnInvertedPolarityChild_CurrentBehaviour` pins the guard's **limit**, since a
+`RuleBlacklist` passes it and still inverts the wrapper (NM-20). Four assertions were added to the interface-id
+suite; `RuleWhitelistWrapperBase` is at 100% statements, branches and functions.
+
+*Layer 2 (read-time containment) not implemented*, as recommended above: it needs an external self-call to make
+the dynamic `bool[]` decode catchable, adds a public helper to the ABI, and introduces silent degradation — all
+against a scenario layer 1 now prevents at configuration time.
+
+*The earlier "why it has stayed open" reasoning was wrong and is corrected.* `RulesManagementModule._checkRule`
+does test only non-zero and duplicate, but `RuleEngineBase` **overrides** it to add an `IRule` ERC-165 check
+(`RuleEngineBase.sol:228-233`), so the engine was already guarded and the wrapper was the outlier. The
+dependency supplied the template rather than an argument against it.
+
+### NM-19 — The wrapper does not implement `IAddressList`, so wrappers cannot nest — 🚫 WON'T DO
+
+**Claim (Low).** `RuleWhitelistWrapperBase` implements `IIdentityRegistryVerified` but omits `areAddressesListed`
+/ `isAddressListed`. A wrapper therefore cannot be a child of another wrapper: the parent's blind
+`areAddressesListed` STATICCALL hits a missing selector with no fallback and reverts every transfer.
+
+**Verdict — confirmed as described; the harm is fixed, the feature is declined.** Two halves, decided
+differently:
+
+- **The DoS half is closed.** A nested wrapper used to be accepted and then bricked every transfer through the
+ parent. Since NM-18 it is **refused at `addRule`** with `RuleWhitelistWrapper_ChildIsNotAnAddressList`, pinned
+ by `test_WW2_NestedWrapperIsRejectedAtAddRule`. Nothing silently breaks any more.
+- **The feature half — actually enabling nesting — is declined.** What remains is a feature request, and it does
+ not earn its cost.
+
+**Why nesting is not worth enabling.**
+
+*It buys zero expressive power.* The wrapper is an OR, and `OR(OR(a,b), OR(c,d))` ≡ `OR(a,b,c,d)`. Nesting an OR
+inside an OR flattens algebraically: there is no policy a nested wrapper can express that a flat child list
+cannot express identically. The composition an integrator might actually want from nesting is already available
+one level up — `RuleEngineBase._detectTransferRestriction` returns the **first non-zero** code, so rules in an
+engine compose with **AND**:
+
+| Composition wanted | How to get it today |
+|---|---|
+| OR of lists | one wrapper, flat children |
+| AND of ORs | several wrappers in the `RuleEngine` |
+| OR of ORs | identical to a flat wrapper — nesting adds nothing |
+
+*The gas is multiplicative on exactly the path that matters.* The measured scan is **~8.8k gas per child**, and
+this page's own analysis notes that the worst case is the common case: a **rejected** transfer never early-exits,
+because the exit only fires once every target address is resolved. A 10 × 10 nest therefore costs **~880k gas per
+transfer** where the equivalent flat wrapper costs **~90k** — the same policy at roughly ten times the price,
+paid by the transferring user on every transfer, forever. The operator guidance is to stay at or below ten
+children; nesting is a way to blow past that budget without it looking like a cap change.
+
+*It introduces a cycle class nothing can prevent cheaply.* A wrapper added to itself, or A → B → A, recurses
+until out-of-gas. That bricks transfers **and** `isVerified`, which sits on the ERC-3643 identity path. Detecting
+cycles on-chain means traversing the whole child graph on every `addRule`, itself unbounded. This matters
+particularly now: NM-18 and NM-20 moved this wrapper *away* from documentation-only discipline, and a feature
+whose only defence is "do not do that" would reverse that direction.
+
+**Disposition: won't do.** The current behaviour is best read as *nesting depth limited to 1, enforced by
+construction* — which, given the algebra above, is the same expressive power without the cost or the cycle
+hazard. Reopen it only if the wrapper's semantics ever stop being a plain OR, since that is the assumption the
+whole argument rests on.
+
+Delegated administration, the one real motivation, already works **flat**: this page's usage scenario is exactly
+three operators each managing their own `RuleWhitelist`, all held by one wrapper. Nesting would only add
+groups-of-groups with delegated *group* management, which nobody has asked for.
+
+### NM-20 — The wrapper reads a `RuleBlacklist` child's membership as eligibility — ✅ FIXED (`v0.6.0`)
+
+**Claim (Low).** The wrapper ORs raw `areAddressesListed` answers and treats `true` as eligible. `RuleBlacklist`
+is a valid `IRule` exposing the same interface with the *opposite* polarity, so adding one as a child makes
+blacklisted addresses whitelisted, and `isVerified` returns `true` for them.
+
+**Verdict — informational; confirmed, a trusted-role misconfiguration.** The polarity inversion is real — the
+wrapper cannot distinguish an allow-list from a deny-list through `IAddressList`, and nothing in `addRule`
+constrains child semantics. It requires the rules manager to add a blacklist to a *whitelist* wrapper, which is a
+category error rather than an attack: the same role can already remove every whitelist child outright. Related to
+the accepted v0.4.0 row "wrapper cross-rule OR (`from` in child A, `to` in child B) — documented design; the
+wrapper's stated semantics are 'listed in **any** child'". **Resolution — `v0.6.0`. Enforced, not merely documented.**
+
+The earlier disposition said no code fix was possible, because an ERC-165 guard cannot distinguish an allow-list
+from a deny-list when the interface genuinely is the same. That was correct about `IAddressList` and wrong as a
+conclusion: it named the remedy — *"distinguishing polarity would need a separate marker interface"* — and then
+treated it as out of proportion. It is one function.
+
+```solidity
+interface IAddressListPolarity {
+ /// @return allowed True when listed addresses are the permitted ones; false for a deny-list.
+ function isAllowList() external view returns (bool allowed);
+}
+```
+
+`RuleWhitelistWrapperBase._checkRule` now asks **two** questions, because membership and meaning are two
+questions:
+
+| Requirement | Interface | Failure |
+|---|---|---|
+| Can you answer "is this address listed?" | `IAddressListBatchQuery` (`0x20e8e17a`) | `..._ChildIsNotAnAddressList` |
+| Do you declare what membership *means*? | `IAddressListPolarity` (`0xdc4efe10`) | `..._ChildDoesNotDeclarePolarity` |
+| Does it mean **allowed**? | `isAllowList() == true` | `..._ChildIsNotAnAllowList` |
+
+**Absence of the polarity declaration is a refusal, never an assumed allow-list.** That is the only reading that
+fails closed for a contract predating the interface — and it is what makes the abstention below work.
+
+| Rule | `isAllowList()` | As a child |
+|---|---|---|
+| `RuleWhitelist`, `RuleReceiverWhitelist` | `true` | accepted |
+| `RuleBlacklist` | `false` | **rejected** — the finding |
+| `RuleSpenderWhitelist` | *declines the interface* | **rejected** — see below |
+| nested `RuleWhitelistWrapper` | *no `areAddressesListed`* | rejected at the first check |
+
+*A second wrong-child class, closed by the same mechanism.* `RuleSpenderWhitelist` deliberately does **not**
+implement the interface, and the contract's NatSpec says it must not be changed to. Its set genuinely is an
+allow-list, so declaring `true` would be honest about polarity and still wrong: the listed addresses are
+permitted **spenders**, not permitted **holders**, and the wrapper would read them as eligible transfer
+participants. Polarity is only half the question; the other half is what the addresses *are*. Withholding the
+declaration is what makes the fail-closed check refuse it — a cheap way to enforce a caveat that was previously
+prose only, noted when the NM-20 documentation pass first tabulated it as "not a child".
+
+*Tests.* `test_WW2_GuardCannotRejectAnInvertedPolarityChild_CurrentBehaviour` — added one round earlier to pin
+the guard's *limit* — duly failed the moment the limit was removed, which is the `_CurrentBehaviour` convention
+working. It is renamed `test_WW2_DenyListChildIsRejectedAtAddRule` and now asserts the rejection, that the child
+was never added, and that `isVerified(blacklistedAddress)` is false. Two more beside it: the abstaining spender
+rule is refused, and genuine allow-lists are still accepted so the guard is not simply refusing everything.
+Three assertions added to the interface-id suite, including that each rule declares the polarity it actually has
+and that `RuleSpenderWhitelist` does not advertise the interface.
+
+*Coverage.* `RuleWhitelistWrapperBase`, `RuleWhitelistBase`, `RuleReceiverWhitelistBase` and `RuleBlacklistBase`
+all at 100% statements, branches and functions.
+
+### NM-21 — `RuleMintAllowance` pre-flight views fail open
+
+**Claim (Low).** `detectTransferRestriction` and `canTransfer` are hardcoded to "allowed" while enforcement
+happens on the 4-arg path, so a token-level pre-flight reports success for a zero-quota minter and the mint then
+reverts.
+
+**Verdict — accepted as design; this is v0.4.0 audit finding F-7, closed as documented.** Verified at
+`RuleMintAllowanceBase.sol:200-208` and `:227-235`. The 3-arg signature has no minter identity, so a truthful
+answer is impossible; returning `TRANSFER_OK` and directing callers to the authoritative view is the documented
+resolution. It is stated in the contract NatSpec ("use `detectTransferRestrictionFrom(minter, address(0), to,
+amount)` to query allowance"), in `CLAUDE.md` ("`canTransfer` is **not** authoritative for this rule — use
+`canTransferFrom(minter, address(0), to, value)`"), in `RULE_SEMANTICS.md` §2, and in the audit's disposition
+table.
+
+### NM-23 / NM-24 — Short successful return data escapes `try/catch`
+
+**Claim (Low ×2).** `BalanceCapManager._balanceOf`, `TokenSupplyReader._currentSupply` and
+`ChainlinkPoRFeedManager._maxBackedSupply` use high-level typed calls in `try/catch`. If a code-bearing
+dependency later returns fewer bytes than the declared return type — e.g. after a proxy implementation change —
+ABI decoding fails in the *caller's* frame, outside `catch`, so the read path reverts instead of returning codes
+83 / 51 / 78 / 79.
+
+**Verdict — accepted as design; correct Solidity semantics, already documented in the same files.** The claim is
+right about the language: a `try` does not catch a decode failure of the return data. It is also already written
+down at `TokenSupplyReader.sol:58-61`: *"A `try` call to a codeless address reverts uncatchably — the ABI decoder
+fails in the caller's frame, outside `catch`'s reach — and this probe cannot contain it. **Note code alone is not
+sufficient either: a contract that returns 0 bytes fails the same way.**"* The same reasoning appears in
+`BalanceCapManager` and `ChainlinkPoRFeedManager`.
+
+Reaching it requires a dependency that **passed** the configuration probe (`_probeTotalSupplyCallable`,
+`balanceOf`, `decimals`) and later changed behaviour — the proxy-upgrade case, the same precondition already
+documented for the code-length guards. Failure is closed and the state is intact.
+
+**Improvement — fully implementable, and it retires a documented deployment precondition as a bonus.** Replace
+the typed `try/catch` with a low-level `staticcall` plus an explicit length check, so decoding only happens on
+data that is known to be long enough.
+
+```solidity
+// TokenSupplyReader
+function _currentSupply() internal view virtual returns (bool available, uint256 supply) {
+ (bool ok, bytes memory data) =
+ address(_supplyToken()).staticcall(abi.encodeCall(ITotalSupply.totalSupply, ()));
+ if (!ok || data.length < 32) {
+ return (false, 0);
+ }
+ return (true, abi.decode(data, (uint256)));
+}
+
+// BalanceCapManager
+function _balanceOf(address account) internal view virtual returns (bool available, uint256 balance) {
+ (bool ok, bytes memory data) =
+ address(balanceToken).staticcall(abi.encodeCall(IBalanceOf.balanceOf, (account)));
+ if (!ok || data.length < 32) {
+ return (false, 0);
+ }
+ return (true, abi.decode(data, (uint256)));
+}
+```
+
+The same shape applies to `ChainlinkPoRFeedManager._maxBackedSupply`'s two feed reads: `decimals()` needs
+`data.length >= 32` (a `uint8` is ABI-encoded as a full word), `latestRoundData()` needs `>= 160` for its five
+return values. `_probeTotalSupplyCallable` should be converted too, or configuration would accept a token the
+read path then rejects.
+
+What this buys, beyond the finding itself:
+
+- **A `staticcall` to a codeless address returns `ok == true` with empty data**, which the length check catches.
+ That makes the read path safe without any code-length guarantee — so the **"assumes a Cancun-or-later chain"
+ deployment precondition documented in `TokenSupplyReader`, `BalanceCapManager` and `ChainlinkPoRFeedManager`
+ can be dropped**, and with it the reasoning about EIP-6780 that three contracts currently carry. That is a
+ meaningful simplification of the invariant surface, not just a bug guard.
+- Failure stays closed and keeps returning the documented codes (51 / 78 / 79 / 83) instead of reverting.
+- Gas is a wash: `staticcall` + `abi.decode` costs about the same as the compiler's own `try` sequence.
+
+Costs, stated honestly:
+
+- **Loses the typed call.** `abi.encodeCall` keeps argument type-checking against the interface, but the return
+ type is asserted by the `abi.decode`, not by the compiler — a signature change in `ITotalSupply` would no
+ longer be caught at the call site. Keep the interfaces as the single source of truth and use `abi.encodeCall`
+ (never a hand-written `abi.encodeWithSignature`) so the selector cannot drift.
+- **Touches four files on the enforcement path of every cap rule**, so it needs the existing suites plus new
+ cases: a mock returning 0 bytes, one returning 31 bytes, one reverting, and a codeless address (which should
+ now yield the unavailable code rather than reverting — the assertion that pins the retired precondition).
+- The three long `@dev` blocks explaining the uncatchable decode would have to be rewritten, not deleted: they
+ become the explanation of *why* the reads are low-level.
+
+**Decision — declined.** Written up as "worth doing" above; re-examined and rejected, because the benefit does
+not survive scrutiny.
+
+*The headline benefit was overstated.* "Retires the Cancun / EIP-6780 precondition" reads as a safety gain and is
+not one: `foundry.toml` targets `evm_version = 'prague'`, so the precondition is **already satisfied**, and
+trivially so for any realistic deployment. Removing it deletes three NatSpec paragraphs, not a risk.
+
+*The behavioural delta is one error message on a token that has already failed.* The hole is real — a callee
+that succeeds while returning fewer bytes than the declared type fails ABI decoding in the **caller's** frame,
+outside `catch` — but work the consequence through:
+
+| Token state | Today | After the change |
+|---|---|---|
+| healthy | code `0` | code `0` |
+| reverts | code 51 / 78 / 83 | code 51 / 78 / 83 |
+| **returns short data** | **the view reverts** | code 51 / 78 / 83 |
+
+Only the last row moves, and it is **fail-closed in both columns**: the transfer is blocked either way. What
+improves is that a pre-flight query returns a diagnostic code instead of reverting, on a dependency that has
+already stopped honouring its own interface.
+
+*The cost is real and larger than first stated.* Not four files — **eight `try` blocks across three**, including
+`latestRoundData` with five return values needing a `>= 160` length check. Each becomes hand-rolled ABI handling
+in a compliance library that is otherwise high-level Solidity: `abi.decode(data, (uint256))` is an *assertion*
+rather than a compiler check, so a signature drift in `ITotalSupply` that the typed call catches at the call site
+would pass silently; eight bespoke `data.length` constants are eight chances to write `<` for `!=` or the wrong
+`N`, in the very code whose purpose is robustness; and a well-understood idiom is replaced by one every future
+reader must re-verify.
+
+*What would change the answer.* Two conditions, either of which makes it worth revisiting:
+
+1. **A concrete proxy-upgrade expectation.** Standard T-REX puts the token behind a `TokenProxy` with a swappable
+ implementation, so a deployment that actually expects implementation churn makes "returns short data" real
+ rather than hypothetical.
+2. **A pre-Cancun target chain**, where the code-length precondition genuinely is not satisfied.
+
+If either arrives, the implementation should be **one small internal library** (`tryReadUint256(address, bytes)`)
+that the eight sites delegate to — written once and tested once — not eight hand-rolled call sites. That is the
+difference between a helper and a hand-rolled workaround.
+
+The limitation itself stays documented in-source in all three contracts, as it already is; this entry records
+why it is not closed, so it does not read as unacknowledged debt.
+
+---
+
+## Delta from previous analyses
+
+This is the **first** Nethermind AuditAgent scan of this repository, so there is no previous AuditAgent run to
+diff. Against the other `v0.5.0` analyses:
+
+| Source | New findings this scan added | Overlap |
+|---|---|---|
+| Slither 0.11.5 / Aderyn 0.6.5 (`v0.5.0`) | All 24 — no pattern-based detector reached any of them | None |
+| `CLAUDE_AUDIT.md` (`v0.4.0`) | NM-3, NM-6, NM-10, NM-11, NM-19, NM-20, NM-23/24 | NM-7/12/15 ≈ F-4; NM-21 ≈ F-7; NM-18 ≈ F-5; NM-14/22 = accepted-risk rows |
+| `CLAUDE_ANALYSIS_MAXBALANCE.md` (`v0.5.0`) | NM-11's ERC-3643 consequence | NM-11's premise = H-1 (pre-update accounting) |
+
+The scan reached a strictly different class of issue than the static analysers, which is the point of running
+both: Slither and Aderyn match syntactic patterns, and every finding here is semantic — about who calls a hook,
+in what order, and with which arguments.
+
+## Executive triage
+
+**Nothing found by this scan is exploitable, and none of the 13 Medium ratings survives verification at Medium.**
+There is no path to unauthorised issuance, no way to move tokens past a rule, and no state corruption. The
+failures the report describes are, without exception, either **fail-closed** (an over-restrictive cap, a
+transfer blocked by a broken oracle) or **inert** (a rule that cannot screen an identity it is never given).
+
+Every one of the 24 findings describes real code — there are no false positives — but 17 restate positions the
+project had already reached and written down, and the 24 items collapse to about 11 distinct claims.
+
+**The one item that warranted new contracts was NM-11, and it has been acted on.** `RuleMaxBalance`, `RuleMaxTotalSupply` and `RuleChainlinkPoR`
+assume the token calls the compliance hook *before* moving value; the vendored ERC-3643 / T-REX token calls it
+*after*, and that integration is one this repository supports and tests. The consequence is over-restriction, not
+over-issuance. **It has since been fixed** for the two supply-based cap rules, which now ship ERC-3643 variants
+(`RuleChainlinkPoRERC3643`, `RuleMaxTotalSupplyERC3643`) verified against the genuine vendored T-REX token;
+`RuleMaxBalance` is deliberately left as CMTAT-path-only, because a post-update variant would revert an agent's
+forced transfer and, on T-REX <= 4.1, brick wallet recovery — a policy decision rather than a hook override.
+
+**Nine improvements are specified**, each with its code, its cost and its limit. Seven are done; the other two
+are listed in rough order of value per unit of risk:
+
+| Improvement | Where | Size | Status / verdict |
+|---|---|---|---|
+| ERC-3643 cap-rule variants (`CapAccounting` + the notify seam) | NM-11 | 2 rules × 2 variants | ✅ **Done in `v0.6.0`** — 49 tests, incl. suites against the genuine T-REX token; `RuleMaxBalance` deliberately excluded |
+| Delegate instead of returning early | NM-3 | ~4 lines | ✅ **Done in `v0.6.0`** — behaviour-preserving, 8 regression tests, mutation-verified |
+| Future-dated PoR answer → code 77 | NM-10 | 1 line | ✅ **Done in `v0.6.0`** — 5 regression tests, mutation-verified |
+| Approval post-condition in `approveAndTransferIfAllowed` | NM-17 | ~5 lines + 1 error, ×2 variants | ✅ **Done in `v0.6.0`** — 5 regression tests, mutation-verified |
+| ERC-165 guard on wrapper children | NM-18 | `_checkRule` override + sub-interface | ✅ **Done in `v0.6.0`** — requires only the one selector the wrapper calls |
+| Normalise `spender == from` on the ERC-7943 overloads | NM-6 | 1 helper + 3 branches | ✅ **Done in `v0.6.0`** — 1 file, corrects one rule, changes no deny-list outcome |
+| Polarity marker interface + guard | NM-20 | 1 interface + 2 checks | ✅ **Done in `v0.6.0`** — makes allow/deny expressible; also closes a second wrong-child class |
+| `staticcall` + length check on the cap reads | NM-23/24 | 8 `try` blocks, 3 files | 🚫 **Declined** — trades a known idiom for hand-rolled ABI plumbing to improve an error message on an already-broken token |
+| Opt-in caller binding on the cap rules | NM-5 | 1 slot + setter, ×3 | **Partial only** — cannot isolate two tokens behind one engine; document and monitor instead for now |
+
+**Status.** The triage itself modified no contract; the seven fixes recorded above were made afterwards through
+the normal fix workflow and are described in each finding's `Resolution` block. Two improvements were specified
+and then **declined with reasons recorded** (**NM-23/24**, **NM-5**) rather than left as open TODOs, and one
+decision is outstanding rather than blocked on effort:
+whether an ERC-3643 agent's `forcedTransfer` should be exempt from `RuleMaxBalance`, which is what a variant of
+that rule waits on. **No finding is left open**: 7 fixed, 16 accepted as design, 1 declined.
diff --git a/doc/security/audits/tools/v0.5.0/nethermind_audit_agent_report_v0.5.0.pdf b/doc/security/audits/tools/v0.5.0/nethermind_audit_agent_report_v0.5.0.pdf
new file mode 100644
index 00000000..3ef612dd
Binary files /dev/null and b/doc/security/audits/tools/v0.5.0/nethermind_audit_agent_report_v0.5.0.pdf differ
diff --git a/doc/security/audits/tools/v0.6.0/CLAUDE_ANALYSIS.md b/doc/security/audits/tools/v0.6.0/CLAUDE_ANALYSIS.md
new file mode 100644
index 00000000..56e4d074
--- /dev/null
+++ b/doc/security/audits/tools/v0.6.0/CLAUDE_ANALYSIS.md
@@ -0,0 +1,251 @@
+# Rules `v0.6.0` — Code Quality Review
+
+Scope: production contracts under `src/` (mocks excluded from the metrics, included where a finding concerns
+them). Compiler solc `0.8.36`, EVM `prague`. Reviewed **2026-08-18** against the `v0.6.0` tree.
+Produced with Claude Code.
+
+**Nothing in this report is a vulnerability.** Nothing found here lets an unauthorised party move value, bypass
+a restriction, or brick a contract. It is a quality review: convention drift, documentation that outgrew its
+code, and one structural inconsistency. Where a check passed, that is recorded too — a "keep this as it is"
+verdict is a result, not an absence of one.
+
+This review deliberately concentrates on **code added in `v0.6.0`**, since `v0.5.0`'s review
+(`CLAUDE_ANALYSIS.md`, 28 findings) covered the pre-existing surface and its dispositions still hold.
+
+## Disposition summary
+
+| ID | Finding | Outcome |
+|---|---|---|
+| A-1 | External calls inside a loop in the wrapper's new `_checkRule` | ⬜ Left — bounded, configuration-only, and the point of the guard |
+| B-1 | No repeated storage reads in the new code | ✅ Checked, nothing to do |
+| C-1 | `RuleIdentityRegistryBase` writes + emits inline in 4 places; every sibling rule uses a `_setX` helper | ⬜ **Decide** — actionable, with a trap; see the entry |
+| D-1 | The notification-seam NatSpec was byte-identical in 3 files, 25 lines each | ✅ Fixed — shortened to 13, derivation already in the docs |
+| E-1 | Two `internal` functions missing `virtual`, against the project's own convention | ✅ Fixed + regression test |
+| F-1 | Interface IDs and ERC-165 advertisement | ✅ Checked, correct |
+| G-1 | 7 NatSpec blocks over the project's stated 20-line ceiling, all added this release | ✅ Fixed — max block now 19 |
+| G-2 | No documentation-path pointers in production contracts | ✅ Checked, convention holds |
+| H-1 | `CapAccounting` members both used; no dead code introduced | ✅ Checked |
+| I-1 | The wrapper requires exactly the one function it calls | ✅ Checked, correct by construction |
+
+**7 checked-and-correct · 3 fixed · 1 left · 1 to decide.** (Rows counted, not estimated.)
+
+## Outstanding
+
+| ID | Item | Why it is still open |
+|---|---|---|
+| C-1 | Extract `_setIdentityRegistry` / `_setCheckSender` / `_setCheckSpender` | Needs a decision: the constructor and the setter have *different* zero-address semantics, so a naive extraction changes behaviour |
+
+---
+
+## A. Loops and iteration
+
+### A-1. `RuleWhitelistWrapperBase._checkRule` makes external calls reachable from a loop — leave
+
+`_checkRule` now performs two `ERC165Checker.supportsInterface` staticcalls plus one `isAllowList()` call. It is
+reached from `setRules`, which loops over the submitted array, so Slither reports `calls-loop`.
+
+**Verdict: leave.** The cost is bounded by `maxRules` (default 10, and `setRules` rejects a longer array), it is
+paid once at configuration by `RULES_MANAGEMENT_ROLE`, and **no holder pays it on a transfer**. Validating each
+candidate requires calling each candidate; hoisting the calls out of the loop would mean not validating them,
+which is the finding the guard exists to close. Recorded so it is not re-opened.
+
+`++i` was checked and is already correct throughout; the pragma is `^0.8.20` and the project compiles at 0.8.36,
+where the bounded-loop overflow check is elided automatically — `unchecked { ++i }` would buy nothing and is
+correctly absent.
+
+## B. Storage reads
+
+### B-1. Nothing to hoist in the new code — checked
+
+`CapAccounting` is `pure` throughout and declares no storage. `_checkRule` reads no storage. The new
+`_detectTransferRestrictionOnNotify` overrides delegate immediately. The cap managers' existing single-read
+pattern (`uint256 cap = maxBalance;`) is unchanged.
+
+No finding. Recorded because "we looked" is worth more than silence.
+
+## C. Events
+
+### C-1. `RuleIdentityRegistryBase` is the only configurable rule that writes and emits inline — decide
+
+`identityRegistry`, `checkSender` and `checkSpender` are each written in more than one place, and every write
+site carries its own `emit`:
+
+| Field | Write sites | Emits |
+|---|---|---|
+| `identityRegistry` | constructor `:61`, `setIdentityRegistry:103`, `clearIdentityRegistry:132` | 3, all inline |
+| `checkSender` | constructor `:64`, `setCheckSender:114` | 2, all inline |
+| `checkSpender` | constructor `:65`, `setCheckSpender:124` | 2, all inline |
+
+So "every write emits" is held **by convention rather than structurally**: nothing forces the next person adding
+a write path to emit, and nothing forces them to validate.
+
+**The evidence that this is the exception, not the style, is the siblings.** Nine `_setX` helpers already exist
+across five contracts, each owning validation + write + event:
+
+```
+RuleWhitelistShared._setCheckSpender / _setAllowMintBurn
+BalanceCapManager._setMaxBalance / _setBalanceToken
+TotalSupplyCapManager._setMaxTotalSupply / _setTokenContract
+ChainlinkPoRFeedManager._setReservesFeed / _setTokenMetadata / _setMaxStalenessSeconds
+```
+
+`RuleWhitelistShared` already has a `_setCheckSpender(bool)` — the **same field name and type** that
+`RuleIdentityRegistryBase` writes inline. That names the helper and settles what the house style is.
+
+**The trap, and why this is a decision rather than a fix.** The constructor and the setter have deliberately
+*different* zero-address semantics:
+
+- `setIdentityRegistry(address(0))` **reverts** (`RuleIdentityRegistry_RegistryAddressZeroNotAllowed`).
+- The constructor treats `address(0)` as "leave unset, emit nothing" — that is how a rule is deployed with checks
+ disabled.
+- `clearIdentityRegistry()` writes `address(0)` **and emits**.
+
+A naive `_setIdentityRegistry` that hoists the `require` would make the three-argument constructor revert on the
+documented "no registry" deployment, and would break `clearIdentityRegistry`. The extraction is still worth
+doing — moving validation into the helper is the *feature*, because it then guards every path — but the helper
+has to model three cases, not one. Suggested shape: `_setIdentityRegistry(address, bool allowZero)`, or a
+separate `_clearIdentityRegistry()`.
+
+**Verdict: decide.** Real inconsistency with a real payoff, but it changes constructor behaviour if done
+carelessly, and `RuleIdentityRegistry` is on the ERC-3643 identity path. Not folded into this release.
+
+## D. Duplication
+
+### D-1. The notification-seam NatSpec was byte-identical across three files — fixed
+
+`_detectTransferRestrictionOnNotify` carried a **25-line** NatSpec block in `RuleChainlinkPoRBase`,
+`RuleMaxTotalSupplyBase` and `RuleMaxBalanceBase`. All three hashed identically: 75 lines of documentation, one
+copy of the information.
+
+**Fixed.** Shortened to 13 lines each, keeping the two things a reader of the source must have — *this is the
+seam an ERC-3643 variant overrides*, and *the read path is deliberately not routed through it* — and dropping
+the worked example and the failure narrative, which `RULE_SEMANTICS.md` §5 already carries in full. No
+cross-reference was added in either direction, per the project's convention.
+
+The four deployment variants (`…ERC3643`, `…ERC3643Ownable2Step`) are near-identical to their pairs, differing
+only in the base they extend. That is the established house pattern for every rule in the library, so it is
+**not** reported as duplication.
+
+## E. `virtual` / override convention
+
+### E-1. Two `internal` functions missing `virtual` — fixed
+
+`CLAUDE.md`: *"All `internal` functions should be marked `virtual`."* Two did not comply:
+
+- `RuleAddressSetInternal._requireNotZeroAddress` (`:64`)
+- `RuleERC2980Internal._requireNotZeroAddress` (`:142`)
+
+Both are the batch zero-address guard, and both are **passed to `AddressSetBatchLib.addBatch` as an internal
+function pointer** — which is also why Slither's `dead-code` detector reports them as unused (it does not trace
+function pointers). Two findings about the same two lines.
+
+**Verified before changing, not assumed:**
+
+| Question | Method | Result |
+|---|---|---|
+| Is `virtual` legal on a function used as a pointer? | compile | yes |
+| Does dispatch actually reach an override *through the pointer*? | harness that overrides it and reverts | **yes** — override reached |
+| Does it cost gas? | `--gas-report`, same test, toggled in place | **identical**: `addAddress` 92 220, `addAddresses` 140 637 both ways |
+
+The middle row is the one that mattered. Solidity resolves an internal function pointer at the point of
+assignment, so it was not obvious the override would be the implementation `addBatch` ends up calling. It is —
+but a compile-only check would have passed either way and left the guard *looking* extensible while the base
+implementation kept running.
+
+**Fixed**, with `test/VirtualHooks/BatchGuardPointerVirtual.t.sol` pinning both properties. Removing `virtual`
+fails the build with *"Trying to override non-virtual function"* — confirmed by mutation, so the guard is not a
+test that has never failed.
+
+## F. ERC / specification conformance
+
+### F-1. Interface IDs and advertisement — checked, correct
+
+- `IADDRESS_LIST_INTERFACE_ID` (`0x5d10e182`) is still computed from the flattened helper interface, and is
+ **unchanged** despite two selectors being factored into parent interfaces this release — the flattened set did
+ not move, which is the property that matters and is asserted in `AddressListInterfaceId.t.sol`.
+- The two new ids are single-function interfaces that inherit nothing, so stating them as literals is safe:
+ `IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID` = `0x20e8e17a`, `IADDRESS_LIST_POLARITY_INTERFACE_ID` = `0xdc4efe10`.
+ Both are asserted equal to `type(I…).interfaceId` and to the function selector.
+- Every implementer advertises the new ids — the step most often missed when splitting an interface, and the one
+ that would otherwise make the new guard reject contracts that were previously fine.
+- `address(0)` sentinel handling is unchanged and still correct: it can never enter an address set, and
+ `isVerified(address(0))` is `false`.
+
+## G. Code / documentation mismatch
+
+### G-1. Seven NatSpec blocks over the project's own ceiling — fixed
+
+`CLAUDE.md` states the ceiling plainly: *"Keep NatSpec blocks short — 20 lines is the ceiling."* Measured across
+`src/` excluding mocks, **824 blocks**:
+
+| | Before | After |
+|---|---|---|
+| median | 4 | 4 |
+| p90 | 9 | 9 |
+| max | **26** | **19** |
+| blocks ≥ 20 lines | **7** | **0** |
+
+All seven were added in this release — the four ERC-3643 variant headers (26 lines each) and the three seam
+blocks from D-1 (25 each). Against a median of 4, a 26-line contract header is not thorough documentation; it is
+a document that happens to live in a comment, and it is the first thing a reader of the contract meets.
+
+**Fixed.** Each keeps its conclusion and its warning — *ERC-3643 only*, *picking the wrong variant breaks the cap
+silently and nothing reverts*, *only the write path is re-phased* — and drops the worked tables and derivations,
+which `doc/technical/contracts/RuleChainlinkPoRERC3643.md`, `RuleMaxTotalSupplyERC3643.md` and
+`RULE_SEMANTICS.md` §5–§6 already carry. Nothing was deleted outright; it was moved to where it already existed.
+
+### G-2. No documentation-path pointers in production contracts — checked, convention holds
+
+`CLAUDE.md` forbids citing a `doc/technical/**` page from contract source, because documentation moves and
+deployed verified source cannot be edited to follow it. Grepping `src/` for `.md`, `doc/` and `docs/` outside
+`src/mocks/` returns **nothing**, and the only remaining citations are audit reports by bare filename
+(`CLAUDE_AUDIT.md`, `CLAUDE_ANALYSIS.md`), which the convention explicitly permits — they are immutable records
+and the bare filename survives a move.
+
+Worth stating so a future reviewer does not propose removing those: **the audit-report citations are correct and
+must stay.**
+
+## H. Weird behaviour
+
+### H-1. No dead or vestigial code introduced — checked
+
+Both `CapAccounting` members are used (`_capExceededBy` 4 references, `_capHeadroom` 2). The new
+`_detectTransferRestrictionOnNotify` is called from both write hooks in each of the three cap rules, and
+`RuleChainlinkPoRBase` is at 100% function coverage — which is the empirical refutation of Slither's `dead-code`
+report on it, triaged separately in `slither-report-feedback.md`.
+
+No fail-open/fail-closed inconsistency was found in the new code: the cap rules fail closed, the wrapper guard
+fails closed (absence of a polarity declaration is a refusal), and the `approveAndTransferIfAllowed`
+post-condition fails closed.
+
+## I. Interface granularity
+
+### I-1. The wrapper requires exactly what it calls — checked, correct
+
+`RuleWhitelistWrapperBase` calls one function on its children, `areAddressesListed`, and its ERC-165 guard
+requires `IAddressListBatchQuery` — that one selector — rather than the eight-selector `IAddressList`. Requiring
+the full id would demand four write functions a read-only child has no reason to expose.
+
+The polarity check is a **separate** interface deliberately, and the limit is stated honestly in both the source
+and the docs: ERC-165 expresses shape, never semantics, so the guard cannot distinguish an allow-list from a
+deny-list by interface alone — hence `IAddressListPolarity` carrying the answer explicitly, and
+`RuleSpenderWhitelist` declining to implement it because its set is spenders rather than holders.
+
+No finding. This check is recorded because the correct outcome here is easy to mistake for an omission.
+
+---
+
+## What was measured, and what was reasoned
+
+Measured: NatSpec block distribution (824 blocks, before and after); gas for E-1 (`--gas-report`, same harness
+toggled in place); the E-1 override-dispatch behaviour (executing harness); the mutation check that E-1's test
+fails without the fix; storage layouts before and after.
+
+Reasoned without executing: A-1's bound (read from `maxRules` and `setRules`), and C-1's proposed helper shape,
+which is a design sketch rather than an implemented change.
+
+## Verification
+
+`forge fmt --check` clean. **882 tests pass** on the default profile (881 + the new E-1 regression) and **53** on
+`FOUNDRY_PROFILE=erc3643`. Storage layouts unchanged for every affected contract — E-1 and G-1 touch only a
+keyword and comments.
diff --git a/doc/security/audits/tools/v0.6.0/aderyn-report-feedback.md b/doc/security/audits/tools/v0.6.0/aderyn-report-feedback.md
new file mode 100644
index 00000000..20f95e2c
--- /dev/null
+++ b/doc/security/audits/tools/v0.6.0/aderyn-report-feedback.md
@@ -0,0 +1,109 @@
+# Aderyn `v0.6.0` — triage
+
+```bash
+aderyn -x mocks --output doc/security/audits/tools/v0.6.0/aderyn-report.md
+```
+
+Tool: **Aderyn 0.6.5** · Compiler: solc `0.8.36` · Run date: **2026-08-21** (re-run after the RuleEngine
+`v3.0.0-rc6` bump; supersedes the 2026-08-18 run)
+Scope: production contracts only, mocks excluded via `-x mocks`. 94 source files, 87 detectors. **4 145 nSLOC.**
+**0 High · 9 Low categories, 346 instances.**
+
+**Executive triage: nothing to fix.** Aderyn reports no High or Medium finding. Every Low category is by design,
+environmental or cosmetic, and **no new category appeared** in a release that added five production contracts and
+two interfaces.
+
+### Scope check
+
+`lib/`, `test/` and `src/mocks/` citations are all **0**. Aderyn reads the Foundry config and scopes to `src/`
+by itself, so it needs no equivalent of Slither's `--filter-paths`; `-x mocks` is the only exclusion.
+
+## Summary
+
+| ID | Finding | Instances | Disposition | Why |
+|---|---|---|---|---|
+| L-1 | Centralization Risk | 80 | **By design** | The regulated-issuer model. Roles gating configuration *are* the product; the trust model is documented in `CLAUDE_AUDIT.md` |
+| L-2 | Unspecific Solidity Pragma | 92 | **By design** | `^0.8.20` is deliberate — this is a library consumed by projects that pin their own compiler |
+| L-3 | Address State Variable Set Without Checks | 3 | **False positive** | Each setter validates: non-zero, `code.length != 0`, and a probe call that must not revert |
+| L-4 | Literal Instead of Constant | 2 | Cosmetic | — |
+| L-5 | PUSH0 Opcode | 94 | **Environment** | solc `0.8.36` targeting `prague`. Relevant only to a chain without PUSH0, which this library does not target |
+| L-6 | Modifier Invoked Only Once | 1 | **By design** | The template-method access-control hook: one modifier per capability, by construction |
+| L-7 | Empty Block | 70 | **By design** | Mostly `_authorize*()` overrides whose entire body is the `onlyRole(...)` / `onlyOwner` modifier — an empty body is the idiom, not an oversight — plus intentional no-op hooks (`RuleSpenderWhitelistBase._transferred`) |
+| L-8 | Costly operations inside loop | 3 | **By design** | Bounded batch operations over an operator-supplied array |
+| L-9 | Unchecked Return | 1 | **False positive** | A configuration probe: the call is made to learn whether it reverts, so discarding the value is the point |
+
+## Delta from `v0.5.0`
+
+**336 → 346 instances (+10)** on **3 942 → 4 145 nSLOC (+203)**. Categories unchanged at 9 — none added, none
+removed.
+
+| ID | v0.5.0 | v0.6.0 | Δ |
+|---|---|---|---|
+| L-2 Unspecific Solidity Pragma | 87 | 92 | **+5** |
+| L-5 PUSH0 Opcode | 89 | 94 | **+5** |
+| L-1, L-3, L-4, L-6, L-7, L-8, L-9 | 160 | 160 | — |
+
+**The delta is exactly the new files, once each in the two per-file categories.** This release added seven files
+under `src/`, of which two are mock harnesses excluded by `-x mocks`, leaving **five production files**:
+
+- `CapAccounting.sol`
+- `RuleChainlinkPoRERC3643.sol` / `RuleChainlinkPoRERC3643Ownable2Step.sol`
+- `RuleMaxTotalSupplyERC3643.sol` / `RuleMaxTotalSupplyERC3643Ownable2Step.sol`
+
+5 files × (1 pragma + 1 PUSH0) = +10. Nothing else moved.
+
+That is a stronger result than the raw number suggests, and worth stating explicitly:
+
+- **`L-1 Centralization Risk` did not grow (80 → 80)** even though four new deployable contracts landed. The
+ ERC-3643 variants subclass the existing deployables and override one `internal` hook, adding **no new
+ privileged external function**. The centralisation surface is unchanged.
+- **`L-7 Empty Block` did not grow (70 → 70)**. The new contracts' `_detectTransferRestrictionOnNotify`
+ overrides have real bodies, and no new `_authorize*()` hook was introduced.
+- **`L-9 Unchecked Return` did not grow**, despite `RuleWhitelistWrapperBase._checkRule` gaining two
+ `ERC165Checker.supportsInterface` calls and one `isAllowList()` — all three are consumed by a `require`.
+
+## Re-run within `v0.6.0` (2026-08-18 → 2026-08-21)
+
+**No detector moved.** All 9 categories hold their exact instance counts, so the summary table above is
+unchanged. Two commits landed between the runs:
+
+- `c1ebe57` — trimmed NatSpec to the 20-line ceiling and marked two pointer-passed guards `virtual`.
+- `f920b07` — RuleEngine `v3.0.0-rc6`: `onlyComplianceManager` renamed to `onlyTokenBindingManager`,
+ `_authorizeComplianceBindingChange` renamed to `_authorizeTokenBindingChange`, and the redundant
+ `RuleConditionalTransferLightMultiTokenBase` binding-authorization override deleted.
+
+The entire body diff is line numbers plus four renamed `L-7` snippets, which is the expected shape: renaming a
+hook cannot change how many empty blocks exist, and the deleted override was **not** an empty block — it had a
+body — so `L-7` correctly stays at 70. nSLOC moved **4 146 → 4 145**: −3 for the deleted override, −12 for
+`forge fmt` re-flowing two multi-line signatures onto one line, +14 for two added imports and two signatures
+that `forge fmt` expanded the other way.
+
+Worth stating for the same reason as the `v0.5.0` delta below:
+
+- **`L-1 Centralization Risk` did not grow (80 → 80).** The rename touched two access-control hooks and one
+ modifier; no privileged external function was added, removed or re-gated. That the count is stable is the
+ cheap confirmation that a rename really was a rename.
+- **`L-6 Modifier Invoked Only Once` did not grow (1 → 1)**, and still points at
+ `RuleWhitelistShared.onlyCheckSpenderManager`. `onlyTokenBindingManager` is invoked four times in
+ `RuleConditionalTransferLightBase` alone, so it correctly does not appear.
+
+## Notes on the two large categories
+
+`L-2` and `L-5` together are **186 of 346 instances (54%)**, and both are one-per-file:
+
+- **`L-2 Unspecific Solidity Pragma`** — Aderyn wants a pinned pragma. For an application this is good advice;
+ for a **library** it is not. Every contract here is `^0.8.20` so consumers can compile against their own
+ pinned version. Pinning would force downstream projects onto this repo's exact compiler.
+- **`L-5 PUSH0 Opcode`** — flags that bytecode from solc ≥ 0.8.20 contains `PUSH0`, which pre-Shanghai chains
+ reject. `foundry.toml` targets `prague`; the deployment targets are all post-Shanghai. If that ever changes,
+ this becomes a real finding — it is environmental, not wrong.
+
+Both will grow by one per file on every future release. **Treat a jump that is not a multiple of the new-file
+count as the signal**, not the totals themselves.
+
+## What a clean report does and does not mean
+
+Aderyn's detectors matched nothing actionable. As with Slither, that is not evidence of correctness: the seven
+findings fixed in this release came from the Nethermind AuditAgent scan and manual review, and **neither static
+analyser reached any of them**. They are semantic — accounting phase, callback ordering, interface polarity —
+and these tools match syntactic patterns.
diff --git a/doc/security/audits/tools/v0.6.0/aderyn-report.md b/doc/security/audits/tools/v0.6.0/aderyn-report.md
new file mode 100644
index 00000000..c895af9c
--- /dev/null
+++ b/doc/security/audits/tools/v0.6.0/aderyn-report.md
@@ -0,0 +1,2359 @@
+# Aderyn Report — `v0.6.0`
+
+```bash
+aderyn -x mocks --output doc/security/audits/tools/v0.6.0/aderyn-report.md
+```
+
+Tool: **Aderyn 0.6.5** · Compiler: solc `0.8.36` · Run date: **2026-08-21** (re-run after the RuleEngine
+`v3.0.0-rc6` bump; supersedes the 2026-08-18 run)
+Scope: production contracts only — **mocks excluded** via `-x mocks`. 94 source files, 87 detectors,
+**4 145 nSLOC**.
+
+**0 High · 9 Low categories, 346 instances.**
+
+| ID | Finding | Severity | Instances | Assessment |
+|---|---|---|---|---|
+| L-1 | Centralization Risk | Low | 80 | By design — the regulated-issuer model; roles are the feature |
+| L-2 | Unspecific Solidity Pragma | Low | 92 | By design — `^0.8.20` is deliberate for a library consumed by other projects |
+| L-3 | Address State Variable Set Without Checks | Low | 3 | False positive — each setter validates (non-zero, has code, probe call) |
+| L-4 | Literal Instead of Constant | Low | 2 | Cosmetic |
+| L-5 | PUSH0 Opcode | Low | 94 | Environment — solc `0.8.36` targeting `prague`; irrelevant on any chain this deploys to |
+| L-6 | Modifier Invoked Only Once | Low | 1 | By design — the template-method access-control hook |
+| L-7 | Empty Block | Low | 70 | By design — `_authorize*()` overrides carrying `onlyRole(...)` / `onlyOwner`, and intentional no-op hooks |
+| L-8 | Costly operations inside loop | Low | 3 | By design — bounded batch operations |
+| L-9 | Unchecked Return | Low | 1 | False positive — the discarded value is the point of a configuration probe |
+
+**Nothing to fix.** No High or Medium finding, and every Low category is by design, environmental or cosmetic.
+The delta from `v0.5.0` is **+10 instances** (336 → 346) on **+203 nSLOC**, entirely the five source files added
+this release appearing once each in `L-2` and `L-5`. **No new category, and no existing category grew for any
+other reason.**
+
+**Re-run delta (2026-08-18 → 2026-08-21): no detector moved.** Two commits landed in between — the NatSpec
+trim and `virtual` fixes, and the RuleEngine `v3.0.0-rc6` bump that renamed `onlyComplianceManager` to
+`onlyTokenBindingManager`, renamed `_authorizeComplianceBindingChange` to `_authorizeTokenBindingChange` and
+deleted the multi-token binding-authorization override. All 9 categories hold their exact instance counts;
+the only changes in the body are line numbers and the four renamed `L-7` snippets. nSLOC moved **4 146 → 4 145**,
+the one line of the deleted override net of `forge fmt` re-flowing four signatures.
+
+Triage: [`aderyn-report-feedback.md`](./aderyn-report-feedback.md) ·
+Overview: [`AUDIT_OVERVIEW.md`](../../AUDIT_OVERVIEW.md)
+
+---
+
+# Aderyn Analysis Report
+
+This report was generated by [Aderyn](https://github.com/Cyfrin/aderyn), a static analysis tool built by [Cyfrin](https://cyfrin.io), a blockchain security company. This report is not a substitute for manual audit or security review. It should not be relied upon for any purpose other than to assist in the identification of potential security vulnerabilities.
+# Table of Contents
+
+- [Summary](#summary)
+ - [Files Summary](#files-summary)
+ - [Files Details](#files-details)
+ - [Issue Summary](#issue-summary)
+- [Low Issues](#low-issues)
+ - [L-1: Centralization Risk](#l-1-centralization-risk)
+ - [L-2: Unspecific Solidity Pragma](#l-2-unspecific-solidity-pragma)
+ - [L-3: Address State Variable Set Without Checks](#l-3-address-state-variable-set-without-checks)
+ - [L-4: Literal Instead of Constant](#l-4-literal-instead-of-constant)
+ - [L-5: PUSH0 Opcode](#l-5-push0-opcode)
+ - [L-6: Modifier Invoked Only Once](#l-6-modifier-invoked-only-once)
+ - [L-7: Empty Block](#l-7-empty-block)
+ - [L-8: Costly operations inside loop](#l-8-costly-operations-inside-loop)
+ - [L-9: Unchecked Return](#l-9-unchecked-return)
+
+
+# Summary
+
+## Files Summary
+
+| Key | Value |
+| --- | --- |
+| .sol Files | 94 |
+| Total nSLOC | 4145 |
+
+
+## Files Details
+
+| Filepath | nSLOC |
+| --- | --- |
+| src/modules/AccessControlModuleStandalone.sol | 24 |
+| src/modules/MetaTxModuleStandalone.sol | 6 |
+| src/modules/Ownable2StepERC165Module.sol | 11 |
+| src/modules/VersionModule.sol | 8 |
+| src/registry/IdentityRegistryWhitelist.sol | 7 |
+| src/registry/abstract/IdentityRegistryWhitelistBase.sol | 52 |
+| src/registry/abstract/IdentityRegistryWhitelistInvariantStorage.sol | 6 |
+| src/registry/interfaces/IIdentityRegistryERC3643.sol | 10 |
+| src/rules/interfaces/AggregatorV3Interface.sol | 14 |
+| src/rules/interfaces/IAddressList.sol | 20 |
+| src/rules/interfaces/IBalanceOf.sol | 4 |
+| src/rules/interfaces/IDecimals.sol | 4 |
+| src/rules/interfaces/IERC2980.sol | 5 |
+| src/rules/interfaces/IERC7943NonFungibleCompliance.sol | 19 |
+| src/rules/interfaces/IIdentityRegistry.sol | 7 |
+| src/rules/interfaces/ISanctionsList.sol | 4 |
+| src/rules/interfaces/ITotalSupply.sol | 4 |
+| src/rules/interfaces/ITransferContext.sol | 22 |
+| src/rules/interfaces/library/AddressListInterfaceId.sol | 6 |
+| src/rules/operation/RuleConditionalTransferLight.sol | 34 |
+| src/rules/operation/RuleConditionalTransferLightMultiToken.sol | 33 |
+| src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol | 32 |
+| src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol | 33 |
+| src/rules/operation/RuleMintAllowance.sol | 28 |
+| src/rules/operation/RuleMintAllowanceOwnable2Step.sol | 27 |
+| src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol | 70 |
+| src/rules/operation/abstract/RuleConditionalTransferLightBase.sol | 156 |
+| src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol | 25 |
+| src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol | 244 |
+| src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol | 30 |
+| src/rules/operation/abstract/RuleMintAllowanceBase.sol | 149 |
+| src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol | 14 |
+| src/rules/validation/abstract/RuleAddressSet/AddressSetBatchLib.sol | 31 |
+| src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol | 67 |
+| src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol | 38 |
+| src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol | 6 |
+| src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetRolesStorage.sol | 5 |
+| src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol | 14 |
+| src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol | 24 |
+| src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol | 65 |
+| src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol | 39 |
+| src/rules/validation/abstract/base/RuleBlacklistBase.sol | 115 |
+| src/rules/validation/abstract/base/RuleChainlinkPoRBase.sol | 107 |
+| src/rules/validation/abstract/base/RuleERC2980Base.sol | 247 |
+| src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol | 125 |
+| src/rules/validation/abstract/base/RuleMaxBalanceBase.sol | 92 |
+| src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol | 87 |
+| src/rules/validation/abstract/base/RuleReceiverWhitelistBase.sol | 76 |
+| src/rules/validation/abstract/base/RuleSanctionsListBase.sol | 108 |
+| src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol | 59 |
+| src/rules/validation/abstract/base/RuleWhitelistBase.sol | 75 |
+| src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol | 163 |
+| src/rules/validation/abstract/core/BalanceCapManager.sol | 97 |
+| src/rules/validation/abstract/core/CapAccounting.sol | 12 |
+| src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol | 112 |
+| src/rules/validation/abstract/core/RuleNFTAdapter.sol | 127 |
+| src/rules/validation/abstract/core/RuleTransferValidation.sol | 70 |
+| src/rules/validation/abstract/core/RuleWhitelistShared.sol | 96 |
+| src/rules/validation/abstract/core/TokenSupplyReader.sol | 19 |
+| src/rules/validation/abstract/core/TotalSupplyCapManager.sol | 45 |
+| src/rules/validation/abstract/invariant/RuleChainlinkPoRInvariantStorage.sol | 32 |
+| src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol | 18 |
+| src/rules/validation/abstract/invariant/RuleMaxBalanceInvariantStorage.sol | 22 |
+| src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol | 17 |
+| src/rules/validation/abstract/invariant/RuleReceiverWhitelistInvariantStorage.sol | 12 |
+| src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol | 18 |
+| src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol | 4 |
+| src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol | 9 |
+| src/rules/validation/deployment/RuleBlacklist.sol | 33 |
+| src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol | 31 |
+| src/rules/validation/deployment/RuleChainlinkPoR.sol | 29 |
+| src/rules/validation/deployment/RuleChainlinkPoRERC3643.sol | 25 |
+| src/rules/validation/deployment/RuleChainlinkPoRERC3643Ownable2Step.sol | 25 |
+| src/rules/validation/deployment/RuleChainlinkPoROwnable2Step.sol | 27 |
+| src/rules/validation/deployment/RuleERC2980.sol | 34 |
+| src/rules/validation/deployment/RuleERC2980Ownable2Step.sol | 35 |
+| src/rules/validation/deployment/RuleIdentityRegistry.sol | 22 |
+| src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol | 23 |
+| src/rules/validation/deployment/RuleMaxBalance.sol | 22 |
+| src/rules/validation/deployment/RuleMaxBalanceOwnable2Step.sol | 23 |
+| src/rules/validation/deployment/RuleMaxTotalSupply.sol | 22 |
+| src/rules/validation/deployment/RuleMaxTotalSupplyERC3643.sol | 20 |
+| src/rules/validation/deployment/RuleMaxTotalSupplyERC3643Ownable2Step.sol | 20 |
+| src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol | 23 |
+| src/rules/validation/deployment/RuleReceiverWhitelist.sol | 33 |
+| src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol | 34 |
+| src/rules/validation/deployment/RuleSanctionsList.sol | 34 |
+| src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol | 35 |
+| src/rules/validation/deployment/RuleSpenderWhitelist.sol | 33 |
+| src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol | 34 |
+| src/rules/validation/deployment/RuleWhitelist.sol | 36 |
+| src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol | 36 |
+| src/rules/validation/deployment/RuleWhitelistWrapper.sol | 54 |
+| src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol | 41 |
+| **Total** | **4145** |
+
+
+## Issue Summary
+
+| Category | No. of Issues |
+| --- | --- |
+| High | 0 |
+| Low | 9 |
+
+
+# Low Issues
+
+## L-1: Centralization Risk
+
+Contracts have owners with privileged rights to perform admin tasks and need to be trusted to not perform malicious updates or drain funds.
+
+]80 Found Instances
+
+
+- Found in src/modules/AccessControlModuleStandalone.sol [Line: 13](../../../../../src/modules/AccessControlModuleStandalone.sol#L13)
+
+ ```solidity
+ abstract contract AccessControlModuleStandalone is AccessControlEnumerable {
+ ```
+
+- Found in src/registry/IdentityRegistryWhitelist.sol [Line: 34](../../../../../src/registry/IdentityRegistryWhitelist.sol#L34)
+
+ ```solidity
+ function _authorizeIdentityRegistrar() internal view virtual override onlyRole(IDENTITY_REGISTRAR_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 62](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L62)
+
+ ```solidity
+ function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 67](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L67)
+
+ ```solidity
+ function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 72](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L72)
+
+ ```solidity
+ function _authorizeTokenBindingChange(address) internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 50](../../../../../src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L50)
+
+ ```solidity
+ function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 55](../../../../../src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L55)
+
+ ```solidity
+ function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 22](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L22)
+
+ ```solidity
+ Ownable2Step,
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 49](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L49)
+
+ ```solidity
+ function _onlyComplianceManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 54](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L54)
+
+ ```solidity
+ function _authorizeTransferApproval() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 21](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L21)
+
+ ```solidity
+ Ownable2Step,
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 60](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L60)
+
+ ```solidity
+ function _onlyComplianceManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 65](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L65)
+
+ ```solidity
+ function _authorizeTransferApproval() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 70](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L70)
+
+ ```solidity
+ function _authorizeTokenBindingChange(address) internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleMintAllowance.sol [Line: 60](../../../../../src/rules/operation/RuleMintAllowance.sol#L60)
+
+ ```solidity
+ function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleMintAllowance.sol [Line: 65](../../../../../src/rules/operation/RuleMintAllowance.sol#L65)
+
+ ```solidity
+ function _authorizeSetMintAllowance() internal view virtual override onlyRole(ALLOWANCE_OPERATOR_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleMintAllowance.sol [Line: 70](../../../../../src/rules/operation/RuleMintAllowance.sol#L70)
+
+ ```solidity
+ function _authorizeTokenBindingChange(address) internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 19](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L19)
+
+ ```solidity
+ contract RuleMintAllowanceOwnable2Step is RuleMintAllowanceBase, Ownable2Step, Ownable2StepERC165Module {
+ ```
+
+- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 57](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L57)
+
+ ```solidity
+ function _onlyComplianceManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 62](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L62)
+
+ ```solidity
+ function _authorizeSetMintAllowance() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 67](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L67)
+
+ ```solidity
+ function _authorizeTokenBindingChange(address) internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 55](../../../../../src/rules/validation/deployment/RuleBlacklist.sol#L55)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 60](../../../../../src/rules/validation/deployment/RuleBlacklist.sol#L60)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 15](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L15)
+
+ ```solidity
+ contract RuleBlacklistOwnable2Step is RuleBlacklistBase, Ownable2Step, Ownable2StepERC165Module {
+ ```
+
+- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 54](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L54)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 59](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L59)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleChainlinkPoR.sol [Line: 65](../../../../../src/rules/validation/deployment/RuleChainlinkPoR.sol#L65)
+
+ ```solidity
+ function _authorizeChainlinkPoRManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleChainlinkPoROwnable2Step.sol [Line: 15](../../../../../src/rules/validation/deployment/RuleChainlinkPoROwnable2Step.sol#L15)
+
+ ```solidity
+ contract RuleChainlinkPoROwnable2Step is RuleChainlinkPoRBase, Ownable2Step, Ownable2StepERC165Module {
+ ```
+
+- Found in src/rules/validation/deployment/RuleChainlinkPoROwnable2Step.sol [Line: 62](../../../../../src/rules/validation/deployment/RuleChainlinkPoROwnable2Step.sol#L62)
+
+ ```solidity
+ function _authorizeChainlinkPoRManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L63)
+
+ ```solidity
+ function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 68](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L68)
+
+ ```solidity
+ function _authorizeWhitelistAdd() internal view virtual override onlyRole(WHITELIST_ADD_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 73](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L73)
+
+ ```solidity
+ function _authorizeWhitelistRemove() internal view virtual override onlyRole(WHITELIST_REMOVE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 78](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L78)
+
+ ```solidity
+ function _authorizeFrozenlistAdd() internal view virtual override onlyRole(FROZENLIST_ADD_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 83](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L83)
+
+ ```solidity
+ function _authorizeFrozenlistRemove() internal view virtual override onlyRole(FROZENLIST_REMOVE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 15](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L15)
+
+ ```solidity
+ contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step, Ownable2StepERC165Module {
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 56](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L56)
+
+ ```solidity
+ function _authorizeMintBurnManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 61](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L61)
+
+ ```solidity
+ function _authorizeWhitelistAdd() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 66](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L66)
+
+ ```solidity
+ function _authorizeWhitelistRemove() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 71](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L71)
+
+ ```solidity
+ function _authorizeFrozenlistAdd() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 76](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L76)
+
+ ```solidity
+ function _authorizeFrozenlistRemove() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleIdentityRegistry.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleIdentityRegistry.sol#L63)
+
+ ```solidity
+ function _authorizeIdentityRegistryManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 14](../../../../../src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14)
+
+ ```solidity
+ contract RuleIdentityRegistryOwnable2Step is RuleIdentityRegistryBase, Ownable2Step, Ownable2StepERC165Module {
+ ```
+
+- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L63)
+
+ ```solidity
+ function _authorizeIdentityRegistryManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxBalance.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleMaxBalance.sol#L58)
+
+ ```solidity
+ function _authorizeMaxBalanceManager() internal view virtual override onlyRole(MAX_BALANCE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxBalanceOwnable2Step.sol [Line: 16](../../../../../src/rules/validation/deployment/RuleMaxBalanceOwnable2Step.sol#L16)
+
+ ```solidity
+ contract RuleMaxBalanceOwnable2Step is RuleMaxBalanceBase, Ownable2Step, Ownable2StepERC165Module {
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxBalanceOwnable2Step.sol [Line: 59](../../../../../src/rules/validation/deployment/RuleMaxBalanceOwnable2Step.sol#L59)
+
+ ```solidity
+ function _authorizeMaxBalanceManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxTotalSupply.sol [Line: 55](../../../../../src/rules/validation/deployment/RuleMaxTotalSupply.sol#L55)
+
+ ```solidity
+ function _authorizeMaxTotalSupplyManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 14](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L14)
+
+ ```solidity
+ contract RuleMaxTotalSupplyOwnable2Step is RuleMaxTotalSupplyBase, Ownable2Step, Ownable2StepERC165Module {
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L57)
+
+ ```solidity
+ function _authorizeMaxTotalSupplyManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleReceiverWhitelist.sol [Line: 56](../../../../../src/rules/validation/deployment/RuleReceiverWhitelist.sol#L56)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleReceiverWhitelist.sol [Line: 61](../../../../../src/rules/validation/deployment/RuleReceiverWhitelist.sol#L61)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol [Line: 15](../../../../../src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol#L15)
+
+ ```solidity
+ contract RuleReceiverWhitelistOwnable2Step is RuleReceiverWhitelistBase, Ownable2Step, Ownable2StepERC165Module {
+ ```
+
+- Found in src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol#L57)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol [Line: 62](../../../../../src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol#L62)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleSanctionsList.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleSanctionsList.sol#L58)
+
+ ```solidity
+ function _authorizeSanctionListManager() internal view virtual override onlyRole(SANCTIONLIST_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 17](../../../../../src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L17)
+
+ ```solidity
+ contract RuleSanctionsListOwnable2Step is RuleSanctionsListBase, Ownable2Step, Ownable2StepERC165Module {
+ ```
+
+- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 60](../../../../../src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L60)
+
+ ```solidity
+ function _authorizeSanctionListManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 56](../../../../../src/rules/validation/deployment/RuleSpenderWhitelist.sol#L56)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 61](../../../../../src/rules/validation/deployment/RuleSpenderWhitelist.sol#L61)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 15](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L15)
+
+ ```solidity
+ contract RuleSpenderWhitelistOwnable2Step is RuleSpenderWhitelistBase, Ownable2Step, Ownable2StepERC165Module {
+ ```
+
+- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L57)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 62](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L62)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 65](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L65)
+
+ ```solidity
+ function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 70](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L70)
+
+ ```solidity
+ function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 75](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L75)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 80](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L80)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 15](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L15)
+
+ ```solidity
+ contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step, Ownable2StepERC165Module {
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L58)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L63)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 68](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L68)
+
+ ```solidity
+ function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 73](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L73)
+
+ ```solidity
+ function _authorizeMintBurnManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 98](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L98)
+
+ ```solidity
+ function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 103](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L103)
+
+ ```solidity
+ function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 109](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L109)
+
+ ```solidity
+ function _onlyRulesManager() internal view virtual override onlyRole(RULES_MANAGEMENT_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 114](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L114)
+
+ ```solidity
+ function _onlyRulesLimitManager() internal view virtual override onlyRole(RULES_MANAGEMENT_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 16](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L16)
+
+ ```solidity
+ contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2Step, Ownable2StepERC165Module {
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L58)
+
+ ```solidity
+ function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L63)
+
+ ```solidity
+ function _authorizeMintBurnManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 69](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L69)
+
+ ```solidity
+ function _onlyRulesManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 74](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L74)
+
+ ```solidity
+ function _onlyRulesLimitManager() internal view virtual override onlyOwner {}
+ ```
+
+
+
+
+
+## L-2: Unspecific Solidity Pragma
+
+Consider using a specific version of Solidity in your contracts instead of a wide version. For example, instead of `pragma solidity ^0.8.0;`, use `pragma solidity 0.8.0;`
+
+92 Found Instances
+
+
+- Found in src/modules/AccessControlModuleStandalone.sol [Line: 3](../../../../../src/modules/AccessControlModuleStandalone.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/modules/MetaTxModuleStandalone.sol [Line: 3](../../../../../src/modules/MetaTxModuleStandalone.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/modules/Ownable2StepERC165Module.sol [Line: 2](../../../../../src/modules/Ownable2StepERC165Module.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/modules/VersionModule.sol [Line: 2](../../../../../src/modules/VersionModule.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/registry/IdentityRegistryWhitelist.sol [Line: 2](../../../../../src/registry/IdentityRegistryWhitelist.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/registry/abstract/IdentityRegistryWhitelistBase.sol [Line: 2](../../../../../src/registry/abstract/IdentityRegistryWhitelistBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/registry/abstract/IdentityRegistryWhitelistInvariantStorage.sol [Line: 2](../../../../../src/registry/abstract/IdentityRegistryWhitelistInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/registry/interfaces/IIdentityRegistryERC3643.sol [Line: 2](../../../../../src/registry/interfaces/IIdentityRegistryERC3643.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/AggregatorV3Interface.sol [Line: 2](../../../../../src/rules/interfaces/AggregatorV3Interface.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/IAddressList.sol [Line: 2](../../../../../src/rules/interfaces/IAddressList.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/IBalanceOf.sol [Line: 2](../../../../../src/rules/interfaces/IBalanceOf.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/IDecimals.sol [Line: 2](../../../../../src/rules/interfaces/IDecimals.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/IERC2980.sol [Line: 2](../../../../../src/rules/interfaces/IERC2980.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/IERC7943NonFungibleCompliance.sol [Line: 3](../../../../../src/rules/interfaces/IERC7943NonFungibleCompliance.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/IIdentityRegistry.sol [Line: 3](../../../../../src/rules/interfaces/IIdentityRegistry.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/ISanctionsList.sol [Line: 3](../../../../../src/rules/interfaces/ISanctionsList.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/ITotalSupply.sol [Line: 2](../../../../../src/rules/interfaces/ITotalSupply.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/ITransferContext.sol [Line: 2](../../../../../src/rules/interfaces/ITransferContext.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/RuleMintAllowance.sol [Line: 2](../../../../../src/rules/operation/RuleMintAllowance.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 2](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleConditionalTransferLightBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleMintAllowanceBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetRolesStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetRolesStorage.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleBlacklistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleBlacklistBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleChainlinkPoRBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleChainlinkPoRBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleERC2980Base.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleMaxBalanceBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleMaxBalanceBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleReceiverWhitelistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleReceiverWhitelistBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleSanctionsListBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleSanctionsListBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleWhitelistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleWhitelistBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol [Line: 3](../../../../../src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/BalanceCapManager.sol [Line: 2](../../../../../src/rules/validation/abstract/core/BalanceCapManager.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/CapAccounting.sol [Line: 2](../../../../../src/rules/validation/abstract/core/CapAccounting.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol [Line: 2](../../../../../src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/RuleNFTAdapter.sol [Line: 2](../../../../../src/rules/validation/abstract/core/RuleNFTAdapter.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/RuleTransferValidation.sol [Line: 3](../../../../../src/rules/validation/abstract/core/RuleTransferValidation.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/RuleWhitelistShared.sol [Line: 2](../../../../../src/rules/validation/abstract/core/RuleWhitelistShared.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/TokenSupplyReader.sol [Line: 2](../../../../../src/rules/validation/abstract/core/TokenSupplyReader.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/TotalSupplyCapManager.sol [Line: 2](../../../../../src/rules/validation/abstract/core/TotalSupplyCapManager.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleChainlinkPoRInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleChainlinkPoRInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleMaxBalanceInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleMaxBalanceInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleReceiverWhitelistInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleReceiverWhitelistInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 3](../../../../../src/rules/validation/deployment/RuleBlacklist.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleChainlinkPoR.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleChainlinkPoR.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleChainlinkPoRERC3643.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleChainlinkPoRERC3643.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleChainlinkPoRERC3643Ownable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleChainlinkPoRERC3643Ownable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleChainlinkPoROwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleChainlinkPoROwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleIdentityRegistry.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleIdentityRegistry.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxBalance.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxBalance.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxBalanceOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxBalanceOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxTotalSupply.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxTotalSupply.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxTotalSupplyERC3643.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyERC3643.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxTotalSupplyERC3643Ownable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyERC3643Ownable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleReceiverWhitelist.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleReceiverWhitelist.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleSanctionsList.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSanctionsList.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSpenderWhitelist.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 3](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 3](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+
+
+
+
+## L-3: Address State Variable Set Without Checks
+
+Check for `address(0)` when assigning values to address state variables.
+
+3 Found Instances
+
+
+- Found in src/rules/validation/abstract/base/RuleSanctionsListBase.sol [Line: 125](../../../../../src/rules/validation/abstract/base/RuleSanctionsListBase.sol#L125)
+
+ ```solidity
+ sanctionsList = sanctionContractOracle_;
+ ```
+
+- Found in src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol [Line: 137](../../../../../src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L137)
+
+ ```solidity
+ reservesFeed = newReservesFeed;
+ ```
+
+- Found in src/rules/validation/abstract/core/TotalSupplyCapManager.sol [Line: 85](../../../../../src/rules/validation/abstract/core/TotalSupplyCapManager.sol#L85)
+
+ ```solidity
+ tokenContract = ITotalSupply(newTokenContract);
+ ```
+
+
+
+
+
+## L-4: Literal Instead of Constant
+
+Define and use `constant` variables instead of using literals. If the same constant literal value is used multiple times, create a constant state variable and reference it throughout the contract.
+
+2 Found Instances
+
+
+- Found in src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol [Line: 256](../../../../../src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L256)
+
+ ```solidity
+ uint256 factor = 10 ** uint256(to - from);
+ ```
+
+- Found in src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol [Line: 263](../../../../../src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L263)
+
+ ```solidity
+ return answer / (10 ** uint256(from - to));
+ ```
+
+
+
+
+
+## L-5: PUSH0 Opcode
+
+Solc compiler version 0.8.20 switches the default target EVM version to Shanghai, which means that the generated bytecode will include PUSH0 opcodes. Be sure to select the appropriate EVM version in case you intend to deploy on a chain other than mainnet like L2 chains that may not support PUSH0, otherwise deployment of your contracts will fail.
+
+94 Found Instances
+
+
+- Found in src/modules/AccessControlModuleStandalone.sol [Line: 3](../../../../../src/modules/AccessControlModuleStandalone.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/modules/MetaTxModuleStandalone.sol [Line: 3](../../../../../src/modules/MetaTxModuleStandalone.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/modules/Ownable2StepERC165Module.sol [Line: 2](../../../../../src/modules/Ownable2StepERC165Module.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/modules/VersionModule.sol [Line: 2](../../../../../src/modules/VersionModule.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/registry/IdentityRegistryWhitelist.sol [Line: 2](../../../../../src/registry/IdentityRegistryWhitelist.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/registry/abstract/IdentityRegistryWhitelistBase.sol [Line: 2](../../../../../src/registry/abstract/IdentityRegistryWhitelistBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/registry/abstract/IdentityRegistryWhitelistInvariantStorage.sol [Line: 2](../../../../../src/registry/abstract/IdentityRegistryWhitelistInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/registry/interfaces/IIdentityRegistryERC3643.sol [Line: 2](../../../../../src/registry/interfaces/IIdentityRegistryERC3643.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/AggregatorV3Interface.sol [Line: 2](../../../../../src/rules/interfaces/AggregatorV3Interface.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/IAddressList.sol [Line: 2](../../../../../src/rules/interfaces/IAddressList.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/IBalanceOf.sol [Line: 2](../../../../../src/rules/interfaces/IBalanceOf.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/IDecimals.sol [Line: 2](../../../../../src/rules/interfaces/IDecimals.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/IERC2980.sol [Line: 2](../../../../../src/rules/interfaces/IERC2980.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/IERC7943NonFungibleCompliance.sol [Line: 3](../../../../../src/rules/interfaces/IERC7943NonFungibleCompliance.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/IIdentityRegistry.sol [Line: 3](../../../../../src/rules/interfaces/IIdentityRegistry.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/ISanctionsList.sol [Line: 3](../../../../../src/rules/interfaces/ISanctionsList.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/ITotalSupply.sol [Line: 2](../../../../../src/rules/interfaces/ITotalSupply.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/ITransferContext.sol [Line: 2](../../../../../src/rules/interfaces/ITransferContext.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/interfaces/library/AddressListInterfaceId.sol [Line: 3](../../../../../src/rules/interfaces/library/AddressListInterfaceId.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 2](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/RuleMintAllowance.sol [Line: 2](../../../../../src/rules/operation/RuleMintAllowance.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 2](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleConditionalTransferLightBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleMintAllowanceBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol [Line: 2](../../../../../src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/AddressSetBatchLib.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleAddressSet/AddressSetBatchLib.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetRolesStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetRolesStorage.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleBlacklistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleBlacklistBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleChainlinkPoRBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleChainlinkPoRBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleERC2980Base.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleMaxBalanceBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleMaxBalanceBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleReceiverWhitelistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleReceiverWhitelistBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleSanctionsListBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleSanctionsListBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleWhitelistBase.sol [Line: 2](../../../../../src/rules/validation/abstract/base/RuleWhitelistBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol [Line: 3](../../../../../src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/BalanceCapManager.sol [Line: 2](../../../../../src/rules/validation/abstract/core/BalanceCapManager.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/CapAccounting.sol [Line: 2](../../../../../src/rules/validation/abstract/core/CapAccounting.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol [Line: 2](../../../../../src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/RuleNFTAdapter.sol [Line: 2](../../../../../src/rules/validation/abstract/core/RuleNFTAdapter.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/RuleTransferValidation.sol [Line: 3](../../../../../src/rules/validation/abstract/core/RuleTransferValidation.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/RuleWhitelistShared.sol [Line: 2](../../../../../src/rules/validation/abstract/core/RuleWhitelistShared.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/TokenSupplyReader.sol [Line: 2](../../../../../src/rules/validation/abstract/core/TokenSupplyReader.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/core/TotalSupplyCapManager.sol [Line: 2](../../../../../src/rules/validation/abstract/core/TotalSupplyCapManager.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleChainlinkPoRInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleChainlinkPoRInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleMaxBalanceInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleMaxBalanceInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleReceiverWhitelistInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleReceiverWhitelistInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol [Line: 3](../../../../../src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol [Line: 2](../../../../../src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 3](../../../../../src/rules/validation/deployment/RuleBlacklist.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleChainlinkPoR.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleChainlinkPoR.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleChainlinkPoRERC3643.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleChainlinkPoRERC3643.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleChainlinkPoRERC3643Ownable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleChainlinkPoRERC3643Ownable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleChainlinkPoROwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleChainlinkPoROwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleIdentityRegistry.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleIdentityRegistry.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxBalance.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxBalance.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxBalanceOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxBalanceOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxTotalSupply.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxTotalSupply.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxTotalSupplyERC3643.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyERC3643.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxTotalSupplyERC3643Ownable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyERC3643Ownable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleReceiverWhitelist.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleReceiverWhitelist.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleSanctionsList.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSanctionsList.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSpenderWhitelist.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 2](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 3](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 3](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+
+
+
+
+## L-6: Modifier Invoked Only Once
+
+Consider removing the modifier or inlining the logic into the calling function.
+
+1 Found Instances
+
+
+- Found in src/rules/validation/abstract/core/RuleWhitelistShared.sol [Line: 51](../../../../../src/rules/validation/abstract/core/RuleWhitelistShared.sol#L51)
+
+ ```solidity
+ modifier onlyCheckSpenderManager() {
+ ```
+
+
+
+
+
+## L-7: Empty Block
+
+Consider removing empty blocks.
+
+70 Found Instances
+
+
+- Found in src/registry/IdentityRegistryWhitelist.sol [Line: 34](../../../../../src/registry/IdentityRegistryWhitelist.sol#L34)
+
+ ```solidity
+ function _authorizeIdentityRegistrar() internal view virtual override onlyRole(IDENTITY_REGISTRAR_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 62](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L62)
+
+ ```solidity
+ function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 67](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L67)
+
+ ```solidity
+ function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 72](../../../../../src/rules/operation/RuleConditionalTransferLight.sol#L72)
+
+ ```solidity
+ function _authorizeTokenBindingChange(address) internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 50](../../../../../src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L50)
+
+ ```solidity
+ function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 55](../../../../../src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L55)
+
+ ```solidity
+ function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 49](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L49)
+
+ ```solidity
+ function _onlyComplianceManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 54](../../../../../src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L54)
+
+ ```solidity
+ function _authorizeTransferApproval() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 60](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L60)
+
+ ```solidity
+ function _onlyComplianceManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 65](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L65)
+
+ ```solidity
+ function _authorizeTransferApproval() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 70](../../../../../src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L70)
+
+ ```solidity
+ function _authorizeTokenBindingChange(address) internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleMintAllowance.sol [Line: 60](../../../../../src/rules/operation/RuleMintAllowance.sol#L60)
+
+ ```solidity
+ function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleMintAllowance.sol [Line: 65](../../../../../src/rules/operation/RuleMintAllowance.sol#L65)
+
+ ```solidity
+ function _authorizeSetMintAllowance() internal view virtual override onlyRole(ALLOWANCE_OPERATOR_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleMintAllowance.sol [Line: 70](../../../../../src/rules/operation/RuleMintAllowance.sol#L70)
+
+ ```solidity
+ function _authorizeTokenBindingChange(address) internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+ ```
+
+- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 57](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L57)
+
+ ```solidity
+ function _onlyComplianceManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 62](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L62)
+
+ ```solidity
+ function _authorizeSetMintAllowance() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 67](../../../../../src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L67)
+
+ ```solidity
+ function _authorizeTokenBindingChange(address) internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 65](../../../../../src/rules/operation/abstract/RuleMintAllowanceBase.sol#L65)
+
+ ```solidity
+ function created(address, uint256) external virtual override onlyBoundToken {}
+ ```
+
+- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 70](../../../../../src/rules/operation/abstract/RuleMintAllowanceBase.sol#L70)
+
+ ```solidity
+ function destroyed(address, uint256) external virtual override onlyBoundToken {}
+ ```
+
+- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 265](../../../../../src/rules/operation/abstract/RuleMintAllowanceBase.sol#L265)
+
+ ```solidity
+ function _transferred(address, address, uint256) internal virtual {
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol [Line: 56](../../../../../src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol#L56)
+
+ ```solidity
+ function transferred(address, address, uint256) public view override(IERC3643IComplianceContract) {}
+ ```
+
+- Found in src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol [Line: 128](../../../../../src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol#L128)
+
+ ```solidity
+ function _transferred(address, address, uint256) internal view virtual override {
+ ```
+
+- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 55](../../../../../src/rules/validation/deployment/RuleBlacklist.sol#L55)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 60](../../../../../src/rules/validation/deployment/RuleBlacklist.sol#L60)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 54](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L54)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 59](../../../../../src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L59)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleChainlinkPoR.sol [Line: 65](../../../../../src/rules/validation/deployment/RuleChainlinkPoR.sol#L65)
+
+ ```solidity
+ function _authorizeChainlinkPoRManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleChainlinkPoROwnable2Step.sol [Line: 62](../../../../../src/rules/validation/deployment/RuleChainlinkPoROwnable2Step.sol#L62)
+
+ ```solidity
+ function _authorizeChainlinkPoRManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L63)
+
+ ```solidity
+ function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 68](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L68)
+
+ ```solidity
+ function _authorizeWhitelistAdd() internal view virtual override onlyRole(WHITELIST_ADD_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 73](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L73)
+
+ ```solidity
+ function _authorizeWhitelistRemove() internal view virtual override onlyRole(WHITELIST_REMOVE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 78](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L78)
+
+ ```solidity
+ function _authorizeFrozenlistAdd() internal view virtual override onlyRole(FROZENLIST_ADD_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 83](../../../../../src/rules/validation/deployment/RuleERC2980.sol#L83)
+
+ ```solidity
+ function _authorizeFrozenlistRemove() internal view virtual override onlyRole(FROZENLIST_REMOVE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 56](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L56)
+
+ ```solidity
+ function _authorizeMintBurnManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 61](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L61)
+
+ ```solidity
+ function _authorizeWhitelistAdd() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 66](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L66)
+
+ ```solidity
+ function _authorizeWhitelistRemove() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 71](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L71)
+
+ ```solidity
+ function _authorizeFrozenlistAdd() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 76](../../../../../src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L76)
+
+ ```solidity
+ function _authorizeFrozenlistRemove() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleIdentityRegistry.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleIdentityRegistry.sol#L63)
+
+ ```solidity
+ function _authorizeIdentityRegistryManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L63)
+
+ ```solidity
+ function _authorizeIdentityRegistryManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxBalance.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleMaxBalance.sol#L58)
+
+ ```solidity
+ function _authorizeMaxBalanceManager() internal view virtual override onlyRole(MAX_BALANCE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxBalanceOwnable2Step.sol [Line: 59](../../../../../src/rules/validation/deployment/RuleMaxBalanceOwnable2Step.sol#L59)
+
+ ```solidity
+ function _authorizeMaxBalanceManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxTotalSupply.sol [Line: 55](../../../../../src/rules/validation/deployment/RuleMaxTotalSupply.sol#L55)
+
+ ```solidity
+ function _authorizeMaxTotalSupplyManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L57)
+
+ ```solidity
+ function _authorizeMaxTotalSupplyManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleReceiverWhitelist.sol [Line: 56](../../../../../src/rules/validation/deployment/RuleReceiverWhitelist.sol#L56)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleReceiverWhitelist.sol [Line: 61](../../../../../src/rules/validation/deployment/RuleReceiverWhitelist.sol#L61)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol#L57)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol [Line: 62](../../../../../src/rules/validation/deployment/RuleReceiverWhitelistOwnable2Step.sol#L62)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleSanctionsList.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleSanctionsList.sol#L58)
+
+ ```solidity
+ function _authorizeSanctionListManager() internal view virtual override onlyRole(SANCTIONLIST_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 60](../../../../../src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L60)
+
+ ```solidity
+ function _authorizeSanctionListManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 56](../../../../../src/rules/validation/deployment/RuleSpenderWhitelist.sol#L56)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 61](../../../../../src/rules/validation/deployment/RuleSpenderWhitelist.sol#L61)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 57](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L57)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 62](../../../../../src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L62)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 65](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L65)
+
+ ```solidity
+ function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 70](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L70)
+
+ ```solidity
+ function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 75](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L75)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 80](../../../../../src/rules/validation/deployment/RuleWhitelist.sol#L80)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L58)
+
+ ```solidity
+ function _authorizeAddressListAdd() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L63)
+
+ ```solidity
+ function _authorizeAddressListRemove() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 68](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L68)
+
+ ```solidity
+ function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 73](../../../../../src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L73)
+
+ ```solidity
+ function _authorizeMintBurnManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 98](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L98)
+
+ ```solidity
+ function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 103](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L103)
+
+ ```solidity
+ function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 109](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L109)
+
+ ```solidity
+ function _onlyRulesManager() internal view virtual override onlyRole(RULES_MANAGEMENT_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 114](../../../../../src/rules/validation/deployment/RuleWhitelistWrapper.sol#L114)
+
+ ```solidity
+ function _onlyRulesLimitManager() internal view virtual override onlyRole(RULES_MANAGEMENT_ROLE) {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 58](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L58)
+
+ ```solidity
+ function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 63](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L63)
+
+ ```solidity
+ function _authorizeMintBurnManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 69](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L69)
+
+ ```solidity
+ function _onlyRulesManager() internal view virtual override onlyOwner {}
+ ```
+
+- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 74](../../../../../src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L74)
+
+ ```solidity
+ function _onlyRulesLimitManager() internal view virtual override onlyOwner {}
+ ```
+
+
+
+
+
+## L-8: Costly operations inside loop
+
+Invoking `SSTORE` operations in loops may waste gas. Use a local variable to hold the loop computation result.
+
+3 Found Instances
+
+
+- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 127](../../../../../src/rules/operation/abstract/RuleMintAllowanceBase.sol#L127)
+
+ ```solidity
+ for (uint256 i = 0; i < minters.length; ++i) {
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/AddressSetBatchLib.sol [Line: 44](../../../../../src/rules/validation/abstract/RuleAddressSet/AddressSetBatchLib.sol#L44)
+
+ ```solidity
+ for (uint256 i = 0; i < addressesToAdd.length; ++i) {
+ ```
+
+- Found in src/rules/validation/abstract/RuleAddressSet/AddressSetBatchLib.sol [Line: 67](../../../../../src/rules/validation/abstract/RuleAddressSet/AddressSetBatchLib.sol#L67)
+
+ ```solidity
+ for (uint256 i = 0; i < addressesToRemove.length; ++i) {
+ ```
+
+
+
+
+
+## L-9: Unchecked Return
+
+Function returns a value but it is ignored. Consider checking the return value.
+
+1 Found Instances
+
+
+- Found in src/modules/AccessControlModuleStandalone.sol [Line: 35](../../../../../src/modules/AccessControlModuleStandalone.sol#L35)
+
+ ```solidity
+ _grantRole(DEFAULT_ADMIN_ROLE, admin);
+ ```
+
+
+
+
+
diff --git a/doc/security/audits/tools/v0.6.0/slither-report-feedback.md b/doc/security/audits/tools/v0.6.0/slither-report-feedback.md
new file mode 100644
index 00000000..738811b5
--- /dev/null
+++ b/doc/security/audits/tools/v0.6.0/slither-report-feedback.md
@@ -0,0 +1,162 @@
+# Slither `v0.6.0` — triage
+
+```bash
+slither . --checklist --filter-paths "node_modules,lib,test,forge-std,mocks" \
+ > doc/security/audits/tools/v0.6.0/slither-report.md
+```
+
+Tool: **Slither 0.11.5** · Compiler: solc `0.8.36` · Run date: **2026-08-21** (re-run after the RuleEngine
+`v3.0.0-rc6` bump; supersedes the 2026-08-18 run)
+Scope: production contracts only. Mocks excluded via the `mocks` filter, vendored dependencies via `lib`.
+225 contracts, 101 detectors, **46 results**.
+
+**Executive triage: nothing to fix.** No finding is exploitable. Both High-impact results are the same
+false positive dismissed in `v0.4.0` and `v0.5.0`, on a permissioned path. The two results new since `v0.5.0`
+were each opened against the source and dismissed; one of them is worth reading, because the natural reaction to
+it would be to delete working code.
+
+### Scope check
+
+Both assertions pass — the filter matched, so nothing outside the project is in scope:
+
+```
+grep -c 'lib/\|node_modules/' slither-report.md → 0
+grep -c 'test/\|src/mocks/' slither-report.md → 0
+```
+
+The filter list must name **`lib`**: this is a Foundry project, and an entry that matches nothing fails open,
+pulling the whole vendored dependency tree in. A previous run with a generic `submodules` filter returned 170
+results, 351 of them citing `lib/openzeppelin-contracts/`.
+
+## Summary
+
+| Detector | Impact | Instances | Disposition |
+|---|---|---|---|
+| `arbitrary-send-erc20` | High | 2 | **False positive** |
+| `uninitialized-local` | Medium | 2 | False positive |
+| `unused-return` | Medium | 9 | False positive |
+| `calls-loop` | Low | 17 | By design |
+| `timestamp` | Low | 1 | By design |
+| `assembly` | Informational | 2 | By design |
+| `dead-code` | Informational | 3 | False positive |
+| `naming-convention` | Informational | 6 | By design |
+| `unused-state` | Informational | 4 | Cosmetic |
+
+## Delta from `v0.5.0`
+
+**44 → 46 results (+2).** Every other detector is unchanged, instance for instance.
+
+| Detector | v0.5.0 | v0.6.0 | Δ |
+|---|---|---|---|
+| `calls-loop` | 16 | 17 | **+1** |
+| `dead-code` | 2 | 3 | **+1** |
+| *(all others)* | 42 | 42 | — |
+
+A delta this small on a release that added five production contracts is the expected shape. Both new results
+are in code added for the Nethermind AuditAgent fixes.
+
+### +1 `calls-loop` — `RuleWhitelistWrapperBase._checkRule`
+
+> `_checkRule(address)` has external calls inside a loop: `require(IAddressListPolarity(rule_).isAllowList(), …)`
+
+**By design.** This is the NM-20 polarity guard. Slither reaches it through `setRules`, which loops over the
+submitted array calling `_addRule` → `_checkRule`, so each candidate costs two `ERC165Checker` staticcalls plus
+one `isAllowList()`. That is:
+
+- **bounded** — `maxRules` defaults to 10 and `setRules` rejects an array longer than it;
+- **configuration-time only** — `RULES_MANAGEMENT_ROLE`, never a transfer path, so no holder pays for it;
+- **the point of the guard** — checking a child's interface and polarity requires calling the child.
+
+The alternative (validating outside the loop) would mean not validating each candidate, which is the finding
+NM-18 and NM-20 exist to close.
+
+### +1 `dead-code` — `RuleChainlinkPoRBase._detectTransferRestrictionOnNotify`
+
+> `_detectTransferRestrictionOnNotify(address,address,uint256)` is never used and should be removed
+
+**False positive, and acting on it would break the ERC-3643 Proof-of-Reserve variant.** The function is the
+notification-phase seam added for NM-11; `RuleChainlinkPoRERC3643` exists solely to override it. Three
+independent confirmations that it is live:
+
+1. **It is called twice in the same file** — `RuleChainlinkPoRBase.sol:202` and `:217`, from `_transferred` and
+ `_transferredFrom`.
+2. **Coverage is 100% of functions** on `RuleChainlinkPoRBase` (10/10). An unreachable function cannot be
+ executed by the test suite.
+3. **`testStockRuleRevertsAFullyBackedMint` and `testMintUpToTheReservesSucceeds`** (real-T-REX suite) differ
+ *only* by which override of this hook is installed. If the seam were dead, both would behave identically and
+ the pair would fail.
+
+Slither's `dead-code` is unreliable for `internal virtual` functions reached through inheritance: the same
+detector already produced the two pre-existing hits below, both dismissed on the same grounds. Note also its
+inconsistency — `RuleMaxTotalSupplyBase` has the byte-identical seam, overridden by
+`RuleMaxTotalSupplyERC3643`, and is **not** flagged.
+
+The two pre-existing instances are unchanged: `RuleERC2980Internal._requireNotZeroAddress` and
+`RuleAddressSetInternal._requireNotZeroAddress`, both internal guards reached from the public layer.
+
+## Re-run within `v0.6.0` (2026-08-18 → 2026-08-21)
+
+**No detector moved.** All nine hold their exact result counts, so the summary table above is unchanged. The
+entire body diff is **line numbers in three files** — `RuleConditionalTransferLightBase`,
+`RuleConditionalTransferLightMultiTokenBase` and `RuleChainlinkPoRBase`. Two commits landed between the runs:
+
+- `c1ebe57` — trimmed NatSpec to the 20-line ceiling and marked two pointer-passed guards `virtual`. This is
+ what moved `RuleChainlinkPoRBase`, a file the rc6 bump never touched, and it means the 2026-08-18 report was
+ **already one commit stale when it was committed**.
+- `f920b07` — RuleEngine `v3.0.0-rc6`: two access-control hooks renamed and one redundant override deleted.
+
+### Contract count 221 → 225 — not this repository's code
+
+Slither walks the full inheritance graph, including the vendored dependencies it then filters out of the
+*results*. `v3.0.0-rc6` split the token-binding registry out of `ERC3643ComplianceModule`, adding
+`TokenBindingModule`, `TokenBindingExtendedModule`, `ITokenBinding`, `ITokenBindingExtended` and
+`TokenBindingModuleInvariantStorage`, and removing `ERC3643ComplianceModuleInvariantStorage` — net **+4**.
+Every one of them is under `lib/` and contributes zero results. This is the one number in the report that
+changed without a corresponding change in `src/`, and it is worth naming explicitly so a future reader does not
+read it as scope creep.
+
+### What did *not* move, and why that is the useful check
+
+- **`dead-code` stayed at 3.** Deleting `RuleConditionalTransferLightMultiTokenBase._authorizeComplianceBindingChange`
+ removed an `internal` function that Slither did *not* consider dead — it was reached through the
+ `bindToken` / `unbindToken` path. Had the count dropped to 2, that would have meant the deletion removed a
+ live authorization check rather than a redundant one.
+- **`arbitrary-send-erc20` stayed at 2**, still on `approveAndTransferIfAllowed` in both conditional-transfer
+ bases. The rename changed which modifier gates `bindToken`, not the gating of the transfer helper.
+- **`unused-return` stayed at 9.** `_bindToken` still consumes the `EnumerableSet.add` return value through a
+ `require`; the registry moved upstream, the call site did not change.
+
+### Scope check re-verified
+
+`grep -c 'lib/\|node_modules/'` on the fresh report is **0**.
+
+## Findings carried over from `v0.5.0`
+
+Unchanged in count and disposition; verified again against the source this run.
+
+- **`arbitrary-send-erc20` (High, 2)** — `approveAndTransferIfAllowed` in the light and multi-token conditional
+ rules. Gated by `onlyTransferApprover`, a recorded approval, an explicit allowance check and a bound token.
+ This release **tightened** the path further (NM-17: the helper now reverts unless the approval it created was
+ consumed), so the detector's premise is weaker than before, not stronger.
+- **`uninitialized-local` (Medium, 2)** — variables assigned inside a `try` whose `catch` returns or reverts.
+- **`unused-return` (Medium, 9)** — `EnumerableSet` add/remove return values deliberately discarded by the batch
+ helpers, and the configuration probes (`totalSupply()`, `balanceOf()`, `decimals()`) whose discarded value is
+ precisely the point: the call is made to learn whether it reverts.
+- **`calls-loop` (Low, 16 of 17)** — the wrapper's child scan. Bounded by `maxRules`, measured at ~8.8k gas per
+ child, and documented with operator guidance.
+- **`timestamp` (Low, 1)** — the Proof-of-Reserve staleness comparison. The feature is a freshness check; it
+ cannot be written without reading `block.timestamp`.
+- **`assembly` (Informational, 2)** — the `_transferHash` preimage, whose exact layout is documented and pinned
+ by `testDocumentedPreimageMatchesTheStorageKey`.
+- **`naming-convention` (Informational, 6)** — parameter names matching the ERC text they implement.
+- **`unused-state` (Informational, 4)** — the four `TRANSFERRED_SELECTOR_*` constants in `RuleNFTAdapter`. Still
+ genuinely unreferenced, as corrected in the `v0.5.0` triage (they had previously been dismissed as a false
+ positive, wrongly). Impact is nil — `internal constant`, so no storage and nothing emitted into bytecode — so
+ the disposition stays cosmetic rather than a fix.
+
+## What a clean report does and does not mean
+
+Slither's pattern set matched nothing actionable. That is not evidence of correctness: every substantive issue
+addressed in this release came from the Nethermind AuditAgent scan and from manual review, and **not one of the
+seven fixed findings was reachable by either static analyser** — they are semantic (who calls a hook, in what
+order, with which arguments), and Slither and Aderyn match syntax.
diff --git a/doc/security/audits/tools/v0.6.0/slither-report.md b/doc/security/audits/tools/v0.6.0/slither-report.md
new file mode 100644
index 00000000..9575b48d
--- /dev/null
+++ b/doc/security/audits/tools/v0.6.0/slither-report.md
@@ -0,0 +1,451 @@
+# Slither Report — `v0.6.0`
+
+```bash
+slither . --checklist --filter-paths "node_modules,lib,test,forge-std,mocks" \
+ > doc/security/audits/tools/v0.6.0/slither-report.md
+```
+
+Tool: **Slither 0.11.5** · Compiler: solc `0.8.36` · Run date: **2026-08-21** (re-run after the RuleEngine
+`v3.0.0-rc6` bump; supersedes the 2026-08-18 run)
+Scope: production contracts only — **mocks excluded**, vendored dependencies excluded via the `lib` filter.
+225 contracts, 101 detectors, **46 results**.
+
+**0 High\* · 2 High-impact (both false positives) · 11 Medium · 18 Low · 15 Informational.**
+
+| Detector | Impact | Instances | Assessment |
+|---|---|---|---|
+| `arbitrary-send-erc20` | High | 2 | **False positive** — `approveAndTransferIfAllowed` is gated by `onlyTransferApprover`, a recorded approval, an allowance check and a bound token |
+| `uninitialized-local` | Medium | 2 | False positive — assigned inside a `try` whose `catch` returns or reverts |
+| `unused-return` | Medium | 9 | False positive — `EnumerableSet` return values deliberately discarded, or configuration probes whose discarded value is the point |
+| `calls-loop` | Low | 17 | By design — the wrapper's bounded child scan and its `_checkRule` guard |
+| `timestamp` | Low | 1 | By design — the Proof-of-Reserve staleness comparison *is* the feature |
+| `assembly` | Informational | 2 | By design — the documented `_transferHash` preimage |
+| `dead-code` | Informational | 3 | False positive — all three are reachable; see the feedback file |
+| `naming-convention` | Informational | 6 | By design — spec-aligned parameter names |
+| `unused-state` | Informational | 4 | Cosmetic — the four `TRANSFERRED_SELECTOR_*` constants are genuinely unreferenced |
+
+\* Slither has no "High severity" column as such; the two `arbitrary-send-erc20` results carry High *impact*
+and are verified false positives.
+
+**Nothing to fix.** No finding is exploitable. The delta from `v0.5.0` is **+2** (44 → 46), both traceable to
+code added in this release and both dismissed against the source.
+
+**Re-run delta (2026-08-18 → 2026-08-21): no detector moved.** Every one of the nine detectors holds its exact
+result count; the entire diff is **line numbers in three files** (`RuleConditionalTransferLightBase`,
+`RuleConditionalTransferLightMultiTokenBase`, `RuleChainlinkPoRBase`). The contract count rose **221 → 225**,
+which is not this repository's code: RuleEngine `v3.0.0-rc6` split the binding registry out of
+`ERC3643ComplianceModule`, adding `TokenBindingModule`, `TokenBindingExtendedModule`, `ITokenBinding`,
+`ITokenBindingExtended` and `TokenBindingModuleInvariantStorage` while removing
+`ERC3643ComplianceModuleInvariantStorage` — net +4 contracts in the inheritance graph Slither walks, all of
+them filtered out of the results by the `lib` path filter.
+
+Triage: [`slither-report-feedback.md`](./slither-report-feedback.md) ·
+Overview: [`AUDIT_OVERVIEW.md`](../../AUDIT_OVERVIEW.md)
+
+---
+
+**THIS CHECKLIST IS NOT COMPLETE**. Use `--show-ignored-findings` to show all the results.
+Summary
+ - [arbitrary-send-erc20](#arbitrary-send-erc20) (2 results) (High)
+ - [uninitialized-local](#uninitialized-local) (2 results) (Medium)
+ - [unused-return](#unused-return) (9 results) (Medium)
+ - [calls-loop](#calls-loop) (17 results) (Low)
+ - [timestamp](#timestamp) (1 results) (Low)
+ - [assembly](#assembly) (2 results) (Informational)
+ - [dead-code](#dead-code) (3 results) (Informational)
+ - [naming-convention](#naming-convention) (6 results) (Informational)
+ - [unused-state](#unused-state) (4 results) (Informational)
+## arbitrary-send-erc20
+Impact: High
+Confidence: High
+ - [ ] ID-0
+[RuleConditionalTransferLightBase.approveAndTransferIfAllowed(address,address,uint256)](src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L115-L141) uses arbitrary from in transferFrom: [IERC20(token).safeTransferFrom(from,to,value)](src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L130)
+
+src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L115-L141
+
+
+ - [ ] ID-1
+[RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed(address,address,address,uint256)](src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L131-L157) uses arbitrary from in transferFrom: [IERC20(token).safeTransferFrom(from,to,value)](src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L147)
+
+src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L131-L157
+
+
+## uninitialized-local
+Impact: Medium
+Confidence: Medium
+ - [ ] ID-2
+[ChainlinkPoRFeedManager._maxBackedSupply().currentFeedDecimals](src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L199) is a local variable never initialized
+
+src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L199
+
+
+ - [ ] ID-3
+[ChainlinkPoRFeedManager._setReservesFeed(AggregatorV3Interface).newFeedDecimals](src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L130) is a local variable never initialized
+
+src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L130
+
+
+## unused-return
+Impact: Medium
+Confidence: Medium
+ - [ ] ID-4
+[RuleERC2980Internal._removeFrozenlistAddresses(address[])](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L110-L116) ignores return value by [_frozenlist.removeBatch(addressesToRemove)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L115)
+
+src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L110-L116
+
+
+ - [ ] ID-5
+[BalanceCapManager._setBalanceToken(address)](src/rules/validation/abstract/core/BalanceCapManager.sol#L176-L189) ignores return value by [IBalanceOf(newBalanceToken).balanceOf(address(this))](src/rules/validation/abstract/core/BalanceCapManager.sol#L181-L186)
+
+src/rules/validation/abstract/core/BalanceCapManager.sol#L176-L189
+
+
+ - [ ] ID-6
+[RuleAddressSetInternal._removeAddresses(address[])](src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L77-L83) ignores return value by [_listedAddresses.removeBatch(addressesToRemove)](src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L82)
+
+src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L77-L83
+
+
+ - [ ] ID-7
+[RuleERC2980Internal._removeWhitelistAddresses(address[])](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L61-L67) ignores return value by [_whitelist.removeBatch(addressesToRemove)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L66)
+
+src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L61-L67
+
+
+ - [ ] ID-8
+[RuleERC2980Internal._addWhitelistAddresses(address[])](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L47-L53) ignores return value by [_whitelist.addBatch(addressesToAdd,_requireNotZeroAddress)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L52)
+
+src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L47-L53
+
+
+ - [ ] ID-9
+[RuleERC2980Internal._addFrozenlistAddresses(address[])](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L96-L102) ignores return value by [_frozenlist.addBatch(addressesToAdd,_requireNotZeroAddress)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L101)
+
+src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L96-L102
+
+
+ - [ ] ID-10
+[RuleAddressSetInternal._addAddresses(address[])](src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L44-L50) ignores return value by [_listedAddresses.addBatch(addressesToAdd,_requireNotZeroAddress)](src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L49)
+
+src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L44-L50
+
+
+ - [ ] ID-11
+[TokenSupplyReader._probeTotalSupplyCallable(address)](src/rules/validation/abstract/core/TokenSupplyReader.sol#L65-L71) ignores return value by [ITotalSupply(candidate).totalSupply()](src/rules/validation/abstract/core/TokenSupplyReader.sol#L66-L70)
+
+src/rules/validation/abstract/core/TokenSupplyReader.sol#L65-L71
+
+
+ - [ ] ID-12
+[ChainlinkPoRFeedManager._maxBackedSupply()](src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L195-L231) ignores return value by [(answer,updatedAt) = feed.latestRoundData()](src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L210-L230)
+
+src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L195-L231
+
+
+## calls-loop
+Impact: Low
+Confidence: Medium
+ - [ ] ID-13
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleWhitelistWrapperHarnessInternal.exposedTransferredSpenderInternal(address,address,address,uint256)
+ RuleWhitelistWrapperBase._transferred(address,address,address,uint256)
+ RuleWhitelistShared._transferredFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-14
+[RuleWhitelistWrapperBase._checkRule(address)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L243-L256) has external calls inside a loop: [require(bool,error)(IAddressListPolarity(rule_).isAllowList(),revert RuleWhitelistWrapper_ChildIsNotAnAllowList(address)(rule_))](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L255)
+ Calls stack containing the loop:
+ RulesManagementModule.setRules(IRule[])
+ RulesManagementModule._addRule(IRule)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L243-L256
+
+
+ - [ ] ID-15
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleNFTAdapter.detectTransferRestriction(address,address,uint256,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-16
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleNFTAdapter.detectTransferRestrictionFrom(address,address,address,uint256,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-17
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleWhitelistShared.transferred(address,address,uint256)
+ RuleWhitelistWrapperBase._transferred(address,address,uint256)
+ RuleWhitelistShared._transferred(address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-18
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleNFTAdapter.canTransfer(address,address,uint256,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-19
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleNFTAdapter.transferred(ITransferContext.FungibleTransferContext)
+ RuleWhitelistShared._transferredFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-20
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleWhitelistWrapperBase.isVerified(address)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-21
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleNFTAdapter.transferred(ITransferContext.MultiTokenTransferContext)
+ RuleWhitelistShared._transferredFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-22
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleTransferValidation.detectTransferRestrictionFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-23
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleNFTAdapter.transferred(address,address,address,uint256,uint256)
+ RuleWhitelistShared._transferredFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-24
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleTransferValidation.canTransferFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-25
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleWhitelistShared.transferred(address,address,address,uint256)
+ RuleWhitelistShared._transferredFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-26
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleNFTAdapter.transferred(address,address,uint256,uint256)
+ RuleWhitelistWrapperBase._transferred(address,address,uint256)
+ RuleWhitelistShared._transferred(address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-27
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleNFTAdapter.canTransferFrom(address,address,address,uint256,uint256)
+ RuleNFTAdapter.detectTransferRestrictionFrom(address,address,address,uint256,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-28
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleTransferValidation.canTransfer(address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+ - [ ] ID-29
+[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293) has external calls inside a loop: [isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L279)
+ Calls stack containing the loop:
+ RuleTransferValidation.detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256)
+ RuleWhitelistWrapperBase._isListedInAnyChild(address)
+
+src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L263-L293
+
+
+## timestamp
+Impact: Low
+Confidence: Medium
+ - [ ] ID-30
+[ChainlinkPoRFeedManager._maxBackedSupply()](src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L195-L231) uses timestamp for comparisons
+ Dangerous comparisons:
+ - [answer < 0 || updatedAt == 0 || updatedAt > block.timestamp](src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L216)
+ - [staleness != 0 && block.timestamp - updatedAt > staleness](src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L221)
+
+src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol#L195-L231
+
+
+## assembly
+Impact: Informational
+Confidence: High
+ - [ ] ID-31
+[RuleConditionalTransferLightApprovalBase._transferHash(address,address,uint256)](src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L163-L174) uses assembly
+ - [INLINE ASM](src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L167-L173)
+
+src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L163-L174
+
+
+ - [ ] ID-32
+[RuleConditionalTransferLightMultiTokenBase._transferHash(address,address,address,uint256)](src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L450-L464) uses assembly
+ - [INLINE ASM](src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L456-L463)
+
+src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L450-L464
+
+
+## dead-code
+Impact: Informational
+Confidence: Medium
+ - [ ] ID-33
+[RuleERC2980Internal._requireNotZeroAddress(address)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L142-L144) is never used and should be removed
+
+src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L142-L144
+
+
+ - [ ] ID-34
+[RuleChainlinkPoRBase._detectTransferRestrictionOnNotify(address,address,uint256)](src/rules/validation/abstract/base/RuleChainlinkPoRBase.sol#L175-L182) is never used and should be removed
+
+src/rules/validation/abstract/base/RuleChainlinkPoRBase.sol#L175-L182
+
+
+ - [ ] ID-35
+[RuleAddressSetInternal._requireNotZeroAddress(address)](src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L64-L66) is never used and should be removed
+
+src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L64-L66
+
+
+## naming-convention
+Impact: Informational
+Confidence: High
+ - [ ] ID-36
+Parameter [RuleERC2980Base.frozenlist(address)._operator](src/rules/validation/abstract/base/RuleERC2980Base.sol#L374) is not in mixedCase
+
+src/rules/validation/abstract/base/RuleERC2980Base.sol#L374
+
+
+ - [ ] ID-37
+Parameter [IdentityRegistryWhitelistBase.isVerified(address)._userAddress](src/registry/abstract/IdentityRegistryWhitelistBase.sol#L92) is not in mixedCase
+
+src/registry/abstract/IdentityRegistryWhitelistBase.sol#L92
+
+
+ - [ ] ID-38
+Parameter [IdentityRegistryWhitelistBase.deleteIdentity(address)._userAddress](src/registry/abstract/IdentityRegistryWhitelistBase.sol#L67) is not in mixedCase
+
+src/registry/abstract/IdentityRegistryWhitelistBase.sol#L67
+
+
+ - [ ] ID-39
+Parameter [RuleERC2980Base.whitelist(address)._operator](src/rules/validation/abstract/base/RuleERC2980Base.sol#L325) is not in mixedCase
+
+src/rules/validation/abstract/base/RuleERC2980Base.sol#L325
+
+
+ - [ ] ID-40
+Parameter [IdentityRegistryWhitelistBase.registerIdentity(address,address,uint16)._identity](src/registry/abstract/IdentityRegistryWhitelistBase.sol#L48) is not in mixedCase
+
+src/registry/abstract/IdentityRegistryWhitelistBase.sol#L48
+
+
+ - [ ] ID-41
+Parameter [IdentityRegistryWhitelistBase.registerIdentity(address,address,uint16)._userAddress](src/registry/abstract/IdentityRegistryWhitelistBase.sol#L47) is not in mixedCase
+
+src/registry/abstract/IdentityRegistryWhitelistBase.sol#L47
+
+
+## unused-state
+Impact: Informational
+Confidence: High
+ - [ ] ID-42
+[RuleNFTAdapter.TRANSFERRED_SELECTOR_RULE_ENGINE](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L37) is never used in [RuleIdentityRegistryOwnable2Step](src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14-L64)
+
+src/rules/validation/abstract/core/RuleNFTAdapter.sol#L37
+
+
+ - [ ] ID-43
+[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC7943](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L41-L42) is never used in [RuleIdentityRegistryOwnable2Step](src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14-L64)
+
+src/rules/validation/abstract/core/RuleNFTAdapter.sol#L41-L42
+
+
+ - [ ] ID-44
+[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC7943_FROM](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L46-L47) is never used in [RuleIdentityRegistryOwnable2Step](src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14-L64)
+
+src/rules/validation/abstract/core/RuleNFTAdapter.sol#L46-L47
+
+
+ - [ ] ID-45
+[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC3643](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L33) is never used in [RuleIdentityRegistryOwnable2Step](src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14-L64)
+
+src/rules/validation/abstract/core/RuleNFTAdapter.sol#L33
+
+
diff --git a/doc/surya/surya_graph/surya_graph_BalanceCapManager.sol.png b/doc/surya/surya_graph/surya_graph_BalanceCapManager.sol.png
index e3c22d68..62811435 100644
Binary files a/doc/surya/surya_graph/surya_graph_BalanceCapManager.sol.png and b/doc/surya/surya_graph/surya_graph_BalanceCapManager.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_CapAccounting.sol.png b/doc/surya/surya_graph/surya_graph_CapAccounting.sol.png
new file mode 100644
index 00000000..c595eda0
Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_CapAccounting.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_ERC3643CapHarnesses.sol.png b/doc/surya/surya_graph/surya_graph_ERC3643CapHarnesses.sol.png
new file mode 100644
index 00000000..388f3dc1
Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_ERC3643CapHarnesses.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_IAddressList.sol.png b/doc/surya/surya_graph/surya_graph_IAddressList.sol.png
index 0e08167b..4a4f7b6a 100644
Binary files a/doc/surya/surya_graph/surya_graph_IAddressList.sol.png and b/doc/surya/surya_graph/surya_graph_IAddressList.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_IdentityRegistryDelegationHarness.sol.png b/doc/surya/surya_graph/surya_graph_IdentityRegistryDelegationHarness.sol.png
new file mode 100644
index 00000000..8293c469
Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_IdentityRegistryDelegationHarness.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleBlacklistBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleBlacklistBase.sol.png
index a9c55680..803094cc 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleBlacklistBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleBlacklistBase.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleChainlinkPoRBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleChainlinkPoRBase.sol.png
index c298df5c..0d7b5315 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleChainlinkPoRBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleChainlinkPoRBase.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleChainlinkPoRERC3643.sol.png b/doc/surya/surya_graph/surya_graph_RuleChainlinkPoRERC3643.sol.png
new file mode 100644
index 00000000..385ea297
Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleChainlinkPoRERC3643.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleChainlinkPoRERC3643Ownable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleChainlinkPoRERC3643Ownable2Step.sol.png
new file mode 100644
index 00000000..27f69cad
Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleChainlinkPoRERC3643Ownable2Step.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png
index 6319530e..6f6f14ee 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightBase.sol.png
index 04a9b436..6574b2fd 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightBase.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiToken.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiToken.sol.png
index 98c2d188..4b0f6052 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiToken.sol.png and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiToken.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenBase.sol.png
index 0cae2e5a..81c14345 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenBase.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png
index 9c84a20b..4a795e7d 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightOwnable2Step.sol.png
index 6ded68fc..7d8307f6 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightOwnable2Step.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleIdentityRegistryBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleIdentityRegistryBase.sol.png
index 746ac67d..1890ef49 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleIdentityRegistryBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleIdentityRegistryBase.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleMaxBalanceBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleMaxBalanceBase.sol.png
index 962f223d..89b3b5e1 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleMaxBalanceBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleMaxBalanceBase.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyBase.sol.png
index 9f4ebebd..b0e2cc6e 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyBase.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyERC3643.sol.png b/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyERC3643.sol.png
new file mode 100644
index 00000000..5510370e
Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyERC3643.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyERC3643Ownable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyERC3643Ownable2Step.sol.png
new file mode 100644
index 00000000..b01a2226
Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyERC3643Ownable2Step.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleMintAllowance.sol.png b/doc/surya/surya_graph/surya_graph_RuleMintAllowance.sol.png
index 0e88e49a..0d67288e 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleMintAllowance.sol.png and b/doc/surya/surya_graph/surya_graph_RuleMintAllowance.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleMintAllowanceOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceOwnable2Step.sol.png
index 2a2ffc65..b2c7c644 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleMintAllowanceOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceOwnable2Step.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleNFTAdapter.sol.png b/doc/surya/surya_graph/surya_graph_RuleNFTAdapter.sol.png
index 68f2165e..e00c3e9b 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleNFTAdapter.sol.png and b/doc/surya/surya_graph/surya_graph_RuleNFTAdapter.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleReceiverWhitelistBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleReceiverWhitelistBase.sol.png
index 268f51d6..a50c980b 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleReceiverWhitelistBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleReceiverWhitelistBase.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelistBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelistBase.sol.png
index d0514cd8..94331336 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelistBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelistBase.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperBase.sol.png
index 26293b2f..abf2fcf4 100644
Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperBase.sol.png differ
diff --git a/doc/surya/surya_graph/surya_graph_TotalSupplyCapManager.sol.png b/doc/surya/surya_graph/surya_graph_TotalSupplyCapManager.sol.png
index 163bdd1e..d2c30923 100644
Binary files a/doc/surya/surya_graph/surya_graph_TotalSupplyCapManager.sol.png and b/doc/surya/surya_graph/surya_graph_TotalSupplyCapManager.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_BalanceCapManager.sol.png b/doc/surya/surya_inheritance/surya_inheritance_BalanceCapManager.sol.png
index 4d48bc9b..dc3f2b0f 100644
Binary files a/doc/surya/surya_inheritance/surya_inheritance_BalanceCapManager.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_BalanceCapManager.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_CapAccounting.sol.png b/doc/surya/surya_inheritance/surya_inheritance_CapAccounting.sol.png
new file mode 100644
index 00000000..979b6b1f
Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_CapAccounting.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_ChainlinkPoRFeedManager.sol.png b/doc/surya/surya_inheritance/surya_inheritance_ChainlinkPoRFeedManager.sol.png
index a2c417ec..868ddfdd 100644
Binary files a/doc/surya/surya_inheritance/surya_inheritance_ChainlinkPoRFeedManager.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_ChainlinkPoRFeedManager.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_ERC3643CapHarnesses.sol.png b/doc/surya/surya_inheritance/surya_inheritance_ERC3643CapHarnesses.sol.png
new file mode 100644
index 00000000..ae42f76e
Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_ERC3643CapHarnesses.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_IAddressList.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IAddressList.sol.png
index 908d7bfe..2c7cc60f 100644
Binary files a/doc/surya/surya_inheritance/surya_inheritance_IAddressList.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_IAddressList.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_IdentityRegistryDelegationHarness.sol.png b/doc/surya/surya_inheritance/surya_inheritance_IdentityRegistryDelegationHarness.sol.png
new file mode 100644
index 00000000..6468a471
Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_IdentityRegistryDelegationHarness.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleBlacklistBase.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleBlacklistBase.sol.png
index dc3d48a2..790d37c7 100644
Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleBlacklistBase.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleBlacklistBase.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleChainlinkPoRERC3643.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleChainlinkPoRERC3643.sol.png
new file mode 100644
index 00000000..3ab8f6de
Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleChainlinkPoRERC3643.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleChainlinkPoRERC3643Ownable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleChainlinkPoRERC3643Ownable2Step.sol.png
new file mode 100644
index 00000000..0a28ac49
Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleChainlinkPoRERC3643Ownable2Step.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyERC3643.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyERC3643.sol.png
new file mode 100644
index 00000000..ffa18066
Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyERC3643.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyERC3643Ownable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyERC3643Ownable2Step.sol.png
new file mode 100644
index 00000000..e4c7d50e
Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyERC3643Ownable2Step.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleReceiverWhitelistBase.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleReceiverWhitelistBase.sol.png
index 86ba2abc..953839dd 100644
Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleReceiverWhitelistBase.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleReceiverWhitelistBase.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistBase.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistBase.sol.png
index 2a2357a6..62dd11be 100644
Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistBase.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistBase.sol.png differ
diff --git a/doc/surya/surya_inheritance/surya_inheritance_TotalSupplyCapManager.sol.png b/doc/surya/surya_inheritance/surya_inheritance_TotalSupplyCapManager.sol.png
index 1b655520..f6a4273d 100644
Binary files a/doc/surya/surya_inheritance/surya_inheritance_TotalSupplyCapManager.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_TotalSupplyCapManager.sol.png differ
diff --git a/doc/surya/surya_report/surya_report_AddressListInterfaceId.sol.md b/doc/surya/surya_report/surya_report_AddressListInterfaceId.sol.md
index 6c2aa2a1..bb97ea63 100644
--- a/doc/surya/surya_report/surya_report_AddressListInterfaceId.sol.md
+++ b/doc/surya/surya_report/surya_report_AddressListInterfaceId.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/interfaces/library/AddressListInterfaceId.sol | 8b08df55a6b20867989fffca8059fb09e3f5c39d |
+| ./rules/interfaces/library/AddressListInterfaceId.sol | fbd91ecbc9b1b60315ed8e497dabbf9b0e80f593 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_AddressSetBatchLib.sol.md b/doc/surya/surya_report/surya_report_AddressSetBatchLib.sol.md
index 2b63c2aa..e2e2ff3d 100644
--- a/doc/surya/surya_report/surya_report_AddressSetBatchLib.sol.md
+++ b/doc/surya/surya_report/surya_report_AddressSetBatchLib.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/RuleAddressSet/AddressSetBatchLib.sol | 1b09ddf8af7c9b32fc572b329b1786122a132dad |
+| ./rules/validation/abstract/RuleAddressSet/AddressSetBatchLib.sol | 720f435fb0932fd60fc43f50f93cc0e1c2711db6 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_BalanceCapManager.sol.md b/doc/surya/surya_report/surya_report_BalanceCapManager.sol.md
index de244d81..605ad41c 100644
--- a/doc/surya/surya_report/surya_report_BalanceCapManager.sol.md
+++ b/doc/surya/surya_report/surya_report_BalanceCapManager.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/core/BalanceCapManager.sol | e9fc2e355458aed8576d8aa0c26daaa9b3b88650 |
+| ./rules/validation/abstract/core/BalanceCapManager.sol | 8d23cbb921630627842b86c40e4941ae4ef676f6 |
### Contracts Description Table
@@ -15,7 +15,7 @@
|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
||||||
-| **BalanceCapManager** | Implementation | RuleAddressSetInternal, RuleMaxBalanceInvariantStorage |||
+| **BalanceCapManager** | Implementation | CapAccounting, RuleAddressSetInternal, RuleMaxBalanceInvariantStorage |||
| └ | setMaxBalance | Public ❗️ | 🛑 | onlyMaxBalanceManager |
| └ | setBalanceToken | Public ❗️ | 🛑 | onlyMaxBalanceManager |
| └ | addExemptAddress | Public ❗️ | 🛑 | onlyMaxBalanceManager |
diff --git a/doc/surya/surya_report/surya_report_CapAccounting.sol.md b/doc/surya/surya_report/surya_report_CapAccounting.sol.md
new file mode 100644
index 00000000..a268e4b8
--- /dev/null
+++ b/doc/surya/surya_report/surya_report_CapAccounting.sol.md
@@ -0,0 +1,28 @@
+## Sūrya's Description Report
+
+### Files Description Table
+
+
+| File Name | SHA-1 Hash |
+|-------------|--------------|
+| ./rules/validation/abstract/core/CapAccounting.sol | 449269c90e1cf8a1bdcd01a8024cd8c8711e534a |
+
+
+### Contracts Description Table
+
+
+| Contract | Type | Bases | | |
+|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
+| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
+||||||
+| **CapAccounting** | Implementation | |||
+| └ | _capExceededBy | Internal 🔒 | | |
+| └ | _capHeadroom | Internal 🔒 | | |
+
+
+### Legend
+
+| Symbol | Meaning |
+|:--------:|-----------|
+| 🛑 | Function can modify state |
+| 💵 | Function is payable |
diff --git a/doc/surya/surya_report/surya_report_ChainlinkPoRFeedManager.sol.md b/doc/surya/surya_report/surya_report_ChainlinkPoRFeedManager.sol.md
index b48f64af..a9bb50e6 100644
--- a/doc/surya/surya_report/surya_report_ChainlinkPoRFeedManager.sol.md
+++ b/doc/surya/surya_report/surya_report_ChainlinkPoRFeedManager.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/core/ChainlinkPoRFeedManager.sol | 4c296843d471b9cc60a86ef8253205dabbc45558 |
+| ./rules/validation/abstract/core/ChainlinkPoRFeedManager.sol | 4d917002775897247442a336203c5436d3f444e3 |
### Contracts Description Table
@@ -15,7 +15,7 @@
|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
||||||
-| **ChainlinkPoRFeedManager** | Implementation | TokenSupplyReader, RuleChainlinkPoRInvariantStorage |||
+| **ChainlinkPoRFeedManager** | Implementation | CapAccounting, TokenSupplyReader, RuleChainlinkPoRInvariantStorage |||
| └ | setReservesFeed | Public ❗️ | 🛑 | onlyChainlinkPoRManager |
| └ | setTokenMetadata | Public ❗️ | 🛑 | onlyChainlinkPoRManager |
| └ | setMaxStalenessSeconds | Public ❗️ | 🛑 | onlyChainlinkPoRManager |
diff --git a/doc/surya/surya_report/surya_report_ERC3643CapHarnesses.sol.md b/doc/surya/surya_report/surya_report_ERC3643CapHarnesses.sol.md
new file mode 100644
index 00000000..a57aad19
--- /dev/null
+++ b/doc/surya/surya_report/surya_report_ERC3643CapHarnesses.sol.md
@@ -0,0 +1,42 @@
+## Sūrya's Description Report
+
+### Files Description Table
+
+
+| File Name | SHA-1 Hash |
+|-------------|--------------|
+| ./mocks/harness/ERC3643CapHarnesses.sol | 1ee6bd8daa637884f5fb5a851f708640f146d380 |
+
+
+### Contracts Description Table
+
+
+| Contract | Type | Bases | | |
+|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
+| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
+||||||
+| **ERC3643MaxTotalSupplyHarness** | Implementation | RuleMaxTotalSupply |||
+| └ | | Public ❗️ | 🛑 | RuleMaxTotalSupply |
+| └ | _detectTransferRestrictionOnNotify | Internal 🔒 | | |
+||||||
+| **ERC3643MaxBalanceHarness** | Implementation | RuleMaxBalance |||
+| └ | | Public ❗️ | 🛑 | RuleMaxBalance |
+| └ | _detectTransferRestrictionOnNotify | Internal 🔒 | | |
+||||||
+| **ERC3643ChainlinkPoRHarness** | Implementation | RuleChainlinkPoR |||
+| └ | | Public ❗️ | 🛑 | RuleChainlinkPoR |
+| └ | _detectTransferRestrictionOnNotify | Internal 🔒 | | |
+||||||
+| **TrackedSupplyHarness** | Implementation | RuleMaxTotalSupply |||
+| └ | | Public ❗️ | 🛑 | RuleMaxTotalSupply |
+| └ | setTrackedSupply | External ❗️ | 🛑 |NO❗️ |
+| └ | _currentSupply | Internal 🔒 | | |
+| └ | _supplyToken | Internal 🔒 | | |
+
+
+### Legend
+
+| Symbol | Meaning |
+|:--------:|-----------|
+| 🛑 | Function can modify state |
+| 💵 | Function is payable |
diff --git a/doc/surya/surya_report/surya_report_ERC3643TokenMock.sol.md b/doc/surya/surya_report/surya_report_ERC3643TokenMock.sol.md
index 65981209..14c3c84d 100644
--- a/doc/surya/surya_report/surya_report_ERC3643TokenMock.sol.md
+++ b/doc/surya/surya_report/surya_report_ERC3643TokenMock.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./mocks/ERC3643TokenMock.sol | 09e3ec529557577b366c9e48ba1be2ed6fda0d4f |
+| ./mocks/ERC3643TokenMock.sol | 1ed75c6e22c5677378835a73273381ce42e7b7aa |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_IAddressList.sol.md b/doc/surya/surya_report/surya_report_IAddressList.sol.md
index 4282f5c6..d0189f7e 100644
--- a/doc/surya/surya_report/surya_report_IAddressList.sol.md
+++ b/doc/surya/surya_report/surya_report_IAddressList.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/interfaces/IAddressList.sol | e043af3e25afec3f5015f668979e5b32cc36f490 |
+| ./rules/interfaces/IAddressList.sol | 2986fa01ee3211e35276e0ef8970a939c1c9f050 |
### Contracts Description Table
@@ -15,14 +15,19 @@
|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
||||||
-| **IAddressList** | Interface | IIdentityRegistryContains |||
+| **IAddressListBatchQuery** | Interface | |||
+| └ | areAddressesListed | External ❗️ | |NO❗️ |
+||||||
+| **IAddressListPolarity** | Interface | |||
+| └ | isAllowList | External ❗️ | |NO❗️ |
+||||||
+| **IAddressList** | Interface | IIdentityRegistryContains, IAddressListBatchQuery |||
| └ | addAddresses | External ❗️ | 🛑 |NO❗️ |
| └ | removeAddresses | External ❗️ | 🛑 |NO❗️ |
| └ | addAddress | External ❗️ | 🛑 |NO❗️ |
| └ | removeAddress | External ❗️ | 🛑 |NO❗️ |
| └ | listedAddressCount | External ❗️ | |NO❗️ |
| └ | isAddressListed | External ❗️ | |NO❗️ |
-| └ | areAddressesListed | External ❗️ | |NO❗️ |
### Legend
diff --git a/doc/surya/surya_report/surya_report_IERC3643ComplianceFull.sol.md b/doc/surya/surya_report/surya_report_IERC3643ComplianceFull.sol.md
index 60e22cd3..335de6fb 100644
--- a/doc/surya/surya_report/surya_report_IERC3643ComplianceFull.sol.md
+++ b/doc/surya/surya_report/surya_report_IERC3643ComplianceFull.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./mocks/IERC3643ComplianceFull.sol | 341ca7a53aeacd897ee359d5e80c1ec7f1fcf6fa |
+| ./mocks/IERC3643ComplianceFull.sol | a636d4fc9ee8540016e54d4e8d43e15d8a3e7302 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_IdentityRegistryDelegationHarness.sol.md b/doc/surya/surya_report/surya_report_IdentityRegistryDelegationHarness.sol.md
new file mode 100644
index 00000000..f5469fc5
--- /dev/null
+++ b/doc/surya/surya_report/surya_report_IdentityRegistryDelegationHarness.sol.md
@@ -0,0 +1,28 @@
+## Sūrya's Description Report
+
+### Files Description Table
+
+
+| File Name | SHA-1 Hash |
+|-------------|--------------|
+| ./mocks/harness/IdentityRegistryDelegationHarness.sol | 07ae2614113053b786bd43f4961c735f2e34ec0c |
+
+
+### Contracts Description Table
+
+
+| Contract | Type | Bases | | |
+|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
+| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
+||||||
+| **IdentityRegistryExtraCheckHarness** | Implementation | RuleIdentityRegistry |||
+| └ | | Public ❗️ | 🛑 | RuleIdentityRegistry |
+| └ | _detectTransferRestriction | Internal 🔒 | | |
+
+
+### Legend
+
+| Symbol | Meaning |
+|:--------:|-----------|
+| 🛑 | Function can modify state |
+| 💵 | Function is payable |
diff --git a/doc/surya/surya_report/surya_report_IdentityRegistryWhitelistBase.sol.md b/doc/surya/surya_report/surya_report_IdentityRegistryWhitelistBase.sol.md
index 84ba3fba..44da9a57 100644
--- a/doc/surya/surya_report/surya_report_IdentityRegistryWhitelistBase.sol.md
+++ b/doc/surya/surya_report/surya_report_IdentityRegistryWhitelistBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./registry/abstract/IdentityRegistryWhitelistBase.sol | 40c79099b5974626719f5083aaa0afe824dd7b1e |
+| ./registry/abstract/IdentityRegistryWhitelistBase.sol | a829964c7d939f9ee5aab8bebf1ae471320606ad |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_RuleAddressSetInternal.sol.md b/doc/surya/surya_report/surya_report_RuleAddressSetInternal.sol.md
index 3438c7ab..fca15c0c 100644
--- a/doc/surya/surya_report/surya_report_RuleAddressSetInternal.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleAddressSetInternal.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol | eddc744d1e6c3c8a68aa267774d77536d986146f |
+| ./rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol | 2432a32f5051d3ca4b0c158ae32a31add67a84db |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_RuleBlacklistBase.sol.md b/doc/surya/surya_report/surya_report_RuleBlacklistBase.sol.md
index 29db00e9..a4b1c887 100644
--- a/doc/surya/surya_report/surya_report_RuleBlacklistBase.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleBlacklistBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/base/RuleBlacklistBase.sol | 8abdb2e4e56f45a1ee470fa4b4951df84280a2d8 |
+| ./rules/validation/abstract/base/RuleBlacklistBase.sol | 164a28f65e9b2baa95956289185ce05ca9ca0137 |
### Contracts Description Table
@@ -15,13 +15,14 @@
|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
||||||
-| **RuleBlacklistBase** | Implementation | RuleAddressSet, RuleNFTAdapter, RuleBlacklistInvariantStorage |||
+| **RuleBlacklistBase** | Implementation | RuleAddressSet, RuleNFTAdapter, RuleBlacklistInvariantStorage, IAddressListPolarity |||
| └ | | Public ❗️ | 🛑 | RuleAddressSet |
| └ | transferred | Public ❗️ | |NO❗️ |
| └ | transferred | Public ❗️ | |NO❗️ |
| └ | canReturnTransferRestrictionCode | Public ❗️ | |NO❗️ |
| └ | messageForTransferRestriction | Public ❗️ | |NO❗️ |
| └ | supportsInterface | Public ❗️ | |NO❗️ |
+| └ | isAllowList | Public ❗️ | |NO❗️ |
| └ | _detectTransferRestriction | Internal 🔒 | | |
| └ | _detectTransferRestrictionFrom | Internal 🔒 | | |
| └ | _transferred | Internal 🔒 | | |
diff --git a/doc/surya/surya_report/surya_report_RuleChainlinkPoRBase.sol.md b/doc/surya/surya_report/surya_report_RuleChainlinkPoRBase.sol.md
index b50ed54e..2b2151b7 100644
--- a/doc/surya/surya_report/surya_report_RuleChainlinkPoRBase.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleChainlinkPoRBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/base/RuleChainlinkPoRBase.sol | a1e200478baa5885cb422102d6800a094cf5abda |
+| ./rules/validation/abstract/base/RuleChainlinkPoRBase.sol | f91ce6934da8dd4f13c8c136d1dea1d549695f45 |
### Contracts Description Table
@@ -23,6 +23,7 @@
| └ | messageForTransferRestriction | Public ❗️ | |NO❗️ |
| └ | _detectTransferRestriction | Internal 🔒 | | |
| └ | _detectTransferRestrictionFrom | Internal 🔒 | | |
+| └ | _detectTransferRestrictionOnNotify | Internal 🔒 | | |
| └ | _transferred | Internal 🔒 | | |
| └ | _transferredFrom | Internal 🔒 | | |
diff --git a/doc/surya/surya_report/surya_report_RuleChainlinkPoRERC3643.sol.md b/doc/surya/surya_report/surya_report_RuleChainlinkPoRERC3643.sol.md
new file mode 100644
index 00000000..c7746975
--- /dev/null
+++ b/doc/surya/surya_report/surya_report_RuleChainlinkPoRERC3643.sol.md
@@ -0,0 +1,28 @@
+## Sūrya's Description Report
+
+### Files Description Table
+
+
+| File Name | SHA-1 Hash |
+|-------------|--------------|
+| ./rules/validation/deployment/RuleChainlinkPoRERC3643.sol | 666fcf4ef0bc5b1e104235514d4a712550bec528 |
+
+
+### Contracts Description Table
+
+
+| Contract | Type | Bases | | |
+|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
+| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
+||||||
+| **RuleChainlinkPoRERC3643** | Implementation | RuleChainlinkPoR |||
+| └ | | Public ❗️ | 🛑 | RuleChainlinkPoR |
+| └ | _detectTransferRestrictionOnNotify | Internal 🔒 | | |
+
+
+### Legend
+
+| Symbol | Meaning |
+|:--------:|-----------|
+| 🛑 | Function can modify state |
+| 💵 | Function is payable |
diff --git a/doc/surya/surya_report/surya_report_RuleChainlinkPoRERC3643Ownable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleChainlinkPoRERC3643Ownable2Step.sol.md
new file mode 100644
index 00000000..c4f8c7e2
--- /dev/null
+++ b/doc/surya/surya_report/surya_report_RuleChainlinkPoRERC3643Ownable2Step.sol.md
@@ -0,0 +1,28 @@
+## Sūrya's Description Report
+
+### Files Description Table
+
+
+| File Name | SHA-1 Hash |
+|-------------|--------------|
+| ./rules/validation/deployment/RuleChainlinkPoRERC3643Ownable2Step.sol | b885da14c378e3fdf5b67dec2d9e00b17a06f042 |
+
+
+### Contracts Description Table
+
+
+| Contract | Type | Bases | | |
+|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
+| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
+||||||
+| **RuleChainlinkPoRERC3643Ownable2Step** | Implementation | RuleChainlinkPoROwnable2Step |||
+| └ | | Public ❗️ | 🛑 | RuleChainlinkPoROwnable2Step |
+| └ | _detectTransferRestrictionOnNotify | Internal 🔒 | | |
+
+
+### Legend
+
+| Symbol | Meaning |
+|:--------:|-----------|
+| 🛑 | Function can modify state |
+| 💵 | Function is payable |
diff --git a/doc/surya/surya_report/surya_report_RuleChainlinkPoRInvariantStorage.sol.md b/doc/surya/surya_report/surya_report_RuleChainlinkPoRInvariantStorage.sol.md
index a7a008ef..4bc074a3 100644
--- a/doc/surya/surya_report/surya_report_RuleChainlinkPoRInvariantStorage.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleChainlinkPoRInvariantStorage.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/invariant/RuleChainlinkPoRInvariantStorage.sol | 64a0b4372644a3b79b325839cdd9897f5c3ad9f0 |
+| ./rules/validation/abstract/invariant/RuleChainlinkPoRInvariantStorage.sol | e8568c6d91ab8630e0ce94d6dc4ca5c436f623ad |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md
index 0c7f3f3c..f4e87047 100644
--- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/operation/RuleConditionalTransferLight.sol | d20cf57626e59b0e16ae7d8ae18d7c7f1c1de7a5 |
+| ./rules/operation/RuleConditionalTransferLight.sol | 4e1e3943d4e53454fa1b24c263871ffd4dc4c0f7 |
### Contracts Description Table
@@ -20,7 +20,7 @@
| └ | supportsInterface | Public ❗️ | |NO❗️ |
| └ | _onlyComplianceManager | Internal 🔒 | | onlyRole |
| └ | _authorizeTransferApproval | Internal 🔒 | | onlyRole |
-| └ | _authorizeComplianceBindingChange | Internal 🔒 | | onlyRole |
+| └ | _authorizeTokenBindingChange | Internal 🔒 | | onlyRole |
### Legend
diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightApprovalBase.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightApprovalBase.sol.md
index d38172c0..0ab34bbf 100644
--- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightApprovalBase.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightApprovalBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol | ac5eac80044e31e99f670ecc63ece95fa1f8326f |
+| ./rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol | 147867677947edf0d0a36476e65fcdad28502e08 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightBase.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightBase.sol.md
index 8fd176df..77f81d7d 100644
--- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightBase.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/operation/abstract/RuleConditionalTransferLightBase.sol | 61bc28f3f3069112e53a7939da7cb4bfeca103f9 |
+| ./rules/operation/abstract/RuleConditionalTransferLightBase.sol | d69293485b40c9f6bd53bb0b7cce2e86938939c2 |
### Contracts Description Table
@@ -23,9 +23,9 @@
| └ | approveAndTransferIfAllowed | Public ❗️ | 🛑 | onlyTransferApprover |
| └ | transferred | Public ❗️ | 🛑 | onlyTransferExecutor |
| └ | transferred | Public ❗️ | 🛑 | onlyTransferExecutor |
-| └ | bindToken | Public ❗️ | 🛑 | onlyComplianceManager |
-| └ | bindRuleEngine | Public ❗️ | 🛑 | onlyComplianceManager |
-| └ | unbindRuleEngine | Public ❗️ | 🛑 | onlyComplianceManager |
+| └ | bindToken | Public ❗️ | 🛑 | onlyTokenBindingManager |
+| └ | bindRuleEngine | Public ❗️ | 🛑 | onlyTokenBindingManager |
+| └ | unbindRuleEngine | Public ❗️ | 🛑 | onlyTokenBindingManager |
| └ | isTransferExecutor | Public ❗️ | |NO❗️ |
| └ | detectTransferRestriction | Public ❗️ | |NO❗️ |
| └ | detectTransferRestrictionFrom | Public ❗️ | |NO❗️ |
diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightInvariantStorage.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightInvariantStorage.sol.md
index 25e75f82..dbd2624c 100644
--- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightInvariantStorage.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightInvariantStorage.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol | b3d88b809eda5e44ef47c6e103a347c0c2ca92f8 |
+| ./rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol | 0b5813c33fbc24b2e09fee9aac35c2efaa87b7c9 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiToken.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiToken.sol.md
index 0e821388..54646157 100644
--- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiToken.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiToken.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/operation/RuleConditionalTransferLightMultiToken.sol | bc1bc27d2f80a0116ae1987cf795ab22032fce60 |
+| ./rules/operation/RuleConditionalTransferLightMultiToken.sol | b79d37d00a665ab19199e41ca8eacaddec678656 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenBase.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenBase.sol.md
index 89ea6ab0..95778c83 100644
--- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenBase.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol | 126be4df25c91c0cace70424f0e0d608b1cf8258 |
+| ./rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol | 920bbe385eb3b6e11472d296798f9a5e259b0a81 |
### Contracts Description Table
@@ -34,7 +34,6 @@
| └ | detectTransferRestrictionFrom | Public ❗️ | |NO❗️ |
| └ | canTransfer | Public ❗️ | |NO❗️ |
| └ | canTransferFrom | Public ❗️ | |NO❗️ |
-| └ | _authorizeComplianceBindingChange | Internal 🔒 | 🛑 | |
| └ | _approveTransfer | Internal 🔒 | 🛑 | |
| └ | _cancelTransferApproval | Internal 🔒 | 🛑 | |
| └ | _transferred | Internal 🔒 | 🛑 | |
diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.md
index 3e19e3be..a72e17b0 100644
--- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol | 69581e98b6b327335f84cefe92a064e0189d9532 |
+| ./rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol | 6e7143e1c1de75bc9c11870f7c9802c23a4628d2 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.md
index db3c2481..0abd2b09 100644
--- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol | e4023e744c8042d7fdc8c117a3c920f14d09e33c |
+| ./rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol | bfac420c101f1b65f8aa912dfa2113d67d071cc9 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightOwnable2Step.sol.md
index f6ead88e..c46981ae 100644
--- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightOwnable2Step.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightOwnable2Step.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/operation/RuleConditionalTransferLightOwnable2Step.sol | 522390c69b70608392e43493c56e72056e45df93 |
+| ./rules/operation/RuleConditionalTransferLightOwnable2Step.sol | 0d55dcd64df3896a3475356a87b823bd24843170 |
### Contracts Description Table
@@ -20,7 +20,7 @@
| └ | supportsInterface | Public ❗️ | |NO❗️ |
| └ | _onlyComplianceManager | Internal 🔒 | | onlyOwner |
| └ | _authorizeTransferApproval | Internal 🔒 | | onlyOwner |
-| └ | _authorizeComplianceBindingChange | Internal 🔒 | | onlyOwner |
+| └ | _authorizeTokenBindingChange | Internal 🔒 | | onlyOwner |
### Legend
diff --git a/doc/surya/surya_report/surya_report_RuleERC2980.sol.md b/doc/surya/surya_report/surya_report_RuleERC2980.sol.md
index 89c0d9dc..9c535fa8 100644
--- a/doc/surya/surya_report/surya_report_RuleERC2980.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleERC2980.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/deployment/RuleERC2980.sol | df7b966dbe91c0ac24efcf6bc7f03cda96f3ed91 |
+| ./rules/validation/deployment/RuleERC2980.sol | 71a7cbdea6d5f4f481ec5fe628343818626dd8d9 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_RuleERC2980Internal.sol.md b/doc/surya/surya_report/surya_report_RuleERC2980Internal.sol.md
index dd9dba73..ad1e2b6b 100644
--- a/doc/surya/surya_report/surya_report_RuleERC2980Internal.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleERC2980Internal.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol | 5566b1b8c48c5f5d89b01d7883f00743bd7bf8f2 |
+| ./rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol | 308b2dd355e02944137733bb93ab247c432f9790 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_RuleIdentityRegistryBase.sol.md b/doc/surya/surya_report/surya_report_RuleIdentityRegistryBase.sol.md
index 59b1c345..c76a4a3d 100644
--- a/doc/surya/surya_report/surya_report_RuleIdentityRegistryBase.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleIdentityRegistryBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/base/RuleIdentityRegistryBase.sol | 2e24eefcee4613fe38e39197faa509d0b69cebbd |
+| ./rules/validation/abstract/base/RuleIdentityRegistryBase.sol | 450bba38258c0d644836710389bb224883a1bc25 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_RuleMaxBalanceBase.sol.md b/doc/surya/surya_report/surya_report_RuleMaxBalanceBase.sol.md
index 678c8198..3a0f61b6 100644
--- a/doc/surya/surya_report/surya_report_RuleMaxBalanceBase.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleMaxBalanceBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/base/RuleMaxBalanceBase.sol | 62f929cdc9564258f602ff2a298f31a304d1c53f |
+| ./rules/validation/abstract/base/RuleMaxBalanceBase.sol | fa72a00a34cec050299ab9f2cd4be2a4ee5030bc |
### Contracts Description Table
@@ -24,6 +24,7 @@
| └ | messageForTransferRestriction | Public ❗️ | |NO❗️ |
| └ | _detectTransferRestriction | Internal 🔒 | | |
| └ | _detectTransferRestrictionFrom | Internal 🔒 | | |
+| └ | _detectTransferRestrictionOnNotify | Internal 🔒 | | |
| └ | _transferred | Internal 🔒 | | |
| └ | _transferredFrom | Internal 🔒 | | |
diff --git a/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyBase.sol.md b/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyBase.sol.md
index 450c634c..2959545a 100644
--- a/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyBase.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol | 645be19fe00a1ab23bdd92491942d97cb31fa383 |
+| ./rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol | dc9656537f33ebce7ffb09435f1620cfc2d0385f |
### Contracts Description Table
@@ -23,6 +23,7 @@
| └ | messageForTransferRestriction | Public ❗️ | |NO❗️ |
| └ | _detectTransferRestriction | Internal 🔒 | | |
| └ | _detectTransferRestrictionFrom | Internal 🔒 | | |
+| └ | _detectTransferRestrictionOnNotify | Internal 🔒 | | |
| └ | _transferred | Internal 🔒 | | |
| └ | _transferredFrom | Internal 🔒 | | |
diff --git a/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyERC3643.sol.md b/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyERC3643.sol.md
new file mode 100644
index 00000000..e66d5506
--- /dev/null
+++ b/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyERC3643.sol.md
@@ -0,0 +1,28 @@
+## Sūrya's Description Report
+
+### Files Description Table
+
+
+| File Name | SHA-1 Hash |
+|-------------|--------------|
+| ./rules/validation/deployment/RuleMaxTotalSupplyERC3643.sol | a7f6bd206a4098f0b1e21426c46d7cc20cdb905f |
+
+
+### Contracts Description Table
+
+
+| Contract | Type | Bases | | |
+|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
+| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
+||||||
+| **RuleMaxTotalSupplyERC3643** | Implementation | RuleMaxTotalSupply |||
+| └ | | Public ❗️ | 🛑 | RuleMaxTotalSupply |
+| └ | _detectTransferRestrictionOnNotify | Internal 🔒 | | |
+
+
+### Legend
+
+| Symbol | Meaning |
+|:--------:|-----------|
+| 🛑 | Function can modify state |
+| 💵 | Function is payable |
diff --git a/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyERC3643Ownable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyERC3643Ownable2Step.sol.md
new file mode 100644
index 00000000..26ec81ac
--- /dev/null
+++ b/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyERC3643Ownable2Step.sol.md
@@ -0,0 +1,28 @@
+## Sūrya's Description Report
+
+### Files Description Table
+
+
+| File Name | SHA-1 Hash |
+|-------------|--------------|
+| ./rules/validation/deployment/RuleMaxTotalSupplyERC3643Ownable2Step.sol | d7ed63426b3a13b1dd0a25b23af6b72bdd56d84a |
+
+
+### Contracts Description Table
+
+
+| Contract | Type | Bases | | |
+|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
+| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
+||||||
+| **RuleMaxTotalSupplyERC3643Ownable2Step** | Implementation | RuleMaxTotalSupplyOwnable2Step |||
+| └ | | Public ❗️ | 🛑 | RuleMaxTotalSupplyOwnable2Step |
+| └ | _detectTransferRestrictionOnNotify | Internal 🔒 | | |
+
+
+### Legend
+
+| Symbol | Meaning |
+|:--------:|-----------|
+| 🛑 | Function can modify state |
+| 💵 | Function is payable |
diff --git a/doc/surya/surya_report/surya_report_RuleMintAllowance.sol.md b/doc/surya/surya_report/surya_report_RuleMintAllowance.sol.md
index 707e831d..451f0e02 100644
--- a/doc/surya/surya_report/surya_report_RuleMintAllowance.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleMintAllowance.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/operation/RuleMintAllowance.sol | e2cf3135bdb23ade767c610bd125d6d39706a617 |
+| ./rules/operation/RuleMintAllowance.sol | 5dffbe6960ee9eb1154007dc3f33dba2ae4601e0 |
### Contracts Description Table
@@ -20,7 +20,7 @@
| └ | supportsInterface | Public ❗️ | |NO❗️ |
| └ | _onlyComplianceManager | Internal 🔒 | | onlyRole |
| └ | _authorizeSetMintAllowance | Internal 🔒 | | onlyRole |
-| └ | _authorizeComplianceBindingChange | Internal 🔒 | | onlyRole |
+| └ | _authorizeTokenBindingChange | Internal 🔒 | | onlyRole |
### Legend
diff --git a/doc/surya/surya_report/surya_report_RuleMintAllowanceBase.sol.md b/doc/surya/surya_report/surya_report_RuleMintAllowanceBase.sol.md
index d8b8f731..3dfa95ff 100644
--- a/doc/surya/surya_report/surya_report_RuleMintAllowanceBase.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleMintAllowanceBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/operation/abstract/RuleMintAllowanceBase.sol | c702b5250dc5dea9ee0a34fb1644def4b6a1385b |
+| ./rules/operation/abstract/RuleMintAllowanceBase.sol | 579db19d0f10e2d0f5e6df95220dc1fd4d3fff42 |
### Contracts Description Table
@@ -23,7 +23,7 @@
| └ | increaseMintAllowance | Public ❗️ | 🛑 | onlyAllowanceOperator |
| └ | decreaseMintAllowance | Public ❗️ | 🛑 | onlyAllowanceOperator |
| └ | clearMintAllowances | Public ❗️ | 🛑 | onlyAllowanceOperator |
-| └ | bindToken | Public ❗️ | 🛑 | onlyComplianceManager |
+| └ | bindToken | Public ❗️ | 🛑 | onlyTokenBindingManager |
| └ | transferred | Public ❗️ | 🛑 | onlyBoundToken |
| └ | transferred | Public ❗️ | 🛑 | onlyBoundToken |
| └ | messageForTransferRestriction | Public ❗️ | |NO❗️ |
diff --git a/doc/surya/surya_report/surya_report_RuleMintAllowanceOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleMintAllowanceOwnable2Step.sol.md
index d15ab5d1..04e89c67 100644
--- a/doc/surya/surya_report/surya_report_RuleMintAllowanceOwnable2Step.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleMintAllowanceOwnable2Step.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/operation/RuleMintAllowanceOwnable2Step.sol | 47d79e70bd08f2719e55e4f27c356b494e25196f |
+| ./rules/operation/RuleMintAllowanceOwnable2Step.sol | 61ec71bd1ee13b2711aea2345717825ddbe0e934 |
### Contracts Description Table
@@ -20,7 +20,7 @@
| └ | supportsInterface | Public ❗️ | |NO❗️ |
| └ | _onlyComplianceManager | Internal 🔒 | | onlyOwner |
| └ | _authorizeSetMintAllowance | Internal 🔒 | | onlyOwner |
-| └ | _authorizeComplianceBindingChange | Internal 🔒 | | onlyOwner |
+| └ | _authorizeTokenBindingChange | Internal 🔒 | | onlyOwner |
### Legend
diff --git a/doc/surya/surya_report/surya_report_RuleNFTAdapter.sol.md b/doc/surya/surya_report/surya_report_RuleNFTAdapter.sol.md
index 87cce742..7d673582 100644
--- a/doc/surya/surya_report/surya_report_RuleNFTAdapter.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleNFTAdapter.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/core/RuleNFTAdapter.sol | 77a5fb86eed3006ca5020e7d6f223758c8b309a1 |
+| ./rules/validation/abstract/core/RuleNFTAdapter.sol | d70064b1ef2bff9e602f871259cd0f17ee44dd5d |
### Contracts Description Table
@@ -24,6 +24,7 @@
| └ | detectTransferRestrictionFrom | Public ❗️ | |NO❗️ |
| └ | canTransfer | Public ❗️ | |NO❗️ |
| └ | canTransferFrom | Public ❗️ | |NO❗️ |
+| └ | _isDelegated | Internal 🔒 | | |
| └ | _transferred | Internal 🔒 | 🛑 | |
| └ | _transferredFrom | Internal 🔒 | 🛑 | |
diff --git a/doc/surya/surya_report/surya_report_RuleReceiverWhitelistBase.sol.md b/doc/surya/surya_report/surya_report_RuleReceiverWhitelistBase.sol.md
index 2e2a285a..a5018c38 100644
--- a/doc/surya/surya_report/surya_report_RuleReceiverWhitelistBase.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleReceiverWhitelistBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/base/RuleReceiverWhitelistBase.sol | d3b77283cf3de8426036be4d0a61e86ae0d1c3c8 |
+| ./rules/validation/abstract/base/RuleReceiverWhitelistBase.sol | 5098d370b3b933847b4e24763466a27a030b26c9 |
### Contracts Description Table
@@ -15,13 +15,14 @@
|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
||||||
-| **RuleReceiverWhitelistBase** | Implementation | RuleAddressSet, RuleNFTAdapter, RuleReceiverWhitelistInvariantStorage |||
+| **RuleReceiverWhitelistBase** | Implementation | RuleAddressSet, RuleNFTAdapter, RuleReceiverWhitelistInvariantStorage, IAddressListPolarity |||
| └ | | Public ❗️ | 🛑 | RuleAddressSet |
| └ | canReturnTransferRestrictionCode | External ❗️ | |NO❗️ |
| └ | transferred | Public ❗️ | |NO❗️ |
| └ | transferred | Public ❗️ | |NO❗️ |
| └ | messageForTransferRestriction | Public ❗️ | |NO❗️ |
| └ | supportsInterface | Public ❗️ | |NO❗️ |
+| └ | isAllowList | Public ❗️ | |NO❗️ |
| └ | _detectTransferRestriction | Internal 🔒 | | |
| └ | _detectTransferRestrictionFrom | Internal 🔒 | | |
| └ | _transferred | Internal 🔒 | | |
diff --git a/doc/surya/surya_report/surya_report_RuleSpenderWhitelistBase.sol.md b/doc/surya/surya_report/surya_report_RuleSpenderWhitelistBase.sol.md
index ce8c5c5a..eea4942e 100644
--- a/doc/surya/surya_report/surya_report_RuleSpenderWhitelistBase.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleSpenderWhitelistBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/base/RuleSpenderWhitelistBase.sol | f98bec5c483d2dc831f4b751f4a4d72a48cddf84 |
+| ./rules/validation/abstract/base/RuleSpenderWhitelistBase.sol | 595702e7fc83aa8743a72a50d1660369cadd6349 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_RuleWhitelistBase.sol.md b/doc/surya/surya_report/surya_report_RuleWhitelistBase.sol.md
index 05bc0295..5f17b19c 100644
--- a/doc/surya/surya_report/surya_report_RuleWhitelistBase.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleWhitelistBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/base/RuleWhitelistBase.sol | 7cb62bf29323cbf092a1f0787d6b7cf6e929d41b |
+| ./rules/validation/abstract/base/RuleWhitelistBase.sol | d5a143b73b371609a2db4d71b61e960d3dd62845 |
### Contracts Description Table
@@ -15,10 +15,11 @@
|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
||||||
-| **RuleWhitelistBase** | Implementation | RuleAddressSet, RuleWhitelistShared, IIdentityRegistryVerified |||
+| **RuleWhitelistBase** | Implementation | RuleAddressSet, RuleWhitelistShared, IIdentityRegistryVerified, IAddressListPolarity |||
| └ | | Public ❗️ | 🛑 | RuleAddressSet |
| └ | isVerified | Public ❗️ | |NO❗️ |
| └ | supportsInterface | Public ❗️ | |NO❗️ |
+| └ | isAllowList | Public ❗️ | |NO❗️ |
| └ | _detectTransferRestriction | Internal 🔒 | | |
| └ | _detectTransferRestrictionFrom | Internal 🔒 | | |
diff --git a/doc/surya/surya_report/surya_report_RuleWhitelistInvariantStorage.sol.md b/doc/surya/surya_report/surya_report_RuleWhitelistInvariantStorage.sol.md
index 5de6dae4..d84d0bb6 100644
--- a/doc/surya/surya_report/surya_report_RuleWhitelistInvariantStorage.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleWhitelistInvariantStorage.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol | 15e5c48a6b853a46f25131198e0415fe0ff91524 |
+| ./rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol | 7471b0561bbee8918921dc9a35f524334e9064d8 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_RuleWhitelistWrapperBase.sol.md b/doc/surya/surya_report/surya_report_RuleWhitelistWrapperBase.sol.md
index d96bc2a5..64ea41b6 100644
--- a/doc/surya/surya_report/surya_report_RuleWhitelistWrapperBase.sol.md
+++ b/doc/surya/surya_report/surya_report_RuleWhitelistWrapperBase.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/base/RuleWhitelistWrapperBase.sol | bb3b6454d4316c7a7bcc82e6e599ffc45bacdd7a |
+| ./rules/validation/abstract/base/RuleWhitelistWrapperBase.sol | 7676e74e58ea3b9b2b56ebefd897a434f79735c6 |
### Contracts Description Table
@@ -24,6 +24,7 @@
| └ | _detectTransferRestrictionFrom | Internal 🔒 | | |
| └ | _transferred | Internal 🔒 | | |
| └ | _transferred | Internal 🔒 | | |
+| └ | _checkRule | Internal 🔒 | | |
| └ | _detectTransferRestrictionForTargets | Internal 🔒 | | |
| └ | _msgSender | Internal 🔒 | | |
| └ | _msgData | Internal 🔒 | | |
diff --git a/doc/surya/surya_report/surya_report_TokenSupplyReader.sol.md b/doc/surya/surya_report/surya_report_TokenSupplyReader.sol.md
index 52bfbde4..f385c3b6 100644
--- a/doc/surya/surya_report/surya_report_TokenSupplyReader.sol.md
+++ b/doc/surya/surya_report/surya_report_TokenSupplyReader.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/core/TokenSupplyReader.sol | d7faee1c8cfbc1c31fb97f65823c1c4648b3d793 |
+| ./rules/validation/abstract/core/TokenSupplyReader.sol | f7d1c2a381bcba1a7224165f1298b98984ebbf60 |
### Contracts Description Table
diff --git a/doc/surya/surya_report/surya_report_TotalSupplyCapManager.sol.md b/doc/surya/surya_report/surya_report_TotalSupplyCapManager.sol.md
index 6984c286..e2940dc9 100644
--- a/doc/surya/surya_report/surya_report_TotalSupplyCapManager.sol.md
+++ b/doc/surya/surya_report/surya_report_TotalSupplyCapManager.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./rules/validation/abstract/core/TotalSupplyCapManager.sol | 2ccec17348c8be7669d4241186fb870cc4069d8a |
+| ./rules/validation/abstract/core/TotalSupplyCapManager.sol | 4b3c48fb12e49a65c1a2ddb3998a5fd7d2cb3b37 |
### Contracts Description Table
@@ -15,7 +15,7 @@
|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
||||||
-| **TotalSupplyCapManager** | Implementation | TokenSupplyReader, RuleMaxTotalSupplyInvariantStorage |||
+| **TotalSupplyCapManager** | Implementation | CapAccounting, TokenSupplyReader, RuleMaxTotalSupplyInvariantStorage |||
| └ | setMaxTotalSupply | Public ❗️ | 🛑 | onlyMaxTotalSupplyManager |
| └ | setTokenContract | Public ❗️ | 🛑 | onlyMaxTotalSupplyManager |
| └ | _setMaxTotalSupply | Internal 🔒 | 🛑 | |
diff --git a/doc/surya/surya_report/surya_report_VersionModule.sol.md b/doc/surya/surya_report/surya_report_VersionModule.sol.md
index aeebbe66..cfc20703 100644
--- a/doc/surya/surya_report/surya_report_VersionModule.sol.md
+++ b/doc/surya/surya_report/surya_report_VersionModule.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./modules/VersionModule.sol | 41780d1380a0071906b292cf236c8b07f81d941d |
+| ./modules/VersionModule.sol | e1382bf6da9625f05c2395a7bf767c61a13fe04a |
### Contracts Description Table
diff --git a/doc/technical/contracts/RuleChainlinkPoR.md b/doc/technical/contracts/RuleChainlinkPoR.md
index c4e6156b..49e13f06 100644
--- a/doc/technical/contracts/RuleChainlinkPoR.md
+++ b/doc/technical/contracts/RuleChainlinkPoR.md
@@ -124,7 +124,7 @@ Setting the threshold to `0` disables the check, so the rule then accepts reserv
| --- | --- | --- |
| `CODE_RESERVES_EXCEEDED` | 75 | `totalSupply + value` would exceed the backed supply |
| `CODE_RESERVES_FEED_STALE` | 76 | The feed has not been updated within `maxStalenessSeconds` |
-| `CODE_RESERVES_ANSWER_INVALID` | 77 | A round **was** returned but cannot be used: a negative reserve, or an incomplete round (`updatedAt == 0`) |
+| `CODE_RESERVES_ANSWER_INVALID` | 77 | A round **was** returned but cannot be used: a negative reserve, an incomplete round (`updatedAt == 0`), or a round stamped in the future (`updatedAt > block.timestamp`) |
| `CODE_RESERVES_FEED_UNAVAILABLE` | 79 | **No usable response** could be obtained: `decimals()` or `latestRoundData()` reverted, or the feed reports more than `MAX_FEED_DECIMALS` |
| `CODE_TOTAL_SUPPLY_UNAVAILABLE` | 78 | `tokenContract.totalSupply()` reverted, or the token has lost its code |
@@ -169,8 +169,8 @@ For a mint (`from == address(0)`):
1. Read `decimals()` and then `latestRoundData()` from `reservesFeed`.
2. Reject with `CODE_RESERVES_FEED_UNAVAILABLE` if either call reverts or the feed reports more than `MAX_FEED_DECIMALS`: there is no answer to judge.
-3. Reject with `CODE_RESERVES_ANSWER_INVALID` if a round was returned but `answer < 0` or `updatedAt == 0`.
-4. Reject with `CODE_RESERVES_FEED_STALE` if `maxStalenessSeconds != 0` and `block.timestamp - updatedAt > maxStalenessSeconds`.
+3. Reject with `CODE_RESERVES_ANSWER_INVALID` if a round was returned but `answer < 0`, `updatedAt == 0`, or `updatedAt > block.timestamp`. A future-dated round is a **malformed answer, not a stale one**, so it is rejected even when `maxStalenessSeconds == 0`.
+4. Reject with `CODE_RESERVES_FEED_STALE` if `maxStalenessSeconds != 0` and `block.timestamp - updatedAt > maxStalenessSeconds`. The subtraction cannot underflow: step 3 has already established `updatedAt <= block.timestamp`.
5. Scale the answer from the feed's live decimals to `tokenDecimals` to obtain `backedSupply`.
6. Read `tokenContract.totalSupply()`; reject with `CODE_TOTAL_SUPPLY_UNAVAILABLE` if it reverts or the token has lost its code.
7. Reject with `CODE_RESERVES_EXCEEDED` if `totalSupply + value > backedSupply`.
@@ -197,7 +197,7 @@ with data, and a view cannot emit an event.
| Code | Meaning | What an operator checks |
| --- | --- | --- |
| `79` | The feed could not be read at all | Feed liveness; is the configured address a compatible `AggregatorV3Interface`? |
-| `77` | A round came back and its contents are unusable | Is this really a Proof of Reserve feed (a price feed can legitimately go negative)? Or wait for the round to complete. |
+| `77` | A round came back and its contents are unusable | Is this really a Proof of Reserve feed (a price feed can legitimately go negative)? Wait for the round to complete, or — for a future-dated `updatedAt` — treat the aggregator as compromised and repoint the feed. |
`80` is left reserved. Splitting `79` further into "reverted" versus "decimals out of range" was considered and
rejected: both mean the configured feed cannot be used, so the remedy is the same.
@@ -268,7 +268,7 @@ The decisive difference is **how a rejection is signalled**. `SecureMintPolicy.r
| Feed decimals bound | Unbounded (`uint8`) | `<= MAX_FEED_DECIMALS` (36), checked at configuration **and** at read time |
| Feed call reverts (`decimals` or `latestRoundData`) | Propagates — mint reverts | `try/catch` → code `77` |
| Incomplete round (`updatedAt == 0`) | Not checked | Code `77` |
-| Staleness arithmetic | `block.timestamp - updatedAt` — underflow-panics on a future timestamp | Guarded with `block.timestamp > updatedAt` |
+| Future-dated round (`updatedAt > block.timestamp`) | Not checked; `block.timestamp - updatedAt` underflow-panics the whole call | Code `77`, unconditionally — not gated on `maxStalenessSeconds` |
| Token decimals accepted | `1` to `18` | `0` to `18` (CMTAT equity tokens report 0) |
| Reserve margin | 5 modes (percentage / absolute, positive / negative) | None — limit equals reserves exactly |
| Scale-up overflow | Checked arithmetic → revert | Saturates at `type(uint256).max` |
@@ -284,7 +284,7 @@ The decisive difference is **how a rejection is signalled**. `SecureMintPolicy.r
### Where this rule is stricter
- **Feed failures degrade to a code, not a revert.** A feed with no code, a reverting `latestRoundData()`, a negative answer or an incomplete round all yield code `77`. ACE has no `updatedAt == 0` check at all, so with `maxStalenessSeconds == 0` an incomplete round is accepted at face value.
-- **No underflow on a future `updatedAt`.** ACE computes `block.timestamp - updatedAt` unguarded; a feed reporting a timestamp ahead of the block panics the whole call. Fail-closed for ACE, but a panic rather than a clean rejection.
+- **A future `updatedAt` is rejected, not merely survived.** ACE computes `block.timestamp - updatedAt` unguarded, so a feed reporting a timestamp ahead of the block panics the whole call — fail-closed, but as a panic rather than a clean rejection. This rule returns code `77`, and does so **regardless of `maxStalenessSeconds`**: a timestamp no aggregator on this chain could have written is a malformed answer, and an operator who disables freshness checking must not thereby accept forged timestamps. (Nethermind AuditAgent NM-10; before the fix the underflow guard `block.timestamp > updatedAt` silently accepted any future stamp, so a feed frozen on an old reserve answer could keep authorising mints until that timestamp elapsed.)
- **Feed decimals are bounded at configuration time**, so the scaling exponent can never overflow. ACE accepts any `uint8`, where a feed reporting e.g. 78 decimals makes `10 ** 78` revert on every mint.
- **`0`-decimals tokens are supported.** ACE requires `decimals > 0`, which excludes CMTAT equity tokens outright.
diff --git a/doc/technical/contracts/RuleChainlinkPoRERC3643.md b/doc/technical/contracts/RuleChainlinkPoRERC3643.md
new file mode 100644
index 00000000..cc86351a
--- /dev/null
+++ b/doc/technical/contracts/RuleChainlinkPoRERC3643.md
@@ -0,0 +1,155 @@
+# Rule Chainlink PoR — ERC-3643 variant
+
+> ⚠️ **For ERC-3643 tokens only.** Use plain [`RuleChainlinkPoR`](./RuleChainlinkPoR.md) with CMTAT.
+> The two are not interchangeable, and choosing the wrong one **silently** mis-caps issuance in one
+> direction or the other. Nothing reverts at deployment to tell you.
+
+`RuleChainlinkPoRERC3643` and `RuleChainlinkPoRERC3643Ownable2Step` cap minting at the reserves
+reported by a Chainlink Proof of Reserve feed, exactly like the stock rule. The reserve logic,
+restriction codes (75–79), configuration, roles and events are **identical and inherited**. The only
+difference is *when the token is assumed to report the mint*.
+
+## Why a separate variant: compliance is called AFTER the transfer
+
+A compliance rule that caps a supply has to know whether the figure it reads already includes the
+amount being moved. The two token families answer differently.
+
+| Token | Order on a mint | `totalSupply()` when the rule is notified | Use |
+|---|---|---|---|
+| **CMTAT** | rule first, then the mint | **excludes** the new tokens | [`RuleChainlinkPoR`](./RuleChainlinkPoR.md) |
+| **ERC-3643 / T-REX** | mint first, then `created` | **includes** the new tokens | `RuleChainlinkPoRERC3643` |
+
+ERC-3643's `Token.mint` is explicit about it — and note it consults compliance **twice**, on either
+side of the state change:
+
+```solidity
+function mint(address _to, uint256 _amount) public override onlyAgent {
+ // ...
+ require(_tokenCompliance.canTransfer(address(0), _to, _amount), ComplianceNotFollowed());
+ _mint(_to, _amount); // <-- supply changes here
+ _tokenCompliance.created(_to, _amount); // <-- rule notified afterwards
+}
+```
+
+**ERC-3643 signals a mint with `created`, not `transferred`.** `RuleEngine` implements the full
+`ICompliance` surface and forwards `created(to, value)` to each rule as the three-argument
+`transferred(address(0), to, value)`, which is the shape every rule in this library already gates on
+(`from == address(0)`). No rule-side change is needed for that; what changes is the accounting.
+
+### What each variant does with it
+
+Only the **write** path is re-phased. The variant overrides one hook:
+
+```solidity
+function _detectTransferRestrictionOnNotify(address from, address to, uint256 /* value */)
+ internal view override returns (uint8)
+{
+ return _detectTransferRestriction(from, to, 0); // the supply already includes the mint
+}
+```
+
+The **read** path is deliberately untouched: `detectTransferRestriction`, `canTransfer` and
+`maxBackedSupply` still project the pending amount, because a pre-flight query always runs *before*
+the movement — as the `require(... canTransfer ...)` line above shows, the ERC-3643 token depends on
+it. Re-phasing the views too would make the pre-flight answer disagree with enforcement.
+
+The two consultations therefore reduce to the same condition, which is the property that makes the
+variant correct: `canTransfer` asks `supply + amount <= reserves` before the mint, and `created` asks
+`supply' <= reserves` after it, where `supply' == supply + amount`.
+
+### What goes wrong with the wrong variant
+
+| Deployment | Effect |
+|---|---|
+| Stock rule on an **ERC-3643** token | The amount is counted twice. `canTransfer` accepts the mint, the token mints, then `created` rejects it and the whole transaction reverts — **fully backed mints fail**. The largest single mint from an empty supply is halved to `reserves / 2`. It is not a uniform halving: a series of small mints can still creep up to the full reserves, so the failure looks intermittent and depends on how issuance is chunked. |
+| This variant on a **CMTAT** token | The pending amount is ignored on enforcement. The pre-flight view still blocks an over-reserve mint, but the write hook would no longer stop one that slipped past — **the backing guarantee is weakened**. |
+
+## Deployment
+
+Constructors match the stock rule exactly.
+
+```solidity
+new RuleChainlinkPoRERC3643(
+ admin, // DEFAULT_ADMIN_ROLE
+ tokenContract, // the ERC-3643 token; must expose totalSupply()
+ tokenDecimals, // 0–18, checked against decimals() when the token exposes it
+ reservesFeed, // AggregatorV3Interface
+ maxStalenessSeconds // 0 disables the staleness check
+);
+```
+
+Wire it as a rule inside a `RuleEngine` occupying the token's **compliance** slot:
+
+```
+ERC-3643 Token ── compliance ──▶ RuleEngine ──▶ RuleChainlinkPoRERC3643
+```
+
+Use `RuleEngine`, not a bare rule: ERC-3643 drives mint and burn through `created` / `destroyed`,
+which the validation rules do not implement. `RuleEngine` implements the full `ICompliance` surface
+and forwards them.
+
+`RuleChainlinkPoRERC3643Ownable2Step` is the same contract under `Ownable2Step` instead of
+`AccessControl`.
+
+### ⚠️ Deployment order: build the rule AFTER `Token.init`
+
+ERC-3643 deploys the token and initialises it in two steps, and **an uninitialised `Token` reports
+`decimals() == 0`**. The rule's constructor probes `decimals()` and accepts a matching value, so a
+rule constructed before `init` is configured for a 0-decimals token — and `init(..., 18, ...)` then
+makes it an 18-decimals token while the rule still believes 0.
+
+Nothing reverts and no event marks it. The reserve answer is simply scaled by `10 ** 18` too little
+and every mint is refused; the same mistake with the decimals reversed would authorise **unbacked
+minting** instead. The constructor probe cannot catch this — it genuinely succeeded at the time.
+
+- **Construct the rule after `token.init(...)`**, or
+- call `setTokenMetadata(token, decimals)` once the token is initialised to re-sync.
+
+In a `TREXFactory.deployTREXSuite` flow the token address only exists after the factory call anyway,
+so the natural order is: deploy the `RuleEngine`, deploy the suite with it as compliance, then deploy
+the rule against the finished token and `engine.addRule(...)`.
+
+Pinned by `testRuleBuiltBeforeInitCachesTheWrongDecimals`.
+
+### On `CODE_TOTAL_SUPPLY_UNAVAILABLE` (78)
+
+`Token.totalSupply()` is `external view { return _totalSupply; }` — no modifier, no external call —
+so it cannot revert, and code 78 is unreachable against a **directly deployed** ERC-3643 token. The
+guarded read is still not dead weight: the standard T-REX deployment puts the token behind a
+`TokenProxy` resolving its implementation through an `ImplementationAuthority`, and a proxy repointed
+at a broken implementation *can* make the call revert. The rule then returns 78 and blocks minting
+instead of breaking the MUST-NOT-revert views. Pinned by
+`testSupplyIsAlwaysReadableOnADirectlyDeployedToken`.
+
+## Behaviour inherited unchanged
+
+- **Mints only.** Transfers and burns always pass, including while the feed is stale, broken or
+ reporting zero — a lapsed feed must never trap holders in their position.
+- **Restriction codes** 75 (reserves exceeded), 76 (feed stale), 77 (answer unusable, including a
+ future-dated round), 78 (total supply unavailable), 79 (feed unreadable).
+- **Live feed decimals**, never cached; `maxBackedSupply()` previews the ceiling; the read path never
+ reverts. See [`RuleChainlinkPoR`](./RuleChainlinkPoR.md) for the full treatment.
+- **One token per instance.** The rule reads `totalSupply()` from its configured `tokenContract`,
+ never from the token that triggered the check, and cannot learn that identity behind a RuleEngine.
+ Do not add one instance to two engines.
+
+## Tests
+
+`test/ERC3643Real/ERC3643RealTokenChainlinkPoR.t.sol` drives the **genuine** vendored
+`lib/ERC-3643/` token (4.2.0-beta1) — not a mock — through this rule: mints up to the reserves,
+rejection past them, incremental issuance against a shared ceiling, a raised feed answer raising the
+ceiling, transfers and burns staying open while reserves are zero, and a stale feed halting issuance
+without trapping holders. Two tests pin the stock rule's failure on the same token so the reason this
+variant exists stays executable.
+
+Run it with the dedicated profile, which `forge test` alone does **not** include:
+
+```bash
+FOUNDRY_PROFILE=erc3643 forge test
+```
+
+## See also
+
+- [`RuleChainlinkPoR`](./RuleChainlinkPoR.md) — the CMTAT rule and the full PoR reference
+- [`RULE_SEMANTICS.md` §5](../guides/RULE_SEMANTICS.md) — the two seams the cap rules expose, and why
+ a tracked-supply rule is a different design
diff --git a/doc/technical/contracts/RuleConditionalTransferLight.md b/doc/technical/contracts/RuleConditionalTransferLight.md
index b6da8586..bb33507e 100644
--- a/doc/technical/contracts/RuleConditionalTransferLight.md
+++ b/doc/technical/contracts/RuleConditionalTransferLight.md
@@ -65,6 +65,44 @@ Approves the transfer and immediately calls `SafeERC20.safeTransferFrom` on the
Works in **both** topologies, provided the bindings are set correctly — see [Binding: token vs RuleEngine](#binding-token-vs-ruleengine).
+#### It requires a token that calls back, and now checks that it did
+
+The helper **inverts checks-effects-interactions on purpose**: it records the approval *before*
+`safeTransferFrom`, so the approval exists while the token runs its compliance callback into this rule and the
+callback can consume it. That is only correct if the callback actually arrives.
+
+It ends with a post-condition:
+
+```solidity
+uint256 approvalsBefore = approvedCount(from, to, value);
+approveTransfer(from, to, value);
+...
+IERC20(token).safeTransferFrom(from, to, value);
+require(
+ approvedCount(from, to, value) == approvalsBefore,
+ RuleConditionalTransferLight_ApprovalNotConsumed(token, from, to, value)
+);
+```
+
+If the count did not come back down, no callback reached the rule — a plain ERC-20 bound with `bindToken`, or a
+RuleEngine never bound or since unbound with `unbindRuleEngine`. Before this check the transfer **succeeded** and
+left the approval standing, indistinguishable from an operator-created one and enough to authorise a later,
+never-approved transfer of exactly `(from, to, value)`. The only remedy was for the operator to notice the
+leftover count and call `resetApproval` (Nethermind AuditAgent `NM-17`).
+
+Points worth knowing:
+
+- It compares against the count **before** the helper ran, not against zero, so an operator's own outstanding
+ approvals for the same tuple survive untouched.
+- Reading state *after* the external call is deliberate. A hostile token can only make the check **fail**, never
+ pass spuriously; a path that consumed more than one approval also fails, which is the direction you want.
+- It is a **behaviour change** for a deployment that ran the helper against a non-callback token: that call now
+ reverts instead of completing. That is the point — the transfer was leaving a compliance hole behind.
+- Cost: two warm `SLOAD`s on an operator-only path.
+
+Pinned by `testRevertsWhenTheTokenDoesNotCallBack`, `testPreExistingApprovalsSurviveTheHelper` and
+`testDirectBindingFlowStillConsumesExactlyOne`.
+
### `approvedCount(address from, address to, uint256 value) → uint256`
Returns the current approval count for the `(from, to, value)` tuple.
diff --git a/doc/technical/contracts/RuleConditionalTransferLightMultiToken.md b/doc/technical/contracts/RuleConditionalTransferLightMultiToken.md
index 9798d0eb..82f9365b 100644
--- a/doc/technical/contracts/RuleConditionalTransferLightMultiToken.md
+++ b/doc/technical/contracts/RuleConditionalTransferLightMultiToken.md
@@ -109,6 +109,24 @@ Returns the remaining count for a specific token key.
Approves and executes `safeTransferFrom` on the specified token, requiring allowance for this rule as spender.
+Like its single-token twin, the helper **inverts checks-effects-interactions on purpose** — the approval is
+recorded *before* `safeTransferFrom` so the token's compliance callback can consume it — and it now ends with a
+post-condition asserting the approval was in fact consumed:
+
+```solidity
+require(
+ approvedCount(token, from, to, value) == approvalsBefore,
+ RuleConditionalTransferLightMultiToken_ApprovalNotConsumed(token, from, to, value)
+);
+```
+
+A count that did not come back down means no callback reached the rule — the token is not bound directly, or is
+a plain ERC-20 that notifies nobody. Before this check the transfer completed and left a spendable approval for
+`(token, from, to, value)` behind, enough to authorise a later never-approved transfer of that exact tuple
+(Nethermind AuditAgent `NM-17`). The comparison is against the count **before** the helper ran, so an operator's
+own outstanding approvals are untouched. Pinned by
+`testApproveAndTransferRevertsWhenTheTokenDoesNotCallBack`.
+
### `transferred(...)`
Only bound tokens can call transfer execution hooks. Approval consumption uses the **caller** (`msg.sender`) as the token key, which is why the rule must be bound directly to each token. See [Deployment topology](#deployment-topology--why-a-ruleengine-does-not-work).
diff --git a/doc/technical/contracts/RuleIdentityRegistry.md b/doc/technical/contracts/RuleIdentityRegistry.md
index 33f05a58..bbae2396 100644
--- a/doc/technical/contracts/RuleIdentityRegistry.md
+++ b/doc/technical/contracts/RuleIdentityRegistry.md
@@ -105,6 +105,19 @@ Returns the current identity registry address. Returns `address(0)` if none is s
This is what lets an **unverified minter** mint to a verified recipient, exactly as ERC-3643 requires
(*"`mint` … only require[s] the receiver to be whitelisted and verified"*).
+### Note for subclasses: the two hooks cannot diverge
+
+`_detectTransferRestrictionFrom` screens the spender and then **always delegates** to
+`_detectTransferRestriction`, including when no registry is set and when the transfer is a burn. Those two cases
+resolve to `TRANSFER_OK` inside the delegate, so the answer is unchanged — but the delegation is what guarantees
+that a subclass overriding **only** `_detectTransferRestriction`, the natural hook for adding a check, has that
+check honoured on `transferFrom` and `burnFrom` as well as on `transfer`.
+
+Until `v0.6.0` the function returned `TRANSFER_OK` directly in those two cases, so such a subclass silently
+screened one entrypoint and not the other. `RuleSanctionsListBase` carries the same guarantee for the same
+reason. If you extend either rule, override `_detectTransferRestriction` and leave the delegation intact; the
+behaviour is pinned by `test/RuleIdentityRegistry/RuleIdentityRegistryDelegation.t.sol`.
+
## Usage scenario
The operator deploys `RuleIdentityRegistry` and calls `setIdentityRegistry(registry)`. The registry is maintained by a compliance provider who verifies investor identities. When Alice (unverified) attempts to receive tokens, `isVerified(alice)` returns `false` and the transfer is rejected with code 56. After the registry marks Alice as verified, the transfer succeeds. Calling `clearIdentityRegistry()` disables checks entirely.
diff --git a/doc/technical/contracts/RuleMaxTotalSupplyERC3643.md b/doc/technical/contracts/RuleMaxTotalSupplyERC3643.md
new file mode 100644
index 00000000..e9c2fa98
--- /dev/null
+++ b/doc/technical/contracts/RuleMaxTotalSupplyERC3643.md
@@ -0,0 +1,130 @@
+# Rule Max Total Supply — ERC-3643 variant
+
+> ⚠️ **For ERC-3643 tokens only.** Use plain [`RuleMaxTotalSupply`](./RuleMaxTotalSupply.md) with CMTAT.
+> The two are not interchangeable, and choosing the wrong one **silently** mis-caps issuance in one
+> direction or the other. Nothing reverts at deployment to tell you.
+
+`RuleMaxTotalSupplyERC3643` and `RuleMaxTotalSupplyERC3643Ownable2Step` cap minting at a static
+maximum supply, exactly like the stock rule. The cap logic, restriction codes (50, 51),
+configuration, roles and events are **identical and inherited**. The only difference is *when the
+token is assumed to report the mint*.
+
+## Why a separate variant: compliance is called AFTER the transfer
+
+A rule that caps a supply has to know whether the figure it reads already includes the amount being
+minted. The two token families answer differently.
+
+| Token | Order on a mint | `totalSupply()` when the rule is notified | Use |
+|---|---|---|---|
+| **CMTAT** | rule first, then the mint | **excludes** the new tokens | [`RuleMaxTotalSupply`](./RuleMaxTotalSupply.md) |
+| **ERC-3643 / T-REX** | mint first, then `created` | **includes** the new tokens | `RuleMaxTotalSupplyERC3643` |
+
+ERC-3643's `Token.mint` is explicit, and consults compliance **twice** — on either side of the state
+change:
+
+```solidity
+function mint(address _to, uint256 _amount) public override onlyAgent {
+ // ...
+ require(_tokenCompliance.canTransfer(address(0), _to, _amount), ComplianceNotFollowed());
+ _mint(_to, _amount); // <-- supply changes here
+ _tokenCompliance.created(_to, _amount); // <-- rule notified afterwards
+}
+```
+
+**ERC-3643 signals a mint with `created`, not `transferred`.** `RuleEngine` implements the full
+`ICompliance` surface and forwards `created(to, value)` to each rule as the three-argument
+`transferred(address(0), to, value)`, which is the shape every rule already gates on
+(`from == address(0)`). No rule-side change is needed for the signal; what changes is the accounting.
+
+### What each variant does with it
+
+Only the **write** path is re-phased. The variant overrides one hook:
+
+```solidity
+function _detectTransferRestrictionOnNotify(address from, address to, uint256 /* value */)
+ internal view override returns (uint8)
+{
+ return _detectTransferRestriction(from, to, 0); // the supply already includes the mint
+}
+```
+
+The **read** path is deliberately untouched: `detectTransferRestriction` and `canTransfer` still
+project the pending amount, because a pre-flight query always runs *before* the movement — as the
+`require(... canTransfer ...)` line above shows, the ERC-3643 token depends on it. Re-phasing the
+views too would make the pre-flight answer disagree with enforcement.
+
+The two consultations therefore reduce to the same condition, which is what makes the variant
+correct: `canTransfer` asks `supply + amount <= cap` before the mint, and `created` asks
+`supply' <= cap` after it, where `supply' == supply + amount`.
+
+### What goes wrong with the wrong variant
+
+| Deployment | Effect |
+|---|---|
+| Stock rule on an **ERC-3643** token | The amount is counted twice. `canTransfer` accepts the mint, the token mints, then `created` rejects it and the whole transaction reverts — **mints within the ceiling fail**. The largest single mint from an empty supply is halved to `cap / 2`. It is not a uniform halving: a series of small mints can still creep to the full cap, so the failure looks intermittent and depends on how issuance is chunked. |
+| This variant on a **CMTAT** token | The pending amount is ignored on enforcement. The pre-flight view still blocks an over-cap mint, but the write hook would no longer stop one that slipped past — **the ceiling is weakened**. |
+
+## Deployment
+
+Constructors match the stock rule exactly.
+
+```solidity
+new RuleMaxTotalSupplyERC3643(
+ admin, // DEFAULT_ADMIN_ROLE
+ tokenContract, // the ERC-3643 token; must expose totalSupply()
+ maxTotalSupply // the ceiling
+);
+```
+
+Wire it as a rule inside a `RuleEngine` occupying the token's **compliance** slot:
+
+```
+ERC-3643 Token ── compliance ──▶ RuleEngine ──▶ RuleMaxTotalSupplyERC3643
+```
+
+Use `RuleEngine`, not a bare rule: ERC-3643 drives mint and burn through `created` / `destroyed`,
+which the validation rules do not implement.
+
+`RuleMaxTotalSupplyERC3643Ownable2Step` is the same contract under `Ownable2Step` instead of
+`AccessControl`.
+
+### Composing with Proof of Reserve
+
+[`RuleChainlinkPoRERC3643`](./RuleChainlinkPoRERC3643.md) caps minting at the reported reserves with
+**no margin parameter**, so pair the two when a static ceiling is wanted alongside the reserve-backed
+one. Add both to the same engine; whichever limit binds first stops the mint. The engine returns the
+**first non-zero code**, so rule order decides whether a rejection is reported as `50` or `75`. Both
+orderings are exercised in the test suite below.
+
+## Behaviour inherited unchanged
+
+- **Mints only.** Transfers always pass; burns always pass and *free headroom*, because the cap is on
+ supply rather than on cumulative issuance.
+- **Restriction codes** 50 (max total supply exceeded) and 51 (total supply unavailable — the token
+ reverted or lost its code; fail-closed, and the read path still never reverts).
+- **Lowering the cap below the current supply** does not claw anything back; it simply blocks further
+ mints until burns bring the supply back under.
+- **One token per instance.** The rule reads `totalSupply()` from its configured `tokenContract`,
+ never from the token that triggered the check, and cannot learn that identity behind a RuleEngine.
+ Do not add one instance to two engines.
+
+## Tests
+
+- `test/RuleMaxTotalSupply/RuleMaxTotalSupplyERC3643.t.sol` — unit coverage in the default profile.
+- `test/ERC3643Real/ERC3643RealTokenMaxTotalSupply.t.sol` — drives the **genuine** vendored
+ `lib/ERC-3643/` token (4.2.0-beta1), not a mock: mints to the ceiling, rejection past it,
+ incremental issuance, burns freeing headroom, a raised cap raising the ceiling, and both
+ compositions with the Proof-of-Reserve variant. Two tests pin the stock rule's failure on the same
+ token so the reason this variant exists stays executable.
+
+The real-token suite needs the dedicated profile, which `forge test` alone does **not** include:
+
+```bash
+FOUNDRY_PROFILE=erc3643 forge test
+```
+
+## See also
+
+- [`RuleMaxTotalSupply`](./RuleMaxTotalSupply.md) — the CMTAT rule and the full reference
+- [`RuleChainlinkPoRERC3643`](./RuleChainlinkPoRERC3643.md) — the reserve-backed sibling
+- [`RULE_SEMANTICS.md` §5](../guides/RULE_SEMANTICS.md) — the two seams the cap rules expose
diff --git a/doc/technical/contracts/RuleWhitelistWrapper.md b/doc/technical/contracts/RuleWhitelistWrapper.md
index ded4baa0..bfbec82f 100644
--- a/doc/technical/contracts/RuleWhitelistWrapper.md
+++ b/doc/technical/contracts/RuleWhitelistWrapper.md
@@ -6,7 +6,9 @@ This rule aggregates multiple child whitelist rules using OR logic. An address i
## Architecture
-Each child rule must implement `IAddressList`. The wrapper iterates through all registered rules and returns `true` for an address as soon as one rule lists it. Iteration stops early once all required addresses are resolved.
+Each child rule must implement `IAddressList` **and must be an allow-list**. The wrapper iterates through all registered rules and returns `true` for an address as soon as one rule lists it. Iteration stops early once all required addresses are resolved.
+
+> ⚠️ **`IAddressList` carries membership, not polarity.** The wrapper reads a child's `areAddressesListed` answer and treats `true` as *eligible*. It has no way to ask whether the child meant "allowed" or "denied", and nothing in `addRule` constrains that — see [Child rules must be allow-lists](#child-rules-must-be-allow-lists).

@@ -71,13 +73,140 @@ The wrapper reuses restriction codes from the whitelist rule:
| `removeRule(address rule_)` | `RULES_MANAGEMENT_ROLE` | Removes a single child rule |
| `clearRules()` | `RULES_MANAGEMENT_ROLE` | Removes all child rules |
+#### Child rules must be allow-lists
+
+**The wrapper cannot tell an allow-list from a deny-list, and adding the wrong one inverts its meaning.**
+
+`IAddressList` expresses only *membership* — "is this address in my set?" — never what membership means. The
+wrapper ORs those answers and reads `true` as **eligible**. A `RuleBlacklist` is a perfectly valid `IRule`,
+exposes the same `IAddressList` surface, and passes every check `addRule` performs, but its set means the
+opposite: listed addresses are the ones that must be **denied**.
+
+Add a `RuleBlacklist` as a child and the wrapper reports its blacklisted addresses as whitelisted. Because the
+wrapper is also the token's `isVerified` answer under ERC-3643, `isVerified(blacklistedAddress)` returns `true`
+as well.
+
+| Safe as a child | Not a child |
+| --- | --- |
+| `RuleWhitelist`, `RuleWhitelistOwnable2Step` | `RuleBlacklist` — inverted polarity |
+| `RuleReceiverWhitelist`, `RuleReceiverWhitelistOwnable2Step` | `RuleSpenderWhitelist` — its set is spenders, not holders |
+| Any custom rule whose listed addresses are the **permitted** ones | Any rule whose `IAddressList` set means something other than "eligible holder" |
+
+**This is now enforced, not merely documented.** It could not be caught by ERC-165 alone — `RuleBlacklist`
+advertises the same `IAddressList` ids as the whitelist rules, because `IAddressList` describes *membership* and
+both kinds of list have members. The fix is the separate marker interface that observation implies:
+[`IAddressListPolarity`](#child-rules-are-erc-165-checked) adds a single `isAllowList()` function, the wrapper
+requires it and refuses any child answering `false`. Pinned by `test_WW2_DenyListChildIsRejectedAtAddRule`.
+
+#### Children are ERC-165-checked
+
+`addRule` and `setRules` both route through `_checkRule`, which requires the candidate to advertise
+**`IAddressListBatchQuery`** via ERC-165, on top of the inherited non-zero and not-already-present checks. A
+candidate that does not is rejected with `RuleWhitelistWrapper_ChildIsNotAnAddressList(rule)`.
+
+This closes the failure where a valid `IRule` that is not an address list — `RuleMaxTotalSupply`, say — was
+accepted and then reverted the blind `areAddressesListed` call during a transfer. The early exit in the child
+scan made that *input-dependent*: an address pair already resolved by an earlier child still worked, so the
+wrapper looked healthy right up until a pair that needed the full scan (audit `F-5`, Nethermind AuditAgent
+`NM-18`). It also refuses a **nested wrapper**, which does not implement `areAddressesListed` and would brick the
+parent the same way.
+
+`ERC165Checker.supportsInterface` is itself non-reverting — a bounded staticcall returning `false` for a codeless
+address, a missing selector or malformed return data — so a hostile candidate cannot brick the setter that is
+screening it.
+
+##### Two questions, two interfaces
+
+Membership and meaning are different questions, so the guard asks both:
+
+| Requirement | Interface | Failure |
+| --- | --- | --- |
+| Can you answer "is this address listed?" | `IAddressListBatchQuery` (`0x20e8e17a`) | `RuleWhitelistWrapper_ChildIsNotAnAddressList` |
+| Do you declare what membership *means*? | `IAddressListPolarity` (`0xdc4efe10`) | `RuleWhitelistWrapper_ChildDoesNotDeclarePolarity` |
+| Does it mean **allowed**? | `isAllowList() == true` | `RuleWhitelistWrapper_ChildIsNotAnAllowList` |
+
+**Absence of the polarity declaration is a refusal, never an assumed allow-list.** That is the only reading that
+fails closed for a contract predating the interface or deliberately declining it.
+
+What each rule declares:
+
+| Rule | `isAllowList()` | As a wrapper child |
+| --- | --- | --- |
+| `RuleWhitelist` | `true` | ✅ accepted |
+| `RuleReceiverWhitelist` | `true` | ✅ accepted |
+| `RuleBlacklist` | `false` | ❌ rejected — deny-list |
+| `RuleSpenderWhitelist` | *does not implement the interface* | ❌ rejected — see below |
+| `RuleWhitelistWrapper` (nested) | *does not implement `areAddressesListed`* | ❌ rejected at the first check |
+
+`RuleSpenderWhitelist` **deliberately abstains, and must not be "fixed" to declare `true`.** Its set genuinely is
+an allow-list, so `true` would be honest about polarity and still wrong: the listed addresses are permitted
+*spenders*, not permitted *holders*, and the wrapper would read them as eligible transfer participants. Polarity
+is only half the question; the other half is what the addresses are. Withholding the declaration is what makes
+the fail-closed check refuse it — pinned by `test_WW2_ChildDecliningToDeclarePolarityIsRejected`.
+
+##### Wrappers cannot nest, deliberately
+
+A `RuleWhitelistWrapper` does not implement `areAddressesListed`, so it fails the first check and cannot be a
+child of another wrapper. That is a decision, not an omission (Nethermind AuditAgent `NM-19`, declined).
+
+**Nesting would buy no expressive power.** The wrapper is an OR, and `OR(OR(a,b), OR(c,d))` ≡ `OR(a,b,c,d)` — an
+OR nested in an OR flattens. Every policy a nested wrapper could express is expressible with a flat child list,
+and the composition integrators actually reach for is already available one level up:
+
+| Composition | How |
+| --- | --- |
+| **OR** of lists | one wrapper, flat children |
+| **AND** of ORs | several wrappers in the `RuleEngine`, which returns the first non-zero code |
+| OR of ORs | identical to a flat wrapper |
+
+It would also cost. The scan is [~8.8k gas per child](#gas-cost-of-the-child-rule-scan) and the *rejected* path
+never early-exits, so a 10 × 10 nest costs **~880k gas per transfer** where the equivalent flat wrapper costs
+**~90k** — the same policy at ten times the price, paid by every transferring holder. And it would open a cycle
+class (`A → B → A`) that recurses to out-of-gas, bricking transfers *and* `isVerified`, with no cheap on-chain
+defence.
+
+Delegated administration — the real motivation — already works flat: see the [usage scenario](#usage-scenario),
+where three operators each manage their own `RuleWhitelist` under one wrapper.
+
+##### Why the check asks for a sub-interface, not all of `IAddressList`
+
+The wrapper calls **one** function on its children:
+
+```solidity
+bool[] memory isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress);
+```
+
+`IAddressList` declares eight (`addAddress`, `removeAddress`, `addAddresses`, `removeAddresses`,
+`listedAddressCount`, `isAddressListed`, `areAddressesListed`, and `contains` inherited from
+`IIdentityRegistryContains`). Requiring the full id would demand seven functions the wrapper never touches —
+including all four **write** functions, which a read-only aggregating child has no reason to expose — and reject
+an otherwise perfectly serviceable child. An ERC-165 check should ask for what is actually called.
+
+`IAddressListBatchQuery` therefore declares `areAddressesListed` alone, and `IAddressList` inherits it:
+
+| Constant | Value | Covers |
+| --- | --- | --- |
+| `IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID` | `0x20e8e17a` | `areAddressesListed(address[])` — **what the wrapper requires** |
+| `IADDRESS_LIST_INTERFACE_ID` | `0x5d10e182` | the full eight-selector hierarchy |
+
+Factoring the selector into a parent left the flattened set unchanged, so `0x5d10e182` keeps its value and every
+rule advertises both ids. The sub-interface id is safe to state as a literal, unlike the full one: it declares a
+single function and inherits nothing, so there is no omitted-parent trap. All of this is asserted in
+`test/InterfaceId/AddressListInterfaceId.t.sol`.
+
+This is a category error by a trusted role rather than an attack — the same role can already remove every child
+outright, which fails closed — but it fails **open**, silently, so it is worth checking at configuration time and
+in any deployment review. Reported as Nethermind AuditAgent `NM-20`.
+
### `setCheckSpender(bool value)`
Enables or disables spender checks. Restricted to `DEFAULT_ADMIN_ROLE`.
### `isVerified(address targetAddress) → bool`
-Returns `true` if the address is listed in at least one child rule.
+Returns `true` if the address is listed in at least one child rule. This is the ERC-3643 eligibility answer, and
+it resolves through the same child scan as the transfer check, so the two can never disagree about an address —
+including when a child's polarity is wrong (see [Child rules must be allow-lists](#child-rules-must-be-allow-lists)).
### `rule(uint256 index) → address`
diff --git a/doc/technical/guides/RULE_SEMANTICS.md b/doc/technical/guides/RULE_SEMANTICS.md
index b66a9f0a..168031ba 100644
--- a/doc/technical/guides/RULE_SEMANTICS.md
+++ b/doc/technical/guides/RULE_SEMANTICS.md
@@ -35,7 +35,7 @@ Legend: ✅ screened / can block · ❌ not screened · ⚙️ conditional (see
| Rule | When its oracle/registry is unset | Stateful on transfer? [7] | Authoritative pre-flight view | Restriction codes |
|---|---|---|---|---|
| `RuleWhitelist` | n/a (local address set) | ❌ | `canTransfer` / `canTransferFrom` | 21–25 |
-| `RuleWhitelistWrapper` | empty wrapper ⇒ **all rejected** (fail-closed) | ❌ | `canTransfer` / `canTransferFrom` | 21–25 |
+| `RuleWhitelistWrapper` | empty wrapper ⇒ **all rejected** (fail-closed); children must be allow-lists [12b] | ❌ | `canTransfer` / `canTransferFrom` | 21–25 |
| `RuleReceiverWhitelist` | n/a (local address set) | ❌ | `canTransfer` / `canTransferFrom` | 81 |
| `RuleSpenderWhitelist` | n/a (local address set) | ❌ | `canTransfer` (always ✓) / `canTransferFrom` | 66 |
| `RuleBlacklist` | n/a (local address set) | ❌ | `canTransfer` / `canTransferFrom` | 36–38 |
@@ -70,7 +70,17 @@ Not every rule exposes the same entrypoints. The ERC-7943 `tokenId` overloads an
| `RuleConditionalTransferLightMultiToken` | ❌ | ✅ | ❌ |
| `RuleMintAllowance` | ❌ | ❌ | ❌ |
-The `tokenId` parameter is **always ignored** by the rules that accept it — `RuleNFTAdapter` exists purely to re-expose the same restriction logic under the ERC-7943 signatures. The `tokenId` overload of any function therefore returns exactly what its fungible counterpart returns, and the `ctx` entrypoints dispatch to the same internal hooks (`ctx.sender == 0` or `ctx.sender == ctx.from` ⇒ the direct hook; otherwise the spender-aware hook). This parity is asserted for every rule above in `test/TransferContext/OverloadParity.t.sol`.
+The `tokenId` parameter is **always ignored** by the rules that accept it — `RuleNFTAdapter` exists purely to re-expose the same restriction logic under the ERC-7943 signatures. Entrypoints describing the same transfer therefore return the same answer, asserted for every rule above in `test/TransferContext/OverloadParity.t.sol`.
+
+**How each interface signals a direct transfer differs, and that decides the routing.** An owner moving their own tokens reaches the adapter as `spender == from` on the ERC-7943 overloads (the spec calls that parameter "the address performing the transfer (owner/operator)") and as `sender == from` on the `ctx` entrypoints, whereas the CMTAT path signals it with the 3-arg overload or `spender == address(0)`:
+
+| Interface | Direct transfer arrives as | Delegated transfer arrives as |
+|---|---|---|
+| CMTAT 3-arg / 4-arg | the 3-arg overload, or `spender == address(0)` | `spender != address(0)` |
+| ERC-7943 `tokenId` overloads | `spender == from` | `spender != from` |
+| `ITransferContext` | `sender == from`, or `sender == address(0)` | `sender != from` |
+
+Every adapter entrypoint normalises `spender == from` to the **direct** hook. The 4-arg CMTAT path deliberately does not, because its own convention already distinguishes the two — so `4-arg(spender == from)` and the ERC-7943 5-arg call with the same arguments describe *different* transfers and are expected to differ. Do not "align" them: an owner-initiated ERC-721 `transferFrom` would then be screened as delegated, which `RuleSpenderWhitelist` documents as always allowed. Both halves are pinned by `test_NM6_SelfSpenderIsNotScreenedByTheSpenderWhitelist` and `test_NM6_CmtatFourArgPathKeepsScreeningASelfSpender`.
**Access control on the `ctx` entrypoints (threat `AC-5`).** `transferred(FungibleTransferContext)` / `transferred(MultiTokenTransferContext)` are `external` with **no caller restriction** on the validation rules. That is safe because those rules' hooks are `view`: an arbitrary caller can run the check and be reverted by it, but cannot mutate any state. The stateful multi-token rule guards its own `ctx` entrypoint with `onlyTransferExecutor`.
@@ -105,8 +115,123 @@ The `tokenId` parameter is **always ignored** by the rules that accept it — `R
11. **`RuleMintAllowance.canTransfer` / `detectTransferRestriction` are NOT authoritative** (finding **F-7**): they are hardcoded to "allowed" because the 3-arg signature has no minter identity. Pre-flight a mint with `canTransferFrom(minter, address(0), to, value)`. See [RuleMintAllowance.md](../contracts/RuleMintAllowance.md#eligibility-views-which-one-is-authoritative).
+12b. **`RuleWhitelistWrapper` children must be ALLOW-lists, and this is now enforced.** `IAddressList` carries *membership*, not polarity: the wrapper ORs its children's `areAddressesListed` answers and reads `true` as **eligible**. A `RuleBlacklist` implements that interface identically and advertises the same ids, so ERC-165 alone could not tell them apart — adding one made its blacklisted addresses whitelisted and `isVerified` returned `true` for them (`NM-20`). Polarity is now declared rather than inferred: **`IAddressListPolarity`** (`0xdc4efe10`) adds a single `isAllowList()`, and `addRule` requires the interface *and* a `true` answer, on top of the `IAddressListBatchQuery` check from `NM-18`. **Absence of the polarity declaration is a refusal, never an assumed allow-list** — the only reading that fails closed. `RuleWhitelist` and `RuleReceiverWhitelist` declare `true`; `RuleBlacklist` declares `false`; `RuleSpenderWhitelist` deliberately declines, because its set is permitted *spenders* rather than permitted *holders* and polarity alone would mislead. A nested wrapper is refused at the first check, since it does not implement `areAddressesListed` — **deliberately**: an OR nested in an OR is algebraically flat (`OR(OR(a,b),OR(c,d))` ≡ `OR(a,b,c,d)`), so nesting adds no expressive power, while costing multiplicatively (~8.8k gas per child, and the rejected path never early-exits) and opening an `A → B → A` cycle class with no cheap on-chain defence. AND-of-ORs is available by putting several wrappers in the `RuleEngine`, which returns the first non-zero code. `NM-19`, declined.
+
12. **The ERC-7943 `tokenId` overloads** are `detectTransferRestriction(from,to,tokenId,value)`, `detectTransferRestrictionFrom(spender,from,to,tokenId,value)`, `canTransfer(from,to,tokenId,amount)`, `canTransferFrom(spender,from,to,tokenId,value)`, `transferred(from,to,tokenId,value)` and `transferred(spender,from,to,tokenId,value)` — all supplied by `RuleNFTAdapter`. Per ERC-7943, `amount`/`value` MUST be `1` for ERC-721. The rules ignore `tokenId` entirely; it exists so an ERC-721/ERC-1155 token can call the same compliance rule without a shim.
+## 5. Cap rules: the two seams for an ERC-3643 variant
+
+`RuleMaxBalance`, `RuleMaxTotalSupply` and `RuleChainlinkPoR` all end in the same question — *would this movement leave an observed figure above its cap?* — and that question has two free variables. Each is a documented `internal virtual` hook, so a variant overrides one line rather than reimplementing a rule.
+
+The shared arithmetic lives in [`CapAccounting`](../../../src/rules/validation/abstract/core/CapAccounting.sol), which holds **no storage** and is deliberately ignorant of both variables.
+
+### Seam 1 — accounting phase: `_detectTransferRestrictionOnNotify`
+
+**The stock rules assume the token calls them BEFORE it moves the value**, so the observation still excludes it and `value` must be counted. CMTAT does this. **ERC-3643 / T-REX calls afterwards** — `Token.transfer` runs `_transfer` then `_tokenCompliance.transferred`, and `mint` runs `_mint` then `created` — so the observation already includes the value and counting it again **halves the effective cap**, rejecting movements that are within it (Nethermind AuditAgent NM-11).
+
+Adapting is one override, because "the observation already includes it" is the same as "there is nothing left to add":
+
+```solidity
+function _detectTransferRestrictionOnNotify(address from, address to, uint256)
+ internal view override returns (uint8)
+{
+ return _detectTransferRestriction(from, to, 0);
+}
+```
+
+**Only the write path is routed through this hook, never the read path.** A pre-flight view (`detectTransferRestriction`, `canTransfer`, `remainingCapacity`) always runs *before* the movement on either kind of token, so it must always project `value`. Re-phasing it too would make the pre-flight answer disagree with enforcement — the mirror image of the bug being fixed.
+
+### Seam 2 — observation source: `_currentSupply` / `_balanceOf`
+
+Both are `internal view virtual`, so a rule may serve the figure from **its own storage** instead of calling the token — the shape needed for a rule that tracks the supply itself from an opening figure set at the start of the token's life.
+
+A rule that keeps its own running total also controls *when* it updates it, so it checks before it records and **seam 1 stops applying to it**: it never depends on the host token's call order.
+
+Two constraints before building one:
+
+- **It must observe every change or it drifts, permanently and silently.** Being installed after issuance has begun, removed and re-added, or served by a second engine all desynchronise it. A rule that reads the token self-heals; an accumulator does not.
+- **Supply can be tracked; per-address balances are version-dependent and therefore unsafe.** How `Token.recoveryAddress` moves a balance **changed across T-REX versions**: up to 4.1 it routed through the public `forcedTransfer`, which *does* call `_tokenCompliance.transferred`; the vendored **4.2.0-beta1 calls `_transfer` directly and notifies nobody** (verified: zero `_tokenCompliance` references in that function body). A tracked per-address ledger is therefore in sync on one minor version and permanently skewed on the next, by an agent-callable path with no on-chain signal — a dependency no rule should carry. Total supply is unaffected by recovery either way, so a tracked-supply rule is safe from this.
+- A tracked rule's write hook mutates state, so it belongs under `src/rules/operation/`, not `src/rules/validation/`.
+
+### Worked examples
+
+`src/mocks/harness/ERC3643CapHarnesses.sol` implements all four (one per cap rule for seam 1, plus a tracked-supply rule for seam 2), and `test/CapAccounting/ERC3643CapSeams.t.sol` asserts that the stock rules double-count under post-update accounting while the variants do not, and that neither ever admits anything above the cap.
+
+---
+
+## 6. ERC-3643 compatibility — which rules actually work on a T-REX token
+
+A rule's guarantees depend on **what the token tells it and when**. CMTAT and ERC-3643 / T-REX differ on both,
+so a rule that is correct on one can be inert or wrong on the other — silently, with nothing reverting at
+deployment. This section is the per-rule answer.
+
+### The two differences that cause everything below
+
+| | CMTAT | ERC-3643 / T-REX |
+|---|---|---|
+| **Spender** | forwarded on the 4-arg `transferred(spender, from, to, value)` (v3.3+) | **never forwarded** — `transfer` *and* `transferFrom` both call the 3-arg `transferred(from, to, value)` |
+| **Ordering** | rule called **before** the value moves | rule called **after** — `_transfer` then `transferred`; `_mint` then `created` |
+| **Mint signal** | 4-arg `transferred(minter, address(0), to, value)` | `created(to, value)`, which `RuleEngine` forwards as 3-arg `transferred(address(0), to, value)` |
+
+Both paths also call `canTransfer` **before** the movement, so the read views are unaffected by the ordering
+difference and must always project the pending amount.
+
+### Per-rule status
+
+| Rule | On ERC-3643 | Why |
+|---|---|---|
+| `RuleWhitelist` | ✅ works, one flag inert | `from`/`to` arrive on the 3-arg path. **`checkSpender` never fires** |
+| `RuleWhitelistWrapper` | ✅ works, one flag inert | as above |
+| `RuleReceiverWhitelist` | ✅ works | screens `to` only, which the 3-arg path carries |
+| `RuleBlacklist` | ✅ works, spender leg inert | blocks a listed `from`/`to`; a listed **spender** moving someone else's tokens is not caught |
+| `RuleSanctionsList` | ✅ works, spender leg inert | as above |
+| `RuleERC2980` | ✅ works, spender leg inert | whitelist and frozen checks on `from`/`to` fire; the frozen-**spender** leg does not |
+| `RuleIdentityRegistry` | ✅ works, one flag inert | `to` is screened. **`checkSpender` never fires**. Usually redundant anyway: the token already calls `isVerified(_to)` itself |
+| `RuleMaxTotalSupply` | ⚠️ **use `RuleMaxTotalSupplyERC3643`** | post-update ordering ⇒ the amount is counted twice and fully-backed mints revert |
+| `RuleChainlinkPoR` | ⚠️ **use `RuleChainlinkPoRERC3643`** | same |
+| `RuleMaxBalance` | ❌ **not supported** | same double-count, and no variant exists — see below |
+| `RuleConditionalTransferLight` | ✅ works | approvals are keyed `(from, to, value)`; ordering is irrelevant to consuming one. Requires `bindRuleEngine` |
+| `RuleConditionalTransferLightMultiToken` | ❌ **not supported** | direct-binding only, and ERC-3643 needs the engine for `created` / `destroyed` |
+| `RuleMintAllowance` | ❌ **inert** | the quota is debited only on the 4-arg path; `created` arrives with no minter identity, so nothing is debited and every mint passes |
+
+### Reading the three failure modes
+
+They are not equally dangerous, and the difference matters more than the symbol:
+
+- **"one flag / leg inert"** — the rule enforces less than its configuration suggests. Fail-**open** for that
+ leg: an operator who set `checkSpender = true` gets no spender screening and no signal. The `from`/`to`
+ screening is unaffected, so the rule still does its main job.
+- **"use the ERC-3643 variant"** — the stock rule fails **closed**: it rejects mints that are within the cap.
+ Nothing is over-issued, but issuance breaks in a way that looks intermittent, because only the amount *in
+ flight* is double-counted. The variants re-phase the write path only.
+- **"inert" / "not supported"** — the rule enforces **nothing**, or cannot be wired at all. `RuleMintAllowance`
+ is the one to watch: it is silently permissive rather than restrictive, and its pre-flight view says
+ `TRANSFER_OK` too, so neither the token nor an integrator sees a problem.
+
+### Why `RuleMaxBalance` has no ERC-3643 variant
+
+Not effort — a policy decision that has not been made. Its observation is **per-address**, so unlike the two
+supply rules it engages on every transfer, and two T-REX agent powers interact badly with a post-update variant:
+
+- **`forcedTransfer` does notify compliance**, so the variant would *revert* an agent's forced transfer that
+ pushes the recipient over the cap. On T-REX ≤ 4.1, where `recoveryAddress` routes through `forcedTransfer`,
+ that **bricks wallet recovery** whenever the destination already holds tokens.
+- On the vendored **4.2.0-beta1 `recoveryAddress` notifies nobody**, so a recovered wallet can silently sit
+ above the cap. A token-reading rule self-heals — further receipts are blocked — but the invariant is violated
+ in state with no event.
+
+Whether an agent action should be cap-exempt is the question to settle first. T-REX's own module library also
+ships a `MaxBalanceModule`, so the marginal value is the lowest of the three.
+
+### Wiring, whichever rules you choose
+
+Use a **`RuleEngine`**, never a bare rule. ERC-3643 drives mint and burn through `created` / `destroyed`, which
+the validation rules do not implement; the engine implements the full `ICompliance` surface and forwards them.
+And build a `RuleChainlinkPoRERC3643` **after** `Token.init` — an uninitialised token reports `decimals() == 0`,
+which the rule's constructor accepts and caches.
+
+---
+
---
See [`CLAUDE_AUDIT.md`](../../security/audits/tools/v0.4.0/claude-audit/CLAUDE_AUDIT.md) for the findings referenced above.
diff --git a/lib/RuleEngine b/lib/RuleEngine
index ab9def2f..ca75429c 160000
--- a/lib/RuleEngine
+++ b/lib/RuleEngine
@@ -1 +1 @@
-Subproject commit ab9def2f19ae71af304127f42d20d9831cad1a2b
+Subproject commit ca75429c581a2eb9043e4719561e941d0b2e1206
diff --git a/src/mocks/IERC3643ComplianceFull.sol b/src/mocks/IERC3643ComplianceFull.sol
index 77f43e40..fd8fb294 100644
--- a/src/mocks/IERC3643ComplianceFull.sol
+++ b/src/mocks/IERC3643ComplianceFull.sol
@@ -7,8 +7,11 @@ pragma solidity ^0.8.20;
* including functions inherited by IERC3643Compliance from its parent interfaces
* (IERC3643ComplianceRead.canTransfer, IERC3643IComplianceContract.transferred).
*
- * Purpose: computing the correct ERC-165 interface ID for the full ERC-3643
- * ICompliance interface via `type(IERC3643ComplianceFull).interfaceId`.
+ * Purpose: pinning the ERC-165 interface ID of the full ERC-3643 ICompliance
+ * interface from an independent source. The rules themselves advertise
+ * `ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID`, which RuleEngine derives from
+ * its own interface hierarchy; this flat redeclaration is the cross-check that the
+ * derivation still yields the wire value, so a refactor upstream cannot silently move it.
*
* Background: `type(IFoo).interfaceId` only XORs selectors defined *directly* on
* `IFoo`, not those inherited from parent interfaces. Using `type(IERC3643Compliance).interfaceId`
diff --git a/src/mocks/harness/ERC3643CapHarnesses.sol b/src/mocks/harness/ERC3643CapHarnesses.sol
new file mode 100644
index 00000000..8de03646
--- /dev/null
+++ b/src/mocks/harness/ERC3643CapHarnesses.sol
@@ -0,0 +1,128 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {AggregatorV3Interface} from "../../rules/interfaces/AggregatorV3Interface.sol";
+import {ITotalSupply} from "../../rules/interfaces/ITotalSupply.sol";
+import {RuleChainlinkPoR} from "../../rules/validation/deployment/RuleChainlinkPoR.sol";
+import {RuleMaxBalance} from "../../rules/validation/deployment/RuleMaxBalance.sol";
+import {RuleMaxTotalSupply} from "../../rules/validation/deployment/RuleMaxTotalSupply.sol";
+
+/**
+ * @title ERC-3643 cap-rule harnesses
+ * @notice Worked examples of the two seams the cap rules expose, used by
+ * `test/CapAccounting/ERC3643CapSeams.t.sol` to prove they are sufficient.
+ *
+ * @dev **Seam 1 — accounting phase (`_detectTransferRestrictionOnNotify`).** CMTAT calls a rule
+ * BEFORE it moves the value, so the observation excludes it. ERC-3643 / T-REX calls AFTER, so the
+ * observation already includes it and counting `value` again halves the effective cap. Overriding the
+ * notification hook to re-ask with `value = 0` is the whole adaptation.
+ *
+ * @dev **Seam 2 — observation source (`_currentSupply` / `_balanceOf`).** Both are `internal view
+ * virtual`, so a rule may serve the figure from its own storage instead of calling the token. A rule
+ * that keeps its own running total also controls when it is updated, which makes seam 1 moot for it.
+ *
+ * These are test doubles, not deployable rules. See
+ * `doc/technical/guides/RULE_SEMANTICS.md` for the write-up.
+ */
+
+/// @notice `RuleMaxTotalSupply` for a token that notifies after minting.
+contract ERC3643MaxTotalSupplyHarness is RuleMaxTotalSupply {
+ constructor(address admin, address tokenContract_, uint256 maxTotalSupply_)
+ RuleMaxTotalSupply(admin, tokenContract_, maxTotalSupply_)
+ {}
+
+ /// @dev Seam 1: the observation already includes the minted value.
+ function _detectTransferRestrictionOnNotify(
+ address from,
+ address to,
+ uint256 /* value */
+ )
+ internal
+ view
+ override
+ returns (uint8)
+ {
+ // `totalSupply()` already includes the mint, so there is nothing left to add.
+ return _detectTransferRestriction(from, to, 0);
+ }
+}
+
+/// @notice `RuleMaxBalance` for a token that notifies after moving the value.
+contract ERC3643MaxBalanceHarness is RuleMaxBalance {
+ constructor(address admin, address balanceToken_, uint256 maxBalance_)
+ RuleMaxBalance(admin, balanceToken_, maxBalance_)
+ {}
+
+ /// @dev Seam 1: the observation already includes the received value.
+ function _detectTransferRestrictionOnNotify(
+ address from,
+ address to,
+ uint256 /* value */
+ )
+ internal
+ view
+ override
+ returns (uint8)
+ {
+ // `balanceOf(to)` already includes the received value.
+ return _detectTransferRestriction(from, to, 0);
+ }
+}
+
+/// @notice `RuleChainlinkPoR` for a token that notifies after minting.
+contract ERC3643ChainlinkPoRHarness is RuleChainlinkPoR {
+ constructor(
+ address admin,
+ address tokenContract_,
+ uint8 tokenDecimals_,
+ AggregatorV3Interface reservesFeed_,
+ uint256 maxStalenessSeconds_
+ ) RuleChainlinkPoR(admin, tokenContract_, tokenDecimals_, reservesFeed_, maxStalenessSeconds_) {}
+
+ /// @dev Seam 1: the observation already includes the minted value.
+ function _detectTransferRestrictionOnNotify(
+ address from,
+ address to,
+ uint256 /* value */
+ )
+ internal
+ view
+ override
+ returns (uint8)
+ {
+ return _detectTransferRestriction(from, to, 0);
+ }
+}
+
+/**
+ * @notice `RuleMaxTotalSupply` serving the supply from its OWN storage instead of the token.
+ * @dev Demonstrates seam 2. A real version would maintain {trackedSupply} from the write hook and is
+ * a larger design: it must observe every supply change or it drifts. The same approach is NOT safe for
+ * per-address balances: `Token.recoveryAddress` notifies compliance on T-REX <= 4.1 (it calls the public
+ * `forcedTransfer`) but not on 4.2.0-beta1 (it calls `_transfer` directly), so a shadow ledger's
+ * correctness would depend on the token's minor version. Total supply is unaffected by recovery.
+ */
+contract TrackedSupplyHarness is RuleMaxTotalSupply {
+ /// @notice Supply as this rule believes it to be; never read from the token.
+ uint256 public trackedSupply;
+
+ constructor(address admin, address tokenContract_, uint256 maxTotalSupply_)
+ RuleMaxTotalSupply(admin, tokenContract_, maxTotalSupply_)
+ {}
+
+ /// @notice Seeds the opening figure; a real rule would restrict and one-shot this.
+ function setTrackedSupply(uint256 supply) external {
+ trackedSupply = supply;
+ }
+
+ /// @dev Seam 2: the observation comes from storage, never from the token.
+ function _currentSupply() internal view override returns (bool available, uint256 supply) {
+ return (true, trackedSupply);
+ }
+
+ /// @dev Unused on the read path once {_currentSupply} is overridden.
+ function _supplyToken() internal view override returns (ITotalSupply) {
+ // Never consulted on the read path; kept so configuration stays valid.
+ return tokenContract;
+ }
+}
diff --git a/src/mocks/harness/IdentityRegistryDelegationHarness.sol b/src/mocks/harness/IdentityRegistryDelegationHarness.sol
new file mode 100644
index 00000000..d2f25270
--- /dev/null
+++ b/src/mocks/harness/IdentityRegistryDelegationHarness.sol
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol";
+import {RuleIdentityRegistry} from "../../rules/validation/deployment/RuleIdentityRegistry.sol";
+
+/**
+ * @title IdentityRegistryExtraCheckHarness
+ * @notice A subclass that adds a screening check which does NOT depend on the identity registry
+ * (Nethermind AuditAgent NM-3, the mirror of `CLAUDE_ANALYSIS.md` F-2).
+ * @dev This is the shape that exposes the defect. `_detectTransferRestrictionFrom` used to return
+ * `TRANSFER_OK` outright when the registry was unset or the transfer was a burn, instead of
+ * delegating to {_detectTransferRestriction}. A subclass extending only that hook -- the
+ * natural place to add a check -- therefore applied to `transfer` but silently not to
+ * `transferFrom` or `burnFrom`. A compliance rule that screens one entrypoint and not the
+ * other is the failure this harness exists to catch.
+ */
+contract IdentityRegistryExtraCheckHarness is RuleIdentityRegistry {
+ /**
+ * @notice Restriction code returned for the extra, registry-independent check.
+ */
+ uint8 public constant CODE_EXTRA_BLOCKED = 202;
+
+ /**
+ * @notice Address this subclass blocks regardless of what the registry says.
+ */
+ address public immutable BLOCKED;
+
+ constructor(address admin, address identityRegistry_, bool checkSender_, bool checkSpender_, address blocked)
+ RuleIdentityRegistry(admin, identityRegistry_, checkSender_, checkSpender_)
+ {
+ BLOCKED = blocked;
+ }
+
+ /**
+ * @notice Applies the base identity screening, then the extra registry-independent check.
+ */
+ function _detectTransferRestriction(address from, address to, uint256 value)
+ internal
+ view
+ virtual
+ override
+ returns (uint8)
+ {
+ uint8 code = super._detectTransferRestriction(from, to, value);
+ if (code != uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK)) {
+ return code;
+ }
+ if (from == BLOCKED || to == BLOCKED) {
+ return CODE_EXTRA_BLOCKED;
+ }
+ return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+ }
+}
diff --git a/src/modules/VersionModule.sol b/src/modules/VersionModule.sol
index 03806432..6669b03f 100644
--- a/src/modules/VersionModule.sol
+++ b/src/modules/VersionModule.sol
@@ -11,7 +11,7 @@ abstract contract VersionModule is IERC3643Version {
/**
* @notice The contract version string returned by {version}.
*/
- string private constant VERSION = "0.5.0";
+ string private constant VERSION = "0.6.0";
/*//////////////////////////////////////////////////////////////
PUBLIC FUNCTIONS
diff --git a/src/rules/interfaces/IAddressList.sol b/src/rules/interfaces/IAddressList.sol
index 768b42ad..378fd8a5 100644
--- a/src/rules/interfaces/IAddressList.sol
+++ b/src/rules/interfaces/IAddressList.sol
@@ -3,10 +3,60 @@ pragma solidity ^0.8.20;
import {IIdentityRegistryContains} from "./IIdentityRegistry.sol";
+/**
+ * @title IAddressListBatchQuery — the batch membership question, and nothing else.
+ * @notice The minimum a contract must expose to be usable as a child of `RuleWhitelistWrapper`.
+ * @dev Split out of {IAddressList} deliberately. The wrapper calls exactly one function on its
+ * children, so demanding the whole of {IAddressList} — which also carries four write functions, two
+ * further read functions and `contains` — would reject a perfectly serviceable read-only child.
+ * ERC-165 checks should ask for what is actually called.
+ *
+ * WARNING: this interface conveys **membership, not polarity**. It says whether an address is in the
+ * implementer's set, never whether being in that set means "allowed" or "denied". A deny-list
+ * implements it just as faithfully as an allow-list, so no ERC-165 check can tell them apart; a
+ * consumer that reads `true` as "eligible" must constrain its children by configuration.
+ */
+interface IAddressListBatchQuery {
+ /**
+ * @notice Checks multiple addresses for listing status.
+ * @param targetAddresses Array of addresses to check.
+ * @return results Boolean array aligned by index with listing results.
+ */
+ function areAddressesListed(address[] memory targetAddresses) external view returns (bool[] memory results);
+}
+
+/**
+ * @title IAddressListPolarity — what membership of the set MEANS.
+ * @notice The half of an address list that {IAddressListBatchQuery} cannot express.
+ * @dev `areAddressesListed` reports *membership*; it says nothing about whether being a member is a
+ * permission or a prohibition. An allow-list and a deny-list implement that interface identically and
+ * advertise the same ERC-165 id, so a consumer reading `true` as "eligible" cannot tell them apart —
+ * add a deny-list to an allow-list aggregator and its blocked addresses silently become permitted.
+ *
+ * Declaring polarity explicitly is what makes it checkable. A consumer requires this interface via
+ * ERC-165 and then reads {isAllowList}, so a wrong-polarity list is refused at configuration time
+ * instead of inverting the consumer's meaning at run time.
+ *
+ * WARNING: polarity is not the only way a list can be the wrong list. It says nothing about WHO the
+ * listed addresses are — a rule listing permitted *spenders* is an allow-list and still meaningless
+ * to a consumer screening *holders*. A contract whose set is not about the subject its consumers
+ * screen should decline to implement this interface at all, so a fail-closed consumer refuses it.
+ */
+interface IAddressListPolarity {
+ /**
+ * @notice Whether membership of this contract's address set means ALLOWED.
+ * @return allowed True when listed addresses are the permitted ones (an allow-list); false when
+ * listed addresses are the prohibited ones (a deny-list).
+ */
+ function isAllowList() external view returns (bool allowed);
+}
+
/**
* @title IAddressList — interface for managing and querying a set of addresses.
+ * @dev Inherits {IAddressListBatchQuery}; the flattened selector set is unchanged, so
+ * {AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID} keeps its value.
*/
-interface IAddressList is IIdentityRegistryContains {
+interface IAddressList is IIdentityRegistryContains, IAddressListBatchQuery {
/* ============ Events ============ */
/**
* @notice Emitted when a batch add completes.
@@ -84,11 +134,4 @@ interface IAddressList is IIdentityRegistryContains {
* @return isListed True if listed, otherwise false.
*/
function isAddressListed(address targetAddress) external view returns (bool isListed);
-
- /**
- * @notice Checks multiple addresses for listing status.
- * @param targetAddresses Array of addresses to check.
- * @return results Boolean array aligned by index with listing results.
- */
- function areAddressesListed(address[] memory targetAddresses) external view returns (bool[] memory results);
}
diff --git a/src/rules/interfaces/library/AddressListInterfaceId.sol b/src/rules/interfaces/library/AddressListInterfaceId.sol
index eb370874..0c447826 100644
--- a/src/rules/interfaces/library/AddressListInterfaceId.sol
+++ b/src/rules/interfaces/library/AddressListInterfaceId.sol
@@ -18,4 +18,30 @@ library AddressListInterfaceId {
* @notice ERC-165 interface ID of the full {IAddressList} hierarchy.
*/
bytes4 public constant IADDRESS_LIST_INTERFACE_ID = 0x5d10e182;
+
+ /**
+ * @notice ERC-165 interface ID of {IAddressListBatchQuery}, the single function
+ * `areAddressesListed(address[])`.
+ * @dev This is what `RuleWhitelistWrapper` requires of a child, because it is the only function
+ * the wrapper ever calls. Demanding {IADDRESS_LIST_INTERFACE_ID} instead would also require four
+ * write functions, `listedAddressCount`, `isAddressListed` and `contains` — none of which the
+ * wrapper uses — and would exclude a read-only child that is otherwise perfectly usable.
+ *
+ * Safe to state as a literal: {IAddressListBatchQuery} declares one function and inherits
+ * nothing, so unlike {IADDRESS_LIST_INTERFACE_ID} there is no omitted-parent trap here. The
+ * value equals the selector of the single function; asserted in
+ * test/InterfaceId/AddressListInterfaceId.t.sol.
+ */
+ bytes4 public constant IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID = 0x20e8e17a;
+
+ /**
+ * @notice ERC-165 interface ID of {IAddressListPolarity}, the single function `isAllowList()`.
+ * @dev Paired with {IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID} by consumers that read membership as
+ * eligibility: the first says the contract can answer, this one says what the answer means. A
+ * consumer must treat its ABSENCE as a refusal, not as an allow-list — that is the only reading
+ * that fails closed for a contract predating the interface or deliberately declining it.
+ *
+ * Safe as a literal for the same reason as the batch-query id: one function, no inheritance.
+ */
+ bytes4 public constant IADDRESS_LIST_POLARITY_INTERFACE_ID = 0xdc4efe10;
}
diff --git a/src/rules/operation/RuleConditionalTransferLight.sol b/src/rules/operation/RuleConditionalTransferLight.sol
index bc75ea71..0fe1c8d0 100644
--- a/src/rules/operation/RuleConditionalTransferLight.sol
+++ b/src/rules/operation/RuleConditionalTransferLight.sol
@@ -3,11 +3,11 @@ pragma solidity ^0.8.20;
import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+import {ComplianceInterfaceId} from "RuleEngine/modules/library/ComplianceInterfaceId.sol";
import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol";
import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol";
import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol";
import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
-import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol";
import {AccessControlModuleStandalone} from "../../modules/AccessControlModuleStandalone.sol";
import {RuleConditionalTransferLightBase} from "./abstract/RuleConditionalTransferLightBase.sol";
import {ERC3643ComplianceRolesStorage} from "RuleEngine/modules/library/ERC3643ComplianceRolesStorage.sol";
@@ -48,7 +48,7 @@ contract RuleConditionalTransferLight is
return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID
|| interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID
|| interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId
- || interfaceId == type(IERC3643ComplianceFull).interfaceId
+ || interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID
|| AccessControlEnumerable.supportsInterface(interfaceId);
}
@@ -69,11 +69,5 @@ contract RuleConditionalTransferLight is
/**
* @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`.
*/
- function _authorizeComplianceBindingChange(address)
- internal
- view
- virtual
- override
- onlyRole(COMPLIANCE_MANAGER_ROLE)
- {}
+ function _authorizeTokenBindingChange(address) internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
}
diff --git a/src/rules/operation/RuleConditionalTransferLightMultiToken.sol b/src/rules/operation/RuleConditionalTransferLightMultiToken.sol
index b3bb8e2e..c7352628 100644
--- a/src/rules/operation/RuleConditionalTransferLightMultiToken.sol
+++ b/src/rules/operation/RuleConditionalTransferLightMultiToken.sol
@@ -3,11 +3,11 @@ pragma solidity ^0.8.20;
import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+import {ComplianceInterfaceId} from "RuleEngine/modules/library/ComplianceInterfaceId.sol";
import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol";
import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol";
import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol";
import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
-import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol";
import {AccessControlModuleStandalone} from "../../modules/AccessControlModuleStandalone.sol";
import {RuleConditionalTransferLightMultiTokenBase} from "./abstract/RuleConditionalTransferLightMultiTokenBase.sol";
import {ERC3643ComplianceRolesStorage} from "RuleEngine/modules/library/ERC3643ComplianceRolesStorage.sol";
@@ -40,7 +40,7 @@ contract RuleConditionalTransferLightMultiToken is
return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID
|| interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID
|| interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId
- || interfaceId == type(IERC3643ComplianceFull).interfaceId
+ || interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID
|| AccessControlEnumerable.supportsInterface(interfaceId);
}
diff --git a/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol b/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol
index cbb4ebf0..ee37e2cf 100644
--- a/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol
+++ b/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol
@@ -4,11 +4,11 @@ pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+import {ComplianceInterfaceId} from "RuleEngine/modules/library/ComplianceInterfaceId.sol";
import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol";
import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol";
import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol";
import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
-import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol";
import {RuleConditionalTransferLightMultiTokenBase} from "./abstract/RuleConditionalTransferLightMultiTokenBase.sol";
import {Ownable2StepERC165Module} from "../../modules/Ownable2StepERC165Module.sol";
@@ -40,7 +40,7 @@ contract RuleConditionalTransferLightMultiTokenOwnable2Step is
|| interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID
|| interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID
|| interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId
- || interfaceId == type(IERC3643ComplianceFull).interfaceId;
+ || interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID;
}
/**
diff --git a/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol b/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol
index f5e585e6..493f4e1d 100644
--- a/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol
+++ b/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol
@@ -4,11 +4,11 @@ pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+import {ComplianceInterfaceId} from "RuleEngine/modules/library/ComplianceInterfaceId.sol";
import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol";
import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol";
import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol";
import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
-import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol";
import {RuleConditionalTransferLightBase} from "./abstract/RuleConditionalTransferLightBase.sol";
import {Ownable2StepERC165Module} from "../../modules/Ownable2StepERC165Module.sol";
@@ -47,7 +47,7 @@ contract RuleConditionalTransferLightOwnable2Step is
|| interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID
|| interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID
|| interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId
- || interfaceId == type(IERC3643ComplianceFull).interfaceId;
+ || interfaceId == ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID;
}
/*//////////////////////////////////////////////////////////////
@@ -67,5 +67,5 @@ contract RuleConditionalTransferLightOwnable2Step is
/**
* @notice Reverts unless the caller is the owner.
*/
- function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {}
+ function _authorizeTokenBindingChange(address) internal view virtual override onlyOwner {}
}
diff --git a/src/rules/operation/RuleMintAllowance.sol b/src/rules/operation/RuleMintAllowance.sol
index 83173831..fb08355e 100644
--- a/src/rules/operation/RuleMintAllowance.sol
+++ b/src/rules/operation/RuleMintAllowance.sol
@@ -67,11 +67,5 @@ contract RuleMintAllowance is AccessControlModuleStandalone, RuleMintAllowanceBa
/**
* @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`.
*/
- function _authorizeComplianceBindingChange(address)
- internal
- view
- virtual
- override
- onlyRole(COMPLIANCE_MANAGER_ROLE)
- {}
+ function _authorizeTokenBindingChange(address) internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
}
diff --git a/src/rules/operation/RuleMintAllowanceOwnable2Step.sol b/src/rules/operation/RuleMintAllowanceOwnable2Step.sol
index 45470feb..9d12b012 100644
--- a/src/rules/operation/RuleMintAllowanceOwnable2Step.sol
+++ b/src/rules/operation/RuleMintAllowanceOwnable2Step.sol
@@ -64,5 +64,5 @@ contract RuleMintAllowanceOwnable2Step is RuleMintAllowanceBase, Ownable2Step, O
/**
* @notice Reverts unless the caller is the owner.
*/
- function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {}
+ function _authorizeTokenBindingChange(address) internal view virtual override onlyOwner {}
}
diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol b/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol
index 7aeeb492..df47d9be 100644
--- a/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol
+++ b/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol
@@ -7,6 +7,8 @@ import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfa
import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
import {IRule} from "RuleEngine/interfaces/IRule.sol";
import {ERC3643ComplianceModule} from "RuleEngine/modules/ERC3643ComplianceModule.sol";
+import {TokenBindingModule} from "RuleEngine/modules/TokenBindingModule.sol";
+import {ITokenBinding} from "RuleEngine/interfaces/ITokenBinding.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {RuleConditionalTransferLightApprovalBase} from "./RuleConditionalTransferLightApprovalBase.sol";
@@ -119,12 +121,22 @@ abstract contract RuleConditionalTransferLightBase is
address token = getTokenBound();
require(token != address(0), RuleConditionalTransferLight_TokenNotBound());
+ uint256 approvalsBefore = approvedCount(from, to, value);
approveTransfer(from, to, value);
uint256 allowed = IERC20(token).allowance(from, address(this));
require(allowed >= value, RuleConditionalTransferLight_InsufficientAllowance(token, from, allowed, value));
IERC20(token).safeTransferFrom(from, to, value);
+
+ // The approval above exists ONLY for the token's compliance callback to consume. If the count
+ // did not come back down, no callback reached this rule -- the binding is wrong -- and leaving
+ // the surplus would authorise a later, never-approved transfer of the same tuple. Read after
+ // the external call deliberately: a hostile token can make this fail, never pass spuriously.
+ require(
+ approvedCount(from, to, value) == approvalsBefore,
+ RuleConditionalTransferLight_ApprovalNotConsumed(token, from, to, value)
+ );
return true;
}
@@ -177,7 +189,12 @@ abstract contract RuleConditionalTransferLightBase is
* {unbindRuleEngine} before rebinding.
* @param token The ERC-20 token to bind to this rule.
*/
- function bindToken(address token) public virtual override onlyComplianceManager {
+ function bindToken(address token)
+ public
+ virtual
+ override(ITokenBinding, TokenBindingModule)
+ onlyTokenBindingManager
+ {
require(getTokenBound() == address(0), RuleConditionalTransferLight_TokenAlreadyBound());
_bindToken(token);
}
@@ -198,7 +215,7 @@ abstract contract RuleConditionalTransferLightBase is
* @param ruleEngine_ The RuleEngine allowed to call `transferred`. It MUST serve only the token
* bound via {bindToken}.
*/
- function bindRuleEngine(address ruleEngine_) public virtual onlyComplianceManager {
+ function bindRuleEngine(address ruleEngine_) public virtual onlyTokenBindingManager {
require(ruleEngine_ != address(0), RuleConditionalTransferLight_RuleEngineAddressZeroNotAllowed());
require(ruleEngine == address(0), RuleConditionalTransferLight_RuleEngineAlreadyBound());
ruleEngine = ruleEngine_;
@@ -209,7 +226,7 @@ abstract contract RuleConditionalTransferLightBase is
* @notice Revokes the bound RuleEngine's authorization to call the transfer execution hooks.
* @dev Does NOT clear `approvalCounts` — see the {bindToken} warning and {resetApproval}.
*/
- function unbindRuleEngine() public virtual onlyComplianceManager {
+ function unbindRuleEngine() public virtual onlyTokenBindingManager {
address previous = ruleEngine;
require(previous != address(0), RuleConditionalTransferLight_RuleEngineNotBound());
ruleEngine = address(0);
diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol b/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol
index 3f5126e2..0e6fd1ae 100644
--- a/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol
+++ b/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol
@@ -77,6 +77,21 @@ abstract contract RuleConditionalTransferLightInvariantStorage is RuleSharedInva
);
error TransferNotApproved();
error TransferApprovalNotFound();
+ /**
+ * @notice The approval created by {approveAndTransferIfAllowed} was not consumed by the transfer.
+ * @dev The helper inverts CEI deliberately so the approval exists while the token runs its
+ * compliance callback. That is only correct if the callback actually reaches this rule; when it
+ * does not -- a plain ERC-20 bound for the helper, or a RuleEngine never bound or since unbound --
+ * the transfer used to succeed and leave a spendable approval behind, authorising a later
+ * never-approved transfer of the same tuple. The post-condition turns that silent hole into this
+ * revert. Nethermind AuditAgent NM-17.
+ * @param token The bound ERC-20 the transfer was executed on.
+ * @param from The sender of the transfer.
+ * @param to The recipient of the transfer.
+ * @param value The amount transferred.
+ */
+ error RuleConditionalTransferLight_ApprovalNotConsumed(address token, address from, address to, uint256 value);
+
error RuleConditionalTransferLight_RuleEngineAddressZeroNotAllowed();
error RuleConditionalTransferLight_RuleEngineNotBound();
error RuleConditionalTransferLight_RuleEngineAlreadyBound();
diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol
index 127c79b1..ecc83791 100644
--- a/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol
+++ b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol
@@ -136,6 +136,7 @@ abstract contract RuleConditionalTransferLightMultiTokenBase is
{
require(isTokenBound(token), RuleConditionalTransferLightMultiToken_InvalidToken());
+ uint256 approvalsBefore = approvedCount(token, from, to, value);
_approveTransfer(token, from, to, value);
uint256 allowed = IERC20(token).allowance(from, address(this));
@@ -144,6 +145,14 @@ abstract contract RuleConditionalTransferLightMultiTokenBase is
);
IERC20(token).safeTransferFrom(from, to, value);
+
+ // See the single-token twin: the approval exists only for the token's compliance callback, so
+ // a count that did not come back down means no callback reached this rule and the surplus
+ // would otherwise stay spendable.
+ require(
+ approvedCount(token, from, to, value) == approvalsBefore,
+ RuleConditionalTransferLightMultiToken_ApprovalNotConsumed(token, from, to, value)
+ );
return true;
}
@@ -321,21 +330,6 @@ abstract contract RuleConditionalTransferLightMultiTokenBase is
== uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
}
- /**
- * @notice Authorizes changes to compliance binding: restricted to the compliance manager.
- * @dev NOT `view`, unlike every other access-control hook in this codebase. This is structural,
- * not an oversight: the implementation delegates to `_onlyComplianceManager()`, which
- * `lib/RuleEngine`'s {ERC3643ComplianceModule} declares as `internal virtual` (non-`view`).
- * Solidity checks mutability against a virtual's DECLARED type, not the installed override,
- * so calling it from a `view` function is a compile error — even though every override of it
- * in this repo is `view`. It can only become `view` once the upstream declaration does.
- * (The single-token rules avoid this by overriding this hook directly with `onlyRole(...)`
- * instead of delegating, which is why they are already `view`.)
- */
- function _authorizeComplianceBindingChange(address) internal virtual override {
- _onlyComplianceManager();
- }
-
/**
* @notice Records a new approval for the given per-token transfer; reverts if the token is not bound.
* @param token The token the transfer applies to.
diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol
index 4e69d930..271615a7 100644
--- a/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol
+++ b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol
@@ -75,6 +75,20 @@ abstract contract RuleConditionalTransferLightMultiTokenInvariantStorage is Rule
error RuleConditionalTransferLightMultiToken_InsufficientAllowance(
address token, address owner, uint256 allowance, uint256 required
);
+ /**
+ * @notice The approval created by {approveAndTransferIfAllowed} was not consumed by the transfer.
+ * @dev See the single-token twin: the helper inverts CEI so the approval exists for the token's
+ * compliance callback, and this post-condition catches the case where no callback reached the
+ * rule and the approval would otherwise have been left spendable. Nethermind AuditAgent NM-17.
+ * @param token The token the transfer was executed on.
+ * @param from The sender of the transfer.
+ * @param to The recipient of the transfer.
+ * @param value The amount transferred.
+ */
+ error RuleConditionalTransferLightMultiToken_ApprovalNotConsumed(
+ address token, address from, address to, uint256 value
+ );
+
error RuleConditionalTransferLightMultiToken_InvalidToken();
error RuleConditionalTransferLightMultiToken_TransferNotApproved();
error RuleConditionalTransferLightMultiToken_TransferApprovalNotFound();
diff --git a/src/rules/operation/abstract/RuleMintAllowanceBase.sol b/src/rules/operation/abstract/RuleMintAllowanceBase.sol
index fee137a1..5eee10d2 100644
--- a/src/rules/operation/abstract/RuleMintAllowanceBase.sol
+++ b/src/rules/operation/abstract/RuleMintAllowanceBase.sol
@@ -7,6 +7,8 @@ import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfa
import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
import {IRule} from "RuleEngine/interfaces/IRule.sol";
import {ERC3643ComplianceModule} from "RuleEngine/modules/ERC3643ComplianceModule.sol";
+import {TokenBindingModule} from "RuleEngine/modules/TokenBindingModule.sol";
+import {ITokenBinding} from "RuleEngine/interfaces/ITokenBinding.sol";
import {VersionModule} from "../../../modules/VersionModule.sol";
import {RuleMintAllowanceInvariantStorage} from "./RuleMintAllowanceInvariantStorage.sol";
@@ -139,7 +141,12 @@ abstract contract RuleMintAllowanceBase is
* behavior. Call {clearMintAllowances} before rebinding to discard the previous quotas.
* @param token The caller (RuleEngine/token) to bind to this rule.
*/
- function bindToken(address token) public virtual override onlyComplianceManager {
+ function bindToken(address token)
+ public
+ virtual
+ override(ITokenBinding, TokenBindingModule)
+ onlyTokenBindingManager
+ {
require(getTokenBound() == address(0), RuleMintAllowance_TokenAlreadyBound());
_bindToken(token);
}
diff --git a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol
index 3af9eee2..104e8278 100644
--- a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol
+++ b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol
@@ -61,7 +61,7 @@ abstract contract RuleAddressSetInternal is RuleAddressSetInvariantStorage {
* the sentinel with THIS rule's error rather than a generic one.
* @param targetAddress The candidate address.
*/
- function _requireNotZeroAddress(address targetAddress) internal pure {
+ function _requireNotZeroAddress(address targetAddress) internal pure virtual {
require(targetAddress != address(0), RuleAddressSet_ZeroAddressNotAllowed());
}
diff --git a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol
index cee1a148..540faee4 100644
--- a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol
+++ b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol
@@ -70,6 +70,34 @@ abstract contract RuleWhitelistInvariantStorage is RuleSharedInvariantStorage {
*/
event AllowBurnUpdated(bool newValue);
+ /**
+ * @notice A candidate child rule does not answer `areAddressesListed(address[])`.
+ * @dev Raised by `RuleWhitelistWrapper` when a rule is added that does not advertise
+ * {AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID} via ERC-165. Without the guard
+ * the wrapper accepted it and then reverted on the blind call during a transfer, bricking every
+ * check whose targets were not already resolved. Nethermind AuditAgent NM-18, audit F-5.
+ * @param rule The rejected candidate.
+ */
+ error RuleWhitelistWrapper_ChildIsNotAnAddressList(address rule);
+
+ /**
+ * @notice A candidate child rule does not declare whether its list means "allowed" or "denied".
+ * @dev Absence is treated as a refusal, never as an assumed allow-list: that is the only reading
+ * that fails closed for a contract predating {IAddressListPolarity} or deliberately declining it
+ * (`RuleSpenderWhitelist` declines, because its set is spenders rather than holders).
+ * @param rule The rejected candidate.
+ */
+ error RuleWhitelistWrapper_ChildDoesNotDeclarePolarity(address rule);
+
+ /**
+ * @notice A candidate child rule declares itself a DENY-list; this wrapper aggregates allow-lists.
+ * @dev The wrapper ORs its children's membership answers and reads `true` as eligible, so a
+ * deny-list child would make its blocked addresses permitted and `isVerified` report them as
+ * verified investors. Nethermind AuditAgent NM-20.
+ * @param rule The rejected candidate.
+ */
+ error RuleWhitelistWrapper_ChildIsNotAnAllowList(address rule);
+
error RuleWhitelist_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code);
error RuleWhitelist_InvalidTransferFrom(
address rule, address spender, address from, address to, uint256 value, uint8 code
diff --git a/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol b/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol
index 1e9033b2..93e0f430 100644
--- a/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol
+++ b/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol
@@ -139,7 +139,7 @@ abstract contract RuleERC2980Internal is RuleERC2980InvariantStorage {
* with THIS rule's error rather than a generic one.
* @param targetAddress The candidate address.
*/
- function _requireNotZeroAddress(address targetAddress) internal pure {
+ function _requireNotZeroAddress(address targetAddress) internal pure virtual {
require(targetAddress != address(0), RuleERC2980_ZeroAddressNotAllowed());
}
diff --git a/src/rules/validation/abstract/base/RuleBlacklistBase.sol b/src/rules/validation/abstract/base/RuleBlacklistBase.sol
index aa7c5258..640cda34 100644
--- a/src/rules/validation/abstract/base/RuleBlacklistBase.sol
+++ b/src/rules/validation/abstract/base/RuleBlacklistBase.sol
@@ -5,6 +5,7 @@ import {RuleAddressSet} from "../RuleAddressSet/RuleAddressSet.sol";
import {RuleNFTAdapter} from "../core/RuleNFTAdapter.sol";
import {RuleTransferValidation} from "../core/RuleTransferValidation.sol";
import {RuleBlacklistInvariantStorage} from "../RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol";
+import {IAddressListPolarity} from "../../../interfaces/IAddressList.sol";
import {AddressListInterfaceId} from "../../../interfaces/library/AddressListInterfaceId.sol";
import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol";
import {IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol";
@@ -15,7 +16,12 @@ import {IRule} from "RuleEngine/interfaces/IRule.sol";
* @title RuleBlacklistBase
* @notice Core blacklist logic without access-control policy.
*/
-abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlacklistInvariantStorage {
+abstract contract RuleBlacklistBase is
+ RuleAddressSet,
+ RuleNFTAdapter,
+ RuleBlacklistInvariantStorage,
+ IAddressListPolarity
+{
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
@@ -98,9 +104,19 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack
// Advertise IAddressList: this rule manages an address set and is callable through
// the IAddressList interface.
return interfaceId == AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID
+ || interfaceId == AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID
+ || interfaceId == AddressListInterfaceId.IADDRESS_LIST_POLARITY_INTERFACE_ID
|| RuleTransferValidation.supportsInterface(interfaceId);
}
+ /**
+ * @inheritdoc IAddressListPolarity
+ * @dev Listed addresses are the BLOCKED ones. A consumer that reads membership as eligibility must refuse this rule.
+ */
+ function isAllowList() public pure virtual override returns (bool) {
+ return false;
+ }
+
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
diff --git a/src/rules/validation/abstract/base/RuleChainlinkPoRBase.sol b/src/rules/validation/abstract/base/RuleChainlinkPoRBase.sol
index c5114f6b..98b3a5a1 100644
--- a/src/rules/validation/abstract/base/RuleChainlinkPoRBase.sol
+++ b/src/rules/validation/abstract/base/RuleChainlinkPoRBase.sol
@@ -137,9 +137,9 @@ abstract contract RuleChainlinkPoRBase is RuleTransferValidation, ChainlinkPoRFe
if (!supplyAvailable) {
return CODE_TOTAL_SUPPLY_UNAVAILABLE;
}
- // Overflow-safe: `currentSupply + value` could exceed uint256 and this is a
- // MUST-NOT-revert ERC-1404/ERC-3643 view, so compare against the remaining headroom.
- if (currentSupply > backedSupply || value > backedSupply - currentSupply) {
+ // The comparison, the overflow-safety and the pre-update accounting assumption all live in
+ // {CapAccounting}; the reserve figure is simply this rule's cap.
+ if (_capExceededBy(currentSupply, backedSupply, value)) {
return CODE_RESERVES_EXCEEDED;
}
return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
@@ -158,6 +158,29 @@ abstract contract RuleChainlinkPoRBase is RuleTransferValidation, ChainlinkPoRFe
return _detectTransferRestriction(from, to, value);
}
+ /**
+ * @notice Restriction code for the NOTIFICATION phase, i.e. what the write hook enforces.
+ * @dev **The seam an ERC-3643 variant overrides.** Defaults to the pre-flight check, correct for a token
+ * that notifies BEFORE moving the value (CMTAT). A token that notifies AFTERWARDS reports an observation
+ * that already includes `value`, and counting it again halves the effective cap; such a variant overrides
+ * this with `_detectTransferRestriction(from, to, 0)`.
+ *
+ * The read path is deliberately NOT routed through here: a pre-flight view always runs before the movement
+ * on either kind of token, so it must always count `value`.
+ * @param from Sender address.
+ * @param to Recipient address.
+ * @param value Amount moved.
+ * @return The restriction code the write hook will enforce.
+ */
+ function _detectTransferRestrictionOnNotify(address from, address to, uint256 value)
+ internal
+ view
+ virtual
+ returns (uint8)
+ {
+ return _detectTransferRestriction(from, to, value);
+ }
+
/**
* @notice Enforces the reserve backing for a direct transfer, reverting on violation.
* @param from Sender address; the zero address denotes a mint whose backing is checked.
@@ -165,7 +188,7 @@ abstract contract RuleChainlinkPoRBase is RuleTransferValidation, ChainlinkPoRFe
* @param value Transfer amount.
*/
function _transferred(address from, address to, uint256 value) internal view virtual {
- uint8 code = _detectTransferRestriction(from, to, value);
+ uint8 code = _detectTransferRestrictionOnNotify(from, to, value);
require(
code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK),
RuleChainlinkPoR_InvalidTransfer(address(this), from, to, value, code)
@@ -180,7 +203,7 @@ abstract contract RuleChainlinkPoRBase is RuleTransferValidation, ChainlinkPoRFe
* @param value Transfer amount.
*/
function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual {
- uint8 code = _detectTransferRestrictionFrom(spender, from, to, value);
+ uint8 code = _detectTransferRestrictionOnNotify(from, to, value);
require(
code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK),
RuleChainlinkPoR_InvalidTransferFrom(address(this), spender, from, to, value, code)
diff --git a/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol b/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol
index b6340a20..5c47cf58 100644
--- a/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol
+++ b/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol
@@ -232,21 +232,20 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist
returns (uint8)
{
IIdentityRegistryVerified registry = identityRegistry;
- if (address(registry) == address(0)) {
- return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
- }
- // ERC-3643: burn bypasses all eligibility checks.
- if (to == address(0)) {
- return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+ // The guard scopes ONLY the spender check; the delegation is unconditional, as in
+ // {RuleSanctionsListBase}. Returning TRANSFER_OK here instead would silently drop any check a
+ // subclass adds by overriding {_detectTransferRestriction} alone. An unset registry and a burn
+ // (to == 0) both resolve to TRANSFER_OK inside the delegate, so no answer changes.
+ if (address(registry) == address(0) || to == address(0)) {
+ return _detectTransferRestriction(from, to, value);
}
// OPT-IN, stricter than ERC-3643 ("`transferFrom` works the same way" — receiver only).
// Mint (from == 0) is exempt: the minter acts on its own authority, not as a delegated
// ERC-20 spender. This is what makes an unverified MINTER able to mint to a verified
// recipient, exactly as the specification requires.
- // Burn (to == 0) is exempt too, but by the early return above -- do NOT re-test `to` here.
- // The condition would be dead, and re-stating it reads as though burn were handled at this
- // point rather than six lines earlier.
+ // Burn (to == 0) never reaches this line -- the guard above delegates it -- so do NOT
+ // re-test `to` here; the condition would be dead.
if (checkSpender && spender != address(0) && from != address(0) && !registry.isVerified(spender)) {
return CODE_ADDRESS_SPENDER_NOT_VERIFIED;
}
diff --git a/src/rules/validation/abstract/base/RuleMaxBalanceBase.sol b/src/rules/validation/abstract/base/RuleMaxBalanceBase.sol
index 346f5912..bc541243 100644
--- a/src/rules/validation/abstract/base/RuleMaxBalanceBase.sol
+++ b/src/rules/validation/abstract/base/RuleMaxBalanceBase.sol
@@ -156,6 +156,29 @@ abstract contract RuleMaxBalanceBase is RuleTransferValidation, BalanceCapManage
return _detectTransferRestriction(from, to, value);
}
+ /**
+ * @notice Restriction code for the NOTIFICATION phase, i.e. what the write hook enforces.
+ * @dev **The seam an ERC-3643 variant overrides.** Defaults to the pre-flight check, correct for a token
+ * that notifies BEFORE moving the value (CMTAT). A token that notifies AFTERWARDS reports an observation
+ * that already includes `value`, and counting it again halves the effective cap; such a variant overrides
+ * this with `_detectTransferRestriction(from, to, 0)`.
+ *
+ * The read path is deliberately NOT routed through here: a pre-flight view always runs before the movement
+ * on either kind of token, so it must always count `value`.
+ * @param from Sender address.
+ * @param to Recipient address.
+ * @param value Amount moved.
+ * @return The restriction code the write hook will enforce.
+ */
+ function _detectTransferRestrictionOnNotify(address from, address to, uint256 value)
+ internal
+ view
+ virtual
+ returns (uint8)
+ {
+ return _detectTransferRestriction(from, to, value);
+ }
+
/**
* @notice Enforces the cap for a direct transfer, reverting on violation.
* @param from Sender address.
@@ -163,7 +186,7 @@ abstract contract RuleMaxBalanceBase is RuleTransferValidation, BalanceCapManage
* @param value Transfer amount.
*/
function _transferred(address from, address to, uint256 value) internal view virtual {
- uint8 code = _detectTransferRestriction(from, to, value);
+ uint8 code = _detectTransferRestrictionOnNotify(from, to, value);
require(
code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK),
RuleMaxBalance_InvalidTransfer(address(this), from, to, value, code)
@@ -178,7 +201,7 @@ abstract contract RuleMaxBalanceBase is RuleTransferValidation, BalanceCapManage
* @param value Transfer amount.
*/
function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual {
- uint8 code = _detectTransferRestrictionFrom(spender, from, to, value);
+ uint8 code = _detectTransferRestrictionOnNotify(from, to, value);
require(
code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK),
RuleMaxBalance_InvalidTransferFrom(address(this), spender, from, to, value, code)
diff --git a/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol b/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol
index 473653aa..afd07132 100644
--- a/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol
+++ b/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol
@@ -122,6 +122,29 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, TotalSupplyC
return _detectTransferRestriction(from, to, value);
}
+ /**
+ * @notice Restriction code for the NOTIFICATION phase, i.e. what the write hook enforces.
+ * @dev **The seam an ERC-3643 variant overrides.** Defaults to the pre-flight check, correct for a token
+ * that notifies BEFORE moving the value (CMTAT). A token that notifies AFTERWARDS reports an observation
+ * that already includes `value`, and counting it again halves the effective cap; such a variant overrides
+ * this with `_detectTransferRestriction(from, to, 0)`.
+ *
+ * The read path is deliberately NOT routed through here: a pre-flight view always runs before the movement
+ * on either kind of token, so it must always count `value`.
+ * @param from Sender address.
+ * @param to Recipient address.
+ * @param value Amount moved.
+ * @return The restriction code the write hook will enforce.
+ */
+ function _detectTransferRestrictionOnNotify(address from, address to, uint256 value)
+ internal
+ view
+ virtual
+ returns (uint8)
+ {
+ return _detectTransferRestriction(from, to, value);
+ }
+
/**
* @notice Enforces the max-total-supply restriction for a direct transfer, reverting on violation.
* @param from Sender address; the zero address denotes a mint whose supply is checked.
@@ -129,7 +152,7 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, TotalSupplyC
* @param value Transfer amount.
*/
function _transferred(address from, address to, uint256 value) internal view virtual {
- uint8 code = _detectTransferRestriction(from, to, value);
+ uint8 code = _detectTransferRestrictionOnNotify(from, to, value);
require(
code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK),
RuleMaxTotalSupply_InvalidTransfer(address(this), from, to, value, code)
@@ -144,7 +167,7 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, TotalSupplyC
* @param value Transfer amount.
*/
function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual {
- uint8 code = _detectTransferRestrictionFrom(spender, from, to, value);
+ uint8 code = _detectTransferRestrictionOnNotify(from, to, value);
require(
code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK),
RuleMaxTotalSupply_InvalidTransferFrom(address(this), spender, from, to, value, code)
diff --git a/src/rules/validation/abstract/base/RuleReceiverWhitelistBase.sol b/src/rules/validation/abstract/base/RuleReceiverWhitelistBase.sol
index ce7cc3bf..b5716ea7 100644
--- a/src/rules/validation/abstract/base/RuleReceiverWhitelistBase.sol
+++ b/src/rules/validation/abstract/base/RuleReceiverWhitelistBase.sol
@@ -5,6 +5,7 @@ import {RuleAddressSet} from "../RuleAddressSet/RuleAddressSet.sol";
import {RuleNFTAdapter} from "../core/RuleNFTAdapter.sol";
import {RuleTransferValidation} from "../core/RuleTransferValidation.sol";
import {RuleReceiverWhitelistInvariantStorage} from "../invariant/RuleReceiverWhitelistInvariantStorage.sol";
+import {IAddressListPolarity} from "../../../interfaces/IAddressList.sol";
import {AddressListInterfaceId} from "../../../interfaces/library/AddressListInterfaceId.sol";
import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol";
import {IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol";
@@ -29,7 +30,12 @@ import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol";
* @dev There is no `allowMint` flag, unlike {RuleWhitelist}: ERC-3643 gates minting on receiver
* eligibility alone. Compose with `RuleMaxTotalSupply` or `RuleChainlinkPoR` to cap issuance.
*/
-abstract contract RuleReceiverWhitelistBase is RuleAddressSet, RuleNFTAdapter, RuleReceiverWhitelistInvariantStorage {
+abstract contract RuleReceiverWhitelistBase is
+ RuleAddressSet,
+ RuleNFTAdapter,
+ RuleReceiverWhitelistInvariantStorage,
+ IAddressListPolarity
+{
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
@@ -93,9 +99,19 @@ abstract contract RuleReceiverWhitelistBase is RuleAddressSet, RuleNFTAdapter, R
// Advertise IAddressList: this rule manages an address set and is callable through
// the IAddressList interface.
return interfaceId == AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID
+ || interfaceId == AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID
+ || interfaceId == AddressListInterfaceId.IADDRESS_LIST_POLARITY_INTERFACE_ID
|| RuleTransferValidation.supportsInterface(interfaceId);
}
+ /**
+ * @inheritdoc IAddressListPolarity
+ * @dev Listed addresses are the permitted receivers.
+ */
+ function isAllowList() public pure virtual override returns (bool) {
+ return true;
+ }
+
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
diff --git a/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol b/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol
index af9d05c6..ab187d51 100644
--- a/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol
+++ b/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol
@@ -14,6 +14,13 @@ import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol";
* @title RuleSpenderWhitelistBase
* @notice Restricts `transferFrom`-style flows to whitelisted spenders only.
* @dev Direct transfers (`transferred(from,to,value)`) are intentionally no-op.
+ *
+ * @dev **Deliberately does NOT implement {IAddressListPolarity}, and must not be made to.** Its set is
+ * an allow-list, so declaring `isAllowList() == true` would be honest about polarity and still wrong:
+ * the listed addresses are permitted **spenders**, not permitted **holders**. Declaring polarity would
+ * let `RuleWhitelistWrapper` accept this rule and then read whitelisted spenders as eligible transfer
+ * participants. Withholding the declaration is what makes the wrapper's fail-closed check refuse it.
+ * Polarity is only half the question; the other half is what the addresses are.
*/
abstract contract RuleSpenderWhitelistBase is RuleAddressSet, RuleNFTAdapter, RuleSpenderWhitelistInvariantStorage {
/*//////////////////////////////////////////////////////////////
@@ -77,6 +84,7 @@ abstract contract RuleSpenderWhitelistBase is RuleAddressSet, RuleNFTAdapter, Ru
// Advertise IAddressList: this rule manages an address set and is callable through
// the IAddressList interface.
return interfaceId == AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID
+ || interfaceId == AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID
|| RuleTransferValidation.supportsInterface(interfaceId);
}
diff --git a/src/rules/validation/abstract/base/RuleWhitelistBase.sol b/src/rules/validation/abstract/base/RuleWhitelistBase.sol
index caa7c355..a6a2ede3 100644
--- a/src/rules/validation/abstract/base/RuleWhitelistBase.sol
+++ b/src/rules/validation/abstract/base/RuleWhitelistBase.sol
@@ -5,13 +5,19 @@ import {RuleAddressSet} from "../RuleAddressSet/RuleAddressSet.sol";
import {RuleWhitelistShared} from "../core/RuleWhitelistShared.sol";
import {RuleTransferValidation} from "../core/RuleTransferValidation.sol";
import {IIdentityRegistryVerified} from "../../../interfaces/IIdentityRegistry.sol";
+import {IAddressListPolarity} from "../../../interfaces/IAddressList.sol";
import {AddressListInterfaceId} from "../../../interfaces/library/AddressListInterfaceId.sol";
/**
* @title RuleWhitelistBase
* @notice Core whitelist logic without access-control policy.
*/
-abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIdentityRegistryVerified {
+abstract contract RuleWhitelistBase is
+ RuleAddressSet,
+ RuleWhitelistShared,
+ IIdentityRegistryVerified,
+ IAddressListPolarity
+{
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
@@ -60,9 +66,19 @@ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIde
// Advertise IAddressList: this rule manages an address set and is usable as a
// child rule of RuleWhitelistWrapper, which calls it through IAddressList.
return interfaceId == AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID
+ || interfaceId == AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID
+ || interfaceId == AddressListInterfaceId.IADDRESS_LIST_POLARITY_INTERFACE_ID
|| RuleTransferValidation.supportsInterface(interfaceId);
}
+ /**
+ * @inheritdoc IAddressListPolarity
+ * @dev Listed addresses are the permitted transfer participants.
+ */
+ function isAllowList() public pure virtual override returns (bool) {
+ return true;
+ }
+
/*//////////////////////////////////////////////////////////////
ACCESS CONTROL
//////////////////////////////////////////////////////////////*/
diff --git a/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol b/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol
index 66896b09..15b73e6b 100644
--- a/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol
+++ b/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol
@@ -9,12 +9,22 @@ import {RuleTransferValidation} from "../core/RuleTransferValidation.sol";
/* ==== RuleEngine === */
import {RulesManagementModule} from "RuleEngine/modules/RulesManagementModule.sol";
/* ==== Interfaces === */
-import {IAddressList} from "../../../interfaces/IAddressList.sol";
+import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
+import {IAddressListBatchQuery, IAddressListPolarity} from "../../../interfaces/IAddressList.sol";
+import {AddressListInterfaceId} from "../../../interfaces/library/AddressListInterfaceId.sol";
import {IIdentityRegistryVerified} from "../../../interfaces/IIdentityRegistry.sol";
/**
* @title Wrapper to call several different whitelist rules (base)
- * @dev Child rules must implement {IAddressList}.
+ * @dev Child rules must implement {IAddressList} and must be ALLOW-lists.
+ *
+ * WARNING: {IAddressList} carries membership, not polarity. This wrapper ORs its children's
+ * `areAddressesListed` answers and reads `true` as ELIGIBLE. A deny-list such as `RuleBlacklist`
+ * satisfies the same interface and passes every check {addRule} performs, yet its set means the
+ * opposite: add one as a child and its blacklisted addresses become whitelisted, and {isVerified}
+ * reports them as verified investors. An ERC-165 guard would not catch this -- a blacklist advertises
+ * the same interface id, because the interface really is the same. Polarity is configuration
+ * discipline enforced by the rules manager, not by this contract. Nethermind AuditAgent NM-20.
*/
abstract contract RuleWhitelistWrapperBase is
RulesManagementModule,
@@ -213,6 +223,38 @@ abstract contract RuleWhitelistWrapperBase is
RuleWhitelistShared._transferredFrom(spender, from, to, value);
}
+ /**
+ * @notice Rejects a child rule that cannot answer the only question this wrapper asks it.
+ * @dev Mirrors `RuleEngineBase._checkRule`, which guards its own children the same way. The
+ * requirement is {IAddressListBatchQuery} — a single function — rather than the whole of
+ * {IAddressList}, because `areAddressesListed` is the only function the wrapper ever calls;
+ * demanding the full interface would also require four write functions and three further reads,
+ * excluding a read-only child that works perfectly.
+ *
+ * `ERC165Checker.supportsInterface` is itself non-reverting -- a bounded staticcall returning
+ * false for a codeless address, a missing selector or malformed return data -- so a hostile
+ * candidate cannot brick the setter screening it.
+ *
+ * WARNING: this cannot check POLARITY. A deny-list answers `areAddressesListed` just as
+ * faithfully as an allow-list and advertises the same id, so it passes here and then inverts the
+ * wrapper's meaning. Children must be allow-lists by configuration; see the contract-level note.
+ * @param rule_ The candidate child rule.
+ */
+ function _checkRule(address rule_) internal view virtual override {
+ RulesManagementModule._checkRule(rule_);
+ require(
+ ERC165Checker.supportsInterface(rule_, AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID),
+ RuleWhitelistWrapper_ChildIsNotAnAddressList(rule_)
+ );
+ // Membership alone is not enough: the child must also say what membership MEANS. Absence of the
+ // declaration is a refusal, never an assumed allow-list -- the only reading that fails closed.
+ require(
+ ERC165Checker.supportsInterface(rule_, AddressListInterfaceId.IADDRESS_LIST_POLARITY_INTERFACE_ID),
+ RuleWhitelistWrapper_ChildDoesNotDeclarePolarity(rule_)
+ );
+ require(IAddressListPolarity(rule_).isAllowList(), RuleWhitelistWrapper_ChildIsNotAnAllowList(rule_));
+ }
+
/**
* @notice Evaluates target addresses across all child rules.
* @param targetAddress Addresses to validate (from/to[/spender]).
@@ -234,7 +276,7 @@ abstract contract RuleWhitelistWrapperBase is
for (uint256 i = 0; i < rulesLength; ++i) {
// Call the whitelist rules
// Gas cost grows with the number of rules. Keep the wrapper list bounded.
- bool[] memory isListed = IAddressList(rule(i)).areAddressesListed(targetAddress);
+ bool[] memory isListed = IAddressListBatchQuery(rule(i)).areAddressesListed(targetAddress);
for (uint256 j = 0; j < targetsLength; ++j) {
if (isListed[j] && !result[j]) {
result[j] = true;
diff --git a/src/rules/validation/abstract/core/BalanceCapManager.sol b/src/rules/validation/abstract/core/BalanceCapManager.sol
index 592d12f6..982f545e 100644
--- a/src/rules/validation/abstract/core/BalanceCapManager.sol
+++ b/src/rules/validation/abstract/core/BalanceCapManager.sol
@@ -4,6 +4,7 @@ pragma solidity ^0.8.20;
import {RuleMaxBalanceInvariantStorage} from "../invariant/RuleMaxBalanceInvariantStorage.sol";
import {IBalanceOf} from "../../../interfaces/IBalanceOf.sol";
import {RuleAddressSetInternal} from "../RuleAddressSet/RuleAddressSetInternal.sol";
+import {CapAccounting} from "./CapAccounting.sol";
/**
* @title BalanceCapManager
@@ -24,7 +25,7 @@ import {RuleAddressSetInternal} from "../RuleAddressSet/RuleAddressSetInternal.s
* @dev The exemption list reuses {RuleAddressSetInternal}, so the set storage, the zero-address guard
* and the batch semantics are shared code rather than a second implementation.
*/
-abstract contract BalanceCapManager is RuleAddressSetInternal, RuleMaxBalanceInvariantStorage {
+abstract contract BalanceCapManager is CapAccounting, RuleAddressSetInternal, RuleMaxBalanceInvariantStorage {
/**
* @notice The token whose balances are observed.
* @dev Trusted to report an accurate balance; not trusted to stay callable.
@@ -212,8 +213,7 @@ abstract contract BalanceCapManager is RuleAddressSetInternal, RuleMaxBalanceInv
if (!available) {
return (false, 0);
}
- uint256 cap = maxBalance;
- return (true, balance >= cap ? 0 : cap - balance);
+ return (true, _capHeadroom(balance, maxBalance));
}
/**
@@ -261,7 +261,6 @@ abstract contract BalanceCapManager is RuleAddressSetInternal, RuleMaxBalanceInv
if (!balanceAvailable) {
return (false, false);
}
- uint256 cap = maxBalance;
- return (true, balance > cap || value > cap - balance);
+ return (true, _capExceededBy(balance, maxBalance, value));
}
}
diff --git a/src/rules/validation/abstract/core/CapAccounting.sol b/src/rules/validation/abstract/core/CapAccounting.sol
new file mode 100644
index 00000000..fdf1a6fa
--- /dev/null
+++ b/src/rules/validation/abstract/core/CapAccounting.sol
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+/**
+ * @title CapAccounting
+ * @notice The one question every cap rule ends in: would adding `value` leave an observed figure
+ * above its cap? Owns that arithmetic; knows nothing about where either number came from.
+ *
+ * @dev Declares **no storage** and no constructor, so adding it to a rule's inheritance chain cannot
+ * move a slot, and an upgradeable variant may adopt it freely.
+ *
+ * @dev Deliberately carries **no notion of pre- or post-update accounting**. Whether the observation
+ * already includes the value being moved depends on WHICH PATH is running, not on the rule: a
+ * pre-flight view always runs before the movement, while the write hook runs after it on a token that
+ * notifies afterwards. A single flag here would answer for both and silently make the pre-flight view
+ * disagree with enforcement. That distinction belongs one level up, in each rule's
+ * `_detectTransferRestrictionOnNotify` hook.
+ */
+abstract contract CapAccounting {
+ /**
+ * @notice Whether adding `value` to `observed` would pass `cap`.
+ * @dev Never reverts and never overflows: the projected total is never formed, the comparison is
+ * against the remaining headroom instead. Both matter because every caller sits on a
+ * MUST-NOT-revert ERC-1404 read path. Pass `value = 0` to ask only whether `observed` is already
+ * over the cap -- which is exactly the question a post-update notification needs to answer.
+ * @param observed The figure read for this check: a holder's balance, or a total supply.
+ * @param cap The ceiling `observed` may not pass.
+ * @param value The amount being added, or `0` when it is already counted in `observed`.
+ * @return True when the result would breach the cap.
+ */
+ function _capExceededBy(uint256 observed, uint256 cap, uint256 value) internal pure virtual returns (bool) {
+ // Already over the line whatever is added. Also guarantees the subtraction below.
+ if (observed > cap) {
+ return true;
+ }
+ return value > cap - observed;
+ }
+
+ /**
+ * @notice How much may still be added before `observed` reaches `cap`.
+ * @param observed The figure read for this check.
+ * @param cap The ceiling.
+ * @return The remaining headroom; `0` when already at or over the cap.
+ */
+ function _capHeadroom(uint256 observed, uint256 cap) internal pure virtual returns (uint256) {
+ return observed >= cap ? 0 : cap - observed;
+ }
+}
diff --git a/src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol b/src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol
index 3b371d7e..ee21cd36 100644
--- a/src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol
+++ b/src/rules/validation/abstract/core/ChainlinkPoRFeedManager.sol
@@ -7,6 +7,7 @@ import {AggregatorV3Interface} from "../../../interfaces/AggregatorV3Interface.s
import {IDecimals} from "../../../interfaces/IDecimals.sol";
import {ITotalSupply} from "../../../interfaces/ITotalSupply.sol";
import {TokenSupplyReader} from "./TokenSupplyReader.sol";
+import {CapAccounting} from "./CapAccounting.sol";
/**
* @title ChainlinkPoRFeedManager
@@ -26,7 +27,7 @@ import {TokenSupplyReader} from "./TokenSupplyReader.sol";
* both the feed and the token to have code and EIP-6780 makes it permanent: a `try` to a codeless
* address reverts *uncatchably*. Assumes a Cancun-or-later chain.
*/
-abstract contract ChainlinkPoRFeedManager is TokenSupplyReader, RuleChainlinkPoRInvariantStorage {
+abstract contract ChainlinkPoRFeedManager is CapAccounting, TokenSupplyReader, RuleChainlinkPoRInvariantStorage {
/**
* @notice The Proof of Reserve data feed consulted before every mint.
*/
@@ -207,12 +208,17 @@ abstract contract ChainlinkPoRFeedManager is TokenSupplyReader, RuleChainlinkPoR
return (CODE_RESERVES_FEED_UNAVAILABLE, 0);
}
try feed.latestRoundData() returns (uint80, int256 answer, uint256, uint256 updatedAt, uint80) {
- // A negative reserve is meaningless and `updatedAt == 0` marks a round that never completed.
- if (answer < 0 || updatedAt == 0) {
+ // Three malformed answers, not stale ones: a negative reserve is meaningless, `updatedAt == 0`
+ // marks a round that never completed, and a round stamped in the FUTURE cannot have been written
+ // by an aggregator on this chain. Rejecting the future stamp here rather than as a staleness case
+ // is deliberate -- `maxStalenessSeconds == 0` disables freshness checking, and a forged timestamp
+ // must not become acceptable because an operator chose not to police staleness.
+ if (answer < 0 || updatedAt == 0 || updatedAt > block.timestamp) {
return (CODE_RESERVES_ANSWER_INVALID, 0);
}
uint256 staleness = maxStalenessSeconds;
- if (staleness != 0 && block.timestamp > updatedAt && block.timestamp - updatedAt > staleness) {
+ // `updatedAt <= block.timestamp` is guaranteed above, so the subtraction cannot underflow.
+ if (staleness != 0 && block.timestamp - updatedAt > staleness) {
return (CODE_RESERVES_FEED_STALE, 0);
}
// `answer >= 0` was just checked, so the cast to uint256 preserves the value.
diff --git a/src/rules/validation/abstract/core/RuleNFTAdapter.sol b/src/rules/validation/abstract/core/RuleNFTAdapter.sol
index 47d70dd0..867f765e 100644
--- a/src/rules/validation/abstract/core/RuleNFTAdapter.sol
+++ b/src/rules/validation/abstract/core/RuleNFTAdapter.sol
@@ -15,6 +15,16 @@ import {ITransferContext} from "../../../interfaces/ITransferContext.sol";
* @title Rule NFT Adapter
* @notice Provides ERC-7943 overloads for rules that already implement core transfer checks.
* @dev Delegates tokenId overloads to RuleTransferValidation's internal hooks.
+ *
+ * @dev **The interfaces here signal "direct transfer" differently, and {_isDelegated} is where that is
+ * reconciled.** ERC-7943 documents its `spender` as "the address performing the transfer
+ * (owner/operator)" and {ITransferContext} documents `sender` as the token's `msg.sender`, so on BOTH
+ * an owner moving their own tokens arrives as `spender == from`. The CMTAT 3-arg/4-arg pair instead
+ * signals it with `spender == address(0)` and the 3-arg overload. Every entrypoint on this adapter
+ * therefore normalises `spender == from` to the direct hook; the 4-arg CMTAT path deliberately does
+ * NOT, because its own convention already distinguishes the two. Do not "align" them: an owner-
+ * initiated ERC-721 `transferFrom` would then be screened as a delegated transfer, which
+ * {RuleSpenderWhitelistBase} documents as always allowed.
*/
abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleComplianceExtend, ITransferContext {
/**
@@ -44,7 +54,7 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC
* @inheritdoc ITransferContext
*/
function transferred(MultiTokenTransferContext calldata ctx) external virtual override {
- if (ctx.sender != address(0) && ctx.sender != ctx.from) {
+ if (_isDelegated(ctx.sender, ctx.from)) {
_transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value);
} else {
_transferred(ctx.from, ctx.to, ctx.value);
@@ -55,7 +65,7 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC
* @inheritdoc ITransferContext
*/
function transferred(FungibleTransferContext calldata ctx) external virtual override {
- if (ctx.sender != address(0) && ctx.sender != ctx.from) {
+ if (_isDelegated(ctx.sender, ctx.from)) {
_transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value);
} else {
_transferred(ctx.from, ctx.to, ctx.value);
@@ -98,7 +108,11 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC
virtual
override(IERC7943NonFungibleComplianceExtend)
{
- _transferredFrom(spender, from, to, value);
+ if (_isDelegated(spender, from)) {
+ _transferredFrom(spender, from, to, value);
+ } else {
+ _transferred(from, to, value);
+ }
}
/**
@@ -137,7 +151,9 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC
override(IERC7943NonFungibleComplianceExtend)
returns (uint8)
{
- return _detectTransferRestrictionFrom(spender, from, to, value);
+ return _isDelegated(spender, from)
+ ? _detectTransferRestrictionFrom(spender, from, to, value)
+ : _detectTransferRestriction(from, to, value);
}
/**
@@ -176,7 +192,7 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC
override(IERC7943NonFungibleComplianceExtend)
returns (bool)
{
- return _detectTransferRestrictionFrom(spender, from, to, value)
+ return detectTransferRestrictionFrom(spender, from, to, 0, value)
== uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
}
@@ -184,6 +200,21 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
+ /**
+ * @notice Returns whether `spender` acts on behalf of `from`, rather than being `from` itself.
+ * @dev The whole adapter routes on this. `spender == from` is an owner-initiated transfer and takes
+ * the direct hook, matching what a plain `transfer` produces on the CMTAT path (`spender == 0`,
+ * 3-arg overload). Nethermind AuditAgent NM-6: the ERC-7943 overloads used to call the
+ * spender-aware hook unconditionally, so an owner-initiated ERC-721 `transferFrom` was screened as
+ * delegated while the identical {ITransferContext} call was not.
+ * @param spender Address performing the transfer, as reported by the calling interface.
+ * @param from Address the tokens leave.
+ * @return True when the transfer is delegated and the spender must be screened.
+ */
+ function _isDelegated(address spender, address from) internal pure virtual returns (bool) {
+ return spender != address(0) && spender != from;
+ }
+
/**
* @notice Internal hook for post-transfer validation or state updates.
* @param from Address tokens are transferred from.
diff --git a/src/rules/validation/abstract/core/TotalSupplyCapManager.sol b/src/rules/validation/abstract/core/TotalSupplyCapManager.sol
index 7d1b3137..c289e850 100644
--- a/src/rules/validation/abstract/core/TotalSupplyCapManager.sol
+++ b/src/rules/validation/abstract/core/TotalSupplyCapManager.sol
@@ -4,6 +4,7 @@ pragma solidity ^0.8.20;
import {RuleMaxTotalSupplyInvariantStorage} from "../invariant/RuleMaxTotalSupplyInvariantStorage.sol";
import {ITotalSupply} from "../../../interfaces/ITotalSupply.sol";
import {TokenSupplyReader} from "./TokenSupplyReader.sol";
+import {CapAccounting} from "./CapAccounting.sol";
/**
* @title TotalSupplyCapManager
@@ -18,7 +19,7 @@ import {TokenSupplyReader} from "./TokenSupplyReader.sol";
* {TokenSupplyReader} via {_supplyToken}; the deployment precondition documented there applies
* unchanged.
*/
-abstract contract TotalSupplyCapManager is TokenSupplyReader, RuleMaxTotalSupplyInvariantStorage {
+abstract contract TotalSupplyCapManager is CapAccounting, TokenSupplyReader, RuleMaxTotalSupplyInvariantStorage {
/**
* @dev tokenContract is trusted to report an *accurate* totalSupply -- nothing on-chain can
* verify that -- but it is NOT trusted to stay callable: a reverting or codeless token yields
@@ -128,7 +129,6 @@ abstract contract TotalSupplyCapManager is TokenSupplyReader, RuleMaxTotalSupply
if (!supplyAvailable) {
return (false, false);
}
- uint256 cap = maxTotalSupply;
- return (true, currentSupply > cap || value > cap - currentSupply);
+ return (true, _capExceededBy(currentSupply, maxTotalSupply, value));
}
}
diff --git a/src/rules/validation/abstract/invariant/RuleChainlinkPoRInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleChainlinkPoRInvariantStorage.sol
index 478fa657..f337e171 100644
--- a/src/rules/validation/abstract/invariant/RuleChainlinkPoRInvariantStorage.sol
+++ b/src/rules/validation/abstract/invariant/RuleChainlinkPoRInvariantStorage.sol
@@ -59,10 +59,13 @@ abstract contract RuleChainlinkPoRInvariantStorage is RuleSharedInvariantStorage
uint8 public constant CODE_RESERVES_FEED_STALE = 76;
/**
* @notice Restriction code returned when the feed responded but the answer cannot be used:
- * a negative reserve, or an incomplete round (`updatedAt == 0`).
+ * a negative reserve, an incomplete round (`updatedAt == 0`), or a round stamped in the future.
* @dev Distinct from {CODE_RESERVES_FEED_UNAVAILABLE}: here a round *was* returned, so the feed
* is reachable and the problem is the data. An operator seeing this checks whether the
* configured address is really a Proof of Reserve feed, or waits for the round to complete.
+ * @dev A future `updatedAt` is rejected here, NOT as staleness: `maxStalenessSeconds == 0` disables
+ * freshness checking, and a forged timestamp must not become acceptable because an operator chose
+ * not to police staleness.
*/
uint8 public constant CODE_RESERVES_ANSWER_INVALID = 77;
/**
diff --git a/src/rules/validation/deployment/RuleChainlinkPoRERC3643.sol b/src/rules/validation/deployment/RuleChainlinkPoRERC3643.sol
new file mode 100644
index 00000000..6db52e97
--- /dev/null
+++ b/src/rules/validation/deployment/RuleChainlinkPoRERC3643.sol
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {AggregatorV3Interface} from "../../interfaces/AggregatorV3Interface.sol";
+import {RuleChainlinkPoR} from "./RuleChainlinkPoR.sol";
+
+/**
+ * @title RuleChainlinkPoRERC3643
+ * @notice {RuleChainlinkPoR} for **ERC-3643 tokens only**. Identical reserve logic; the sole difference is WHEN the
+ * token reports the mint.
+ *
+ * @dev **Use this variant if and only if the token calls compliance AFTER it has moved the value.** ERC-3643 /
+ * T-REX does: `mint` runs `_mint` and only then `_tokenCompliance.created`, so `totalSupply()` already includes
+ * the new tokens. CMTAT calls the rule first and must use plain {RuleChainlinkPoR}.
+ *
+ * @dev **Picking the wrong variant breaks the cap silently, and nothing reverts at configuration time.** The
+ * stock rule on ERC-3643 counts the minted amount twice and rejects mints that are within the reserves reported by the feed; this variant
+ * on CMTAT ignores the pending amount and weakens enforcement.
+ *
+ * @dev Only the WRITE path is re-phased — the read views still project the pending amount, because ERC-3643
+ * calls `canTransfer` before `_mint`.
+ */
+contract RuleChainlinkPoRERC3643 is RuleChainlinkPoR {
+ /*//////////////////////////////////////////////////////////////
+ CONSTRUCTOR
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @param admin Address that receives the default admin role.
+ * @param tokenContract_ Token contract that exposes totalSupply (must be non-zero).
+ * @param tokenDecimals_ Decimals of that token (0 to 18, checked against `decimals()` when exposed).
+ * @param reservesFeed_ Proof of Reserve data feed implementing `AggregatorV3Interface`.
+ * @param maxStalenessSeconds_ Initial staleness threshold in seconds; 0 disables the check.
+ */
+ constructor(
+ address admin,
+ address tokenContract_,
+ uint8 tokenDecimals_,
+ AggregatorV3Interface reservesFeed_,
+ uint256 maxStalenessSeconds_
+ ) RuleChainlinkPoR(admin, tokenContract_, tokenDecimals_, reservesFeed_, maxStalenessSeconds_) {}
+
+ /*//////////////////////////////////////////////////////////////
+ INTERNAL FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @notice Enforcement for a token that reports the mint after performing it.
+ * @dev Re-asks the standard check with nothing left to add: `totalSupply()` already includes the
+ * minted amount, so the comparison reduces to "is the post-mint supply within the reserves".
+ * @param from Sender address; the zero address denotes the mint this rule gates.
+ * @param to Recipient address.
+ * @return The restriction code the write hook enforces.
+ */
+ function _detectTransferRestrictionOnNotify(
+ address from,
+ address to,
+ uint256 /* value */
+ )
+ internal
+ view
+ virtual
+ override
+ returns (uint8)
+ {
+ return _detectTransferRestriction(from, to, 0);
+ }
+}
diff --git a/src/rules/validation/deployment/RuleChainlinkPoRERC3643Ownable2Step.sol b/src/rules/validation/deployment/RuleChainlinkPoRERC3643Ownable2Step.sol
new file mode 100644
index 00000000..7bd3a60e
--- /dev/null
+++ b/src/rules/validation/deployment/RuleChainlinkPoRERC3643Ownable2Step.sol
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {AggregatorV3Interface} from "../../interfaces/AggregatorV3Interface.sol";
+import {RuleChainlinkPoROwnable2Step} from "./RuleChainlinkPoROwnable2Step.sol";
+
+/**
+ * @title RuleChainlinkPoRERC3643Ownable2Step
+ * @notice {RuleChainlinkPoROwnable2Step} for **ERC-3643 tokens only**. Identical reserve logic; the sole difference is WHEN the
+ * token reports the mint.
+ *
+ * @dev **Use this variant if and only if the token calls compliance AFTER it has moved the value.** ERC-3643 /
+ * T-REX does: `mint` runs `_mint` and only then `_tokenCompliance.created`, so `totalSupply()` already includes
+ * the new tokens. CMTAT calls the rule first and must use plain {RuleChainlinkPoROwnable2Step}.
+ *
+ * @dev **Picking the wrong variant breaks the cap silently, and nothing reverts at configuration time.** The
+ * stock rule on ERC-3643 counts the minted amount twice and rejects mints that are within the reserves reported by the feed; this variant
+ * on CMTAT ignores the pending amount and weakens enforcement.
+ *
+ * @dev Only the WRITE path is re-phased — the read views still project the pending amount, because ERC-3643
+ * calls `canTransfer` before `_mint`.
+ */
+contract RuleChainlinkPoRERC3643Ownable2Step is RuleChainlinkPoROwnable2Step {
+ /*//////////////////////////////////////////////////////////////
+ CONSTRUCTOR
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @param owner Contract owner.
+ * @param tokenContract_ Token contract that exposes totalSupply (must be non-zero).
+ * @param tokenDecimals_ Decimals of that token (0 to 18, checked against `decimals()` when exposed).
+ * @param reservesFeed_ Proof of Reserve data feed implementing `AggregatorV3Interface`.
+ * @param maxStalenessSeconds_ Initial staleness threshold in seconds; 0 disables the check.
+ */
+ constructor(
+ address owner,
+ address tokenContract_,
+ uint8 tokenDecimals_,
+ AggregatorV3Interface reservesFeed_,
+ uint256 maxStalenessSeconds_
+ ) RuleChainlinkPoROwnable2Step(owner, tokenContract_, tokenDecimals_, reservesFeed_, maxStalenessSeconds_) {}
+
+ /*//////////////////////////////////////////////////////////////
+ INTERNAL FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @notice Enforcement for a token that reports the mint after performing it.
+ * @dev Re-asks the standard check with nothing left to add: `totalSupply()` already includes the
+ * minted amount, so the comparison reduces to "is the post-mint supply within the reserves".
+ * @param from Sender address; the zero address denotes the mint this rule gates.
+ * @param to Recipient address.
+ * @return The restriction code the write hook enforces.
+ */
+ function _detectTransferRestrictionOnNotify(
+ address from,
+ address to,
+ uint256 /* value */
+ )
+ internal
+ view
+ virtual
+ override
+ returns (uint8)
+ {
+ return _detectTransferRestriction(from, to, 0);
+ }
+}
diff --git a/src/rules/validation/deployment/RuleMaxTotalSupplyERC3643.sol b/src/rules/validation/deployment/RuleMaxTotalSupplyERC3643.sol
new file mode 100644
index 00000000..2eef9881
--- /dev/null
+++ b/src/rules/validation/deployment/RuleMaxTotalSupplyERC3643.sol
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {RuleMaxTotalSupply} from "./RuleMaxTotalSupply.sol";
+
+/**
+ * @title RuleMaxTotalSupplyERC3643
+ * @notice {RuleMaxTotalSupply} for **ERC-3643 tokens only**. Identical supply-cap logic; the sole difference is WHEN the
+ * token reports the mint.
+ *
+ * @dev **Use this variant if and only if the token calls compliance AFTER it has moved the value.** ERC-3643 /
+ * T-REX does: `mint` runs `_mint` and only then `_tokenCompliance.created`, so `totalSupply()` already includes
+ * the new tokens. CMTAT calls the rule first and must use plain {RuleMaxTotalSupply}.
+ *
+ * @dev **Picking the wrong variant breaks the cap silently, and nothing reverts at configuration time.** The
+ * stock rule on ERC-3643 counts the minted amount twice and rejects mints that are within the configured ceiling; this variant
+ * on CMTAT ignores the pending amount and weakens enforcement.
+ *
+ * @dev Only the WRITE path is re-phased — the read views still project the pending amount, because ERC-3643
+ * calls `canTransfer` before `_mint`.
+ */
+contract RuleMaxTotalSupplyERC3643 is RuleMaxTotalSupply {
+ /*//////////////////////////////////////////////////////////////
+ CONSTRUCTOR
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @param admin Address that receives the default admin role.
+ * @param tokenContract_ Token contract that exposes totalSupply (must be non-zero).
+ * @param maxTotalSupply_ Initial maximum supply.
+ */
+ constructor(address admin, address tokenContract_, uint256 maxTotalSupply_)
+ RuleMaxTotalSupply(admin, tokenContract_, maxTotalSupply_)
+ {}
+
+ /*//////////////////////////////////////////////////////////////
+ INTERNAL FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @notice Enforcement for a token that reports the mint after performing it.
+ * @dev Re-asks the standard check with nothing left to add: `totalSupply()` already includes the
+ * minted amount, so the comparison reduces to "is the post-mint supply within the ceiling".
+ * @param from Sender address; the zero address denotes the mint this rule gates.
+ * @param to Recipient address.
+ * @return The restriction code the write hook enforces.
+ */
+ function _detectTransferRestrictionOnNotify(
+ address from,
+ address to,
+ uint256 /* value */
+ )
+ internal
+ view
+ virtual
+ override
+ returns (uint8)
+ {
+ return _detectTransferRestriction(from, to, 0);
+ }
+}
diff --git a/src/rules/validation/deployment/RuleMaxTotalSupplyERC3643Ownable2Step.sol b/src/rules/validation/deployment/RuleMaxTotalSupplyERC3643Ownable2Step.sol
new file mode 100644
index 00000000..c3ee962b
--- /dev/null
+++ b/src/rules/validation/deployment/RuleMaxTotalSupplyERC3643Ownable2Step.sol
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {RuleMaxTotalSupplyOwnable2Step} from "./RuleMaxTotalSupplyOwnable2Step.sol";
+
+/**
+ * @title RuleMaxTotalSupplyERC3643Ownable2Step
+ * @notice {RuleMaxTotalSupplyOwnable2Step} for **ERC-3643 tokens only**. Identical supply-cap logic; the sole difference is WHEN the
+ * token reports the mint.
+ *
+ * @dev **Use this variant if and only if the token calls compliance AFTER it has moved the value.** ERC-3643 /
+ * T-REX does: `mint` runs `_mint` and only then `_tokenCompliance.created`, so `totalSupply()` already includes
+ * the new tokens. CMTAT calls the rule first and must use plain {RuleMaxTotalSupplyOwnable2Step}.
+ *
+ * @dev **Picking the wrong variant breaks the cap silently, and nothing reverts at configuration time.** The
+ * stock rule on ERC-3643 counts the minted amount twice and rejects mints that are within the configured ceiling; this variant
+ * on CMTAT ignores the pending amount and weakens enforcement.
+ *
+ * @dev Only the WRITE path is re-phased — the read views still project the pending amount, because ERC-3643
+ * calls `canTransfer` before `_mint`.
+ */
+contract RuleMaxTotalSupplyERC3643Ownable2Step is RuleMaxTotalSupplyOwnable2Step {
+ /*//////////////////////////////////////////////////////////////
+ CONSTRUCTOR
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @param owner Contract owner.
+ * @param tokenContract_ Token contract that exposes totalSupply (must be non-zero).
+ * @param maxTotalSupply_ Initial maximum supply.
+ */
+ constructor(address owner, address tokenContract_, uint256 maxTotalSupply_)
+ RuleMaxTotalSupplyOwnable2Step(owner, tokenContract_, maxTotalSupply_)
+ {}
+
+ /*//////////////////////////////////////////////////////////////
+ INTERNAL FUNCTIONS
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @notice Enforcement for a token that reports the mint after performing it.
+ * @dev Re-asks the standard check with nothing left to add: `totalSupply()` already includes the
+ * minted amount, so the comparison reduces to "is the post-mint supply within the ceiling".
+ * @param from Sender address; the zero address denotes the mint this rule gates.
+ * @param to Recipient address.
+ * @return The restriction code the write hook enforces.
+ */
+ function _detectTransferRestrictionOnNotify(
+ address from,
+ address to,
+ uint256 /* value */
+ )
+ internal
+ view
+ virtual
+ override
+ returns (uint8)
+ {
+ return _detectTransferRestriction(from, to, 0);
+ }
+}
diff --git a/test/CapAccounting/ERC3643CapSeams.t.sol b/test/CapAccounting/ERC3643CapSeams.t.sol
new file mode 100644
index 00000000..fce7e777
--- /dev/null
+++ b/test/CapAccounting/ERC3643CapSeams.t.sol
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {HelperContract} from "../HelperContract.sol";
+import {
+ ERC3643ChainlinkPoRHarness,
+ ERC3643MaxBalanceHarness,
+ ERC3643MaxTotalSupplyHarness,
+ TrackedSupplyHarness
+} from "src/mocks/harness/ERC3643CapHarnesses.sol";
+import {AggregatorV3Mock} from "src/mocks/AggregatorV3Mock.sol";
+import {AggregatorV3Interface} from "src/rules/interfaces/AggregatorV3Interface.sol";
+import {BalanceOfMock} from "src/mocks/BalanceOfMock.sol";
+import {RuleChainlinkPoR} from "src/rules/validation/deployment/RuleChainlinkPoR.sol";
+import {RuleMaxBalance} from "src/rules/validation/deployment/RuleMaxBalance.sol";
+import {RuleMaxTotalSupply} from "src/rules/validation/deployment/RuleMaxTotalSupply.sol";
+import {TotalSupplyMock} from "src/mocks/TotalSupplyMock.sol";
+
+/**
+ * @title ERC3643CapSeams
+ * @notice Proves the two seams the cap rules expose are sufficient to build an ERC-3643 variant,
+ * without changing what the stock (CMTAT) rules do.
+ * @dev The three cap rules assume the token calls them BEFORE moving the value, so the observation
+ * still excludes it. ERC-3643 / T-REX calls AFTER — `Token.transfer` runs `_transfer` then
+ * `_tokenCompliance.transferred`, and `mint` runs `_mint` then `created` — so the observation
+ * already includes it and the stock rule counts it twice, rejecting transfers that are within
+ * the cap (Nethermind AuditAgent NM-11).
+ *
+ * Each test below simulates both call orders against the same cap and asserts:
+ * - the stock rule is correct pre-update and double-counts post-update;
+ * - the harness, which overrides one hook, is correct post-update;
+ * - neither rule ever admits anything ABOVE the cap.
+ */
+contract ERC3643CapSeams is Test, HelperContract {
+ uint256 private constant CAP = 1000;
+
+ /*//////////////////////////////////////////////////////////////
+ SEAM 1 — MAX TOTAL SUPPLY
+ //////////////////////////////////////////////////////////////*/
+
+ function testMaxTotalSupply_StockRuleDoubleCountsUnderPostUpdateAccounting() public {
+ TotalSupplyMock token = new TotalSupplyMock();
+ RuleMaxTotalSupply rule = new RuleMaxTotalSupply(DEFAULT_ADMIN_ADDRESS, address(token), CAP);
+
+ // Pre-update (CMTAT): supply still 0 when the rule is called. A mint of exactly the cap fits.
+ token.setTotalSupply(0);
+ assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, CAP), TRANSFER_OK);
+
+ // Post-update (T-REX): the mint already landed, so totalSupply == CAP when the rule is called.
+ // The stock rule adds CAP again and rejects a mint that exactly fills the cap.
+ token.setTotalSupply(CAP);
+ assertEq(
+ rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, CAP),
+ CODE_MAX_TOTAL_SUPPLY_EXCEEDED,
+ "NM-11: the stock rule counts the value twice on a post-update token"
+ );
+ }
+
+ function testMaxTotalSupply_Erc3643HarnessIsCorrectUnderPostUpdateAccounting() public {
+ TotalSupplyMock token = new TotalSupplyMock();
+ ERC3643MaxTotalSupplyHarness rule = new ERC3643MaxTotalSupplyHarness(DEFAULT_ADMIN_ADDRESS, address(token), CAP);
+
+ // A mint that exactly fills the cap: post-mint supply == CAP, which is allowed.
+ token.setTotalSupply(CAP);
+ vm.prank(address(token));
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, CAP);
+
+ // One unit more: post-mint supply == CAP + 1, which is not.
+ token.setTotalSupply(CAP + 1);
+ vm.prank(address(token));
+ vm.expectRevert();
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, 1);
+ }
+
+ function testMaxTotalSupply_PreFlightViewStillCountsTheValue() public {
+ // The read path must NOT be re-phased: a pre-flight query always runs before the movement,
+ // on either kind of token, so it still has to add `value`.
+ TotalSupplyMock token = new TotalSupplyMock();
+ ERC3643MaxTotalSupplyHarness rule = new ERC3643MaxTotalSupplyHarness(DEFAULT_ADMIN_ADDRESS, address(token), CAP);
+
+ token.setTotalSupply(CAP);
+ assertEq(
+ rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, 1),
+ CODE_MAX_TOTAL_SUPPLY_EXCEEDED,
+ "pre-flight must still project the pending value"
+ );
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ SEAM 1 — MAX BALANCE
+ //////////////////////////////////////////////////////////////*/
+
+ function testMaxBalance_StockRuleDoubleCountsUnderPostUpdateAccounting() public {
+ BalanceOfMock token = new BalanceOfMock();
+ RuleMaxBalance rule = new RuleMaxBalance(DEFAULT_ADMIN_ADDRESS, address(token), CAP);
+
+ token.setBalance(ADDRESS1, 0);
+ assertEq(rule.detectTransferRestriction(ADDRESS2, ADDRESS1, CAP), TRANSFER_OK);
+
+ token.setBalance(ADDRESS1, CAP);
+ assertEq(
+ rule.detectTransferRestriction(ADDRESS2, ADDRESS1, CAP),
+ rule.CODE_MAX_BALANCE_EXCEEDED(),
+ "NM-11: the stock rule counts the value twice on a post-update token"
+ );
+ }
+
+ function testMaxBalance_Erc3643HarnessIsCorrectUnderPostUpdateAccounting() public {
+ BalanceOfMock token = new BalanceOfMock();
+ ERC3643MaxBalanceHarness rule = new ERC3643MaxBalanceHarness(DEFAULT_ADMIN_ADDRESS, address(token), CAP);
+
+ token.setBalance(ADDRESS1, CAP);
+ vm.prank(address(token));
+ rule.transferred(ADDRESS2, ADDRESS1, CAP);
+
+ token.setBalance(ADDRESS1, CAP + 1);
+ vm.prank(address(token));
+ vm.expectRevert();
+ rule.transferred(ADDRESS2, ADDRESS1, 1);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ SEAM 1 — CHAINLINK PROOF OF RESERVE
+ //////////////////////////////////////////////////////////////*/
+
+ function testChainlinkPoR_Erc3643HarnessIsCorrectUnderPostUpdateAccounting() public {
+ TotalSupplyMock token = new TotalSupplyMock();
+ AggregatorV3Mock feed = new AggregatorV3Mock(0, int256(CAP));
+
+ RuleChainlinkPoR stock =
+ new RuleChainlinkPoR(DEFAULT_ADMIN_ADDRESS, address(token), 0, AggregatorV3Interface(address(feed)), 0);
+ ERC3643ChainlinkPoRHarness harness = new ERC3643ChainlinkPoRHarness(
+ DEFAULT_ADMIN_ADDRESS, address(token), 0, AggregatorV3Interface(address(feed)), 0
+ );
+
+ // Reserves back exactly CAP. Post-mint supply is CAP, so the mint is fully backed.
+ token.setTotalSupply(CAP);
+ assertEq(
+ stock.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, CAP),
+ CODE_RESERVES_EXCEEDED,
+ "NM-11: the stock rule counts the minted value twice against the reserves"
+ );
+ vm.prank(address(token));
+ harness.transferred(ZERO_ADDRESS, ADDRESS1, CAP);
+
+ // Minting past the reserves is still rejected by the harness.
+ token.setTotalSupply(CAP + 1);
+ vm.prank(address(token));
+ vm.expectRevert();
+ harness.transferred(ZERO_ADDRESS, ADDRESS1, 1);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ SEAM 2 — OBSERVATION SOURCE
+ //////////////////////////////////////////////////////////////*/
+
+ function testTrackedSupply_ObservationCanComeFromTheRuleInsteadOfTheToken() public {
+ TotalSupplyMock token = new TotalSupplyMock();
+ TrackedSupplyHarness rule = new TrackedSupplyHarness(DEFAULT_ADMIN_ADDRESS, address(token), CAP);
+
+ // The token reports a supply the rule must ignore entirely.
+ token.setTotalSupply(type(uint256).max);
+
+ rule.setTrackedSupply(0);
+ assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, CAP), TRANSFER_OK);
+
+ rule.setTrackedSupply(CAP);
+ assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, 1), CODE_MAX_TOTAL_SUPPLY_EXCEEDED);
+ }
+}
diff --git a/test/ERC3643Compliance/ERC3643RuleEngineWhitelist.t.sol b/test/ERC3643Compliance/ERC3643RuleEngineWhitelist.t.sol
index 5ad8399d..1d14976b 100644
--- a/test/ERC3643Compliance/ERC3643RuleEngineWhitelist.t.sol
+++ b/test/ERC3643Compliance/ERC3643RuleEngineWhitelist.t.sol
@@ -22,7 +22,7 @@ import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol";
* Wiring, transcribed from `Token.setCompliance` (`Token.sol:515-522`): the token calls
* `bindToken(address(this))` on the compliance contract **itself**, so the engine needs
* `setTokenSelfBindingApproval(token, true)` beforehand. That path exists in
- * `ERC3643ComplianceExtendedModule._authorizeComplianceBindingChange` specifically for
+ * `TokenBindingExtendedModule._authorizeTokenBindingChange` specifically for
* ERC-3643 compatibility.
*/
contract ERC3643RuleEngineWhitelist is Test, HelperContract, IdentityRegistryWhitelistInvariantStorage {
diff --git a/test/ERC3643Real/ERC3643RealTokenChainlinkPoR.t.sol b/test/ERC3643Real/ERC3643RealTokenChainlinkPoR.t.sol
new file mode 100644
index 00000000..e15dc25d
--- /dev/null
+++ b/test/ERC3643Real/ERC3643RealTokenChainlinkPoR.t.sol
@@ -0,0 +1,346 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity 0.8.30;
+
+import {Test} from "forge-std/Test.sol";
+import {ComplianceNotFollowed, Token} from "ERC3643/token/Token.sol";
+import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol";
+import {AggregatorV3Mock} from "src/mocks/AggregatorV3Mock.sol";
+import {IdentityRegistryWhitelist} from "src/registry/IdentityRegistryWhitelist.sol";
+import {AggregatorV3Interface} from "src/rules/interfaces/AggregatorV3Interface.sol";
+import {
+ RuleChainlinkPoRInvariantStorage
+} from "src/rules/validation/abstract/invariant/RuleChainlinkPoRInvariantStorage.sol";
+import {RuleChainlinkPoR} from "src/rules/validation/deployment/RuleChainlinkPoR.sol";
+import {RuleChainlinkPoRERC3643} from "src/rules/validation/deployment/RuleChainlinkPoRERC3643.sol";
+
+/**
+ * @title Proof of Reserve against the REAL vendored ERC-3643 token
+ * @notice Deploys the genuine `Token` from `lib/ERC-3643/` (4.2.0-beta1) and drives it through:
+ *
+ * real ERC-3643 Token ── compliance slot ──▶ RuleEngine ──▶ RuleChainlinkPoR[ERC3643]
+ * └─ identity slot ────▶ IdentityRegistryWhitelist
+ *
+ * @dev The point of this suite is the ORDER in which the real token consults compliance on a mint:
+ *
+ * ```solidity
+ * function mint(address _to, uint256 _amount) public onlyAgent {
+ * require(_tokenCompliance.canTransfer(address(0), _to, _amount), ComplianceNotFollowed());
+ * _mint(_to, _amount); // <-- supply changes HERE
+ * _tokenCompliance.created(_to, _amount); // <-- rule notified AFTERWARDS
+ * }
+ * ```
+ *
+ * One transaction, both paths, different accounting. `canTransfer` runs BEFORE the mint, so it
+ * must project `_amount`; `created` runs AFTER, and `RuleEngine` forwards it as the three-argument
+ * `transferred(address(0), to, value)`, by which point `totalSupply()` already includes `_amount`.
+ * {RuleChainlinkPoRERC3643} re-phases only the second. The stock {RuleChainlinkPoR}, built for
+ * CMTAT (which calls the rule first), counts the amount twice and reverts a fully backed mint --
+ * `testStockRuleRevertsAFullyBackedMint` is that regression, run against the real token rather
+ * than a mock.
+ *
+ * @dev Built by the dedicated profile because `Token.sol` pins `pragma solidity 0.8.30` exactly:
+ *
+ * FOUNDRY_PROFILE=erc3643 forge test
+ */
+contract ERC3643RealTokenChainlinkPoR is Test, RuleChainlinkPoRInvariantStorage {
+ address private constant ADMIN = address(1);
+ address private constant AGENT = address(10);
+ address private constant INVESTOR = address(11);
+ address private constant INVESTOR2 = address(12);
+
+ /// @dev Feed and token both report 0 decimals, so a reserve answer is a token amount as-is.
+ uint256 private constant RESERVES = 1000;
+ uint8 private constant TRANSFER_OK_CODE = 0;
+
+ IdentityRegistryWhitelist private registry;
+ AggregatorV3Mock private feed;
+ RuleEngine private engine;
+ Token private token;
+
+ function setUp() public {
+ token = new Token();
+ feed = new AggregatorV3Mock(0, int256(RESERVES));
+
+ vm.startPrank(ADMIN);
+ registry = new IdentityRegistryWhitelist(ADMIN);
+ engine = new RuleEngine(ADMIN, address(0), address(0));
+ vm.stopPrank();
+
+ _wire();
+ }
+
+ /// @dev Everything except which rule sits in the engine; the rule is added per test.
+ function _wire() private {
+ vm.prank(ADMIN);
+ engine.setTokenSelfBindingApproval(address(token), true);
+
+ token.init(address(registry), address(engine), "Real ERC-3643 PoR", "R3643", 0, address(0));
+
+ token.addAgent(AGENT);
+ vm.prank(AGENT);
+ token.unpause();
+
+ bytes32 registrarRole = registry.IDENTITY_REGISTRAR_ROLE();
+ vm.startPrank(ADMIN);
+ registry.grantRole(registrarRole, AGENT);
+ registry.grantRole(registrarRole, address(token));
+ vm.stopPrank();
+
+ vm.startPrank(AGENT);
+ registry.registerIdentity(INVESTOR, address(0), 0);
+ registry.registerIdentity(INVESTOR2, address(0), 0);
+ vm.stopPrank();
+ }
+
+ function _useErc3643Rule() private returns (RuleChainlinkPoRERC3643 rule) {
+ rule = new RuleChainlinkPoRERC3643(ADMIN, address(token), 0, AggregatorV3Interface(address(feed)), 0);
+ vm.prank(ADMIN);
+ engine.addRule(rule);
+ }
+
+ function _useStockRule() private returns (RuleChainlinkPoR rule) {
+ rule = new RuleChainlinkPoR(ADMIN, address(token), 0, AggregatorV3Interface(address(feed)), 0);
+ vm.prank(ADMIN);
+ engine.addRule(rule);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ THE ERC-3643 VARIANT
+ //////////////////////////////////////////////////////////////*/
+
+ function testMintUpToTheReservesSucceeds() public {
+ _useErc3643Rule();
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, RESERVES);
+
+ assertEq(token.totalSupply(), RESERVES, "a mint of exactly the backed supply must land");
+ assertEq(token.balanceOf(INVESTOR), RESERVES);
+ }
+
+ function testMintBeyondTheReservesIsRejected() public {
+ _useErc3643Rule();
+
+ // Blocked by the pre-flight `canTransfer` the token runs before `_mint`.
+ vm.prank(AGENT);
+ vm.expectRevert(ComplianceNotFollowed.selector);
+ token.mint(INVESTOR, RESERVES + 1);
+
+ assertEq(token.totalSupply(), 0, "nothing may be issued past the reserves");
+ }
+
+ function testIncrementalMintsShareTheSameReserveCeiling() public {
+ _useErc3643Rule();
+
+ vm.startPrank(AGENT);
+ token.mint(INVESTOR, 600);
+ token.mint(INVESTOR2, 400);
+ vm.stopPrank();
+ assertEq(token.totalSupply(), RESERVES);
+
+ // The reserves are now fully committed; one more unit is not backed.
+ vm.prank(AGENT);
+ vm.expectRevert(ComplianceNotFollowed.selector);
+ token.mint(INVESTOR, 1);
+ }
+
+ function testRaisingTheReservesRaisesTheCeiling() public {
+ _useErc3643Rule();
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, RESERVES);
+
+ feed.setAnswer(int256(RESERVES * 2));
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, RESERVES);
+ assertEq(token.totalSupply(), RESERVES * 2);
+ }
+
+ function testTransfersAndBurnsAreNeverGatedByTheFeed() public {
+ _useErc3643Rule();
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, RESERVES);
+
+ // Reserves collapse to nothing: issuance stops, holders stay mobile.
+ feed.setAnswer(0);
+
+ vm.prank(INVESTOR);
+ token.transfer(INVESTOR2, 400);
+ assertEq(token.balanceOf(INVESTOR2), 400);
+
+ vm.prank(AGENT);
+ token.burn(INVESTOR2, 400);
+ assertEq(token.totalSupply(), RESERVES - 400);
+
+ vm.prank(AGENT);
+ vm.expectRevert(ComplianceNotFollowed.selector);
+ token.mint(INVESTOR, 1);
+ }
+
+ function testAStaleFeedStopsIssuanceButNotHolders() public {
+ RuleChainlinkPoRERC3643 rule = _useErc3643Rule();
+
+ vm.prank(ADMIN);
+ rule.setMaxStalenessSeconds(1 hours);
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, 100);
+
+ vm.warp(block.timestamp + 2 hours);
+
+ vm.prank(AGENT);
+ vm.expectRevert(ComplianceNotFollowed.selector);
+ token.mint(INVESTOR, 1);
+
+ // ...but the existing holder can still move and exit.
+ vm.prank(INVESTOR);
+ token.transfer(INVESTOR2, 100);
+ assertEq(token.balanceOf(INVESTOR2), 100);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ WHY THE STOCK RULE IS NOT USABLE HERE (NM-11)
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @notice The stock, CMTAT-shaped rule double-counts the mint on a real ERC-3643 token.
+ * @dev `canTransfer` passes (supply 0 + 1000 <= 1000), the token mints, and then `created` finds
+ * `totalSupply() == 1000` and adds the amount a second time. The mint reverts even though it
+ * is exactly and fully backed -- the pre-flight answer and enforcement disagree inside one
+ * transaction.
+ */
+ function testStockRuleRevertsAFullyBackedMint() public {
+ _useStockRule();
+
+ vm.prank(AGENT);
+ vm.expectRevert();
+ token.mint(INVESTOR, RESERVES);
+
+ assertEq(token.totalSupply(), 0, "the fully backed mint was rejected");
+ }
+
+ /**
+ * @notice The stock rule halves the largest SINGLE mint it will accept.
+ * @dev Every mint has its amount counted twice -- once by the post-mint `totalSupply()` and once
+ * as `value` -- so from an empty supply the ceiling on one mint is `RESERVES / 2`. Note the
+ * damage is not a uniform halving of the cap: a series of small mints can still creep up to
+ * the full reserves, since only the amount in flight is double-counted. What is guaranteed
+ * is that some fully backed mints are refused, and which ones depends on how issuance is
+ * chunked -- a worse failure mode than a plainly halved cap, because it looks intermittent.
+ */
+ function testStockRuleHalvesTheLargestSingleMint() public {
+ _useStockRule();
+
+ // One over half the reserves: post-mint supply 501 plus 501 again exceeds 1000.
+ vm.prank(AGENT);
+ vm.expectRevert();
+ token.mint(INVESTOR, RESERVES / 2 + 1);
+
+ // Exactly half is the most it will take in one go.
+ vm.prank(AGENT);
+ token.mint(INVESTOR, RESERVES / 2);
+ assertEq(token.totalSupply(), RESERVES / 2);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ THE READ PATH IS NOT RE-PHASED
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @notice The ERC-3643 variant still projects the pending amount on the read path.
+ * @dev The token itself depends on this: it calls `canTransfer(address(0), to, amount)` BEFORE
+ * `_mint`, so a view that ignored `amount` would wave through a mint the write hook then
+ * reverts. Only the notification is re-phased.
+ */
+ function testPreFlightViewStillProjectsThePendingAmount() public {
+ RuleChainlinkPoRERC3643 rule = _useErc3643Rule();
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, RESERVES);
+
+ assertEq(
+ rule.detectTransferRestriction(address(0), INVESTOR, 1),
+ CODE_RESERVES_EXCEEDED,
+ "pre-flight must still count the amount being requested"
+ );
+ assertFalse(rule.canTransfer(address(0), INVESTOR, 1));
+
+ // And it agrees with the token's own pre-flight consultation.
+ assertFalse(engine.canTransfer(address(0), INVESTOR, 1));
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ THE SUPPLY READ ITSELF
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @notice Code 78 (`CODE_TOTAL_SUPPLY_UNAVAILABLE`) is unreachable against a directly deployed
+ * ERC-3643 token, so the guarded read costs nothing but is not dead weight either.
+ * @dev `Token.totalSupply()` is `external view { return _totalSupply; }` — no modifier, no
+ * external call, so it cannot revert and `_currentSupply()` always reports available. The
+ * branch still earns its place: the standard T-REX deployment puts the token behind a
+ * `TokenProxy` whose implementation is resolved through an `ImplementationAuthority`, and a
+ * proxy repointed at a bad implementation *can* make `totalSupply()` revert. The rule then
+ * returns 78 and blocks minting rather than breaking the MUST-NOT-revert views.
+ */
+ function testSupplyIsAlwaysReadableOnADirectlyDeployedToken() public {
+ RuleChainlinkPoRERC3643 rule = _useErc3643Rule();
+
+ assertEq(rule.detectTransferRestriction(address(0), INVESTOR, 1), TRANSFER_OK_CODE);
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, RESERVES);
+
+ // Still readable with a non-zero supply; the ceiling, not the read, is what now binds.
+ assertEq(rule.detectTransferRestriction(address(0), INVESTOR, 1), CODE_RESERVES_EXCEEDED);
+ }
+
+ /**
+ * @notice DEPLOYMENT ORDER: build the rule AFTER `Token.init`, or its cached decimals are wrong.
+ * @dev ERC-3643 deploys then initialises, and an uninitialised `Token` reports `decimals() == 0`.
+ * The rule's constructor probes `decimals()` and accepts a matching `0`, so a rule built
+ * first is happily configured for a 0-decimals token — and then `init(..., 18, ...)` makes it
+ * an 18-decimals token while the rule still believes 0. Nothing reverts and no event marks
+ * it; the reserve answer is simply scaled by `10 ** 18` too little, and every mint is
+ * refused. The same mistake with the decimals reversed would over-mint instead.
+ *
+ * There is no on-chain fix: the constructor probe genuinely succeeded. The remedy is
+ * ordering (construct after `init`) or calling `setTokenMetadata` afterwards to re-sync.
+ */
+ function testRuleBuiltBeforeInitCachesTheWrongDecimals() public {
+ Token fresh = new Token();
+ assertEq(fresh.decimals(), 0, "an uninitialised token reports 0 decimals");
+
+ AggregatorV3Mock scaledFeed = new AggregatorV3Mock(8, int256(RESERVES * 1e8));
+ RuleChainlinkPoRERC3643 early =
+ new RuleChainlinkPoRERC3643(ADMIN, address(fresh), 0, AggregatorV3Interface(address(scaledFeed)), 0);
+
+ RuleEngine freshEngine = new RuleEngine(ADMIN, address(0), address(0));
+ vm.prank(ADMIN);
+ freshEngine.setTokenSelfBindingApproval(address(fresh), true);
+ fresh.init(address(registry), address(freshEngine), "Late init", "LATE", 18, address(0));
+
+ assertEq(fresh.decimals(), 18, "the token is now an 18-decimals token");
+ assertEq(early.tokenDecimals(), 0, "but the rule still believes 0");
+
+ (, uint256 backed) = early.maxBackedSupply();
+ assertEq(backed, RESERVES, "reserves scaled into 0 decimals");
+
+ // Re-syncing after init is the operator-side remedy.
+ vm.prank(ADMIN);
+ early.setTokenMetadata(address(fresh), 18);
+ (, uint256 corrected) = early.maxBackedSupply();
+ assertEq(corrected, RESERVES * 1e18, "and now the ceiling is in the token's own units");
+ }
+
+ /// @notice The variant reports the same reserve ceiling as the stock rule; only enforcement differs.
+ function testMaxBackedSupplyIsUnchanged() public {
+ RuleChainlinkPoRERC3643 rule = _useErc3643Rule();
+
+ (uint8 code, uint256 backed) = rule.maxBackedSupply();
+ assertEq(code, 0);
+ assertEq(backed, RESERVES);
+ }
+}
diff --git a/test/ERC3643Real/ERC3643RealTokenMaxTotalSupply.t.sol b/test/ERC3643Real/ERC3643RealTokenMaxTotalSupply.t.sol
new file mode 100644
index 00000000..12a613f2
--- /dev/null
+++ b/test/ERC3643Real/ERC3643RealTokenMaxTotalSupply.t.sol
@@ -0,0 +1,258 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity 0.8.30;
+
+import {Test} from "forge-std/Test.sol";
+import {ComplianceNotFollowed, Token} from "ERC3643/token/Token.sol";
+import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol";
+import {AggregatorV3Mock} from "src/mocks/AggregatorV3Mock.sol";
+import {IdentityRegistryWhitelist} from "src/registry/IdentityRegistryWhitelist.sol";
+import {AggregatorV3Interface} from "src/rules/interfaces/AggregatorV3Interface.sol";
+import {
+ RuleMaxTotalSupplyInvariantStorage
+} from "src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol";
+import {RuleChainlinkPoRERC3643} from "src/rules/validation/deployment/RuleChainlinkPoRERC3643.sol";
+import {RuleMaxTotalSupply} from "src/rules/validation/deployment/RuleMaxTotalSupply.sol";
+import {RuleMaxTotalSupplyERC3643} from "src/rules/validation/deployment/RuleMaxTotalSupplyERC3643.sol";
+
+/**
+ * @title Max total supply against the REAL vendored ERC-3643 token
+ * @notice Deploys the genuine `Token` from `lib/ERC-3643/` (4.2.0-beta1) and drives it through:
+ *
+ * real ERC-3643 Token ── compliance slot ──▶ RuleEngine ──▶ RuleMaxTotalSupply[ERC3643]
+ * └─ identity slot ────▶ IdentityRegistryWhitelist
+ *
+ * @dev Same accounting question as the Proof-of-Reserve suite: the token calls compliance on BOTH
+ * sides of the mint --
+ *
+ * ```solidity
+ * require(_tokenCompliance.canTransfer(address(0), _to, _amount), ComplianceNotFollowed());
+ * _mint(_to, _amount); // supply changes HERE
+ * _tokenCompliance.created(_to, _amount); // rule notified AFTERWARDS
+ * ```
+ *
+ * -- and `RuleEngine` forwards `created` as the three-argument `transferred(address(0), to, value)`.
+ * {RuleMaxTotalSupplyERC3643} re-phases only the notification.
+ *
+ * @dev The last section covers the composition the documentation prescribes: {RuleChainlinkPoRERC3643}
+ * has no margin parameter, so a static ceiling is added by putting both rules in the same engine.
+ *
+ * @dev Run with the dedicated profile: `FOUNDRY_PROFILE=erc3643 forge test`.
+ */
+contract ERC3643RealTokenMaxTotalSupply is Test, RuleMaxTotalSupplyInvariantStorage {
+ address private constant ADMIN = address(1);
+ address private constant AGENT = address(10);
+ address private constant INVESTOR = address(11);
+ address private constant INVESTOR2 = address(12);
+
+ uint256 private constant CAP = 1000;
+
+ IdentityRegistryWhitelist private registry;
+ RuleEngine private engine;
+ Token private token;
+
+ function setUp() public {
+ token = new Token();
+
+ vm.startPrank(ADMIN);
+ registry = new IdentityRegistryWhitelist(ADMIN);
+ engine = new RuleEngine(ADMIN, address(0), address(0));
+ vm.stopPrank();
+
+ vm.prank(ADMIN);
+ engine.setTokenSelfBindingApproval(address(token), true);
+
+ token.init(address(registry), address(engine), "Real ERC-3643 Cap", "R3643", 0, address(0));
+
+ token.addAgent(AGENT);
+ vm.prank(AGENT);
+ token.unpause();
+
+ bytes32 registrarRole = registry.IDENTITY_REGISTRAR_ROLE();
+ vm.startPrank(ADMIN);
+ registry.grantRole(registrarRole, AGENT);
+ registry.grantRole(registrarRole, address(token));
+ vm.stopPrank();
+
+ vm.startPrank(AGENT);
+ registry.registerIdentity(INVESTOR, address(0), 0);
+ registry.registerIdentity(INVESTOR2, address(0), 0);
+ vm.stopPrank();
+ }
+
+ function _useErc3643Rule() private returns (RuleMaxTotalSupplyERC3643 rule) {
+ rule = new RuleMaxTotalSupplyERC3643(ADMIN, address(token), CAP);
+ vm.prank(ADMIN);
+ engine.addRule(rule);
+ }
+
+ function _useStockRule() private returns (RuleMaxTotalSupply rule) {
+ rule = new RuleMaxTotalSupply(ADMIN, address(token), CAP);
+ vm.prank(ADMIN);
+ engine.addRule(rule);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ THE ERC-3643 VARIANT
+ //////////////////////////////////////////////////////////////*/
+
+ function testMintUpToTheCapSucceeds() public {
+ _useErc3643Rule();
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, CAP);
+
+ assertEq(token.totalSupply(), CAP, "a mint of exactly the cap must land");
+ }
+
+ function testMintBeyondTheCapIsRejected() public {
+ _useErc3643Rule();
+
+ vm.prank(AGENT);
+ vm.expectRevert(ComplianceNotFollowed.selector);
+ token.mint(INVESTOR, CAP + 1);
+
+ assertEq(token.totalSupply(), 0);
+ }
+
+ function testIncrementalMintsShareTheSameCeiling() public {
+ _useErc3643Rule();
+
+ vm.startPrank(AGENT);
+ token.mint(INVESTOR, 600);
+ token.mint(INVESTOR2, 400);
+ vm.stopPrank();
+ assertEq(token.totalSupply(), CAP);
+
+ vm.prank(AGENT);
+ vm.expectRevert(ComplianceNotFollowed.selector);
+ token.mint(INVESTOR, 1);
+ }
+
+ /// @notice Burning frees headroom, because the cap is on supply rather than on cumulative issuance.
+ function testBurningFreesHeadroom() public {
+ _useErc3643Rule();
+
+ vm.startPrank(AGENT);
+ token.mint(INVESTOR, CAP);
+ token.burn(INVESTOR, 400);
+ assertEq(token.totalSupply(), CAP - 400);
+
+ token.mint(INVESTOR2, 400);
+ vm.stopPrank();
+ assertEq(token.totalSupply(), CAP);
+ }
+
+ function testTransfersAreNeverGatedByTheCap() public {
+ _useErc3643Rule();
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, CAP);
+
+ vm.prank(INVESTOR);
+ token.transfer(INVESTOR2, 400);
+ assertEq(token.balanceOf(INVESTOR2), 400);
+ }
+
+ function testRaisingTheCapRaisesTheCeiling() public {
+ RuleMaxTotalSupplyERC3643 rule = _useErc3643Rule();
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, CAP);
+
+ vm.prank(ADMIN);
+ rule.setMaxTotalSupply(CAP * 2);
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, CAP);
+ assertEq(token.totalSupply(), CAP * 2);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ WHY THE STOCK RULE IS NOT USABLE HERE (NM-11)
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @notice The stock, CMTAT-shaped rule double-counts the mint on a real ERC-3643 token.
+ * @dev `canTransfer` passes (0 + 1000 <= 1000), the token mints, then `created` finds
+ * `totalSupply() == 1000` and adds the amount again. A mint that exactly fills the cap
+ * reverts -- pre-flight and enforcement disagree inside one transaction.
+ */
+ function testStockRuleRevertsAMintThatExactlyFillsTheCap() public {
+ _useStockRule();
+
+ vm.prank(AGENT);
+ vm.expectRevert();
+ token.mint(INVESTOR, CAP);
+
+ assertEq(token.totalSupply(), 0);
+ }
+
+ /**
+ * @notice The stock rule halves the largest SINGLE mint it will accept.
+ * @dev Only the amount in flight is double-counted, so a series of small mints can still creep to
+ * the full cap. That makes the damage look intermittent rather than a clean halving.
+ */
+ function testStockRuleHalvesTheLargestSingleMint() public {
+ _useStockRule();
+
+ vm.prank(AGENT);
+ vm.expectRevert();
+ token.mint(INVESTOR, CAP / 2 + 1);
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, CAP / 2);
+ assertEq(token.totalSupply(), CAP / 2);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ COMPOSITION WITH THE PROOF-OF-RESERVE VARIANT
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @notice The documented pairing: PoR has no margin parameter, so a static ceiling is added by
+ * putting {RuleMaxTotalSupplyERC3643} in the same engine.
+ * @dev Whichever limit binds first stops the mint. The engine returns the FIRST non-zero code, so
+ * rule order decides whether a rejection is reported as `50` or `75`.
+ */
+ function testComposesWithTheProofOfReserveVariant() public {
+ // Reserves are generous; the static cap is the binding constraint.
+ AggregatorV3Mock feed = new AggregatorV3Mock(0, int256(CAP * 10));
+ RuleChainlinkPoRERC3643 por =
+ new RuleChainlinkPoRERC3643(ADMIN, address(token), 0, AggregatorV3Interface(address(feed)), 0);
+ RuleMaxTotalSupplyERC3643 cap = new RuleMaxTotalSupplyERC3643(ADMIN, address(token), CAP);
+
+ vm.startPrank(ADMIN);
+ engine.addRule(por);
+ engine.addRule(cap);
+ vm.stopPrank();
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, CAP);
+
+ vm.prank(AGENT);
+ vm.expectRevert(ComplianceNotFollowed.selector);
+ token.mint(INVESTOR, 1);
+ assertEq(engine.detectTransferRestriction(address(0), INVESTOR, 1), CODE_MAX_TOTAL_SUPPLY_EXCEEDED);
+ }
+
+ /// @notice ...and with the reserves as the binding constraint instead, the PoR code is reported.
+ function testTheTighterOfTheTwoLimitsBinds() public {
+ AggregatorV3Mock feed = new AggregatorV3Mock(0, int256(CAP / 2));
+ RuleChainlinkPoRERC3643 por =
+ new RuleChainlinkPoRERC3643(ADMIN, address(token), 0, AggregatorV3Interface(address(feed)), 0);
+ RuleMaxTotalSupplyERC3643 cap = new RuleMaxTotalSupplyERC3643(ADMIN, address(token), CAP);
+
+ vm.startPrank(ADMIN);
+ engine.addRule(por);
+ engine.addRule(cap);
+ vm.stopPrank();
+
+ vm.prank(AGENT);
+ token.mint(INVESTOR, CAP / 2);
+
+ vm.prank(AGENT);
+ vm.expectRevert(ComplianceNotFollowed.selector);
+ token.mint(INVESTOR, 1);
+ assertEq(token.totalSupply(), CAP / 2, "reserves bound before the static cap");
+ }
+}
diff --git a/test/InterfaceId/AddressListInterfaceId.t.sol b/test/InterfaceId/AddressListInterfaceId.t.sol
index 92ba907d..ed76aef0 100644
--- a/test/InterfaceId/AddressListInterfaceId.t.sol
+++ b/test/InterfaceId/AddressListInterfaceId.t.sol
@@ -8,6 +8,7 @@ import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol";
import {AddressListInterfaceId} from "src/rules/interfaces/library/AddressListInterfaceId.sol";
import {IAddressListInterfaceIdHelper, IAddressListAllFunctions} from "src/mocks/IAddressListInterfaceIdHelper.sol";
+import {IAddressListBatchQuery, IAddressListPolarity} from "src/rules/interfaces/IAddressList.sol";
import {IIdentityRegistryContains} from "src/rules/interfaces/IIdentityRegistry.sol";
import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol";
@@ -16,6 +17,7 @@ import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol";
import {RuleBlacklistOwnable2Step} from "src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol";
import {RuleSpenderWhitelist} from "src/rules/validation/deployment/RuleSpenderWhitelist.sol";
import {RuleSpenderWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol";
+import {RuleReceiverWhitelist} from "src/rules/validation/deployment/RuleReceiverWhitelist.sol";
import {RuleWhitelistWrapper} from "src/rules/validation/deployment/RuleWhitelistWrapper.sol";
import {RuleMaxTotalSupply} from "src/rules/validation/deployment/RuleMaxTotalSupply.sol";
import {TotalSupplyMock} from "src/mocks/TotalSupplyMock.sol";
@@ -54,17 +56,122 @@ contract AddressListInterfaceIdTest is Test, HelperContract {
/**
* @notice Guards the reason the flat-helper pattern is required: `type(IAddressList).interfaceId`
- * omits `contains(address)`, inherited from `IIdentityRegistryContains`, so it must NOT
- * be used for the ERC-165 check.
+ * omits every selector it inherits, so it must NOT be used for the ERC-165 check.
+ * @dev `IAddressList` now inherits from **two** parents — `IIdentityRegistryContains` for
+ * `contains(address)` and `IAddressListBatchQuery` for `areAddressesListed(address[])` —
+ * so the naive id omits both. That makes the point more sharply than before: the omission
+ * grows silently every time a selector is factored out into a parent interface, which is
+ * exactly why the flattened constant exists.
*/
function test_NaiveInterfaceIdIsWrongAndMustNotBeUsed() public view {
bytes4 naive = helper.getIAddressListInterfaceId();
bytes4 full = AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID;
assertTrue(naive != full, "naive id unexpectedly equals the full id");
- // The difference is exactly the inherited selector, `contains(address)`.
- assertEq(naive ^ full, helper.getIIdentityRegistryContainsInterfaceId());
- assertEq(naive ^ full, IIdentityRegistryContains.contains.selector);
+ // The difference is exactly the two inherited selectors.
+ assertEq(
+ naive ^ full,
+ IIdentityRegistryContains.contains.selector ^ AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID
+ );
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ THE BATCH-QUERY SUB-INTERFACE (NM-18)
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @notice The sub-interface id is the selector of its single function.
+ * @dev `IAddressListBatchQuery` declares one function and inherits nothing, so unlike the full
+ * hierarchy it has no omitted-parent trap and the literal is safe to state.
+ */
+ function test_BatchQueryInterfaceIdIsTheSingleSelector() public pure {
+ assertEq(AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID, bytes4(0x20e8e17a));
+ assertEq(
+ AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID,
+ IAddressListBatchQuery.areAddressesListed.selector
+ );
+ assertEq(
+ AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID, type(IAddressListBatchQuery).interfaceId
+ );
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ THE POLARITY INTERFACE (NM-20)
+ //////////////////////////////////////////////////////////////*/
+
+ /// @notice The polarity id is the selector of its single function.
+ function test_PolarityInterfaceIdIsTheSingleSelector() public pure {
+ assertEq(AddressListInterfaceId.IADDRESS_LIST_POLARITY_INTERFACE_ID, bytes4(0xdc4efe10));
+ assertEq(AddressListInterfaceId.IADDRESS_LIST_POLARITY_INTERFACE_ID, IAddressListPolarity.isAllowList.selector);
+ assertEq(AddressListInterfaceId.IADDRESS_LIST_POLARITY_INTERFACE_ID, type(IAddressListPolarity).interfaceId);
+ }
+
+ /**
+ * @notice Polarity is a SEPARATE id from membership, which is the whole point.
+ * @dev If the two were the same interface, an allow-list and a deny-list would be
+ * indistinguishable again — a consumer needs to require both and then read the answer.
+ */
+ function test_PolarityIsIndependentOfMembership() public pure {
+ assertTrue(
+ AddressListInterfaceId.IADDRESS_LIST_POLARITY_INTERFACE_ID
+ != AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID
+ );
+ assertTrue(
+ AddressListInterfaceId.IADDRESS_LIST_POLARITY_INTERFACE_ID
+ != AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID
+ );
+ }
+
+ /// @notice Each rule declares the polarity it actually has, and advertises the interface.
+ function test_RulesDeclareTheirPolarityHonestly() public {
+ bytes4 polarity = AddressListInterfaceId.IADDRESS_LIST_POLARITY_INTERFACE_ID;
+
+ vm.startPrank(DEFAULT_ADMIN_ADDRESS);
+ RuleWhitelist whitelist = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false);
+ RuleBlacklist blacklist = new RuleBlacklist(DEFAULT_ADMIN_ADDRESS, FORWARDER);
+ RuleSpenderWhitelist spender = new RuleSpenderWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER);
+ vm.stopPrank();
+
+ assertTrue(IERC165(address(whitelist)).supportsInterface(polarity), "whitelist advertises polarity");
+ assertTrue(whitelist.isAllowList(), "whitelist is an allow-list");
+
+ assertTrue(IERC165(address(blacklist)).supportsInterface(polarity), "blacklist advertises polarity");
+ assertFalse(blacklist.isAllowList(), "blacklist is a deny-list");
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ RuleReceiverWhitelist receiver = new RuleReceiverWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER);
+ assertTrue(IERC165(address(receiver)).supportsInterface(polarity), "receiver whitelist advertises polarity");
+ assertTrue(receiver.isAllowList(), "receiver whitelist is an allow-list");
+
+ // Deliberate abstention: its set is spenders, not holders, so polarity alone would mislead.
+ assertFalse(
+ IERC165(address(spender)).supportsInterface(polarity),
+ "RuleSpenderWhitelist must NOT declare holder polarity"
+ );
+ }
+
+ /// @notice The sub-interface is a strict subset: the full id contains its selector.
+ function test_BatchQueryIsASubsetOfTheFullInterface() public view {
+ bytes4 full = AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID;
+ bytes4 sub = AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID;
+ assertTrue(full != sub, "the wrapper must not be able to confuse the two");
+ // Removing the sub-interface selector from the flattened id leaves the other seven.
+ assertTrue((full ^ sub) != full, "the full id must actually include the sub-interface selector");
+ }
+
+ /// @notice Every rule usable as a wrapper child advertises the sub-interface, not just the full one.
+ function test_AddressListRulesAdvertiseTheBatchQuerySubInterface() public {
+ bytes4 sub = AddressListInterfaceId.IADDRESS_LIST_BATCH_QUERY_INTERFACE_ID;
+
+ vm.startPrank(DEFAULT_ADMIN_ADDRESS);
+ RuleWhitelist whitelist = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false);
+ RuleBlacklist blacklist = new RuleBlacklist(DEFAULT_ADMIN_ADDRESS, FORWARDER);
+ RuleSpenderWhitelist spender = new RuleSpenderWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER);
+ vm.stopPrank();
+
+ assertTrue(IERC165(address(whitelist)).supportsInterface(sub), "RuleWhitelist");
+ assertTrue(IERC165(address(blacklist)).supportsInterface(sub), "RuleBlacklist");
+ assertTrue(IERC165(address(spender)).supportsInterface(sub), "RuleSpenderWhitelist");
}
/*//////////////////////////////////////////////////////////////
diff --git a/test/InterfaceId/ComplianceInterfaceId.t.sol b/test/InterfaceId/ComplianceInterfaceId.t.sol
new file mode 100644
index 00000000..c8567dbb
--- /dev/null
+++ b/test/InterfaceId/ComplianceInterfaceId.t.sol
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {ComplianceInterfaceId} from "RuleEngine/modules/library/ComplianceInterfaceId.sol";
+
+import {IERC3643ComplianceFull} from "src/mocks/IERC3643ComplianceFull.sol";
+
+/**
+ * @title ComplianceInterfaceIdTest
+ * @notice Pins the ERC-3643 ICompliance interface ID the operation rules advertise.
+ * @dev RuleEngine v3.0.0-rc6 derives {ComplianceInterfaceId-ERC3643_COMPLIANCE_INTERFACE_ID} from
+ * its own interface hierarchy instead of hardcoding it, so a refactor upstream -- such as the
+ * rc6 split of the binding functions into `ITokenBinding` -- can now move the value silently.
+ * These assertions are the guard: the constant must stay equal to the flattened redeclaration
+ * in {IERC3643ComplianceFull} and to the literal wire value.
+ */
+contract ComplianceInterfaceIdTest is Test {
+ function testConstantMatchesFlattenedInterface() public pure {
+ assertEq(
+ ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID,
+ type(IERC3643ComplianceFull).interfaceId,
+ "upstream derivation diverged from the flattened ERC-3643 ICompliance surface"
+ );
+ }
+
+ function testConstantMatchesWireValue() public pure {
+ assertEq(ComplianceInterfaceId.ERC3643_COMPLIANCE_INTERFACE_ID, bytes4(0x3144991c), "wire value moved");
+ }
+}
diff --git a/test/RuleChainlinkPoR/RuleChainlinkPoRERC3643.t.sol b/test/RuleChainlinkPoR/RuleChainlinkPoRERC3643.t.sol
new file mode 100644
index 00000000..fa850db3
--- /dev/null
+++ b/test/RuleChainlinkPoR/RuleChainlinkPoRERC3643.t.sol
@@ -0,0 +1,168 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {HelperContract} from "../HelperContract.sol";
+import {AggregatorV3Mock} from "src/mocks/AggregatorV3Mock.sol";
+import {TotalSupplyMock} from "src/mocks/TotalSupplyMock.sol";
+import {AggregatorV3Interface} from "src/rules/interfaces/AggregatorV3Interface.sol";
+import {RuleChainlinkPoR} from "src/rules/validation/deployment/RuleChainlinkPoR.sol";
+import {RuleChainlinkPoRERC3643} from "src/rules/validation/deployment/RuleChainlinkPoRERC3643.sol";
+import {
+ RuleChainlinkPoRERC3643Ownable2Step
+} from "src/rules/validation/deployment/RuleChainlinkPoRERC3643Ownable2Step.sol";
+
+/**
+ * @title RuleChainlinkPoRERC3643Unit
+ * @notice Unit coverage for the ERC-3643 Proof-of-Reserve variants, in the default profile.
+ * @dev The end-to-end proof runs against the genuine vendored token in
+ * `test/ERC3643Real/ERC3643RealTokenChainlinkPoR.t.sol` under `FOUNDRY_PROFILE=erc3643`, which
+ * `forge test` and `forge coverage` do not include. These tests exercise the same override with
+ * mocks so the variants are covered by the ordinary run too.
+ *
+ * The rule is notified as an ERC-3643 token would notify it: the supply is set to its POST-mint
+ * value first, then the write hook is called.
+ */
+contract RuleChainlinkPoRERC3643Unit is Test, HelperContract {
+ uint256 private constant RESERVES = 1000;
+
+ TotalSupplyMock private token;
+ AggregatorV3Mock private feed;
+
+ function setUp() public {
+ token = new TotalSupplyMock();
+ feed = new AggregatorV3Mock(0, int256(RESERVES));
+ }
+
+ function _accessControlVariant() private returns (RuleChainlinkPoRERC3643) {
+ return
+ new RuleChainlinkPoRERC3643(
+ DEFAULT_ADMIN_ADDRESS, address(token), 0, AggregatorV3Interface(address(feed)), 0
+ );
+ }
+
+ function _ownableVariant() private returns (RuleChainlinkPoRERC3643Ownable2Step) {
+ return new RuleChainlinkPoRERC3643Ownable2Step(
+ DEFAULT_ADMIN_ADDRESS, address(token), 0, AggregatorV3Interface(address(feed)), 0
+ );
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ ENFORCEMENT (POST-MINT)
+ //////////////////////////////////////////////////////////////*/
+
+ function testNotifyAcceptsAMintThatLandsExactlyOnTheReserves() public {
+ RuleChainlinkPoRERC3643 rule = _accessControlVariant();
+ token.setTotalSupply(RESERVES); // the mint has already happened
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, RESERVES);
+ }
+
+ function testNotifyRejectsAMintThatLandsAboveTheReserves() public {
+ RuleChainlinkPoRERC3643 rule = _accessControlVariant();
+ token.setTotalSupply(RESERVES + 1);
+ vm.expectRevert();
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, 1);
+ }
+
+ function testNotifyAcceptsTransfersAndBurnsWhateverTheReserves() public {
+ RuleChainlinkPoRERC3643 rule = _accessControlVariant();
+ feed.setAnswer(0);
+ token.setTotalSupply(RESERVES);
+
+ rule.transferred(ADDRESS1, ADDRESS2, 10); // transfer
+ rule.transferred(ADDRESS1, ZERO_ADDRESS, 10); // burn
+ rule.transferred(ADDRESS3, ADDRESS1, ADDRESS2, 10); // delegated transfer
+ }
+
+ function testDelegatedNotifyIsRePhasedToo() public {
+ RuleChainlinkPoRERC3643 rule = _accessControlVariant();
+ token.setTotalSupply(RESERVES);
+ rule.transferred(ADDRESS3, ZERO_ADDRESS, ADDRESS1, RESERVES);
+
+ token.setTotalSupply(RESERVES + 1);
+ vm.expectRevert();
+ rule.transferred(ADDRESS3, ZERO_ADDRESS, ADDRESS1, 1);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ THE READ PATH IS NOT RE-PHASED
+ //////////////////////////////////////////////////////////////*/
+
+ function testReadPathStillProjectsThePendingAmount() public {
+ RuleChainlinkPoRERC3643 rule = _accessControlVariant();
+ token.setTotalSupply(RESERVES);
+
+ assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, 1), CODE_RESERVES_EXCEEDED);
+ assertFalse(rule.canTransfer(ZERO_ADDRESS, ADDRESS1, 1));
+
+ token.setTotalSupply(0);
+ assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, RESERVES), TRANSFER_OK);
+ assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, RESERVES + 1), CODE_RESERVES_EXCEEDED);
+ }
+
+ /// @notice Pre-flight and enforcement must agree on the same mint, as the ERC-3643 token calls both.
+ function testPreFlightAndEnforcementAgree() public {
+ RuleChainlinkPoRERC3643 rule = _accessControlVariant();
+
+ // Pre-flight, before the mint: supply 0, asking for the full reserves.
+ token.setTotalSupply(0);
+ assertTrue(rule.canTransfer(ZERO_ADDRESS, ADDRESS1, RESERVES));
+
+ // Enforcement, after the mint: supply is now RESERVES.
+ token.setTotalSupply(RESERVES);
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, RESERVES);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ CONTRAST WITH THE STOCK RULE
+ //////////////////////////////////////////////////////////////*/
+
+ function testStockRuleDoubleCountsWhereTheVariantDoesNot() public {
+ RuleChainlinkPoR stock =
+ new RuleChainlinkPoR(DEFAULT_ADMIN_ADDRESS, address(token), 0, AggregatorV3Interface(address(feed)), 0);
+ RuleChainlinkPoRERC3643 variant = _accessControlVariant();
+
+ token.setTotalSupply(RESERVES);
+
+ vm.expectRevert();
+ stock.transferred(ZERO_ADDRESS, ADDRESS1, RESERVES);
+
+ variant.transferred(ZERO_ADDRESS, ADDRESS1, RESERVES);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ OWNABLE2STEP VARIANT
+ //////////////////////////////////////////////////////////////*/
+
+ function testOwnableVariantEnforcesIdentically() public {
+ RuleChainlinkPoRERC3643Ownable2Step rule = _ownableVariant();
+
+ token.setTotalSupply(RESERVES);
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, RESERVES);
+
+ token.setTotalSupply(RESERVES + 1);
+ vm.expectRevert();
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, 1);
+ }
+
+ function testOwnableVariantKeepsItsOwnerGatedConfiguration() public {
+ RuleChainlinkPoRERC3643Ownable2Step rule = _ownableVariant();
+
+ vm.prank(ATTACKER);
+ vm.expectRevert();
+ rule.setMaxStalenessSeconds(1 days);
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.setMaxStalenessSeconds(1 days);
+ assertEq(rule.maxStalenessSeconds(), 1 days);
+ }
+
+ function testBothVariantsReportTheSameBackedSupply() public {
+ (uint8 codeA, uint256 backedA) = _accessControlVariant().maxBackedSupply();
+ (uint8 codeB, uint256 backedB) = _ownableVariant().maxBackedSupply();
+ assertEq(codeA, 0);
+ assertEq(codeA, codeB);
+ assertEq(backedA, RESERVES);
+ assertEq(backedA, backedB);
+ }
+}
diff --git a/test/RuleChainlinkPoR/RuleChainlinkPoRUnit.t.sol b/test/RuleChainlinkPoR/RuleChainlinkPoRUnit.t.sol
index e7e2d386..d6f35a24 100644
--- a/test/RuleChainlinkPoR/RuleChainlinkPoRUnit.t.sol
+++ b/test/RuleChainlinkPoR/RuleChainlinkPoRUnit.t.sol
@@ -296,6 +296,58 @@ contract RuleChainlinkPoRUnit is Test, HelperContract {
assertEq(resUint8, CODE_RESERVES_FEED_UNAVAILABLE);
}
+ /*//////////////////////////////////////////////////////////////
+ FUTURE-DATED ROUND (NM-10 REGRESSION)
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @notice A round stamped in the future is a malformed answer, not a fresh one.
+ * @dev THE REGRESSION: the staleness comparison was guarded by `block.timestamp > updatedAt` to keep
+ * the subtraction from underflowing, which silently accepted ANY future timestamp -- a feed
+ * frozen on an old reserve answer could keep authorising mints until that timestamp elapsed.
+ */
+ function testDetectRestriction_FutureDatedRoundBlocksMint() public {
+ feed.setUpdatedAt(block.timestamp + 1);
+ token.setTotalSupply(0);
+ assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, 1), CODE_RESERVES_ANSWER_INVALID);
+ assertFalse(rule.canTransfer(ZERO_ADDRESS, ADDRESS1, 1));
+ }
+
+ function testDetectRestriction_FutureDatedRoundIsRejectedEvenWithStalenessDisabled() public {
+ // `maxStalenessSeconds == 0` disables FRESHNESS checking. It must not also disable the
+ // malformed-answer check, or an operator who opts out of staleness opts into forged timestamps.
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.setMaxStalenessSeconds(0);
+
+ feed.setUpdatedAt(block.timestamp + 3650 days);
+ token.setTotalSupply(0);
+ assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, 1), CODE_RESERVES_ANSWER_INVALID);
+ }
+
+ function testDetectRestriction_RoundAtExactlyTheCurrentBlockIsAccepted() public {
+ // The boundary: `updatedAt == block.timestamp` is the normal case for a just-published round.
+ feed.setUpdatedAt(block.timestamp);
+ token.setTotalSupply(0);
+ assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, 1), TRANSFER_OK);
+ }
+
+ function testMaxBackedSupply_ReportsTheFutureDatedRound() public {
+ // The preview accessor must agree with what a mint would return, and must not revert.
+ feed.setUpdatedAt(block.timestamp + 1);
+ (uint8 code, uint256 backed) = rule.maxBackedSupply();
+ assertEq(code, CODE_RESERVES_ANSWER_INVALID);
+ assertEq(backed, 0);
+ }
+
+ function testTransferred_FutureDatedRoundRevertsTheMint() public {
+ // Enforcement, not just the view: the write hook must reject the mint.
+ feed.setUpdatedAt(block.timestamp + 1);
+ token.setTotalSupply(0);
+ vm.prank(address(token));
+ vm.expectRevert();
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, 1);
+ }
+
function testDetectRestriction_ZeroReserveBlocksAnyMint() public {
feed.setAnswer(0);
token.setTotalSupply(0);
diff --git a/test/RuleConditionalTransferLight/Ownable/ConditionalTransferOwnable2StepBindingAuthorization.t.sol b/test/RuleConditionalTransferLight/Ownable/ConditionalTransferOwnable2StepBindingAuthorization.t.sol
index 4e22dc05..a3c5f144 100644
--- a/test/RuleConditionalTransferLight/Ownable/ConditionalTransferOwnable2StepBindingAuthorization.t.sol
+++ b/test/RuleConditionalTransferLight/Ownable/ConditionalTransferOwnable2StepBindingAuthorization.t.sol
@@ -15,12 +15,12 @@ import {
* @dev These hooks had no coverage before: the only test naming
* `RuleConditionalTransferLightMultiTokenOwnable2Step` was an ERC-165 support check, which never
* reaches an access-control path. Three concrete overrides were therefore unexercised —
- * `_authorizeComplianceBindingChange` on the single-token variant, and `_onlyComplianceManager`
+ * `_authorizeTokenBindingChange` on the single-token variant, and `_onlyComplianceManager`
* plus `_authorizeTransferApproval` on the multi-token one.
*
* Note which entrypoint reaches which hook. `RuleConditionalTransferLightBase` overrides
- * `bindToken` with its own `onlyComplianceManager` modifier, so on the single-token rule the
- * only route to `_authorizeComplianceBindingChange` is the inherited `unbindToken`.
+ * `bindToken` with its own `onlyTokenBindingManager` modifier, so on the single-token rule the
+ * only route to `_authorizeTokenBindingChange` is the inherited `unbindToken`.
*/
contract ConditionalTransferOwnable2StepBindingAuthorizationTest is Test {
address constant OWNER = address(0xA11CE);
@@ -39,7 +39,7 @@ contract ConditionalTransferOwnable2StepBindingAuthorizationTest is Test {
}
/*//////////////////////////////////////////////////////////////
- SINGLE TOKEN -- _authorizeComplianceBindingChange
+ SINGLE TOKEN -- _authorizeTokenBindingChange
//////////////////////////////////////////////////////////////*/
function testSingleUnbindTokenRejectsNonOwner() public {
diff --git a/test/RuleConditionalTransferLight/RuleConditionalTransferLightApproveAndTransfer.t.sol b/test/RuleConditionalTransferLight/RuleConditionalTransferLightApproveAndTransfer.t.sol
index 21e296dc..9da0ff96 100644
--- a/test/RuleConditionalTransferLight/RuleConditionalTransferLightApproveAndTransfer.t.sol
+++ b/test/RuleConditionalTransferLight/RuleConditionalTransferLightApproveAndTransfer.t.sol
@@ -36,6 +36,87 @@ contract RuleConditionalTransferLightApproveAndTransfer is Test, HelperContract
assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, 10), 0);
}
+ /*//////////////////////////////////////////////////////////////
+ NM-17: THE APPROVAL MUST BE CONSUMED
+ //////////////////////////////////////////////////////////////*/
+
+ /**
+ * @notice A token that never calls back leaves the helper's approval unconsumed, and the helper
+ * must reject that rather than complete.
+ * @dev THE REGRESSION. The helper inverts CEI on purpose: it records the approval BEFORE
+ * `safeTransferFrom` so the token's compliance callback can consume it. Nothing used to
+ * verify the callback happened. A plain ERC-20 bound with `bindToken`, or a RuleEngine never
+ * bound or since unbound, therefore completed the transfer and left the approval standing —
+ * indistinguishable from an operator-created one, and enough to authorise a later,
+ * never-approved transfer of exactly `(from, to, value)`.
+ */
+ function testRevertsWhenTheTokenDoesNotCallBack() public {
+ MockERC20WithTransferContext silentToken = new MockERC20WithTransferContext("Silent", "SIL");
+ // Deliberately NOT `setRule`: this token moves value and tells nobody.
+ silentToken.mint(ADDRESS1, 100);
+
+ vm.startPrank(DEFAULT_ADMIN_ADDRESS);
+ RuleConditionalTransferLight silentRule = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS);
+ silentRule.bindToken(address(silentToken));
+ vm.stopPrank();
+
+ vm.prank(ADDRESS1);
+ silentToken.approve(address(silentRule), 10);
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ RuleConditionalTransferLight_ApprovalNotConsumed.selector,
+ address(silentToken),
+ ADDRESS1,
+ ADDRESS2,
+ uint256(10)
+ )
+ );
+ silentRule.approveAndTransferIfAllowed(ADDRESS1, ADDRESS2, 10);
+
+ // The whole call reverted, so no residual approval and no value moved.
+ assertEq(silentRule.approvedCount(ADDRESS1, ADDRESS2, 10), 0, "no approval may be left behind");
+ assertEq(silentToken.balanceOf(ADDRESS1), 100, "the transfer was rolled back");
+ assertEq(silentToken.balanceOf(ADDRESS2), 0);
+ }
+
+ /**
+ * @notice The post-condition compares against the count BEFORE the helper ran, not against zero.
+ * @dev An operator may legitimately hold outstanding approvals for the same tuple. The helper adds
+ * one, the callback consumes one, and the pre-existing approvals must survive untouched.
+ */
+ function testPreExistingApprovalsSurviveTheHelper() public {
+ vm.startPrank(DEFAULT_ADMIN_ADDRESS);
+ rule.approveTransfer(ADDRESS1, ADDRESS2, 10);
+ rule.approveTransfer(ADDRESS1, ADDRESS2, 10);
+ vm.stopPrank();
+ assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, 10), 2);
+
+ vm.prank(ADDRESS1);
+ token.approve(address(rule), 10);
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.approveAndTransferIfAllowed(ADDRESS1, ADDRESS2, 10);
+
+ assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, 10), 2, "the operator's own approvals are untouched");
+ assertEq(token.balanceOf(ADDRESS2), 10);
+ }
+
+ /// @notice The normal direct-binding flow still works: the callback consumes exactly one approval.
+ function testDirectBindingFlowStillConsumesExactlyOne() public {
+ vm.prank(ADDRESS1);
+ token.approve(address(rule), 20);
+
+ vm.startPrank(DEFAULT_ADMIN_ADDRESS);
+ rule.approveAndTransferIfAllowed(ADDRESS1, ADDRESS2, 10);
+ rule.approveAndTransferIfAllowed(ADDRESS1, ADDRESS2, 10);
+ vm.stopPrank();
+
+ assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, 10), 0);
+ assertEq(token.balanceOf(ADDRESS2), 20);
+ }
+
function testApproveAndTransferIfAllowedRevertsWhenNoTokenBound() public {
RuleConditionalTransferLight freshRule = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS);
vm.expectRevert(RuleConditionalTransferLight_TokenNotBound.selector);
diff --git a/test/RuleConditionalTransferLightMultiToken/MultiTokenGuardReverts.t.sol b/test/RuleConditionalTransferLightMultiToken/MultiTokenGuardReverts.t.sol
new file mode 100644
index 00000000..d671305f
--- /dev/null
+++ b/test/RuleConditionalTransferLightMultiToken/MultiTokenGuardReverts.t.sol
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {HelperContract} from "../HelperContract.sol";
+import {MockERC20WithTransferContext} from "src/mocks/MockERC20WithTransferContext.sol";
+import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol";
+
+/**
+ * @title MultiTokenGuardReverts
+ * @notice The reject side of every guard in {RuleConditionalTransferLightMultiTokenBase}.
+ * @dev These four branches were the only uncovered ones in `src/` — each guard's accept path was
+ * exercised, its `require` never taken. A rule whose whole purpose is to refuse transfers needs
+ * its refusals asserted, not just its permissions: a guard that has never been observed to
+ * reject is a guard nobody has tested.
+ */
+contract MultiTokenGuardReverts is Test, HelperContract {
+ /// @dev Re-declared locally: `HelperContract` cannot inherit the multi-token invariant storage
+ /// alongside the single-token one (`OPERATOR_ROLE` and the code constants clash).
+ error RuleConditionalTransferLightMultiToken_InvalidToken();
+ error RuleConditionalTransferLightMultiToken_InsufficientAllowance(
+ address token, address from, uint256 allowance, uint256 value
+ );
+ error RuleConditionalTransferLightMultiToken_TransferExecutorUnauthorized(address account);
+
+ RuleConditionalTransferLightMultiToken private rule;
+ MockERC20WithTransferContext private boundToken;
+ MockERC20WithTransferContext private strangerToken;
+
+ function setUp() public {
+ boundToken = new MockERC20WithTransferContext("Bound", "BND");
+ strangerToken = new MockERC20WithTransferContext("Stranger", "STR");
+
+ rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS);
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.bindToken(address(boundToken));
+ boundToken.setRule(address(rule));
+
+ boundToken.mint(ADDRESS1, 100);
+ strangerToken.mint(ADDRESS1, 100);
+ }
+
+ /// @notice L137: `approveAndTransferIfAllowed` refuses a token that was never bound.
+ function testApproveAndTransferRejectsAnUnboundToken() public {
+ vm.prank(ADDRESS1);
+ strangerToken.approve(address(rule), 10);
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ vm.expectRevert(RuleConditionalTransferLightMultiToken_InvalidToken.selector);
+ rule.approveAndTransferIfAllowed(address(strangerToken), ADDRESS1, ADDRESS2, 10);
+ }
+
+ /// @notice L143: `approveAndTransferIfAllowed` refuses when the holder's allowance is short.
+ function testApproveAndTransferRejectsAnInsufficientAllowance() public {
+ vm.prank(ADDRESS1);
+ boundToken.approve(address(rule), 4); // less than the 10 requested
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ RuleConditionalTransferLightMultiToken_InsufficientAllowance.selector,
+ address(boundToken),
+ ADDRESS1,
+ uint256(4),
+ uint256(10)
+ )
+ );
+ rule.approveAndTransferIfAllowed(address(boundToken), ADDRESS1, ADDRESS2, 10);
+
+ // The rejection is total: no approval was recorded and no value moved.
+ assertEq(rule.approvedCount(address(boundToken), ADDRESS1, ADDRESS2, 10), 0);
+ assertEq(boundToken.balanceOf(ADDRESS2), 0);
+ }
+
+ /// @notice L371: `cancelTransferApproval` refuses a token that was never bound.
+ function testCancelTransferApprovalRejectsAnUnboundToken() public {
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ vm.expectRevert(RuleConditionalTransferLightMultiToken_InvalidToken.selector);
+ rule.cancelTransferApproval(address(strangerToken), ADDRESS1, ADDRESS2, 10);
+ }
+
+ /// @notice L440: the execution hook refuses a caller that is not a bound token.
+ /// @dev This rule is direct-binding only, so the executor check *is* the token check: approval
+ /// consumption is keyed on `msg.sender`. An unbound caller must never consume one.
+ function testTransferredRejectsACallerThatIsNotABoundToken() public {
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.approveTransfer(address(boundToken), ADDRESS1, ADDRESS2, 10);
+
+ vm.prank(ATTACKER);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ RuleConditionalTransferLightMultiToken_TransferExecutorUnauthorized.selector, ATTACKER
+ )
+ );
+ rule.transferred(ADDRESS1, ADDRESS2, 10);
+
+ // The approval survives the rejected attempt.
+ assertEq(rule.approvedCount(address(boundToken), ADDRESS1, ADDRESS2, 10), 1);
+ }
+}
diff --git a/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiToken.t.sol b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiToken.t.sol
index 9cbd9505..82120616 100644
--- a/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiToken.t.sol
+++ b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiToken.t.sol
@@ -7,6 +7,13 @@ import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleCo
import {MockERC20WithTransferContext} from "src/mocks/MockERC20WithTransferContext.sol";
contract RuleConditionalTransferLightMultiTokenTest is Test, HelperContract {
+ /// @dev Re-declared locally: `HelperContract` cannot inherit the multi-token invariant storage
+ /// alongside the single-token one (`OPERATOR_ROLE` and the code constants clash), which is the
+ /// same reason `MultiTokenSurface.t.sol` re-declares its errors.
+ error RuleConditionalTransferLightMultiToken_ApprovalNotConsumed(
+ address token, address from, address to, uint256 value
+ );
+
RuleConditionalTransferLightMultiToken private rule;
MockERC20WithTransferContext private tokenA;
MockERC20WithTransferContext private tokenB;
@@ -29,6 +36,53 @@ contract RuleConditionalTransferLightMultiTokenTest is Test, HelperContract {
tokenB.mint(ADDRESS1, 100);
}
+ /**
+ * @notice NM-17: a bound token that never calls back leaves the helper's approval unconsumed.
+ * @dev Same inverted-CEI shape as the single-token rule: `approveAndTransferIfAllowed` records the
+ * approval before `safeTransferFrom` so the compliance callback can consume it, and nothing
+ * used to check the callback happened. Here the token is bound but has no rule set, so it
+ * moves value silently — the helper must now reject rather than complete and leave a
+ * spendable approval for `(tokenC, from, to, value)`.
+ */
+ function testApproveAndTransferRevertsWhenTheTokenDoesNotCallBack() public {
+ MockERC20WithTransferContext silentToken = new MockERC20WithTransferContext("Silent", "SIL");
+ // Bound to the rule, but deliberately NOT `setRule`: it tells nobody.
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.bindToken(address(silentToken));
+ silentToken.mint(ADDRESS1, 100);
+
+ vm.prank(ADDRESS1);
+ silentToken.approve(address(rule), 10);
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ RuleConditionalTransferLightMultiToken_ApprovalNotConsumed.selector,
+ address(silentToken),
+ ADDRESS1,
+ ADDRESS2,
+ uint256(10)
+ )
+ );
+ rule.approveAndTransferIfAllowed(address(silentToken), ADDRESS1, ADDRESS2, 10);
+
+ assertEq(rule.approvedCount(address(silentToken), ADDRESS1, ADDRESS2, 10), 0, "no residual approval");
+ assertEq(silentToken.balanceOf(ADDRESS1), 100, "the transfer was rolled back");
+ }
+
+ /// @notice NM-17: a token that does call back is unaffected, and the count is per-token.
+ function testApproveAndTransferStillWorksAndIsPerToken() public {
+ vm.prank(ADDRESS1);
+ tokenA.approve(address(rule), 10);
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.approveAndTransferIfAllowed(address(tokenA), ADDRESS1, ADDRESS2, 10);
+
+ assertEq(tokenA.balanceOf(ADDRESS2), 10);
+ assertEq(rule.approvedCount(address(tokenA), ADDRESS1, ADDRESS2, 10), 0);
+ assertEq(rule.approvedCount(address(tokenB), ADDRESS1, ADDRESS2, 10), 0, "token B untouched");
+ }
+
function testApprovalForTokenADoesNotAuthorizeTokenB() public {
vm.prank(DEFAULT_ADMIN_ADDRESS);
rule.approveTransfer(address(tokenA), ADDRESS1, ADDRESS2, 10);
diff --git a/test/RuleIdentityRegistry/RuleIdentityRegistryDelegation.t.sol b/test/RuleIdentityRegistry/RuleIdentityRegistryDelegation.t.sol
new file mode 100644
index 00000000..c4bdbbe4
--- /dev/null
+++ b/test/RuleIdentityRegistry/RuleIdentityRegistryDelegation.t.sol
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {HelperContract} from "../HelperContract.sol";
+import {IdentityRegistryExtraCheckHarness} from "src/mocks/harness/IdentityRegistryDelegationHarness.sol";
+import {IdentityRegistryMock} from "src/mocks/IdentityRegistryMock.sol";
+
+/**
+ * @title RuleIdentityRegistryDelegation
+ * @notice The `transferFrom` path must always consult the direct restriction check, whether or not a
+ * registry is configured and whether or not the transfer is a burn (Nethermind AuditAgent
+ * NM-3; the mirror of `CLAUDE_ANALYSIS.md` F-2 on {RuleSanctionsListBase}).
+ * @dev The subclass under test adds a registry-independent check. Before the fix,
+ * `_detectTransferRestrictionFrom` returned `TRANSFER_OK` outright when the registry was unset
+ * or `to == address(0)`, so the subclass's check applied to `transfer` but not to
+ * `transferFrom`. `testExtraCheckAppliesToTransferFromWithNoRegistry` and
+ * `testExtraCheckAppliesToBurnFrom` fail against that implementation and are the reason the
+ * restructure exists.
+ */
+contract RuleIdentityRegistryDelegation is Test, HelperContract {
+ address private constant BLOCKED = address(0xB10C);
+ address private constant UNVERIFIED = address(98);
+
+ IdentityRegistryMock private registry;
+
+ function setUp() public {
+ registry = new IdentityRegistryMock();
+ registry.setVerified(ADDRESS1, true);
+ registry.setVerified(ADDRESS2, true);
+ registry.setVerified(ADDRESS3, true);
+ registry.setVerified(BLOCKED, true);
+ }
+
+ function _withoutRegistry() internal returns (IdentityRegistryExtraCheckHarness) {
+ return new IdentityRegistryExtraCheckHarness(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, false, false, BLOCKED);
+ }
+
+ function _withRegistry(bool checkSpender_) internal returns (IdentityRegistryExtraCheckHarness) {
+ return
+ new IdentityRegistryExtraCheckHarness(
+ DEFAULT_ADMIN_ADDRESS, address(registry), false, checkSpender_, BLOCKED
+ );
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ No registry configured
+ //////////////////////////////////////////////////////////////*/
+
+ function testExtraCheckAppliesToTransferWithNoRegistry() public {
+ // This direction always worked: the direct path calls the hook unconditionally.
+ IdentityRegistryExtraCheckHarness rule = _withoutRegistry();
+ assertEq(rule.detectTransferRestriction(BLOCKED, ADDRESS2, 10), rule.CODE_EXTRA_BLOCKED());
+ }
+
+ function testExtraCheckAppliesToTransferFromWithNoRegistry() public {
+ // THE REGRESSION: with the early return in place this returned TRANSFER_OK, so `transfer`
+ // and `transferFrom` disagreed about the same pair of addresses.
+ IdentityRegistryExtraCheckHarness rule = _withoutRegistry();
+ assertEq(
+ rule.detectTransferRestrictionFrom(ADDRESS3, BLOCKED, ADDRESS2, 10),
+ rule.CODE_EXTRA_BLOCKED(),
+ "transferFrom must reach the same hook as transfer"
+ );
+ assertFalse(rule.canTransferFrom(ADDRESS3, BLOCKED, ADDRESS2, 10));
+ }
+
+ function testTheTwoEntrypointsAgreeWithNoRegistry() public {
+ IdentityRegistryExtraCheckHarness rule = _withoutRegistry();
+ assertEq(
+ rule.detectTransferRestriction(ADDRESS1, BLOCKED, 10),
+ rule.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, BLOCKED, 10),
+ "the receiver leg must be screened identically on both paths"
+ );
+ // An unrelated pair is still unrestricted; the rule is not simply rejecting everything.
+ assertEq(rule.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ADDRESS2, 10), TRANSFER_OK);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ Burn (to == address(0))
+ //////////////////////////////////////////////////////////////*/
+
+ function testExtraCheckAppliesToBurnFrom() public {
+ // THE SECOND REGRESSION: the burn early return skipped the delegation too, so a subclass
+ // check on a burning `from` applied to `burn` but not to `burnFrom`.
+ IdentityRegistryExtraCheckHarness rule = _withRegistry(false);
+ assertEq(rule.detectTransferRestriction(BLOCKED, ZERO_ADDRESS, 10), rule.CODE_EXTRA_BLOCKED());
+ assertEq(
+ rule.detectTransferRestrictionFrom(ADDRESS3, BLOCKED, ZERO_ADDRESS, 10),
+ rule.CODE_EXTRA_BLOCKED(),
+ "burnFrom must reach the same hook as burn"
+ );
+ }
+
+ function testBurnStaysExemptFromTheSpenderCheck() public {
+ // Delegating the burn must NOT expose it to the opt-in spender check: ERC-3643 states that
+ // burn bypasses all eligibility checks.
+ IdentityRegistryExtraCheckHarness rule = _withRegistry(true);
+ assertEq(rule.detectTransferRestrictionFrom(UNVERIFIED, ADDRESS1, ZERO_ADDRESS, 10), TRANSFER_OK);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ Registry configured
+ //////////////////////////////////////////////////////////////*/
+
+ function testExtraCheckStillAppliesWithARegistry() public {
+ IdentityRegistryExtraCheckHarness rule = _withRegistry(false);
+ assertEq(rule.detectTransferRestriction(BLOCKED, ADDRESS2, 10), rule.CODE_EXTRA_BLOCKED());
+ assertEq(rule.detectTransferRestrictionFrom(ADDRESS3, BLOCKED, ADDRESS2, 10), rule.CODE_EXTRA_BLOCKED());
+ }
+
+ function testTheSpenderCheckStillTakesPriority() public {
+ // The registry-driven spender check must still short-circuit ahead of the delegated hook.
+ IdentityRegistryExtraCheckHarness rule = _withRegistry(true);
+ assertEq(
+ rule.detectTransferRestrictionFrom(UNVERIFIED, BLOCKED, ADDRESS2, 10), CODE_ADDRESS_SPENDER_NOT_VERIFIED
+ );
+ }
+
+ function testBaseScreeningIsUnchanged() public {
+ IdentityRegistryExtraCheckHarness rule = _withRegistry(false);
+ // ERC-3643: only the receiver must be verified.
+ assertEq(rule.detectTransferRestriction(ADDRESS1, UNVERIFIED, 10), CODE_ADDRESS_TO_NOT_VERIFIED);
+ assertEq(rule.detectTransferRestriction(UNVERIFIED, ADDRESS2, 10), TRANSFER_OK);
+ assertEq(rule.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ADDRESS2, 10), TRANSFER_OK);
+ // Mint is screened on the receiver only; an unverified minter is not blocked.
+ assertEq(rule.detectTransferRestrictionFrom(UNVERIFIED, ZERO_ADDRESS, ADDRESS2, 10), TRANSFER_OK);
+ }
+}
diff --git a/test/RuleMaxTotalSupply/RuleMaxTotalSupplyERC3643.t.sol b/test/RuleMaxTotalSupply/RuleMaxTotalSupplyERC3643.t.sol
new file mode 100644
index 00000000..eb63da7f
--- /dev/null
+++ b/test/RuleMaxTotalSupply/RuleMaxTotalSupplyERC3643.t.sol
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {HelperContract} from "../HelperContract.sol";
+import {TotalSupplyMock} from "src/mocks/TotalSupplyMock.sol";
+import {RuleMaxTotalSupply} from "src/rules/validation/deployment/RuleMaxTotalSupply.sol";
+import {RuleMaxTotalSupplyERC3643} from "src/rules/validation/deployment/RuleMaxTotalSupplyERC3643.sol";
+import {
+ RuleMaxTotalSupplyERC3643Ownable2Step
+} from "src/rules/validation/deployment/RuleMaxTotalSupplyERC3643Ownable2Step.sol";
+
+/**
+ * @title RuleMaxTotalSupplyERC3643Unit
+ * @notice Unit coverage for the ERC-3643 supply-cap variants, in the default profile.
+ * @dev The end-to-end proof runs against the genuine vendored token in
+ * `test/ERC3643Real/ERC3643RealTokenMaxTotalSupply.t.sol` under `FOUNDRY_PROFILE=erc3643`, which
+ * `forge test` and `forge coverage` do not include. These tests exercise the same override with
+ * a mock so the variants are covered by the ordinary run too.
+ *
+ * The rule is notified as an ERC-3643 token would notify it: the supply is set to its POST-mint
+ * value first, then the write hook is called.
+ */
+contract RuleMaxTotalSupplyERC3643Unit is Test, HelperContract {
+ uint256 private constant CAP = 1000;
+
+ TotalSupplyMock private token;
+
+ function setUp() public {
+ token = new TotalSupplyMock();
+ }
+
+ function _accessControlVariant() private returns (RuleMaxTotalSupplyERC3643) {
+ return new RuleMaxTotalSupplyERC3643(DEFAULT_ADMIN_ADDRESS, address(token), CAP);
+ }
+
+ function _ownableVariant() private returns (RuleMaxTotalSupplyERC3643Ownable2Step) {
+ return new RuleMaxTotalSupplyERC3643Ownable2Step(DEFAULT_ADMIN_ADDRESS, address(token), CAP);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ ENFORCEMENT (POST-MINT)
+ //////////////////////////////////////////////////////////////*/
+
+ function testNotifyAcceptsAMintThatLandsExactlyOnTheCap() public {
+ RuleMaxTotalSupplyERC3643 rule = _accessControlVariant();
+ token.setTotalSupply(CAP); // the mint has already happened
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, CAP);
+ }
+
+ function testNotifyRejectsAMintThatLandsAboveTheCap() public {
+ RuleMaxTotalSupplyERC3643 rule = _accessControlVariant();
+ token.setTotalSupply(CAP + 1);
+ vm.expectRevert();
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, 1);
+ }
+
+ function testNotifyAcceptsTransfersAndBurnsRegardlessOfSupply() public {
+ RuleMaxTotalSupplyERC3643 rule = _accessControlVariant();
+ token.setTotalSupply(CAP * 10); // already far over the cap
+
+ rule.transferred(ADDRESS1, ADDRESS2, 10); // transfer
+ rule.transferred(ADDRESS1, ZERO_ADDRESS, 10); // burn
+ rule.transferred(ADDRESS3, ADDRESS1, ADDRESS2, 10); // delegated transfer
+ }
+
+ function testDelegatedNotifyIsRePhasedToo() public {
+ RuleMaxTotalSupplyERC3643 rule = _accessControlVariant();
+ token.setTotalSupply(CAP);
+ rule.transferred(ADDRESS3, ZERO_ADDRESS, ADDRESS1, CAP);
+
+ token.setTotalSupply(CAP + 1);
+ vm.expectRevert();
+ rule.transferred(ADDRESS3, ZERO_ADDRESS, ADDRESS1, 1);
+ }
+
+ function testLoweringTheCapBelowTheSupplyBlocksFurtherMints() public {
+ RuleMaxTotalSupplyERC3643 rule = _accessControlVariant();
+ token.setTotalSupply(CAP);
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.setMaxTotalSupply(CAP / 2);
+
+ vm.expectRevert();
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, 1);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ THE READ PATH IS NOT RE-PHASED
+ //////////////////////////////////////////////////////////////*/
+
+ function testReadPathStillProjectsThePendingAmount() public {
+ RuleMaxTotalSupplyERC3643 rule = _accessControlVariant();
+ token.setTotalSupply(CAP);
+
+ assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, 1), CODE_MAX_TOTAL_SUPPLY_EXCEEDED);
+ assertFalse(rule.canTransfer(ZERO_ADDRESS, ADDRESS1, 1));
+
+ token.setTotalSupply(0);
+ assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, CAP), TRANSFER_OK);
+ assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, CAP + 1), CODE_MAX_TOTAL_SUPPLY_EXCEEDED);
+ }
+
+ /// @notice Pre-flight and enforcement must agree on the same mint, as the ERC-3643 token calls both.
+ function testPreFlightAndEnforcementAgree() public {
+ RuleMaxTotalSupplyERC3643 rule = _accessControlVariant();
+
+ token.setTotalSupply(0);
+ assertTrue(rule.canTransfer(ZERO_ADDRESS, ADDRESS1, CAP));
+
+ token.setTotalSupply(CAP);
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, CAP);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ CONTRAST WITH THE STOCK RULE
+ //////////////////////////////////////////////////////////////*/
+
+ function testStockRuleDoubleCountsWhereTheVariantDoesNot() public {
+ RuleMaxTotalSupply stock = new RuleMaxTotalSupply(DEFAULT_ADMIN_ADDRESS, address(token), CAP);
+ RuleMaxTotalSupplyERC3643 variant = _accessControlVariant();
+
+ token.setTotalSupply(CAP);
+
+ vm.expectRevert();
+ stock.transferred(ZERO_ADDRESS, ADDRESS1, CAP);
+
+ variant.transferred(ZERO_ADDRESS, ADDRESS1, CAP);
+ }
+
+ /*//////////////////////////////////////////////////////////////
+ OWNABLE2STEP VARIANT
+ //////////////////////////////////////////////////////////////*/
+
+ function testOwnableVariantEnforcesIdentically() public {
+ RuleMaxTotalSupplyERC3643Ownable2Step rule = _ownableVariant();
+
+ token.setTotalSupply(CAP);
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, CAP);
+
+ token.setTotalSupply(CAP + 1);
+ vm.expectRevert();
+ rule.transferred(ZERO_ADDRESS, ADDRESS1, 1);
+ }
+
+ function testOwnableVariantKeepsItsOwnerGatedConfiguration() public {
+ RuleMaxTotalSupplyERC3643Ownable2Step rule = _ownableVariant();
+
+ vm.prank(ATTACKER);
+ vm.expectRevert();
+ rule.setMaxTotalSupply(1);
+
+ vm.prank(DEFAULT_ADMIN_ADDRESS);
+ rule.setMaxTotalSupply(1);
+ assertEq(rule.maxTotalSupply(), 1);
+ }
+}
diff --git a/test/ThreatModel/ThreatModelTests.t.sol b/test/ThreatModel/ThreatModelTests.t.sol
index c618e877..ec609f45 100644
--- a/test/ThreatModel/ThreatModelTests.t.sol
+++ b/test/ThreatModel/ThreatModelTests.t.sol
@@ -12,6 +12,8 @@ import {IdentityRegistryMock} from "src/mocks/IdentityRegistryMock.sol";
import {TotalSupplyMock} from "src/mocks/TotalSupplyMock.sol";
import {MockERC20WithTransferContext} from "src/mocks/MockERC20WithTransferContext.sol";
+import {RuleSpenderWhitelist} from "src/rules/validation/deployment/RuleSpenderWhitelist.sol";
+import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol";
import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol";
import {RuleWhitelistWrapper} from "src/rules/validation/deployment/RuleWhitelistWrapper.sol";
import {RuleMaxTotalSupply} from "src/rules/validation/deployment/RuleMaxTotalSupply.sol";
@@ -618,13 +620,20 @@ contract ThreatModelTests is Test, HelperContract {
}
/**
- * @notice WW-2: unlike `RuleEngineBase`, the wrapper does not ERC-165-check that a child
- * rule implements `IAddressList`. Adding a conformant `IRule` that is not an
- * address list bricks every transfer check that has to scan past the first child.
- * Note the early-exit in `_detectTransferRestrictionForTargets`: a pair already
- * resolved by an earlier child still succeeds, so the breakage is input-dependent.
+ * @notice WW-2: **FIXED.** The wrapper now ERC-165-checks its children, so a conformant `IRule`
+ * that is not an address list is rejected at `addRule` instead of being accepted and
+ * bricking later transfer checks.
+ * @dev This test formerly asserted the broken behaviour and was named `..._CurrentBehaviour`:
+ * the wrapper accepted `RuleMaxTotalSupply` as a child, and any check whose targets were
+ * not already resolved by an earlier child reverted on the blind `areAddressesListed` call.
+ * The early exit in `_detectTransferRestrictionForTargets` made that input-dependent —
+ * `(ADDRESS1, ADDRESS2)` still passed while `(ADDRESS1, ADDRESS3)` reverted — which is what
+ * made it hard to notice.
+ *
+ * The guard requires {IAddressListBatchQuery}, the single function the wrapper actually
+ * calls, rather than the whole of `IAddressList`. Nethermind AuditAgent NM-18, audit F-5.
*/
- function test_WW2_NonAddressListChildRuleBricksWrapper_CurrentBehaviour() public {
+ function test_WW2_NonAddressListChildRuleIsRejectedAtAddRule() public {
TotalSupplyMock token = new TotalSupplyMock();
vm.startPrank(DEFAULT_ADMIN_ADDRESS);
RuleWhitelist childA = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false);
@@ -637,16 +646,95 @@ contract ThreatModelTests is Test, HelperContract {
// RuleMaxTotalSupply is a valid IRule but exposes no `areAddressesListed`.
RuleMaxTotalSupply notAnAddressList = new RuleMaxTotalSupply(DEFAULT_ADMIN_ADDRESS, address(token), 1000);
+ vm.expectRevert(
+ abi.encodeWithSelector(RuleWhitelistWrapper_ChildIsNotAnAddressList.selector, address(notAnAddressList))
+ );
wrapper.addRule(IRule(address(notAnAddressList)));
vm.stopPrank();
- // Both endpoints resolved by childA: the early-exit never reaches the broken child.
- assertEq(wrapper.detectTransferRestriction(ADDRESS1, ADDRESS2, 10), TRANSFER_OK);
+ // The wrapper is intact: the pair that used to revert now answers normally.
+ assertEq(wrapper.detectTransferRestriction(ADDRESS1, ADDRESS3, 10), CODE_ADDRESS_TO_NOT_WHITELISTED);
+ assertEq(wrapper.rulesCount(), 1, "the bad child was never added");
+ }
- // ADDRESS3 is listed nowhere, so the scan continues into the broken child and reverts
- // instead of returning CODE_ADDRESS_TO_NOT_WHITELISTED.
- vm.expectRevert();
- wrapper.detectTransferRestriction(ADDRESS1, ADDRESS3, 10);
+ /**
+ * @notice WW-2: the guard also refuses a nested wrapper, which would otherwise brick the parent.
+ * @dev `RuleWhitelistWrapper` aggregates children but does not itself implement
+ * `areAddressesListed`, so it cannot be a child of another wrapper. Before the guard that
+ * configuration was accepted and reverted every transfer through the parent; now it is
+ * refused up front. Enabling nesting is a separate change (NM-19).
+ */
+ function test_WW2_NestedWrapperIsRejectedAtAddRule() public {
+ vm.startPrank(DEFAULT_ADMIN_ADDRESS);
+ RuleWhitelistWrapper inner = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true);
+ RuleWhitelistWrapper outer = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true);
+
+ vm.expectRevert(abi.encodeWithSelector(RuleWhitelistWrapper_ChildIsNotAnAddressList.selector, address(inner)));
+ outer.addRule(IRule(address(inner)));
+ vm.stopPrank();
+ }
+
+ /**
+ * @notice WW-2 / NM-20: **FIXED.** A deny-list child is now refused at `addRule`.
+ * @dev This test formerly asserted the opposite and was named `..._CurrentBehaviour`: `RuleBlacklist`
+ * answers `areAddressesListed` just as faithfully as a whitelist and advertises the same
+ * interface ids, so the ERC-165 guard added for NM-18 could not tell them apart — the wrapper
+ * accepted it and then reported blacklisted addresses as eligible investors, `isVerified`
+ * included.
+ *
+ * Polarity is now declared rather than inferred: {IAddressListPolarity} adds `isAllowList()`,
+ * the wrapper requires it via ERC-165 and refuses any child answering `false`. Membership and
+ * meaning are separate questions, so they need separate interfaces.
+ */
+ function test_WW2_DenyListChildIsRejectedAtAddRule() public {
+ vm.startPrank(DEFAULT_ADMIN_ADDRESS);
+ RuleBlacklist denyList = new RuleBlacklist(DEFAULT_ADMIN_ADDRESS, FORWARDER);
+ denyList.addAddress(ATTACKER);
+ assertFalse(denyList.isAllowList(), "a blacklist declares itself a deny-list");
+
+ RuleWhitelistWrapper wrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true);
+ vm.expectRevert(abi.encodeWithSelector(RuleWhitelistWrapper_ChildIsNotAnAllowList.selector, address(denyList)));
+ wrapper.addRule(IRule(address(denyList)));
+ vm.stopPrank();
+
+ assertEq(wrapper.rulesCount(), 0, "the deny-list was never added");
+ // The inversion this finding described can no longer be configured.
+ assertFalse(wrapper.isVerified(ATTACKER));
+ }
+
+ /**
+ * @notice WW-2 / NM-20: a rule that declines to declare polarity is refused too.
+ * @dev `RuleSpenderWhitelist` deliberately does not implement {IAddressListPolarity}. Its set IS an
+ * allow-list, so declaring `true` would be honest about polarity and still wrong — the listed
+ * addresses are permitted *spenders*, not permitted *holders*, and the wrapper would read them
+ * as eligible transfer participants. Withholding the declaration is what makes the wrapper's
+ * fail-closed check refuse it: absence is a refusal, never an assumed allow-list.
+ */
+ function test_WW2_ChildDecliningToDeclarePolarityIsRejected() public {
+ vm.startPrank(DEFAULT_ADMIN_ADDRESS);
+ RuleSpenderWhitelist spenderList = new RuleSpenderWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER);
+ RuleWhitelistWrapper wrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true);
+
+ vm.expectRevert(
+ abi.encodeWithSelector(RuleWhitelistWrapper_ChildDoesNotDeclarePolarity.selector, address(spenderList))
+ );
+ wrapper.addRule(IRule(address(spenderList)));
+ vm.stopPrank();
+ }
+
+ /// @notice WW-2 / NM-20: genuine allow-lists are still accepted, so the guard is not simply refusing all.
+ function test_WW2_AllowListChildrenAreStillAccepted() public {
+ vm.startPrank(DEFAULT_ADMIN_ADDRESS);
+ RuleWhitelist allowList = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false);
+ allowList.addAddress(ADDRESS1);
+ assertTrue(allowList.isAllowList(), "a whitelist declares itself an allow-list");
+
+ RuleWhitelistWrapper wrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true);
+ wrapper.addRule(IRule(address(allowList)));
+ vm.stopPrank();
+
+ assertEq(wrapper.rulesCount(), 1);
+ assertTrue(wrapper.isVerified(ADDRESS1));
}
/**
diff --git a/test/TransferContext/OverloadParity.t.sol b/test/TransferContext/OverloadParity.t.sol
index 24e3327d..0235bae1 100644
--- a/test/TransferContext/OverloadParity.t.sol
+++ b/test/TransferContext/OverloadParity.t.sol
@@ -62,9 +62,22 @@ interface INFTAdapterRule {
* struct entrypoints on EVERY rule that inherits {RuleNFTAdapter}, closing the residual
* coverage gap in `RuleNFTAdapter` / `RuleTransferValidation`.
* @dev The property under test is **parity**: `RuleNFTAdapter` exists only to re-expose the same
- * restriction logic under extra signatures, ignoring `tokenId`. So for every rule and every
- * input, the `tokenId` overload MUST be indistinguishable from its fungible counterpart, and
- * the `ctx` entrypoints MUST dispatch to the same internal hooks. Any divergence is a bug.
+ * restriction logic under extra signatures, ignoring `tokenId`. So for every rule, entrypoints
+ * that describe the SAME transfer MUST return the same answer, and any divergence is a bug.
+ *
+ * "The same transfer" is the subtlety, because the interfaces signal a direct transfer
+ * differently — this is what NM-6 turned on, and stating it loosely is what hid the gap:
+ *
+ * | Interface | A direct transfer arrives as | A delegated one as |
+ * |---|---|---|
+ * | CMTAT 3-arg / 4-arg | 3-arg, or `spender == address(0)` | `spender != 0`, any value |
+ * | ERC-7943 5-arg | `spender == from` (the spec calls it "owner/operator") | `spender != from` |
+ * | {ITransferContext} | `sender == from`, or `sender == 0` | `sender != from` |
+ *
+ * So `4-arg(spender == from)` and `5-arg(spender == from)` describe DIFFERENT transfers and are
+ * expected to differ; `test_NM6_CmtatFourArgPathKeepsScreeningASelfSpender` pins that on purpose.
+ * Everything that does describe the same transfer must agree, which is what
+ * `_assertSelfSpenderIsDirect` adds to the original two cases (`sender == 0`, `sender != from`).
*
* This also pins threat `AC-5`: the `ctx` entrypoints are `external` with no access control on
* validation rules. That is acceptable precisely because they are view-only — an unprivileged
@@ -154,6 +167,49 @@ contract OverloadParity is Test, HelperContract {
);
}
+ /**
+ * @dev NM-6: `spender == from` is an OWNER-INITIATED transfer, and every entrypoint whose
+ * interface reports the initiator (the ERC-7943 spender-aware overloads and both
+ * {ITransferContext} structs) must route it to the DIRECT hook — the same answer a plain
+ * `transfer` gets. Before the fix the ERC-7943 overloads called the spender-aware hook
+ * unconditionally, so an owner-initiated ERC-721 `transferFrom` was screened as delegated
+ * while the identical `ctx` call was not.
+ */
+ function _assertSelfSpenderIsDirect(address rule, address from, address to, uint256 value, string memory w)
+ internal
+ {
+ INFTAdapterRule r = INFTAdapterRule(rule);
+ uint8 directCode = r.detectTransferRestriction(from, to, value);
+
+ assertEq(
+ r.detectTransferRestrictionFrom(from, from, to, TOKEN_ID, value),
+ directCode,
+ string.concat(w, ": ERC-7943 detectTransferRestrictionFrom(spender==from) must equal the direct code")
+ );
+ assertEq(
+ r.canTransferFrom(from, from, to, TOKEN_ID, value),
+ r.canTransfer(from, to, value),
+ string.concat(w, ": ERC-7943 canTransferFrom(spender==from) must equal canTransfer")
+ );
+
+ bool direct = _try3(rule, from, to, value);
+ assertEq(
+ _try5Nft(rule, from, from, to, value),
+ direct,
+ string.concat(w, ": ERC-7943 transferred(spender==from) must match transferred(from,to,value)")
+ );
+ assertEq(
+ _tryFungibleCtx(rule, from, from, to, value),
+ direct,
+ string.concat(w, ": FungibleContext(sender==from) must match transferred(from,to,value)")
+ );
+ assertEq(
+ _tryMultiCtx(rule, from, from, to, value),
+ direct,
+ string.concat(w, ": MultiTokenContext(sender==from) must match transferred(from,to,value)")
+ );
+ }
+
/// @dev Runs both parity checks for an allowed pair and a blocked pair.
function _assertParity(
address rule,
@@ -169,6 +225,9 @@ contract OverloadParity is Test, HelperContract {
_assertReadParity(rule, spender, badFrom, badTo, 10, string.concat(what, " [blocked]"));
_assertWriteParity(rule, spender, badFrom, badTo, 10, string.concat(what, " [blocked]"));
+
+ _assertSelfSpenderIsDirect(rule, okFrom, okTo, 10, string.concat(what, " [self-spender, allowed]"));
+ _assertSelfSpenderIsDirect(rule, badFrom, badTo, 10, string.concat(what, " [self-spender, blocked]"));
}
/*//////////////////////////////////////////////////////////////
@@ -220,6 +279,67 @@ contract OverloadParity is Test, HelperContract {
_assertWriteParity(address(rule), ADDRESS3, ADDRESS1, ADDRESS2, 10, "RuleSpenderWhitelist [ok spender]");
_assertReadParity(address(rule), ATTACKER, ADDRESS1, ADDRESS2, 10, "RuleSpenderWhitelist [bad spender]");
_assertWriteParity(address(rule), ATTACKER, ADDRESS1, ADDRESS2, 10, "RuleSpenderWhitelist [bad spender]");
+
+ // NM-6. ADDRESS1 is NOT on the spender whitelist, so this is the rule where the self-spender
+ // routing is observable rather than merely tidy.
+ _assertSelfSpenderIsDirect(address(rule), ADDRESS1, ADDRESS2, 10, "RuleSpenderWhitelist [self-spender]");
+ }
+
+ /**
+ * @notice NM-6: an owner moving their own tokens is never blocked by the spender whitelist,
+ * whichever spender-reporting entrypoint the token uses.
+ * @dev This rule documents that direct transfers are always allowed and only delegated ones are
+ * screened. An owner-initiated ERC-721 `transferFrom` arrives as `spender == from` per the
+ * ERC-7943 interface ("the address performing the transfer (owner/operator)"), so routing it
+ * to the spender-aware hook contradicted that contract.
+ */
+ function test_NM6_SelfSpenderIsNotScreenedByTheSpenderWhitelist() public {
+ vm.startPrank(DEFAULT_ADMIN_ADDRESS);
+ RuleSpenderWhitelist rule = new RuleSpenderWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER);
+ rule.addAddress(ADDRESS3);
+ vm.stopPrank();
+
+ // ADDRESS1 is not a whitelisted spender, but it owns the tokens.
+ assertFalse(rule.isAddressListed(ADDRESS1), "precondition: owner is not a listed spender");
+
+ assertEq(
+ rule.detectTransferRestrictionFrom(ADDRESS1, ADDRESS1, ADDRESS2, TOKEN_ID, 10),
+ TRANSFER_OK,
+ "owner-initiated ERC-7943 transfer must not be screened as delegated"
+ );
+ assertTrue(_try5Nft(address(rule), ADDRESS1, ADDRESS1, ADDRESS2, 10), "write path must accept it too");
+ assertTrue(_tryFungibleCtx(address(rule), ADDRESS1, ADDRESS1, ADDRESS2, 10), "ctx path already accepted it");
+
+ // A genuine delegated transfer by the same unlisted address is still rejected: the fix
+ // narrows the screen to what it was always documented to cover, it does not remove it.
+ assertEq(
+ rule.detectTransferRestrictionFrom(ADDRESS1, ADDRESS3, ADDRESS2, TOKEN_ID, 10),
+ rule.CODE_ADDRESS_SPENDER_NOT_WHITELISTED(),
+ "an unlisted spender acting for someone else must still be blocked"
+ );
+ assertFalse(_try5Nft(address(rule), ADDRESS1, ADDRESS3, ADDRESS2, 10), "and blocked on the write path");
+ }
+
+ /**
+ * @notice NM-6, the deliberate asymmetry: the CMTAT 4-arg path is NOT normalised.
+ * @dev It signals a direct transfer with `spender == address(0)` and the 3-arg overload, so
+ * `spender == from` there means the caller explicitly named a spender. The ERC-7943 and
+ * `ctx` interfaces have no zero-sentinel convention, which is why only they normalise.
+ * Pinned so nobody "aligns" the two and silently disables spender screening on the main path.
+ */
+ function test_NM6_CmtatFourArgPathKeepsScreeningASelfSpender() public {
+ vm.startPrank(DEFAULT_ADMIN_ADDRESS);
+ RuleSpenderWhitelist rule = new RuleSpenderWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER);
+ rule.addAddress(ADDRESS3);
+ vm.stopPrank();
+
+ assertEq(
+ rule.detectTransferRestrictionFrom(ADDRESS1, ADDRESS1, ADDRESS2, 10),
+ rule.CODE_ADDRESS_SPENDER_NOT_WHITELISTED(),
+ "the 4-arg CMTAT path screens whatever spender it is given"
+ );
+ // ...while a plain transfer on that path carries no spender and passes.
+ assertEq(rule.detectTransferRestriction(ADDRESS1, ADDRESS2, 10), TRANSFER_OK);
}
function test_Parity_RuleSanctionsList() public {
diff --git a/test/Version.t.sol b/test/Version.t.sol
index cdb5dcf3..49a37ee4 100644
--- a/test/Version.t.sol
+++ b/test/Version.t.sol
@@ -8,6 +8,10 @@ import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol";
import {RuleSanctionsList} from "src/rules/validation/deployment/RuleSanctionsList.sol";
import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol";
import {RuleMaxTotalSupply} from "src/rules/validation/deployment/RuleMaxTotalSupply.sol";
+import {RuleMaxTotalSupplyERC3643} from "src/rules/validation/deployment/RuleMaxTotalSupplyERC3643.sol";
+import {
+ RuleMaxTotalSupplyERC3643Ownable2Step
+} from "src/rules/validation/deployment/RuleMaxTotalSupplyERC3643Ownable2Step.sol";
import {TotalSupplyMock} from "src/mocks/TotalSupplyMock.sol";
import {RuleMaxBalance} from "src/rules/validation/deployment/RuleMaxBalance.sol";
import {RuleMaxBalanceOwnable2Step} from "src/rules/validation/deployment/RuleMaxBalanceOwnable2Step.sol";
@@ -21,13 +25,17 @@ import {RuleSpenderWhitelist} from "src/rules/validation/deployment/RuleSpenderW
import {RuleReceiverWhitelist} from "src/rules/validation/deployment/RuleReceiverWhitelist.sol";
import {RuleIdentityRegistry} from "src/rules/validation/deployment/RuleIdentityRegistry.sol";
import {RuleChainlinkPoR} from "src/rules/validation/deployment/RuleChainlinkPoR.sol";
+import {RuleChainlinkPoRERC3643} from "src/rules/validation/deployment/RuleChainlinkPoRERC3643.sol";
+import {
+ RuleChainlinkPoRERC3643Ownable2Step
+} from "src/rules/validation/deployment/RuleChainlinkPoRERC3643Ownable2Step.sol";
import {IdentityRegistryWhitelist} from "src/registry/IdentityRegistryWhitelist.sol";
import {AggregatorV3Interface} from "src/rules/interfaces/AggregatorV3Interface.sol";
import {AggregatorV3Mock} from "src/mocks/AggregatorV3Mock.sol";
import {TotalSupplyDecimalsMock} from "src/mocks/TotalSupplyDecimalsMock.sol";
contract VersionTest is Test, HelperContract {
- string constant EXPECTED_VERSION = "0.5.0";
+ string constant EXPECTED_VERSION = "0.6.0";
function testVersionRuleWhitelist() public {
RuleWhitelist rule = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, false);
@@ -100,6 +108,37 @@ contract VersionTest is Test, HelperContract {
assertEq(rule.version(), EXPECTED_VERSION);
}
+ function testVersionRuleMaxTotalSupplyERC3643() public {
+ TotalSupplyDecimalsMock token = new TotalSupplyDecimalsMock(18);
+ RuleMaxTotalSupplyERC3643 rule = new RuleMaxTotalSupplyERC3643(DEFAULT_ADMIN_ADDRESS, address(token), 1000);
+ assertEq(rule.version(), EXPECTED_VERSION);
+ }
+
+ function testVersionRuleMaxTotalSupplyERC3643Ownable2Step() public {
+ TotalSupplyDecimalsMock token = new TotalSupplyDecimalsMock(18);
+ RuleMaxTotalSupplyERC3643Ownable2Step rule =
+ new RuleMaxTotalSupplyERC3643Ownable2Step(DEFAULT_ADMIN_ADDRESS, address(token), 1000);
+ assertEq(rule.version(), EXPECTED_VERSION);
+ }
+
+ function testVersionRuleChainlinkPoRERC3643() public {
+ TotalSupplyDecimalsMock token = new TotalSupplyDecimalsMock(18);
+ AggregatorV3Mock feed = new AggregatorV3Mock(8, 1000 * 1e8);
+ RuleChainlinkPoRERC3643 rule = new RuleChainlinkPoRERC3643(
+ DEFAULT_ADMIN_ADDRESS, address(token), 18, AggregatorV3Interface(address(feed)), 1 days
+ );
+ assertEq(rule.version(), EXPECTED_VERSION);
+ }
+
+ function testVersionRuleChainlinkPoRERC3643Ownable2Step() public {
+ TotalSupplyDecimalsMock token = new TotalSupplyDecimalsMock(18);
+ AggregatorV3Mock feed = new AggregatorV3Mock(8, 1000 * 1e8);
+ RuleChainlinkPoRERC3643Ownable2Step rule = new RuleChainlinkPoRERC3643Ownable2Step(
+ DEFAULT_ADMIN_ADDRESS, address(token), 18, AggregatorV3Interface(address(feed)), 1 days
+ );
+ assertEq(rule.version(), EXPECTED_VERSION);
+ }
+
function testVersionRuleConditionalTransferLightMultiToken() public {
RuleConditionalTransferLightMultiToken rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS);
assertEq(rule.version(), EXPECTED_VERSION);
diff --git a/test/VirtualHooks/BatchGuardPointerVirtual.t.sol b/test/VirtualHooks/BatchGuardPointerVirtual.t.sol
new file mode 100644
index 00000000..e143e6d5
--- /dev/null
+++ b/test/VirtualHooks/BatchGuardPointerVirtual.t.sol
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {RuleAddressSetInternal} from "src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol";
+
+/**
+ * @notice A-1/E-1: the batch zero-address guard is passed to `AddressSetBatchLib` as an **internal function
+ * pointer**, and must stay overridable.
+ * @dev Two things this pins, neither of which a compile-only check would catch:
+ *
+ * 1. `_requireNotZeroAddress` is `virtual`. Removing the keyword breaks this file's compilation, because
+ * the harness below declares `override`.
+ * 2. Virtual dispatch actually reaches the override **through the function pointer**. Solidity resolves an
+ * internal function pointer at the point of assignment, so it is not obvious that an override installed
+ * by a derived contract is the one `addBatch` ends up calling. It is — asserted here rather than
+ * assumed, because a silently shadowed override would leave the guard looking extensible while the base
+ * implementation kept running.
+ */
+contract BatchGuardPointerHarness is RuleAddressSetInternal {
+ error OverrideWasReached();
+
+ function _requireNotZeroAddress(address) internal pure override {
+ revert OverrideWasReached();
+ }
+
+ function addAddressesPublic(address[] calldata targets) external returns (uint256 added, uint256 skipped) {
+ return _addAddresses(targets);
+ }
+}
+
+contract BatchGuardPointerVirtual is Test {
+ function testOverrideIsReachedThroughTheFunctionPointer() public {
+ BatchGuardPointerHarness harness = new BatchGuardPointerHarness();
+ address[] memory targets = new address[](1);
+ targets[0] = address(0x1234);
+
+ vm.expectRevert(BatchGuardPointerHarness.OverrideWasReached.selector);
+ harness.addAddressesPublic(targets);
+ }
+}
|