@coti-io/coti-contracts 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (433) hide show
  1. package/.github/workflows/npm-publish.yml +19 -15
  2. package/README.md +3 -1
  3. package/contracts/disperse/coinByRatio/FixedRatioCoinDisperser.sol +452 -0
  4. package/contracts/disperse/coinByRatio/FixedRatioCoinDisperserLeftoverS1.sol +490 -0
  5. package/contracts/disperse/coinByRatio/FixedRatioCoinDisperserUnlimitedWindow.sol +432 -0
  6. package/contracts/disperse/disperseToken/MintDisperser.sol +120 -0
  7. package/contracts/disperse/disperseToken/TokenDisperser.sol +139 -0
  8. package/contracts/messaging/PrivateMessaging.sol +456 -0
  9. package/contracts/mocks/messaging/PrivateMessagingHarness.sol +54 -0
  10. package/contracts/mocks/oracle/CotiPriceConsumerMock.sol +31 -0
  11. package/contracts/mocks/token/ERC20DecimalsMock.sol +21 -0
  12. package/contracts/mocks/token/ERC20Mock.sol +19 -0
  13. package/contracts/mocks/token/PrivateERC20/EncryptedTokenReceiverMock.sol +18 -0
  14. package/contracts/mocks/token/PrivateERC20/PrivacyBridgeERC20Mock.sol +18 -0
  15. package/contracts/mocks/token/PrivateERC20/PrivateERC20AuditorSupplyMock.sol +44 -0
  16. package/contracts/mocks/token/PrivateERC20/PrivateERC20CappedMock.sol +23 -0
  17. package/contracts/mocks/token/PrivateERC20/PrivateERC20GtCallerMock.sol +29 -0
  18. package/contracts/mocks/token/PrivateERC20/PrivateERC20Mock.sol +9 -4
  19. package/contracts/mocks/token/PrivateERC20/PrivateERC20ReentrantReceiverMock.sol +39 -0
  20. package/contracts/mocks/token/PrivateERC20/PublicReentrantTokenReceiverMock.sol +29 -0
  21. package/contracts/mocks/token/PrivateERC20/PublicTokenReceiverBoolMock.sol +18 -0
  22. package/contracts/mocks/token/PrivateERC20/PublicTokenReceiverMock.sol +12 -0
  23. package/contracts/mocks/utils/mpc/Arithmetic128TestsContract.sol +123 -0
  24. package/contracts/mocks/utils/mpc/Arithmetic256TestsContract.sol +122 -0
  25. package/contracts/mocks/utils/mpc/Bitwise128TestsContract.sol +65 -0
  26. package/contracts/mocks/utils/mpc/Bitwise256TestsContract.sol +65 -0
  27. package/contracts/mocks/utils/mpc/CheckedArithmetic128WithOverflowBitTestsContract.sol +64 -0
  28. package/contracts/mocks/utils/mpc/CheckedArithmetic256WithOverflowBitTestsContract.sol +64 -0
  29. package/contracts/mocks/utils/mpc/Comparison128TestsContract.sol +98 -0
  30. package/contracts/mocks/utils/mpc/Comparison256TestsContract.sol +98 -0
  31. package/contracts/mocks/utils/mpc/MinMax128TestsContract.sol +44 -0
  32. package/contracts/mocks/utils/mpc/MinMax256TestsContract.sol +44 -0
  33. package/contracts/mocks/utils/mpc/MinimalImplementation.sol +55 -0
  34. package/contracts/mocks/utils/mpc/MinimalProxy.sol +27 -0
  35. package/contracts/mocks/utils/mpc/MpcOperations128TestContract.sol +267 -0
  36. package/contracts/mocks/utils/mpc/MpcOperationsTestContract.sol +334 -0
  37. package/contracts/mocks/utils/mpc/Mux128TestsContract.sol +24 -0
  38. package/contracts/mocks/utils/mpc/Mux256TestsContract.sol +24 -0
  39. package/contracts/mocks/utils/mpc/OnBoard128TestsContract.sol +93 -0
  40. package/contracts/mocks/utils/mpc/OnBoard256TestsContract.sol +116 -0
  41. package/contracts/mocks/utils/mpc/PrivacyImplementationV1.sol +157 -0
  42. package/contracts/mocks/utils/mpc/PrivacyImplementationV2.sol +133 -0
  43. package/contracts/mocks/utils/mpc/PrivacyProxy.sol +137 -0
  44. package/contracts/mocks/utils/mpc/Random128TestsContract.sol +32 -0
  45. package/contracts/mocks/utils/mpc/Random256TestsContract.sol +32 -0
  46. package/contracts/mocks/utils/mpc/Shift128TestsContract.sol +36 -0
  47. package/contracts/mocks/utils/mpc/Shift256TestsContract.sol +36 -0
  48. package/contracts/mocks/utils/mpc/Transfer128TestsContract.sol +29 -0
  49. package/contracts/mocks/utils/mpc/Transfer256TestsContract.sol +29 -0
  50. package/contracts/mocks/utils/mpc/TransferWithAllowance128TestsContract.sol +32 -0
  51. package/contracts/mocks/utils/mpc/TransferWithAllowance256TestsContract.sol +32 -0
  52. package/contracts/mocks/utils/mpc/ValidateCiphertext128TestsContract.sol +59 -0
  53. package/contracts/mocks/utils/mpc/ValidateCiphertext256TestsContract.sol +63 -0
  54. package/contracts/mocks/utils/mpc/ValidateCiphertextTestsContract.sol +99 -0
  55. package/contracts/mocks/wallet/PrivateERC20Wallet/PrivateERC20WalletMock.sol +7 -6
  56. package/contracts/node/CotiNodeRewards.sol +199 -0
  57. package/contracts/node/SoulboundNodeNFT.sol +519 -0
  58. package/contracts/oracle/CotiPriceConsumer.sol +103 -0
  59. package/contracts/oracle/ICotiPriceConsumer.sol +15 -0
  60. package/contracts/oracle/IStdReference.sol +26 -0
  61. package/contracts/privacyBridge/PrivacyBridge.sol +600 -0
  62. package/contracts/privacyBridge/PrivacyBridgeCotiNative.sol +316 -0
  63. package/contracts/privacyBridge/PrivacyBridgeERC20.sol +333 -0
  64. package/contracts/privacyBridge/PrivacyBridgeUSDCe.sol +22 -0
  65. package/contracts/privacyBridge/PrivacyBridgeUSDT.sol +23 -0
  66. package/contracts/privacyBridge/PrivacyBridgeWADA.sol +23 -0
  67. package/contracts/privacyBridge/PrivacyBridgeWBTC.sol +23 -0
  68. package/contracts/privacyBridge/PrivacyBridgeWETH.sol +23 -0
  69. package/contracts/privacyBridge/PrivacyBridgegCoti.sol +23 -0
  70. package/contracts/token/PrivateERC20/IPrivateERC20.sol +273 -40
  71. package/contracts/token/PrivateERC20/ITokenReceiver.sol +17 -0
  72. package/contracts/token/PrivateERC20/ITokenReceiverEncrypted.sol +20 -0
  73. package/contracts/token/PrivateERC20/PrivateERC20.sol +861 -152
  74. package/contracts/token/PrivateERC20/tokens/PrivateBridgedUSDC.sol +17 -0
  75. package/contracts/token/PrivateERC20/tokens/PrivateCOTI.sol +13 -0
  76. package/contracts/token/PrivateERC20/tokens/PrivateCOTITreasuryGovernanceToken.sol +17 -0
  77. package/contracts/token/PrivateERC20/tokens/PrivateTetherUSD.sol +17 -0
  78. package/contracts/token/PrivateERC20/tokens/PrivateWrappedADA.sol +17 -0
  79. package/contracts/token/PrivateERC20/tokens/PrivateWrappedBTC.sol +17 -0
  80. package/contracts/token/PrivateERC20/tokens/PrivateWrappedEther.sol +13 -0
  81. package/contracts/utils/mpc/MpcCore.sol +10780 -4962
  82. package/contracts/utils/mpc/MpcInterface.sol +7 -2
  83. package/hardhat/gasPriceBump.ts +60 -0
  84. package/hardhat.config.ts +63 -12
  85. package/package.json +22 -4
  86. package/scripts/add-bridge-operators.cjs +87 -0
  87. package/scripts/deploy-coti-price-consumer-mainnet.cjs +49 -0
  88. package/scripts/deployCotiNodeRewards.ts +51 -0
  89. package/scripts/deploySoulboundNodeNFT.ts +57 -0
  90. package/scripts/redeploy-private-and-bridges.cjs +201 -0
  91. package/scripts/set-bridge-limits-mainnet.cjs +177 -0
  92. package/scripts/verify-mainnet-private-bridges.cjs +289 -0
  93. package/sonar-project.properties +11 -0
  94. package/test/messaging/PrivateMessaging.test.ts +158 -0
  95. package/test/privacyBridge/UnifiedPrivacyBridges.test.cjs +1663 -0
  96. package/test/token/PrivateERC20/PrivacyBridge.fees.test.ts +250 -0
  97. package/test/token/PrivateERC20/PrivateERC20.test.ts +1109 -268
  98. package/test/utils/mpc/Bitwise128.test.ts +92 -0
  99. package/test/utils/mpc/Bitwise256.test.ts +101 -0
  100. package/test/utils/mpc/BuildInputText128.test.ts +398 -0
  101. package/test/utils/mpc/CheckedArithmetic128.test.ts +45 -0
  102. package/test/utils/mpc/CheckedArithmetic256.test.ts +45 -0
  103. package/test/utils/mpc/Comparison128.test.ts +48 -0
  104. package/test/utils/mpc/Comparison256.test.ts +48 -0
  105. package/test/utils/mpc/InvalidCiphertext.test.ts +179 -0
  106. package/test/utils/mpc/InvalidCiphertext128.test.ts +179 -0
  107. package/test/utils/mpc/InvalidCiphertext256.test.ts +201 -0
  108. package/test/utils/mpc/InvalidTransfer128.test.ts +166 -0
  109. package/test/utils/mpc/InvalidTransfer256.test.ts +158 -0
  110. package/test/utils/mpc/MinMax128.test.ts +44 -0
  111. package/test/utils/mpc/MinMax256.test.ts +44 -0
  112. package/test/utils/mpc/MinimalProxyDemo.test.ts +280 -0
  113. package/test/utils/mpc/MpcOperations.test.ts +589 -0
  114. package/test/utils/mpc/MpcOperations128.test.ts +632 -0
  115. package/test/utils/mpc/Mux128.test.ts +41 -0
  116. package/test/utils/mpc/Mux256.test.ts +41 -0
  117. package/test/utils/mpc/NegativeCases.test.ts +478 -0
  118. package/test/utils/mpc/NegativeCases128.test.ts +237 -0
  119. package/test/utils/mpc/NegativeCases256.test.ts +236 -0
  120. package/test/utils/mpc/OnBoard128.test.ts +90 -0
  121. package/test/utils/mpc/OnBoard256.test.ts +114 -0
  122. package/test/utils/mpc/Precompile128.test.ts +51 -0
  123. package/test/utils/mpc/Precompile256.test.ts +115 -0
  124. package/test/utils/mpc/ProxyValidation.test.ts +199 -0
  125. package/test/utils/mpc/Random128.test.ts +47 -0
  126. package/test/utils/mpc/Random256.test.ts +47 -0
  127. package/test/utils/mpc/Shift128.test.ts +41 -0
  128. package/test/utils/mpc/Shift256.test.ts +41 -0
  129. package/test/utils/mpc/Transfer128.test.ts +46 -0
  130. package/test/utils/mpc/Transfer256.test.ts +46 -0
  131. package/test/utils/mpc/TransferWithAllowance128.test.ts +48 -0
  132. package/test/utils/mpc/TransferWithAllowance256.test.ts +48 -0
  133. package/test/utils/mpc/UpgradeablePrivacyProxy.test.ts +307 -0
  134. package/test/utils/mpc/ValidateCiphertext.test.ts +221 -0
  135. package/test/utils/mpc/ValidateCiphertext256.test.ts +161 -0
  136. package/test/utils/mpc/ValidateCiphertext256Decryption.test.ts +383 -0
  137. package/test/utils/mpc/WalletEncryptionFailures.test.ts +494 -0
  138. package/test/utils/privateErc20Helpers.ts +97 -0
  139. package/typechain-types/@openzeppelin/contracts/access/AccessControl.ts +324 -0
  140. package/typechain-types/@openzeppelin/contracts/access/AccessControlEnumerable.ts +361 -0
  141. package/typechain-types/@openzeppelin/contracts/access/IAccessControl.ts +292 -0
  142. package/typechain-types/@openzeppelin/contracts/access/IAccessControlEnumerable.ts +329 -0
  143. package/typechain-types/@openzeppelin/contracts/access/Ownable.ts +153 -0
  144. package/typechain-types/@openzeppelin/contracts/access/index.ts +8 -0
  145. package/typechain-types/@openzeppelin/contracts/index.ts +4 -0
  146. package/typechain-types/@openzeppelin/contracts/security/Pausable.ts +150 -0
  147. package/typechain-types/@openzeppelin/contracts/security/index.ts +4 -0
  148. package/typechain-types/@openzeppelin/contracts/token/ERC20/ERC20.ts +330 -0
  149. package/typechain-types/@openzeppelin/contracts/token/ERC20/IERC20.ts +262 -0
  150. package/typechain-types/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.ts +286 -0
  151. package/typechain-types/@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.ts +143 -0
  152. package/typechain-types/@openzeppelin/contracts/token/ERC20/extensions/index.ts +5 -0
  153. package/typechain-types/@openzeppelin/contracts/token/ERC20/index.ts +7 -0
  154. package/typechain-types/@openzeppelin/contracts/token/ERC721/ERC721.ts +420 -0
  155. package/typechain-types/@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.ts +420 -0
  156. package/typechain-types/@openzeppelin/contracts/token/ERC721/extensions/index.ts +4 -0
  157. package/typechain-types/@openzeppelin/contracts/token/ERC721/index.ts +3 -0
  158. package/typechain-types/@openzeppelin/contracts/token/index.ts +2 -0
  159. package/typechain-types/contracts/disperse/coinByRatio/FixedRatioCoinDisperser.ts +679 -0
  160. package/typechain-types/contracts/disperse/coinByRatio/FixedRatioCoinDisperserLeftoverS1.ts +735 -0
  161. package/typechain-types/contracts/disperse/coinByRatio/FixedRatioCoinDisperserUnlimitedWindow.ts +734 -0
  162. package/typechain-types/contracts/disperse/coinByRatio/index.ts +6 -0
  163. package/typechain-types/contracts/disperse/disperseToken/MintDisperser.sol/IMintableERC20.ts +97 -0
  164. package/typechain-types/contracts/disperse/disperseToken/MintDisperser.sol/IOwnable.ts +95 -0
  165. package/typechain-types/contracts/disperse/disperseToken/MintDisperser.sol/MintDisperser.ts +228 -0
  166. package/typechain-types/contracts/disperse/disperseToken/MintDisperser.sol/index.ts +6 -0
  167. package/typechain-types/contracts/disperse/disperseToken/TokenDisperser.ts +191 -0
  168. package/typechain-types/contracts/disperse/disperseToken/index.ts +6 -0
  169. package/typechain-types/contracts/disperse/index.ts +7 -0
  170. package/typechain-types/contracts/index.ts +10 -0
  171. package/typechain-types/contracts/messaging/PrivateMessaging.ts +871 -0
  172. package/typechain-types/contracts/messaging/index.ts +4 -0
  173. package/typechain-types/contracts/mocks/index.ts +4 -0
  174. package/typechain-types/contracts/mocks/messaging/PrivateMessagingHarness.ts +951 -0
  175. package/typechain-types/contracts/mocks/messaging/index.ts +4 -0
  176. package/typechain-types/contracts/mocks/oracle/CotiPriceConsumerMock.ts +163 -0
  177. package/typechain-types/contracts/mocks/oracle/index.ts +4 -0
  178. package/typechain-types/contracts/mocks/token/ERC20DecimalsMock.ts +349 -0
  179. package/typechain-types/contracts/mocks/token/ERC20Mock.ts +368 -0
  180. package/typechain-types/contracts/mocks/token/PrivateERC20/EncryptedTokenReceiverMock.ts +108 -0
  181. package/typechain-types/contracts/mocks/token/PrivateERC20/PrivacyBridgeERC20Mock.ts +1892 -0
  182. package/typechain-types/contracts/mocks/token/PrivateERC20/PrivateERC20AuditorSupplyMock.ts +1296 -0
  183. package/typechain-types/contracts/mocks/token/PrivateERC20/PrivateERC20CappedMock.ts +1270 -0
  184. package/typechain-types/contracts/mocks/token/PrivateERC20/PrivateERC20GtCallerMock.ts +166 -0
  185. package/typechain-types/contracts/mocks/token/PrivateERC20/PrivateERC20Mock.ts +827 -78
  186. package/typechain-types/contracts/mocks/token/PrivateERC20/PrivateERC20ReentrantReceiverMock.sol/IPrivateERC20TransferAndCallEncrypted.ts +102 -0
  187. package/typechain-types/contracts/mocks/token/PrivateERC20/PrivateERC20ReentrantReceiverMock.sol/PrivateERC20ReentrantReceiverMock.ts +154 -0
  188. package/typechain-types/contracts/mocks/token/PrivateERC20/PrivateERC20ReentrantReceiverMock.sol/index.ts +5 -0
  189. package/typechain-types/contracts/mocks/token/PrivateERC20/PublicReentrantTokenReceiverMock.ts +155 -0
  190. package/typechain-types/contracts/mocks/token/PrivateERC20/PublicTokenReceiverBoolMock.ts +107 -0
  191. package/typechain-types/contracts/mocks/token/PrivateERC20/PublicTokenReceiverMock.ts +100 -0
  192. package/typechain-types/contracts/mocks/token/PrivateERC20/index.ts +10 -0
  193. package/typechain-types/contracts/mocks/token/index.ts +2 -0
  194. package/typechain-types/contracts/mocks/utils/mpc/{Arithmetic128BitTestsContract.ts → Arithmetic128TestsContract.ts} +96 -126
  195. package/typechain-types/contracts/mocks/utils/mpc/{Arithmetic256BitTestsContract.ts → Arithmetic256TestsContract.ts} +97 -133
  196. package/typechain-types/contracts/mocks/utils/mpc/{Bitwise128BitTestsContract.ts → Bitwise128TestsContract.ts} +52 -59
  197. package/typechain-types/contracts/mocks/utils/mpc/{Bitwise256BitTestsContract.ts → Bitwise256TestsContract.ts} +52 -59
  198. package/typechain-types/contracts/mocks/utils/mpc/CheckedArithmetic128WithOverflowBitTestsContract.ts +206 -0
  199. package/typechain-types/contracts/mocks/utils/mpc/CheckedArithmetic256WithOverflowBitTestsContract.ts +206 -0
  200. package/typechain-types/contracts/mocks/utils/mpc/{Comparison128BitTestsContract.ts → Comparison128TestsContract.ts} +115 -97
  201. package/typechain-types/contracts/mocks/utils/mpc/{Comparison256BitTestsContract.ts → Comparison256TestsContract.ts} +115 -97
  202. package/typechain-types/contracts/mocks/utils/mpc/MinMax128TestsContract.ts +142 -0
  203. package/typechain-types/contracts/mocks/utils/mpc/MinMax256TestsContract.ts +142 -0
  204. package/typechain-types/contracts/mocks/utils/mpc/MinimalImplementation.ts +259 -0
  205. package/typechain-types/contracts/mocks/utils/mpc/MinimalProxy.ts +90 -0
  206. package/typechain-types/contracts/mocks/utils/mpc/MpcOperations128TestContract.ts +388 -0
  207. package/typechain-types/contracts/mocks/utils/mpc/MpcOperationsTestContract.ts +455 -0
  208. package/typechain-types/contracts/mocks/utils/mpc/Mux128TestsContract.ts +109 -0
  209. package/typechain-types/contracts/mocks/utils/mpc/Mux256TestsContract.ts +109 -0
  210. package/typechain-types/contracts/mocks/utils/mpc/OnBoard128TestsContract.ts +166 -0
  211. package/typechain-types/contracts/mocks/utils/mpc/OnBoard256TestsContract.ts +194 -0
  212. package/typechain-types/contracts/mocks/utils/mpc/PrivacyImplementationV1.ts +385 -0
  213. package/typechain-types/contracts/mocks/utils/mpc/PrivacyImplementationV2.ts +441 -0
  214. package/typechain-types/contracts/mocks/utils/mpc/PrivacyProxy.ts +201 -0
  215. package/typechain-types/contracts/mocks/utils/mpc/Random128TestsContract.ts +131 -0
  216. package/typechain-types/contracts/mocks/utils/mpc/Random256TestsContract.ts +131 -0
  217. package/typechain-types/contracts/mocks/utils/mpc/Shift128TestsContract.ts +142 -0
  218. package/typechain-types/contracts/mocks/utils/mpc/Shift256TestsContract.ts +142 -0
  219. package/typechain-types/contracts/mocks/utils/mpc/Transfer128TestsContract.ts +109 -0
  220. package/typechain-types/contracts/mocks/utils/mpc/Transfer256TestsContract.ts +109 -0
  221. package/typechain-types/contracts/mocks/utils/mpc/TransferWithAllowance128TestsContract.ts +128 -0
  222. package/typechain-types/contracts/mocks/utils/mpc/TransferWithAllowance256TestsContract.ts +128 -0
  223. package/typechain-types/contracts/mocks/utils/mpc/ValidateCiphertext128TestsContract.ts +198 -0
  224. package/typechain-types/contracts/mocks/utils/mpc/ValidateCiphertext256TestsContract.ts +182 -0
  225. package/typechain-types/contracts/mocks/utils/mpc/ValidateCiphertextTestsContract.ts +257 -0
  226. package/typechain-types/contracts/mocks/utils/mpc/index.ts +32 -8
  227. package/typechain-types/contracts/mocks/wallet/PrivateERC20Wallet/PrivateERC20WalletMock.ts +6 -6
  228. package/typechain-types/contracts/node/CotiNodeRewards.ts +566 -0
  229. package/typechain-types/contracts/node/SoulboundNodeNFT.sol/IERC5192.ts +123 -0
  230. package/typechain-types/contracts/node/SoulboundNodeNFT.sol/SoulboundNodeNFT.ts +975 -0
  231. package/typechain-types/contracts/node/SoulboundNodeNFT.sol/index.ts +5 -0
  232. package/typechain-types/contracts/node/index.ts +6 -0
  233. package/typechain-types/contracts/oracle/CotiPriceConsumer.ts +256 -0
  234. package/typechain-types/contracts/oracle/ICotiPriceConsumer.ts +119 -0
  235. package/typechain-types/contracts/oracle/IStdReference.ts +136 -0
  236. package/typechain-types/contracts/oracle/index.ts +6 -0
  237. package/typechain-types/contracts/privacyBridge/PrivacyBridge.ts +1628 -0
  238. package/typechain-types/contracts/privacyBridge/PrivacyBridgeCotiNative.ts +1870 -0
  239. package/typechain-types/contracts/privacyBridge/PrivacyBridgeERC20.sol/IHasDecimals.ts +84 -0
  240. package/typechain-types/contracts/privacyBridge/PrivacyBridgeERC20.sol/PrivacyBridgeERC20.ts +1892 -0
  241. package/typechain-types/contracts/privacyBridge/PrivacyBridgeERC20.sol/index.ts +5 -0
  242. package/typechain-types/contracts/privacyBridge/PrivacyBridgeUSDCe.ts +1892 -0
  243. package/typechain-types/contracts/privacyBridge/PrivacyBridgeUSDT.ts +1892 -0
  244. package/typechain-types/contracts/privacyBridge/PrivacyBridgeWADA.ts +1892 -0
  245. package/typechain-types/contracts/privacyBridge/PrivacyBridgeWBTC.ts +1892 -0
  246. package/typechain-types/contracts/privacyBridge/PrivacyBridgeWETH.ts +1892 -0
  247. package/typechain-types/contracts/privacyBridge/PrivacyBridgegCoti.ts +1892 -0
  248. package/typechain-types/contracts/privacyBridge/index.ts +13 -0
  249. package/typechain-types/contracts/token/PrivateERC20/IPrivateERC20.ts +507 -70
  250. package/typechain-types/contracts/token/PrivateERC20/ITokenReceiver.ts +100 -0
  251. package/typechain-types/contracts/token/PrivateERC20/ITokenReceiverEncrypted.ts +99 -0
  252. package/typechain-types/contracts/token/PrivateERC20/PrivateERC20.ts +833 -68
  253. package/typechain-types/contracts/token/PrivateERC20/index.ts +4 -0
  254. package/typechain-types/contracts/token/PrivateERC20/tokens/PrivateBridgedUSDC.ts +1270 -0
  255. package/typechain-types/contracts/token/PrivateERC20/tokens/PrivateCOTI.ts +1270 -0
  256. package/typechain-types/contracts/token/PrivateERC20/tokens/PrivateCOTITreasuryGovernanceToken.ts +1270 -0
  257. package/typechain-types/contracts/token/PrivateERC20/tokens/PrivateTetherUSD.ts +1270 -0
  258. package/typechain-types/contracts/token/PrivateERC20/tokens/PrivateWrappedADA.ts +1270 -0
  259. package/typechain-types/contracts/token/PrivateERC20/tokens/PrivateWrappedBTC.ts +1270 -0
  260. package/typechain-types/contracts/token/PrivateERC20/tokens/PrivateWrappedEther.ts +1270 -0
  261. package/typechain-types/contracts/token/PrivateERC20/tokens/index.ts +10 -0
  262. package/typechain-types/contracts/utils/mpc/MpcInterface.sol/ExtendedOperations.ts +111 -10
  263. package/typechain-types/factories/@openzeppelin/contracts/access/AccessControlEnumerable__factory.ts +276 -0
  264. package/typechain-types/factories/@openzeppelin/contracts/access/AccessControl__factory.ts +229 -0
  265. package/typechain-types/factories/@openzeppelin/contracts/access/IAccessControlEnumerable__factory.ts +244 -0
  266. package/typechain-types/factories/@openzeppelin/contracts/access/IAccessControl__factory.ts +197 -0
  267. package/typechain-types/factories/@openzeppelin/contracts/access/Ownable__factory.ts +74 -0
  268. package/typechain-types/factories/@openzeppelin/contracts/access/index.ts +8 -0
  269. package/typechain-types/factories/@openzeppelin/contracts/index.ts +2 -0
  270. package/typechain-types/factories/@openzeppelin/contracts/security/Pausable__factory.ts +61 -0
  271. package/typechain-types/factories/@openzeppelin/contracts/security/index.ts +4 -0
  272. package/typechain-types/factories/@openzeppelin/contracts/token/ERC20/ERC20__factory.ts +356 -0
  273. package/typechain-types/factories/@openzeppelin/contracts/token/ERC20/IERC20__factory.ts +205 -0
  274. package/typechain-types/factories/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata__factory.ts +247 -0
  275. package/typechain-types/factories/@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit__factory.ts +100 -0
  276. package/typechain-types/factories/@openzeppelin/contracts/token/ERC20/extensions/index.ts +5 -0
  277. package/typechain-types/factories/@openzeppelin/contracts/token/ERC20/index.ts +6 -0
  278. package/typechain-types/factories/@openzeppelin/contracts/token/ERC721/ERC721__factory.ts +416 -0
  279. package/typechain-types/factories/@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata__factory.ts +355 -0
  280. package/typechain-types/factories/@openzeppelin/contracts/token/ERC721/extensions/index.ts +4 -0
  281. package/typechain-types/factories/@openzeppelin/contracts/token/ERC721/index.ts +2 -0
  282. package/typechain-types/factories/@openzeppelin/contracts/token/index.ts +1 -0
  283. package/typechain-types/factories/contracts/disperse/coinByRatio/FixedRatioCoinDisperserLeftoverS1__factory.ts +791 -0
  284. package/typechain-types/factories/contracts/disperse/coinByRatio/FixedRatioCoinDisperserUnlimitedWindow__factory.ts +711 -0
  285. package/typechain-types/factories/contracts/disperse/coinByRatio/FixedRatioCoinDisperser__factory.ts +759 -0
  286. package/typechain-types/factories/contracts/disperse/coinByRatio/index.ts +6 -0
  287. package/typechain-types/factories/contracts/disperse/disperseToken/MintDisperser.sol/IMintableERC20__factory.ts +43 -0
  288. package/typechain-types/factories/contracts/disperse/disperseToken/MintDisperser.sol/IOwnable__factory.ts +35 -0
  289. package/typechain-types/factories/contracts/disperse/disperseToken/MintDisperser.sol/MintDisperser__factory.ts +203 -0
  290. package/typechain-types/factories/contracts/disperse/disperseToken/MintDisperser.sol/index.ts +6 -0
  291. package/typechain-types/factories/contracts/disperse/disperseToken/TokenDisperser__factory.ts +198 -0
  292. package/typechain-types/factories/contracts/disperse/disperseToken/index.ts +5 -0
  293. package/typechain-types/factories/contracts/disperse/index.ts +5 -0
  294. package/typechain-types/factories/contracts/index.ts +5 -0
  295. package/typechain-types/factories/contracts/messaging/PrivateMessaging__factory.ts +1035 -0
  296. package/typechain-types/factories/contracts/messaging/index.ts +4 -0
  297. package/typechain-types/factories/contracts/mocks/access/DataPrivacyFramework/DataPrivacyFrameworkMock__factory.ts +1 -1
  298. package/typechain-types/factories/contracts/mocks/index.ts +2 -0
  299. package/typechain-types/factories/contracts/mocks/messaging/PrivateMessagingHarness__factory.ts +1161 -0
  300. package/typechain-types/factories/contracts/mocks/messaging/index.ts +4 -0
  301. package/typechain-types/factories/contracts/mocks/oracle/CotiPriceConsumerMock__factory.ts +159 -0
  302. package/typechain-types/factories/contracts/mocks/oracle/index.ts +4 -0
  303. package/typechain-types/factories/contracts/mocks/token/ERC20DecimalsMock__factory.ts +394 -0
  304. package/typechain-types/factories/contracts/mocks/token/ERC20Mock__factory.ts +392 -0
  305. package/typechain-types/factories/contracts/mocks/token/PrivateERC20/EncryptedTokenReceiverMock__factory.ts +125 -0
  306. package/typechain-types/factories/contracts/mocks/token/PrivateERC20/PrivacyBridgeERC20Mock__factory.ts +1797 -0
  307. package/typechain-types/factories/contracts/mocks/token/PrivateERC20/PrivateERC20AuditorSupplyMock__factory.ts +1514 -0
  308. package/typechain-types/factories/contracts/mocks/token/PrivateERC20/PrivateERC20CappedMock__factory.ts +1466 -0
  309. package/typechain-types/factories/contracts/mocks/token/PrivateERC20/PrivateERC20GtCallerMock__factory.ts +165 -0
  310. package/typechain-types/factories/contracts/mocks/token/PrivateERC20/PrivateERC20Mock__factory.ts +975 -127
  311. package/typechain-types/factories/contracts/mocks/token/PrivateERC20/PrivateERC20ReentrantReceiverMock.sol/IPrivateERC20TransferAndCallEncrypted__factory.ts +54 -0
  312. package/typechain-types/factories/contracts/mocks/token/PrivateERC20/PrivateERC20ReentrantReceiverMock.sol/PrivateERC20ReentrantReceiverMock__factory.ts +159 -0
  313. package/typechain-types/factories/contracts/mocks/token/PrivateERC20/PrivateERC20ReentrantReceiverMock.sol/index.ts +5 -0
  314. package/typechain-types/factories/contracts/mocks/token/PrivateERC20/PublicReentrantTokenReceiverMock__factory.ts +164 -0
  315. package/typechain-types/factories/contracts/mocks/token/PrivateERC20/PublicTokenReceiverBoolMock__factory.ts +130 -0
  316. package/typechain-types/factories/contracts/mocks/token/PrivateERC20/PublicTokenReceiverMock__factory.ts +102 -0
  317. package/typechain-types/factories/contracts/mocks/token/PrivateERC20/index.ts +9 -0
  318. package/typechain-types/factories/contracts/mocks/token/PrivateERC721/PrivateERC721URIStorageMock__factory.ts +1 -1
  319. package/typechain-types/factories/contracts/mocks/token/index.ts +2 -0
  320. package/typechain-types/factories/contracts/mocks/utils/mpc/Arithmetic128TestsContract__factory.ts +330 -0
  321. package/typechain-types/factories/contracts/mocks/utils/mpc/Arithmetic256TestsContract__factory.ts +330 -0
  322. package/typechain-types/factories/contracts/mocks/utils/mpc/ArithmeticTestsContract__factory.ts +1 -1
  323. package/typechain-types/factories/contracts/mocks/utils/mpc/Bitwise128TestsContract__factory.ts +184 -0
  324. package/typechain-types/factories/contracts/mocks/utils/mpc/Bitwise256TestsContract__factory.ts +184 -0
  325. package/typechain-types/factories/contracts/mocks/utils/mpc/BitwiseTestsContract__factory.ts +1 -1
  326. package/typechain-types/factories/contracts/mocks/utils/mpc/CheckedArithmetic128WithOverflowBitTestsContract__factory.ts +203 -0
  327. package/typechain-types/factories/contracts/mocks/utils/mpc/CheckedArithmetic256WithOverflowBitTestsContract__factory.ts +203 -0
  328. package/typechain-types/factories/contracts/mocks/utils/mpc/CheckedArithmeticWIthOverflowBitTestsContract.sol/CheckedArithmeticWithOverflowBitTestsContract__factory.ts +1 -1
  329. package/typechain-types/factories/contracts/mocks/utils/mpc/Comparison128TestsContract__factory.ts +295 -0
  330. package/typechain-types/factories/contracts/mocks/utils/mpc/Comparison1TestsContract__factory.ts +1 -1
  331. package/typechain-types/factories/contracts/mocks/utils/mpc/Comparison256TestsContract__factory.ts +295 -0
  332. package/typechain-types/factories/contracts/mocks/utils/mpc/Comparison2TestsContract__factory.ts +1 -1
  333. package/typechain-types/factories/contracts/mocks/utils/mpc/MinMax128TestsContract__factory.ts +147 -0
  334. package/typechain-types/factories/contracts/mocks/utils/mpc/MinMax256TestsContract__factory.ts +147 -0
  335. package/typechain-types/factories/contracts/mocks/utils/mpc/MinMaxTestsContract__factory.ts +1 -1
  336. package/typechain-types/factories/contracts/mocks/utils/mpc/MinimalImplementation__factory.ts +263 -0
  337. package/typechain-types/factories/contracts/mocks/utils/mpc/MinimalProxy__factory.ts +104 -0
  338. package/typechain-types/factories/contracts/mocks/utils/mpc/Miscellaneous1TestsContract__factory.ts +1 -1
  339. package/typechain-types/factories/contracts/mocks/utils/mpc/MiscellaneousTestsContract__factory.ts +1 -1
  340. package/typechain-types/factories/contracts/mocks/utils/mpc/MpcOperations128TestContract__factory.ts +723 -0
  341. package/typechain-types/factories/contracts/mocks/utils/mpc/MpcOperationsTestContract__factory.ts +1410 -0
  342. package/typechain-types/factories/contracts/mocks/utils/mpc/Mux128TestsContract__factory.ts +115 -0
  343. package/typechain-types/factories/contracts/mocks/utils/mpc/Mux256TestsContract__factory.ts +115 -0
  344. package/typechain-types/factories/contracts/mocks/utils/mpc/OffboardToUserKeyTestContract__factory.ts +1 -1
  345. package/typechain-types/factories/contracts/mocks/utils/mpc/OnBoard128TestsContract__factory.ts +175 -0
  346. package/typechain-types/factories/contracts/mocks/utils/mpc/OnBoard256TestsContract__factory.ts +205 -0
  347. package/typechain-types/factories/contracts/mocks/utils/mpc/PrivacyImplementationV1__factory.ts +521 -0
  348. package/typechain-types/factories/contracts/mocks/utils/mpc/PrivacyImplementationV2__factory.ts +648 -0
  349. package/typechain-types/factories/contracts/mocks/utils/mpc/PrivacyProxy__factory.ts +179 -0
  350. package/typechain-types/factories/contracts/mocks/utils/mpc/Random128TestsContract__factory.ts +131 -0
  351. package/typechain-types/factories/contracts/mocks/utils/mpc/Random256TestsContract__factory.ts +131 -0
  352. package/typechain-types/factories/contracts/mocks/utils/mpc/Shift128TestsContract__factory.ts +147 -0
  353. package/typechain-types/factories/contracts/mocks/utils/mpc/Shift256TestsContract__factory.ts +147 -0
  354. package/typechain-types/factories/contracts/mocks/utils/mpc/ShiftTestsContract__factory.ts +1 -1
  355. package/typechain-types/factories/contracts/mocks/utils/mpc/StringTestsContract__factory.ts +1 -1
  356. package/typechain-types/factories/contracts/mocks/utils/mpc/Transfer128TestsContract__factory.ts +135 -0
  357. package/typechain-types/factories/contracts/mocks/utils/mpc/Transfer256TestsContract__factory.ts +135 -0
  358. package/typechain-types/factories/contracts/mocks/utils/mpc/TransferScalarTestsContract__factory.ts +1 -1
  359. package/typechain-types/factories/contracts/mocks/utils/mpc/TransferTestsContract__factory.ts +1 -1
  360. package/typechain-types/factories/contracts/mocks/utils/mpc/TransferWithAllowance128TestsContract__factory.ts +154 -0
  361. package/typechain-types/factories/contracts/mocks/utils/mpc/TransferWithAllowance256TestsContract__factory.ts +154 -0
  362. package/typechain-types/factories/contracts/mocks/utils/mpc/TransferWithAllowance64_16TestsContract__factory.ts +1 -1
  363. package/typechain-types/factories/contracts/mocks/utils/mpc/TransferWithAllowance64_32TestsContract__factory.ts +1 -1
  364. package/typechain-types/factories/contracts/mocks/utils/mpc/TransferWithAllowance64_64TestsContract__factory.ts +1 -1
  365. package/typechain-types/factories/contracts/mocks/utils/mpc/TransferWithAllowance64_8TestsContract__factory.ts +1 -1
  366. package/typechain-types/factories/contracts/mocks/utils/mpc/TransferWithAllowanceScalarTestsContract__factory.ts +1 -1
  367. package/typechain-types/factories/contracts/mocks/utils/mpc/TransferWithAllowanceTestsContract__factory.ts +1 -1
  368. package/typechain-types/factories/contracts/mocks/utils/mpc/ValidateCiphertext128TestsContract__factory.ts +190 -0
  369. package/typechain-types/factories/contracts/mocks/utils/mpc/ValidateCiphertext256TestsContract__factory.ts +210 -0
  370. package/typechain-types/factories/contracts/mocks/utils/mpc/ValidateCiphertextTestsContract__factory.ts +260 -0
  371. package/typechain-types/factories/contracts/mocks/utils/mpc/index.ts +32 -8
  372. package/typechain-types/factories/contracts/mocks/wallet/PrivateERC20Wallet/PrivateERC20WalletMock__factory.ts +10 -28
  373. package/typechain-types/factories/contracts/node/CotiNodeRewards__factory.ts +549 -0
  374. package/typechain-types/factories/contracts/node/SoulboundNodeNFT.sol/IERC5192__factory.ts +54 -0
  375. package/typechain-types/factories/contracts/node/SoulboundNodeNFT.sol/SoulboundNodeNFT__factory.ts +947 -0
  376. package/typechain-types/factories/contracts/node/SoulboundNodeNFT.sol/index.ts +5 -0
  377. package/typechain-types/factories/contracts/node/index.ts +5 -0
  378. package/typechain-types/factories/contracts/onboard/AccountOnboard__factory.ts +1 -1
  379. package/typechain-types/factories/contracts/oracle/CotiPriceConsumer__factory.ts +295 -0
  380. package/typechain-types/factories/contracts/oracle/ICotiPriceConsumer__factory.ts +73 -0
  381. package/typechain-types/factories/contracts/oracle/IStdReference__factory.ts +107 -0
  382. package/typechain-types/factories/contracts/oracle/index.ts +6 -0
  383. package/typechain-types/factories/contracts/privacyBridge/PrivacyBridgeCotiNative__factory.ts +1676 -0
  384. package/typechain-types/factories/contracts/privacyBridge/PrivacyBridgeERC20.sol/IHasDecimals__factory.ts +38 -0
  385. package/typechain-types/factories/contracts/privacyBridge/PrivacyBridgeERC20.sol/PrivacyBridgeERC20__factory.ts +1678 -0
  386. package/typechain-types/factories/contracts/privacyBridge/PrivacyBridgeERC20.sol/index.ts +5 -0
  387. package/typechain-types/factories/contracts/privacyBridge/PrivacyBridgeUSDCe__factory.ts +1782 -0
  388. package/typechain-types/factories/contracts/privacyBridge/PrivacyBridgeUSDT__factory.ts +1782 -0
  389. package/typechain-types/factories/contracts/privacyBridge/PrivacyBridgeWADA__factory.ts +1782 -0
  390. package/typechain-types/factories/contracts/privacyBridge/PrivacyBridgeWBTC__factory.ts +1782 -0
  391. package/typechain-types/factories/contracts/privacyBridge/PrivacyBridgeWETH__factory.ts +1782 -0
  392. package/typechain-types/factories/contracts/privacyBridge/PrivacyBridge__factory.ts +1367 -0
  393. package/typechain-types/factories/contracts/privacyBridge/PrivacyBridgegCoti__factory.ts +1782 -0
  394. package/typechain-types/factories/contracts/privacyBridge/index.ts +12 -0
  395. package/typechain-types/factories/contracts/token/PrivateERC20/IPrivateERC20__factory.ts +639 -61
  396. package/typechain-types/factories/contracts/token/PrivateERC20/ITokenReceiverEncrypted__factory.ts +53 -0
  397. package/typechain-types/factories/contracts/token/PrivateERC20/ITokenReceiver__factory.ts +54 -0
  398. package/typechain-types/factories/contracts/token/PrivateERC20/PrivateERC20__factory.ts +986 -120
  399. package/typechain-types/factories/contracts/token/PrivateERC20/index.ts +3 -0
  400. package/typechain-types/factories/contracts/token/PrivateERC20/tokens/PrivateBridgedUSDC__factory.ts +1445 -0
  401. package/typechain-types/factories/contracts/token/PrivateERC20/tokens/PrivateCOTITreasuryGovernanceToken__factory.ts +1451 -0
  402. package/typechain-types/factories/contracts/token/PrivateERC20/tokens/PrivateCOTI__factory.ts +1442 -0
  403. package/typechain-types/factories/contracts/token/PrivateERC20/tokens/PrivateTetherUSD__factory.ts +1445 -0
  404. package/typechain-types/factories/contracts/token/PrivateERC20/tokens/PrivateWrappedADA__factory.ts +1445 -0
  405. package/typechain-types/factories/contracts/token/PrivateERC20/tokens/PrivateWrappedBTC__factory.ts +1445 -0
  406. package/typechain-types/factories/contracts/token/PrivateERC20/tokens/PrivateWrappedEther__factory.ts +1451 -0
  407. package/typechain-types/factories/contracts/token/PrivateERC20/tokens/index.ts +10 -0
  408. package/typechain-types/factories/contracts/utils/mpc/MpcCore__factory.ts +1 -1
  409. package/typechain-types/factories/contracts/utils/mpc/MpcInterface.sol/ExtendedOperations__factory.ts +126 -0
  410. package/typechain-types/hardhat.d.ts +1772 -278
  411. package/typechain-types/index.ts +182 -16
  412. package/contracts/mocks/utils/mpc/Arithmetic128BitTestsContract.sol +0 -260
  413. package/contracts/mocks/utils/mpc/Arithmetic256BitTestsContract.sol +0 -264
  414. package/contracts/mocks/utils/mpc/Bitwise128BitTestsContract.sol +0 -91
  415. package/contracts/mocks/utils/mpc/Bitwise256BitTestsContract.sol +0 -91
  416. package/contracts/mocks/utils/mpc/Comparison128BitTestsContract.sol +0 -158
  417. package/contracts/mocks/utils/mpc/Comparison256BitTestsContract.sol +0 -158
  418. package/contracts/mocks/utils/mpc/Miscellaneous128BitTestsContract.sol +0 -185
  419. package/contracts/mocks/utils/mpc/Miscellaneous256BitTestsContract.sol +0 -190
  420. package/contracts/mocks/utils/mpc/README.md +0 -486
  421. package/test/utils/mpc/Unsigned128BitIntegers.test.ts +0 -945
  422. package/test/utils/mpc/Unsigned256BitIntegers.test.ts +0 -1124
  423. package/test/utils/mpc/helpers.ts +0 -77
  424. package/typechain-types/contracts/mocks/utils/mpc/Miscellaneous128BitTestsContract.ts +0 -302
  425. package/typechain-types/contracts/mocks/utils/mpc/Miscellaneous256BitTestsContract.ts +0 -322
  426. package/typechain-types/factories/contracts/mocks/utils/mpc/Arithmetic128BitTestsContract__factory.ts +0 -349
  427. package/typechain-types/factories/contracts/mocks/utils/mpc/Arithmetic256BitTestsContract__factory.ts +0 -364
  428. package/typechain-types/factories/contracts/mocks/utils/mpc/Bitwise128BitTestsContract__factory.ts +0 -182
  429. package/typechain-types/factories/contracts/mocks/utils/mpc/Bitwise256BitTestsContract__factory.ts +0 -182
  430. package/typechain-types/factories/contracts/mocks/utils/mpc/Comparison128BitTestsContract__factory.ts +0 -255
  431. package/typechain-types/factories/contracts/mocks/utils/mpc/Comparison256BitTestsContract__factory.ts +0 -255
  432. package/typechain-types/factories/contracts/mocks/utils/mpc/Miscellaneous128BitTestsContract__factory.ts +0 -332
  433. package/typechain-types/factories/contracts/mocks/utils/mpc/Miscellaneous256BitTestsContract__factory.ts +0 -380
@@ -0,0 +1,1663 @@
1
+ "use strict";
2
+
3
+ const { expect } = require("chai");
4
+ const { ethers } = require("hardhat");
5
+ const hre = require("hardhat");
6
+ const { prepareIT256 } = require("@coti-io/coti-sdk-typescript");
7
+
8
+ /**
9
+ * Unified Privacy Bridge Integration Tests (Full Suite)
10
+ * Consolidates Native and all ERC20 bridge tests into a single suite with numbered outputs.
11
+ */
12
+
13
+ /**
14
+ * Validates environment variables required for test execution.
15
+ * Normalizes PRIVATE_AES_KEY_TESTNET by stripping any accidental 0x prefix.
16
+ * @returns {Object} Environment validation result
17
+ */
18
+ function validateEnvironment() {
19
+ let aesKey = process.env.PRIVATE_AES_KEY_TESTNET || null;
20
+ // Strip 0x prefix if present — encodeKey expects raw 32 hex chars
21
+ if (aesKey?.startsWith('0x')) aesKey = aesKey.slice(2);
22
+ // Validate: must be exactly 32 hex characters (16 bytes)
23
+ const isValidKey = aesKey && /^[0-9a-fA-F]{32}$/.test(aesKey);
24
+ return {
25
+ hasAesKey: !!isValidKey,
26
+ aesKey: isValidKey ? aesKey : null,
27
+ canRunEncryptedTests: !!isValidKey
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Creates an ethers Wallet from PRIVATE_KEY env var.
33
+ * Normalizes the key by adding 0x prefix if missing.
34
+ * Uses this instead of passing a HardhatEthersSigner directly — HardhatEthersSigner
35
+ * does NOT expose a public .privateKey property, so getBytes(signer.privateKey)
36
+ * inside prepareIT256 throws "invalid BytesLike value".
37
+ * @returns {ethers.Wallet}
38
+ */
39
+ function makeSdkWallet() {
40
+ const raw = process.env.PRIVATE_KEY;
41
+ if (!raw) throw new Error('PRIVATE_KEY not set in .env');
42
+ const pk = raw.startsWith('0x') ? raw : '0x' + raw;
43
+ return new ethers.Wallet(pk, ethers.provider);
44
+ }
45
+
46
+ /**
47
+ * Shared helper — builds an itUint256 payload for any contract function selector.
48
+ * Centralises all calls to prepareIT256 to avoid copy-pasted implementations
49
+ * that historically passed the HardhatEthersSigner directly and triggered
50
+ * "invalid BytesLike value" from getBytes(sender.wallet.privateKey).
51
+ *
52
+ * @param {bigint|number} plaintext - Plain amount to encrypt
53
+ * @param {string} contractAddress - Target contract address (0x-prefixed)
54
+ * @param {string} selector - 4-byte function selector (0x + 8 hex chars)
55
+ * @returns {Promise<[[bigint,bigint], Uint8Array]>} Encoded itUint256 tuple
56
+ */
57
+ async function buildItUint256(plaintext, contractAddress, selector) {
58
+ const env = validateEnvironment();
59
+ if (!env.hasAesKey) {
60
+ throw new Error(
61
+ 'PRIVATE_AES_KEY_TESTNET not set or invalid in .env. ' +
62
+ 'Expected a 32-character hex string (no 0x prefix).'
63
+ );
64
+ }
65
+ const wallet = makeSdkWallet();
66
+ console.log(` [SDK] prepareIT256 plaintext=${plaintext} contract=${contractAddress} selector=${selector}`);
67
+ try {
68
+ const it = prepareIT256(
69
+ BigInt(plaintext),
70
+ { wallet, userKey: env.aesKey },
71
+ contractAddress,
72
+ selector
73
+ );
74
+ return [[it.ciphertext.ciphertextHigh, it.ciphertext.ciphertextLow], it.signature];
75
+ } catch (error) {
76
+ console.error(` [SDK ERROR] prepareIT256 failed: ${error.message}`);
77
+ console.error(` [SDK DEBUG] wallet.address=${wallet.address} selector=${selector} keyLen=${env.aesKey?.length}`);
78
+ throw new Error(`prepareIT256 failed: ${error.message}`);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Waits for transaction receipt with retry logic and exponential backoff
84
+ * @param {Object} tx - Transaction object
85
+ * @param {number} maxRetries - Maximum number of retry attempts (default: 3)
86
+ * @returns {Promise<Object>} Transaction receipt
87
+ */
88
+ async function waitForReceiptWithRetry(tx, maxRetries = 3) {
89
+ for (let i = 0; i < maxRetries; i++) {
90
+ try {
91
+ const receipt = await tx.wait(1, 120000); // 2-minute timeout per attempt
92
+ if (receipt) return receipt;
93
+
94
+ // Wait with exponential backoff
95
+ await new Promise(r => setTimeout(r, 2000 * (i + 1)));
96
+ } catch (error) {
97
+ if (i === maxRetries - 1) throw error;
98
+ await new Promise(r => setTimeout(r, 2000 * (i + 1)));
99
+ }
100
+ }
101
+ throw new Error("Failed to get transaction receipt after retries");
102
+ }
103
+
104
+ // ─────────────────────────────────────────────────────────────────────────────
105
+ // SHARED DEPLOYMENT & ASSERTION HELPERS (reduces duplication across suites)
106
+ // ─────────────────────────────────────────────────────────────────────────────
107
+
108
+ /**
109
+ * Deploys a MockCotiPriceConsumer with COTI at $0.05 and optional token price.
110
+ * Sets lastUpdated to the current block timestamp.
111
+ * @param {Object} [opts] - Optional: { tokenSymbol, tokenPrice }
112
+ * @returns {Promise<Contract>} Deployed oracle contract
113
+ */
114
+ async function deployMockOracle(opts = {}) {
115
+ const OracleFactory = await ethers.getContractFactory("MockCotiPriceConsumer");
116
+ const oracle = await OracleFactory.deploy({ gasLimit: 12000000 });
117
+ await (oracle.waitForDeployment ? oracle.waitForDeployment() : oracle.deployed());
118
+ await oracle.setCotiPrice(ethers.parseEther("0.05"), { gasLimit: 2000000 });
119
+ if (opts.tokenSymbol && opts.tokenPrice) {
120
+ await oracle.setPrice(opts.tokenSymbol, ethers.parseEther(opts.tokenPrice), { gasLimit: 2000000 });
121
+ }
122
+ const currentBlock = await ethers.provider.getBlock("latest");
123
+ await oracle.setLastUpdated(currentBlock.timestamp, { gasLimit: 2000000 });
124
+ return oracle;
125
+ }
126
+
127
+ /**
128
+ * Deploys a PrivacyBridgeCotiNative, sets oracle, and grants MINTER_ROLE.
129
+ * @returns {Promise<Contract>} Deployed bridge contract
130
+ */
131
+ async function deployNativeBridge(privateCoti, mockOracle, owner, logTx, addr, MINTER_ROLE) {
132
+ const BridgeFactory = await ethers.getContractFactory("PrivacyBridgeCotiNative");
133
+ const pCotiAddr = await addr(privateCoti);
134
+ const bridge = await BridgeFactory.deploy(pCotiAddr, owner.address, owner.address, await addr(mockOracle), { gasLimit: 30000000 });
135
+ await (bridge.waitForDeployment ? bridge.waitForDeployment() : bridge.deployed());
136
+ const bridgeAddr = await addr(bridge);
137
+ await logTx(await privateCoti.grantRole(MINTER_ROLE, bridgeAddr, { gasLimit: 12000000 }), "Grant MINTER_ROLE to Native Bridge", "grantRole", ["MINTER_ROLE", bridgeAddr]);
138
+ return bridge;
139
+ }
140
+
141
+ /**
142
+ * Deploys a PrivacyBridgeWETH, sets oracle, and grants MINTER_ROLE.
143
+ * @returns {Promise<Contract>} Deployed bridge contract
144
+ */
145
+ async function deployERC20Bridge(publicToken, privateToken, mockOracle, owner, logTx, addr, MINTER_ROLE) {
146
+ const pubAddr = await addr(publicToken);
147
+ const privAddr = await addr(privateToken);
148
+ const bridge = await (await ethers.getContractFactory("PrivacyBridgeWETH")).deploy(pubAddr, privAddr, owner.address, owner.address, await addr(mockOracle), { gasLimit: 12000000 });
149
+ await (bridge.waitForDeployment ? bridge.waitForDeployment() : bridge.deployed());
150
+ const bridgeAddr = await addr(bridge);
151
+ await logTx(await privateToken.grantRole(MINTER_ROLE, bridgeAddr, { gasLimit: 12000000 }), "Grant MINTER_ROLE to ERC20 Bridge", "grantRole", ["MINTER_ROLE", bridgeAddr]);
152
+ return bridge;
153
+ }
154
+
155
+ /**
156
+ * Gets or deploys a PrivateCOTI-compatible token based on chain.
157
+ * On testnet (chainId 7082400), attaches to pre-deployed PrivateCOTI.
158
+ * On local/hardhat, deploys a fresh PrivateERC20Mock.
159
+ * @param {string} [factoryName] - Factory to use on local ("PrivateERC20Mock" or "PrivateCOTI")
160
+ * @returns {Promise<Contract>}
161
+ */
162
+ async function getOrDeployPrivateCoti(factoryName = "PrivateERC20Mock") {
163
+ const chainId = (await ethers.provider.getNetwork()).chainId;
164
+ if (chainId === 7082400n) {
165
+ const token = await ethers.getContractAt("PrivateCOTI", "0x03eeA59b1F0Dfeaece75531b27684DD882f79759");
166
+ console.log(" [Info] Using pre-deployed PrivateCOTI at 0x03eeA59b1F0Dfeaece75531b27684DD882f79759");
167
+ return token;
168
+ }
169
+ const Factory = await ethers.getContractFactory(factoryName);
170
+ const token = await Factory.deploy({ gasLimit: 30000000 });
171
+ await (token.waitForDeployment ? token.waitForDeployment() : token.deployed());
172
+ return token;
173
+ }
174
+
175
+ /**
176
+ * Full ERC20 bridge setup: deploys or attaches tokens based on chain, deploys bridge, mints initial supply.
177
+ * Consolidates the repeated testnet-vs-local branching across multiple test suites.
178
+ * @param {Object} opts - { publicFactory, decimals, mockOracle, owner, logTx, addr, MINTER_ROLE, mintAmount }
179
+ * @returns {Promise<{publicToken, privateToken, bridge}>}
180
+ */
181
+ async function setupERC20BridgeEnv(opts) {
182
+ const { publicFactory, decimals, mockOracle, owner, logTx, addr, MINTER_ROLE, mintAmount } = opts;
183
+ const UNIT = BigInt(10 ** decimals);
184
+ let publicToken, privateToken, bridge;
185
+
186
+ const chainId = (await ethers.provider.getNetwork()).chainId;
187
+ if (chainId === 7082400n) {
188
+ const WETH_ADDRESS = "0x8bca4e6bbE402DB4aD189A316137aD08206154FB";
189
+ const PRIVATE_WETH_ADDRESS = "0x6f7E5eE3a913aa00c6eB9fEeCad57a7d02F7f45c";
190
+ publicToken = await ethers.getContractAt(publicFactory, WETH_ADDRESS);
191
+ privateToken = await ethers.getContractAt("PrivateWrappedEther", PRIVATE_WETH_ADDRESS);
192
+ console.log(` [Info] Using pre-deployed ${publicFactory} at ${WETH_ADDRESS}`);
193
+ console.log(` [Info] Using pre-deployed PrivateWrappedEther at ${PRIVATE_WETH_ADDRESS}`);
194
+ bridge = await deployERC20Bridge(publicToken, privateToken, mockOracle, owner, logTx, addr, MINTER_ROLE);
195
+ } else {
196
+ publicToken = await (await ethers.getContractFactory(publicFactory)).deploy("Wrapped Ether", "WETH", decimals, { gasLimit: 12000000 });
197
+ await (publicToken.waitForDeployment ? publicToken.waitForDeployment() : publicToken.deployed());
198
+ privateToken = await (await ethers.getContractFactory("PrivateERC20Mock")).deploy({ gasLimit: 12000000 });
199
+ await (privateToken.waitForDeployment ? privateToken.waitForDeployment() : privateToken.deployed());
200
+ bridge = await deployERC20Bridge(publicToken, privateToken, mockOracle, owner, logTx, addr, MINTER_ROLE);
201
+ if (mintAmount) {
202
+ await logTx(await publicToken.mint(owner.address, mintAmount * UNIT, { gasLimit: 2000000 }), `Mint ${mintAmount} WETH to owner`, "mint", [owner.address, String(mintAmount)]);
203
+ }
204
+ }
205
+ return { publicToken, privateToken, bridge };
206
+ }
207
+
208
+ /**
209
+ * Asserts that a transaction promise reverts matching the given pattern.
210
+ * @param {Promise} txPromise - The transaction promise expected to revert
211
+ * @param {RegExp} pattern - Regex pattern to match against the error message
212
+ * @param {string} [logMsg] - Optional console message on success
213
+ */
214
+ async function expectRevert(txPromise, pattern, logMsg) {
215
+ try {
216
+ const tx = await txPromise;
217
+ await waitForReceiptWithRetry(tx);
218
+ expect.fail("Expected revert but succeeded");
219
+ } catch (error) {
220
+ if (error.message === "Expected revert but succeeded") throw error;
221
+ expect(error.message).to.match(pattern);
222
+ if (logMsg) console.log(logMsg);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Handles the encrypted test skip pattern.
228
+ * @param {number} testCounter - Current test counter for log message
229
+ * @param {Object} cfg - Config object with .name property
230
+ * @param {string} testType - Description of the test type (e.g. "encrypted transfer")
231
+ * @returns {boolean} true if test should be skipped
232
+ */
233
+ function skipIfNoEncryption(testCounter, cfg, testType) {
234
+ const env = validateEnvironment();
235
+ if (!env.canRunEncryptedTests) {
236
+ console.log(` [SKIP] Test ${testCounter} - ${cfg.name} ${testType}: PRIVATE_AES_KEY_TESTNET not configured`);
237
+ console.log(` [INFO] Add PRIVATE_AES_KEY_TESTNET to your .env file to enable encrypted payload tests`);
238
+ return true;
239
+ }
240
+ return false;
241
+ }
242
+
243
+ describe("Unified Privacy Bridges Suite", function () {
244
+ this.timeout(1200000); // 20 minutes
245
+
246
+ // Ensure tests run on cotiTestnet or hardhat (for coverage)
247
+ before(function () {
248
+ const networkName = hre.network.name;
249
+ if (networkName !== "coti-testnet" && networkName !== "cotiTestnet" && networkName !== "hardhat") {
250
+ throw new Error(
251
+ `These tests must run on coti-testnet, cotiTestnet, or hardhat network (current: ${networkName}). ` +
252
+ `Run with: npx hardhat test --network coti-testnet`
253
+ );
254
+ }
255
+ });
256
+
257
+ let testCounter = 0;
258
+ let currentTestFullTitle = '';
259
+ let owner, user1;
260
+
261
+ // Shared map between test file and reporter (same Node.js process).
262
+ // Keyed by test fullTitle → array of call entries.
263
+ if (!process.__testCallLog) process.__testCallLog = {};
264
+
265
+ // Contract address registry — array of { contractName, address, suite }.
266
+ // Populated by registerContract() in each before() block.
267
+ if (!process.__contractAddresses) process.__contractAddresses = [];
268
+
269
+ async function registerContract(contractName, contract, suite) {
270
+ const address = await (contract.getAddress ? contract.getAddress() : Promise.resolve(contract.address));
271
+ process.__contractAddresses.push({ contractName, address, suite });
272
+ }
273
+
274
+ // Fix for Ethers v5/v6 compatibility
275
+ const toBytes = ethers.toUtf8Bytes || ethers.utils?.toUtf8Bytes;
276
+ const keccak = ethers.keccak256 || ethers.utils?.keccak256;
277
+ const MINTER_ROLE = keccak(toBytes("MINTER_ROLE"));
278
+
279
+ const addr = async (contract) =>
280
+ contract.getAddress ? contract.getAddress() : Promise.resolve(contract.address);
281
+
282
+ // Set ONLY_PRIVATE_ERC20=1 to skip all non-PrivateERC20 suites (faster iteration)
283
+ const ONLY_PRIVATE_ERC20 = !!process.env.ONLY_PRIVATE_ERC20;
284
+
285
+ // ─────────────────────────────────────────────────────────────────────────
286
+ // DEDUPLICATION HELPERS (reduce repeated before() patterns)
287
+ // ─────────────────────────────────────────────────────────────────────────
288
+
289
+ /**
290
+ * One-liner check: skips the current suite if ONLY_PRIVATE_ERC20 is set.
291
+ * Must be called with the Mocha context: skipIfOnlyPrivateERC20.call(this)
292
+ */
293
+ function skipIfOnlyPrivateERC20() {
294
+ if (ONLY_PRIVATE_ERC20) { this.skip(); return true; }
295
+ return false;
296
+ }
297
+
298
+ /**
299
+ * Encapsulates the common native bridge before() setup logic.
300
+ * @param {string} suiteName - Name for contract registration
301
+ * @param {string} [factoryName] - Factory to use for PrivateCOTI ("PrivateERC20Mock" or "PrivateCOTI")
302
+ * @returns {Promise<{privateCoti, bridge, mockOracle}>}
303
+ */
304
+ async function setupNativeBridgeEnv(suiteName, factoryName = "PrivateERC20Mock") {
305
+ const mockOracle = await deployMockOracle();
306
+ const privateCoti = await getOrDeployPrivateCoti(factoryName);
307
+ const bridge = await deployNativeBridge(privateCoti, mockOracle, owner, logTx, addr, MINTER_ROLE);
308
+ await registerContract("PrivacyBridgeCotiNative", bridge, suiteName);
309
+ await new Promise(r => setTimeout(r, 5000));
310
+ return { privateCoti, bridge, mockOracle };
311
+ }
312
+
313
+ /**
314
+ * Encapsulates the common ERC20 bridge before() setup logic.
315
+ * @param {string} suiteName - Name for contract registration
316
+ * @param {bigint} [mintAmount] - Amount to mint to owner (default 10000n)
317
+ * @returns {Promise<{publicToken, privateToken, bridge, mockOracle}>}
318
+ */
319
+ async function setupERC20BridgeSuite(suiteName, mintAmount = 10000n) {
320
+ const mockOracle = await deployMockOracle({ tokenSymbol: "ETH", tokenPrice: "2300" });
321
+ const result = await setupERC20BridgeEnv({
322
+ publicFactory: "ERC20Mock", decimals: 18, mockOracle, owner, logTx, addr, MINTER_ROLE, mintAmount
323
+ });
324
+ await registerContract("PrivacyBridgeWETH", result.bridge, suiteName);
325
+ await new Promise(r => setTimeout(r, 5000));
326
+ return { publicToken: result.publicToken, privateToken: result.privateToken, bridge: result.bridge, mockOracle };
327
+ }
328
+
329
+ const logTx = async (tx, description, methodName = "Unknown", args = [], expectRevert = false) => {
330
+ const argsStr = args.length > 0 ? `(${args.map(a => (typeof a === 'string' && a.startsWith('0x')) ? a.slice(0, 10) + '...' : a).join(', ')})` : "()";
331
+ console.log(` [Method] ${methodName}${argsStr}`);
332
+ console.log(` [Action] ${description}`);
333
+ console.log(` [Tx] https://testnet.cotiscan.io/tx/${tx.hash}`);
334
+
335
+ // Record call for JSON report — write immediately so the reporter
336
+ // can read it at EVENT_TEST_PASS (which fires before afterEach).
337
+ const callEntry = {
338
+ method: methodName,
339
+ args: args.map(String),
340
+ txHash: tx.hash,
341
+ description,
342
+ };
343
+ if (currentTestFullTitle) {
344
+ if (!process.__testCallLog[currentTestFullTitle]) process.__testCallLog[currentTestFullTitle] = [];
345
+ process.__testCallLog[currentTestFullTitle].push(callEntry);
346
+ }
347
+
348
+ const receipt = expectRevert
349
+ ? await waitForReceiptWithRetry(tx)
350
+ : await tx.wait(1, 300000); // 5-minute timeout; prevents indefinite hang on slow/unresponsive testnet
351
+
352
+ // Mandatory wait for cotiTestnet stability - COTI MPC state needs time to settle
353
+ await new Promise(r => setTimeout(r, 10000));
354
+ return receipt;
355
+ };
356
+
357
+ before(async function () {
358
+ const signers = await ethers.getSigners();
359
+ owner = signers[0];
360
+ user1 = signers.length > 1 ? signers[1] : owner;
361
+ console.log("\n===========================================================");
362
+ console.log("STARTING FULL UNIFIED BRIDGE TESTS");
363
+ console.log("Deployer:", owner.address);
364
+ console.log("User1 :", user1.address);
365
+ console.log("===========================================================\n");
366
+ });
367
+
368
+ beforeEach(function () {
369
+ testCounter++;
370
+ currentTestFullTitle = this.currentTest.fullTitle();
371
+ process.__testCallLog[currentTestFullTitle] = [];
372
+ console.log(`\nTest ${testCounter} - ${this.currentTest.title}`);
373
+ });
374
+
375
+ afterEach(async function () {
376
+ const state = this.currentTest.state;
377
+ let label = 'FAILED';
378
+ if (state === 'passed') label = 'PASSED';
379
+ else if (state === 'pending') label = 'SKIPPED';
380
+ console.log(`Test ${testCounter} - result: ${label}`);
381
+ // Extra wait between separate test cases
382
+ await new Promise(r => setTimeout(r, 5000));
383
+ });
384
+
385
+ // ─────────────────────────────────────────────────────────────────────────
386
+ // NATIVE BRIDGE TESTS
387
+ // ─────────────────────────────────────────────────────────────────────────
388
+ describe("Native Bridge (PrivacyBridgeCotiNative)", function () {
389
+ let privateCoti, bridge, mockOracle;
390
+
391
+ before(async function () {
392
+ if (skipIfOnlyPrivateERC20.call(this)) return;
393
+ const PrivateCotiFactory = await ethers.getContractFactory("PrivateERC20Mock");
394
+ privateCoti = await PrivateCotiFactory.deploy({ gasLimit: 12000000 });
395
+ await (privateCoti.waitForDeployment ? privateCoti.waitForDeployment() : privateCoti.deployed());
396
+
397
+ // Deploy mock oracle and set COTI price ($0.05)
398
+ mockOracle = await deployMockOracle();
399
+
400
+ bridge = await deployNativeBridge(privateCoti, mockOracle, owner, logTx, addr, MINTER_ROLE);
401
+ await registerContract("PrivacyBridgeCotiNative", bridge, "Native Bridge");
402
+ await registerContract("PrivateERC20Mock", privateCoti, "Native Bridge");
403
+ await registerContract("MockCotiPriceConsumer", mockOracle, "Native Bridge");
404
+ await new Promise(r => setTimeout(r, 5000)); // Extra settle time after role grant
405
+ });
406
+
407
+ it("Test 1: native: Should set correct initial state", async function () {
408
+ const pCotiAddr = await addr(privateCoti);
409
+ expect(await bridge.privateCoti()).to.equal(pCotiAddr);
410
+ expect(await bridge.owner()).to.equal(owner.address);
411
+ });
412
+
413
+ it("Test 2: native: Should allow deposit of native COTI", async function () {
414
+ const amount = ethers.parseEther("100");
415
+ const bridgeAddr = await addr(bridge);
416
+ const initialBalance = await ethers.provider.getBalance(bridgeAddr);
417
+
418
+ // Estimate fee and get oracle timestamp
419
+ const [fee, cotiLastUpdated, blockTimestamp] = await bridge.estimateDepositFee(amount);
420
+ console.log(` [Fee Estimation] fee=${ethers.formatEther(fee)} COTI, blockTimestamp=${blockTimestamp}`);
421
+
422
+ const tx = await bridge.connect(user1)["deposit(uint256,uint256)"](cotiLastUpdated, cotiLastUpdated, { value: amount, gasLimit: 12000000 });
423
+ await logTx(tx, `Deposit ${ethers.formatEther(amount)} Native COTI`, "PrivacyBridgeCotiNative.deposit() -> PrivateERC20Mock.mint", [ethers.formatEther(amount)]);
424
+
425
+ await expect(tx).to.emit(bridge, "Deposit");
426
+ expect(await ethers.provider.getBalance(bridgeAddr)).to.be.at.least(initialBalance + amount);
427
+ });
428
+
429
+ it("Test 3: native: Should allow withdrawal of native COTI", async function () {
430
+ const amount = ethers.parseEther("50");
431
+ const bridgeAddr = await addr(bridge);
432
+
433
+ await logTx(await privateCoti.connect(user1)["approve(address,uint256)"](bridgeAddr, amount, { gasLimit: 2000000 }), "Approve private COTI for withdrawal", "PrivateERC20Mock.approve", [bridgeAddr, ethers.formatEther(amount)]);
434
+
435
+ // Estimate fee and get oracle timestamp
436
+ const [fee, cotiLastUpdated, blockTimestamp] = await bridge.estimateWithdrawFee(amount);
437
+ console.log(` [Fee Estimation] fee=${ethers.formatEther(fee)} COTI, blockTimestamp=${blockTimestamp}`);
438
+
439
+ const tx = await bridge.connect(user1)["withdraw(uint256,uint256,uint256)"](amount, cotiLastUpdated, cotiLastUpdated, { gasLimit: 12000000 });
440
+ await logTx(tx, `Withdraw ${ethers.formatEther(amount)} Native COTI`, "PrivacyBridgeCotiNative.withdraw() -> PrivateERC20Mock.burn", [ethers.formatEther(amount)]);
441
+
442
+ await expect(tx).to.emit(bridge, "Withdraw");
443
+ });
444
+
445
+ it("Test 4: native: Should deduct dynamic fee and let owner withdraw fees", async function () {
446
+ // Dynamic fee uses oracle-based calculation. With default params:
447
+ // depositFixedFee=10 COTI, depositPercentageBps=500, depositMaxFee=3000 COTI
448
+ // For a 0.1 COTI deposit at $0.05/COTI: txValueUsd=$0.005, pctFee=$0.0000025, pctFeeCoti=0.00005 COTI
449
+ // fee = max(10 COTI, 0.00005 COTI) = 10 COTI (floor dominates for small amounts)
450
+ // So we need to deposit more than the fixed fee floor (10 COTI)
451
+ const gross = ethers.parseEther("100");
452
+
453
+ const feeBefore = await bridge.accumulatedCotiFees();
454
+
455
+ // Estimate fee and get oracle timestamp
456
+ const [fee, cotiLastUpdated, blockTimestamp] = await bridge.estimateDepositFee(gross);
457
+ console.log(` [Fee Estimation] fee=${ethers.formatEther(fee)} COTI, blockTimestamp=${blockTimestamp}`);
458
+
459
+ await logTx(await bridge["deposit(uint256,uint256)"](cotiLastUpdated, cotiLastUpdated, { value: gross, gasLimit: 12000000 }), "Deposit for dynamic fee accumulation", "PrivacyBridgeCotiNative.deposit()", [ethers.formatEther(gross)]);
460
+
461
+ const feeAfter = await bridge.accumulatedCotiFees();
462
+ const actualFee = feeAfter - feeBefore;
463
+ expect(actualFee).to.be.gt(0n);
464
+ console.log(` [Info] Dynamic fee charged: ${ethers.formatEther(actualFee)} COTI`);
465
+
466
+ // Withdraw fees
467
+ await logTx(await bridge.withdrawFees(actualFee, { gasLimit: 2000000 }), "Withdraw accumulated COTI fees", "PrivacyBridgeCotiNative.withdrawFees", [ethers.formatEther(actualFee)]);
468
+ expect(await bridge.accumulatedCotiFees()).to.equal(feeBefore);
469
+ });
470
+
471
+ it("Test 5: native: Should rescue native COTI", async function () {
472
+ // bridge should already have some balance
473
+ const bridgeAddr = await addr(bridge);
474
+ const bal = await ethers.provider.getBalance(bridgeAddr);
475
+ if (bal > 0n) {
476
+ const amount = bal / 2n;
477
+ const tx = await bridge.rescueNative(amount, { gasLimit: 2000000 });
478
+ await logTx(tx, `rescueNative ${ethers.formatEther(amount)} COTI`, "PrivacyBridgeCotiNative.rescueNative", [ethers.formatEther(amount)]);
479
+ }
480
+ });
481
+ });
482
+
483
+ // ─────────────────────────────────────────────────────────────────────────
484
+ // ERC20 BRIDGE TESTS
485
+ // ─────────────────────────────────────────────────────────────────────────
486
+ const BRIDGE_CONFIGS = [
487
+ { name: "WETH", publicFactory: "ERC20Mock", bridgeFactory: "PrivacyBridgeWETH", decimals: 18, testStart: 6 },
488
+ ];
489
+
490
+ for (const cfg of BRIDGE_CONFIGS) {
491
+ describe(`ERC20 Bridge (${cfg.name})`, function () {
492
+ let publicToken, privateToken, bridge, mockOracle;
493
+ const UNIT = BigInt(10 ** cfg.decimals);
494
+ // Generous COTI fee to cover any dynamic fee calculation
495
+ const COTI_FEE_BUFFER = ethers.parseEther("3100");
496
+
497
+ before(async function () {
498
+ if (skipIfOnlyPrivateERC20.call(this)) return;
499
+
500
+ // Deploy mock oracle
501
+ mockOracle = await deployMockOracle({ tokenSymbol: "ETH", tokenPrice: "2300" });
502
+
503
+ const result = await setupERC20BridgeEnv({
504
+ publicFactory: cfg.publicFactory, decimals: cfg.decimals, mockOracle, owner, logTx, addr, MINTER_ROLE, mintAmount: 1000n
505
+ });
506
+ publicToken = result.publicToken;
507
+ privateToken = result.privateToken;
508
+ bridge = result.bridge;
509
+
510
+ await registerContract(cfg.bridgeFactory, bridge, `ERC20 Bridge (${cfg.name})`);
511
+ await registerContract(cfg.publicFactory, publicToken, `ERC20 Bridge (${cfg.name})`);
512
+ await registerContract("PrivateToken", privateToken, `ERC20 Bridge (${cfg.name})`);
513
+ await new Promise(r => setTimeout(r, 5000));
514
+ });
515
+
516
+ it(`Test ${cfg.testStart}: ${cfg.name}: Should set correct initial state`, async function () {
517
+ expect(await bridge.token()).to.equal(await addr(publicToken));
518
+ expect(await bridge.privateToken()).to.equal(await addr(privateToken));
519
+ expect(await bridge.owner()).to.equal(owner.address);
520
+ });
521
+
522
+ it(`Test ${cfg.testStart + 1}: ${cfg.name}: Should allow deposit`, async function () {
523
+ const amount = 10n * UNIT;
524
+ const bridgeAddr = await addr(bridge);
525
+ await logTx(await publicToken.approve(bridgeAddr, amount, { gasLimit: 2000000 }), `Approve ${cfg.name} for bridge`, "MockWETH.approve", [bridgeAddr, "10"]);
526
+
527
+ // Estimate fee and get oracle timestamp
528
+ const [fee, cotiLastUpdated, tokenLastUpdated, blockTimestamp] = await bridge.estimateDepositFee(amount);
529
+ console.log(` [Fee Estimation] fee=${ethers.formatEther(fee)} COTI, blockTimestamp=${blockTimestamp}`);
530
+
531
+ const tx = await bridge["deposit(uint256,uint256,uint256)"](amount, cotiLastUpdated, tokenLastUpdated, { value: COTI_FEE_BUFFER, gasLimit: 12000000 });
532
+ await logTx(tx, `Deposit ${amount / UNIT} ${cfg.name}`, `PrivacyBridgeERC20.deposit()`, ["10"]);
533
+ await expect(tx).to.emit(bridge, "Deposit");
534
+ });
535
+
536
+ it(`Test ${cfg.testStart + 2}: ${cfg.name}: Should allow withdrawal`, async function () {
537
+ const amount = 5n * UNIT;
538
+ const bridgeAddr = await addr(bridge);
539
+ await logTx(await privateToken["approve(address,uint256)"](bridgeAddr, amount, { gasLimit: 2000000 }), `Approve private ${cfg.name}`, "PrivateERC20Mock.approve", [bridgeAddr, "5"]);
540
+
541
+ // Estimate fee and get oracle timestamp
542
+ const [fee, cotiLastUpdated, tokenLastUpdated, blockTimestamp] = await bridge.estimateWithdrawFee(amount);
543
+ console.log(` [Fee Estimation] fee=${ethers.formatEther(fee)} COTI, blockTimestamp=${blockTimestamp}`);
544
+
545
+ const tx = await bridge["withdraw(uint256,uint256,uint256)"](amount, cotiLastUpdated, tokenLastUpdated, { value: COTI_FEE_BUFFER, gasLimit: 12000000 });
546
+ await logTx(tx, `Withdraw ${amount / UNIT} ${cfg.name}`, "PrivacyBridgeERC20.withdraw()", ["5"]);
547
+ await expect(tx).to.emit(bridge, "Withdraw");
548
+ });
549
+
550
+ it(`Test ${cfg.testStart + 3}: ${cfg.name}: Should track dynamic fees in accumulatedCotiFees`, async function () {
551
+ const bridgeAddr = await addr(bridge);
552
+ const amount = 100n * UNIT;
553
+ await logTx(await publicToken.approve(bridgeAddr, amount, { gasLimit: 2000000 }), "Approve for fee test", "MockWETH.approve", [bridgeAddr, "100"]);
554
+ const feeBefore = await bridge.accumulatedCotiFees();
555
+
556
+ // Estimate fee and get oracle timestamp
557
+ const [fee, cotiLastUpdated, tokenLastUpdated, blockTimestamp] = await bridge.estimateDepositFee(amount);
558
+ console.log(` [Fee Estimation] fee=${ethers.formatEther(fee)} COTI, blockTimestamp=${blockTimestamp}`);
559
+
560
+ await logTx(await bridge["deposit(uint256,uint256,uint256)"](amount, cotiLastUpdated, tokenLastUpdated, { value: COTI_FEE_BUFFER, gasLimit: 12000000 }), `Deposit 100 ${cfg.name} for fee test`, "PrivacyBridgeERC20.deposit()", ["100"]);
561
+ const feeAfter = await bridge.accumulatedCotiFees();
562
+ const actualFee = feeAfter - feeBefore;
563
+ expect(actualFee).to.be.gt(0n);
564
+ console.log(` [Info] Dynamic COTI fee charged: ${ethers.formatEther(actualFee)} COTI`);
565
+ });
566
+
567
+ it(`Test ${cfg.testStart + 4}: ${cfg.name}: Should rescue redundant ERC20 tokens`, async function () {
568
+ const bridgeAddr = await addr(bridge);
569
+
570
+ // Deploy a different ERC20 token (not the bridge token) to test rescue functionality
571
+ const StrayTokenFactory = await ethers.getContractFactory("ERC20Mock");
572
+ const strayToken = await StrayTokenFactory.deploy("Stray Token", "STRAY", 18, { gasLimit: 12000000 });
573
+ await (strayToken.waitForDeployment ? strayToken.waitForDeployment() : strayToken.deployed());
574
+ const strayAddr = await addr(strayToken);
575
+
576
+ // Mint stray tokens to bridge
577
+ await logTx(await strayToken.mint(bridgeAddr, UNIT, { gasLimit: 2000000 }), "Mint stray tokens to bridge", "ERC20Mock.mint", [bridgeAddr, "1"]);
578
+
579
+ // Rescue the stray tokens (not the bridge token)
580
+ await logTx(await bridge.rescueERC20(strayAddr, UNIT, { gasLimit: 2000000 }), "Rescue stray tokens", "PrivacyBridgeERC20.rescueERC20", [strayAddr, "1"]);
581
+
582
+ // Verify owner received the rescued tokens
583
+ const ownerBalance = await strayToken.balanceOf(owner.address);
584
+ expect(ownerBalance).to.be.at.least(UNIT);
585
+ });
586
+ });
587
+ }
588
+
589
+ // ─────────────────────────────────────────────────────────────────────────
590
+ // PRIVATE ERC20 PUBLIC-AMOUNT FUNCTION TESTS
591
+ // ─────────────────────────────────────────────────────────────────────────
592
+ const PRIVATE_TOKEN_CONFIGS = [
593
+ { name: "PrivateCOTI", factory: "PrivateCOTI", testStart: 22 },
594
+ { name: "PrivateWrappedEther", factory: "PrivateWrappedEther", testStart: 33 },
595
+ ];
596
+
597
+ for (const cfg of PRIVATE_TOKEN_CONFIGS) {
598
+ describe(`PrivateERC20 Public-Amount Functions (${cfg.name})`, function () {
599
+ let token;
600
+ const MINT_AMOUNT = ethers.parseEther("10");
601
+ const BURN_AMOUNT = ethers.parseEther("1");
602
+ const TRANSFER_AMOUNT = ethers.parseEther("2");
603
+ const APPROVE_AMOUNT = ethers.parseEther("2");
604
+
605
+ before(async function () {
606
+ const chainId = (await ethers.provider.getNetwork()).chainId;
607
+ if (chainId === 7082400n) {
608
+ // Pre-deployed testnet contracts — fresh PrivateERC20-based deployments
609
+ // exceed the block gas limit on current COTI testnet
610
+ const TESTNET_TOKENS = {
611
+ "PrivateCOTI": "0x03eeA59b1F0Dfeaece75531b27684DD882f79759",
612
+ "PrivateWrappedEther": "0x6f7E5eE3a913aa00c6eB9fEeCad57a7d02F7f45c"
613
+ };
614
+ token = await ethers.getContractAt(cfg.factory, TESTNET_TOKENS[cfg.name]);
615
+ console.log(` [Info] Using pre-deployed ${cfg.name} at ${TESTNET_TOKENS[cfg.name]}`);
616
+
617
+ // Onboard wallet with COTI MPC so validateCiphertext works for encrypted ops
618
+ const { Wallet: CotiWallet, onboard: cotiOnboard, ONBOARD_CONTRACT_ADDRESS: cotiOnboardAddr } = require('@coti-io/coti-ethers');
619
+ const pk = process.env.PRIVATE_KEY || '';
620
+ const cotiWallet = new CotiWallet(pk.startsWith('0x') ? pk : '0x' + pk, ethers.provider);
621
+ try {
622
+ console.log(` [Info] Onboarding wallet ${cotiWallet.address} for MPC encrypted operations...`);
623
+ const { aesKey } = await cotiOnboard(cotiOnboardAddr, cotiWallet);
624
+ process.env.PRIVATE_AES_KEY_TESTNET = aesKey;
625
+ console.log(` [Info] Onboarding complete. AES key (first 8 chars): ${aesKey.slice(0, 8)}...`);
626
+ } catch(e) {
627
+ console.log(` [Warn] Onboarding failed: ${e.message}. Encrypted tests may fail.`);
628
+ }
629
+ } else {
630
+ const Factory = await ethers.getContractFactory(cfg.factory);
631
+ token = await Factory.deploy({ gasLimit: 30000000 });
632
+ await (token.waitForDeployment ? token.waitForDeployment() : token.deployed());
633
+ }
634
+
635
+ // deployer holds DEFAULT_ADMIN_ROLE → grant MINTER_ROLE to owner
636
+ await logTx(
637
+ await token.grantRole(MINTER_ROLE, owner.address, { gasLimit: 2000000 }),
638
+ `Grant MINTER_ROLE to owner for ${cfg.name}`,
639
+ `${cfg.name}.grantRole`,
640
+ ["MINTER_ROLE", owner.address]
641
+ );
642
+ await registerContract(cfg.factory, token, `PrivateERC20 Public-Amount (${cfg.name})`);
643
+ await new Promise(r => setTimeout(r, 5000));
644
+ });
645
+
646
+ it(`Test ${cfg.testStart}: ${cfg.name}: mint(address,uint256) should mint tokens and emit Transfer`, async function () {
647
+ const tx = await token.connect(owner)["mint(address,uint256)"](owner.address, MINT_AMOUNT, { gasLimit: 12000000 });
648
+ await logTx(tx, `mint(owner, 10) on ${cfg.name}`, `${cfg.name}.mint(address,uint256)`, [owner.address, "10"]);
649
+ await expect(tx).to.emit(token, "Transfer");
650
+ });
651
+
652
+ it(`Test ${cfg.testStart + 1}: ${cfg.name}: mint(address,uint256) should revert for non-MINTER_ROLE`, async function () {
653
+ try {
654
+ const tx = await token.connect(user1)["mint(address,uint256)"](user1.address, MINT_AMOUNT, { gasLimit: 2000000 });
655
+ // If transaction is submitted, wait for it to be processed with retry logic
656
+ await waitForReceiptWithRetry(tx);
657
+ // If we reach here, the transaction didn't revert as expected
658
+ expect.fail("Expected transaction to revert but it succeeded");
659
+ } catch (error) {
660
+ // Transaction should revert - verify it's an expected revert
661
+ expect(error.message).to.match(/revert|AccessControl/i);
662
+ }
663
+ });
664
+
665
+ it(`Test ${cfg.testStart + 2}: ${cfg.name}: burn(uint256) should burn tokens and emit Transfer`, async function () {
666
+ const tx = await token.connect(owner)["burn(uint256)"](BURN_AMOUNT, { gasLimit: 12000000 });
667
+ await logTx(tx, `burn(1) on ${cfg.name}`, `${cfg.name}.burn(uint256)`, ["1"]);
668
+ await expect(tx).to.emit(token, "Transfer");
669
+ });
670
+
671
+ it(`Test ${cfg.testStart + 3}: ${cfg.name}: transfer(address,uint256) should transfer tokens and emit Transfer`, async function () {
672
+ const tx = await token.connect(owner)["transfer(address,uint256)"](user1.address, TRANSFER_AMOUNT, { gasLimit: 12000000 });
673
+ await logTx(tx, `transfer(user1, 2) on ${cfg.name}`, `${cfg.name}.transfer(address,uint256)`, [user1.address, "2"]);
674
+ await expect(tx).to.emit(token, "Transfer");
675
+ });
676
+
677
+ it(`Test ${cfg.testStart + 4}: ${cfg.name}: approve(address,uint256) should set allowance and emit Approval`, async function () {
678
+ const tx = await token.connect(owner)["approve(address,uint256)"](user1.address, APPROVE_AMOUNT, { gasLimit: 12000000 });
679
+ await logTx(tx, `approve(user1, 2) on ${cfg.name}`, `${cfg.name}.approve(address,uint256)`, [user1.address, "2"]);
680
+ await expect(tx).to.emit(token, "Approval");
681
+ });
682
+
683
+ it(`Test ${cfg.testStart + 5}: ${cfg.name}: transferFrom(address,address,uint256) should spend allowance and emit Transfer`, async function () {
684
+ const tx = await token.connect(user1)["transferFrom(address,address,uint256)"](owner.address, user1.address, APPROVE_AMOUNT, { gasLimit: 12000000 });
685
+ await logTx(tx, `transferFrom(owner→user1, 2) on ${cfg.name}`, `${cfg.name}.transferFrom(address,address,uint256)`, [owner.address, user1.address, "2"]);
686
+ await expect(tx).to.emit(token, "Transfer");
687
+ });
688
+
689
+ // --- NEW EXTENDED EXPERIMENTAL TESTS ---
690
+
691
+ it(`Test ${cfg.testStart + 6}: ${cfg.name} (extend): setAccountEncryptionAddress & accountEncryptionAddress`, async function () {
692
+ // Use a valid Ethereum address to avoid ENS resolution by provider
693
+ const mockEncryptionAddress = ethers.Wallet.createRandom().address;
694
+ const tx = await token.connect(user1).setAccountEncryptionAddress(mockEncryptionAddress, { gasLimit: 2000000 });
695
+ await logTx(tx, `setAccountEncryptionAddress on ${cfg.name}`, `${cfg.name}.setAccountEncryptionAddress`, ["<mock_address>"]);
696
+
697
+ const registeredKey = await token.accountEncryptionAddress(user1.address);
698
+ expect(registeredKey).to.not.be.empty;
699
+ });
700
+
701
+ it(`Test ${cfg.testStart + 7}: ${cfg.name} (extend): transferAndCall (ERC677) triggers onTokenReceived`, async function () {
702
+ const ReceiverFactory = await ethers.getContractFactory("PublicTokenReceiverMock");
703
+ const receiver = await ReceiverFactory.deploy({ gasLimit: 12000000 });
704
+ await (receiver.waitForDeployment ? receiver.waitForDeployment() : receiver.deployed());
705
+ const receiverAddr = await addr(receiver);
706
+
707
+ // Allow COTI MPC cluster to settle after the receiver deployment before the
708
+ // transferAndCall transaction (mirrors the fix applied to the encrypted variant).
709
+ await new Promise(r => setTimeout(r, 5000));
710
+
711
+ const amount = ethers.parseEther("0.1");
712
+ const data = ethers.toUtf8Bytes("hello world");
713
+
714
+ const tx = await token.connect(owner)["transferAndCall(address,uint256,bytes)"](receiverAddr, amount, data, { gasLimit: 12000000 });
715
+ // logTx awaits tx.wait() + mandatory 10-second MPC settle. Store the receipt so
716
+ // that event assertions below use it directly and avoid a second tx.wait() call —
717
+ // on COTI testnet the RPC can return null for a receipt that was just retrieved
718
+ // moments ago (MPC state still finalizing), which would cause tx.wait() to poll
719
+ // indefinitely and make the test appear to loop in the terminal.
720
+ const receipt = await logTx(tx, `transferAndCall to PublicTokenReceiverMock on ${cfg.name}`, `${cfg.name}.transferAndCall`, [receiverAddr, "0.1", "0x68656c..."]);
721
+
722
+ // PublicTokenReceiverMock returns true and does not emit a callback event.
723
+ // Verify token transfer event from the token logs.
724
+ const transferEvents = receipt.logs.filter(log => {
725
+ try { return token.interface.parseLog(log)?.name === "Transfer"; } catch { return false; }
726
+ });
727
+ expect(transferEvents.length, "Expected Transfer event on token").to.be.greaterThan(0);
728
+ });
729
+
730
+ it(`Test ${cfg.testStart + 8}: ${cfg.name} (extend): transfer(itUint256,address) with encrypted payload`, async function () {
731
+ if (skipIfNoEncryption(testCounter, cfg, "encrypted transfer")) {
732
+ this.skip();
733
+ return;
734
+ }
735
+
736
+ const amount = ethers.parseEther("0.5");
737
+ const tokenAddr = await addr(token);
738
+ // The signature for the overloaded encrypted transfer in PrivateERC20 (address first, itUint256 second)
739
+ const selector = ethers.id("transfer(address,((uint256,uint256),bytes))").slice(0, 10);
740
+
741
+ const itAmount = await buildItUint256(amount, tokenAddr, selector);
742
+
743
+ const tx = await token.connect(owner)["transfer(address,((uint256,uint256),bytes))"](user1.address, itAmount, { gasLimit: 12000000 });
744
+ await logTx(tx, `transfer(itUint256) on ${cfg.name}`, `${cfg.name}.transfer(to, itAmount)`, [user1.address, "<encrypted>"]);
745
+ expect(tx.hash).to.not.be.empty;
746
+ });
747
+
748
+ it(`Test ${cfg.testStart + 9}: ${cfg.name} (extend): approve(address,itUint256) and allowance checks`, async function () {
749
+ if (skipIfNoEncryption(testCounter, cfg, "encrypted approve")) {
750
+ this.skip();
751
+ return;
752
+ }
753
+
754
+ const amount = ethers.parseEther("2");
755
+ const tokenAddr = await addr(token);
756
+ const selector = ethers.id("approve(address,((uint256,uint256),bytes))").slice(0, 10);
757
+
758
+ const itAmount = await buildItUint256(amount, tokenAddr, selector);
759
+
760
+ const tx = await token.connect(owner)["approve(address,((uint256,uint256),bytes))"](user1.address, itAmount, { gasLimit: 12000000 });
761
+ await logTx(tx, `approve(itUint256) on ${cfg.name}`, `${cfg.name}.approve(spender, itAmount)`, [user1.address, "<encrypted>"]);
762
+
763
+ // Run allowance re-encryption
764
+ const reencryptTx = await token.connect(owner)["reencryptAllowance(address,bool)"](user1.address, true, { gasLimit: 2000000 });
765
+ await logTx(reencryptTx, `reencryptAllowance on ${cfg.name}`, `${cfg.name}.reencryptAllowance`, [user1.address, true]);
766
+ });
767
+
768
+ it(`Test ${cfg.testStart + 10}: ${cfg.name} (extend): transferFrom(address,address,itUint256)`, async function () {
769
+ if (skipIfNoEncryption(testCounter, cfg, "encrypted transferFrom")) {
770
+ this.skip();
771
+ return;
772
+ }
773
+
774
+ const amount = ethers.parseEther("0.1");
775
+ const tokenAddr = await addr(token);
776
+ // msg.sender (owner) acts as both from and spender — ciphertext is signed for owner's wallet.
777
+ const selector = ethers.id("transferFrom(address,address,((uint256,uint256),bytes))").slice(0, 10);
778
+
779
+ // Approve first, then build the ciphertext so the AES key and on-chain state
780
+ // are both settled before validateCiphertext runs.
781
+ // Use logTx so the mandatory 10-second MPC state-settle window runs before
782
+ // transferFrom reads _allowances[owner][owner] from the MPC precompile. A bare
783
+ // 3-second wait is insufficient on COTI testnet and causes validateCiphertext /
784
+ // _safeOnboard to read a stale (zero) allowance, reverting with
785
+ // "ERC20: insufficient allowance".
786
+ await logTx(
787
+ await token.connect(owner)["approve(address,uint256)"](owner.address, ethers.parseEther("1"), { gasLimit: 2000000 }),
788
+ `Approve owner as spender for ${cfg.name} encrypted transferFrom`,
789
+ `${cfg.name}.approve(address,uint256)`,
790
+ [owner.address, "1"]
791
+ );
792
+
793
+ // Build the itUint256 AFTER the approve has settled so the ciphertext is
794
+ // freshly signed against the confirmed on-chain state.
795
+ const itAmount = await buildItUint256(amount, tokenAddr, selector);
796
+
797
+ const tx = await token.connect(owner)["transferFrom(address,address,((uint256,uint256),bytes))"](owner.address, user1.address, itAmount, { gasLimit: 12000000 });
798
+ await logTx(tx, `transferFrom(itUint256) on ${cfg.name}`, `${cfg.name}.transferFrom(from, to, itAmount)`, [owner.address, user1.address, "<encrypted>"]);
799
+ expect(tx.hash).to.not.be.empty;
800
+ });
801
+ });
802
+ }
803
+
804
+ // ─────────────────────────────────────────────────────────────────────────
805
+ // COVERAGE IMPROVEMENT TESTS
806
+ // ─────────────────────────────────────────────────────────────────────────
807
+
808
+ describe("Coverage Improvements - PrivacyBridgeCotiNative", function () {
809
+ let privateCoti, bridge, mockOracle;
810
+
811
+ before(async function () {
812
+ if (skipIfOnlyPrivateERC20.call(this)) return;
813
+ const env = await setupNativeBridgeEnv("Dynamic Fee - Native", "PrivateCOTI");
814
+ privateCoti = env.privateCoti; bridge = env.bridge; mockOracle = env.mockOracle;
815
+ });
816
+
817
+ it("Test 63: dynamic-fee: Should verify default fee parameters", async function () {
818
+ const depositFixed = await bridge.depositFixedFee();
819
+ const depositPct = await bridge.depositPercentageBps();
820
+ const depositMax = await bridge.depositMaxFee();
821
+ const withdrawFixed = await bridge.withdrawFixedFee();
822
+ const withdrawPct = await bridge.withdrawPercentageBps();
823
+ const withdrawMax = await bridge.withdrawMaxFee();
824
+
825
+ console.log(` [Info] Deposit: fixed=${ethers.formatEther(depositFixed)}, pct=${depositPct}, max=${ethers.formatEther(depositMax)}`);
826
+ console.log(` [Info] Withdraw: fixed=${ethers.formatEther(withdrawFixed)}, pct=${withdrawPct}, max=${ethers.formatEther(withdrawMax)}`);
827
+
828
+ expect(depositFixed).to.equal(ethers.parseEther("10"));
829
+ expect(depositPct).to.equal(500n);
830
+ expect(depositMax).to.equal(ethers.parseEther("3000"));
831
+ expect(withdrawFixed).to.equal(ethers.parseEther("3"));
832
+ expect(withdrawPct).to.equal(250n);
833
+ expect(withdrawMax).to.equal(ethers.parseEther("1500"));
834
+ });
835
+
836
+ it("Test 64: dynamic-fee: estimateDepositFee returns correct fee and timestamps", async function () {
837
+ const amount = ethers.parseEther("100");
838
+ const [fee, cotiLastUpdated, blockTimestamp] = await bridge.estimateDepositFee(amount);
839
+
840
+ console.log(` [Info] estimateDepositFee(100 COTI): fee=${ethers.formatEther(fee)}, lastUpdated=${cotiLastUpdated}, blockTs=${blockTimestamp}`);
841
+
842
+ // 100 COTI at $0.05 = $5 USD value
843
+ // pctFee = $5 * 500/1000000 = $0.0025
844
+ // pctFeeCoti = $0.0025 / $0.05 = 0.05 COTI
845
+ // fee = max(10 COTI, 0.05 COTI) = 10 COTI (floor dominates)
846
+ expect(fee).to.equal(ethers.parseEther("10"));
847
+ expect(cotiLastUpdated).to.be.gt(0n);
848
+ expect(blockTimestamp).to.be.gt(0n);
849
+ });
850
+
851
+ it("Test 65: dynamic-fee: estimateWithdrawFee returns correct fee and timestamps", async function () {
852
+ const amount = ethers.parseEther("100");
853
+ const [fee, cotiLastUpdated, blockTimestamp] = await bridge.estimateWithdrawFee(amount);
854
+
855
+ console.log(` [Info] estimateWithdrawFee(100 COTI): fee=${ethers.formatEther(fee)}, lastUpdated=${cotiLastUpdated}, blockTs=${blockTimestamp}`);
856
+
857
+ // 100 COTI at $0.05 = $5 USD value
858
+ // pctFee = $5 * 250/1000000 = $0.00125
859
+ // pctFeeCoti = $0.00125 / $0.05 = 0.025 COTI
860
+ // fee = max(3 COTI, 0.025 COTI) = 3 COTI (floor dominates)
861
+ expect(fee).to.equal(ethers.parseEther("3"));
862
+ expect(cotiLastUpdated).to.be.gt(0n);
863
+ });
864
+
865
+ it("Test 66: dynamic-fee: Percentage fee dominates for large deposits", async function () {
866
+ // 1,000,000 COTI at $0.05 = $50,000 USD
867
+ // pctFee = $50,000 * 500/1,000,000 = $25
868
+ // pctFeeCoti = $25 / $0.05 = 500 COTI
869
+ // fee = max(10, 500) = 500, min(500, 3000) = 500 COTI
870
+ const amount = ethers.parseEther("1000000");
871
+ const [fee] = await bridge.estimateDepositFee(amount);
872
+ console.log(` [Info] estimateDepositFee(1M COTI): fee=${ethers.formatEther(fee)} COTI`);
873
+ expect(fee).to.equal(ethers.parseEther("500"));
874
+ });
875
+
876
+ it("Test 67: dynamic-fee: Max fee cap applies for very large deposits", async function () {
877
+ // 100,000,000 COTI at $0.05 = $5,000,000 USD
878
+ // pctFee = $5M * 500/1M = $2,500
879
+ // pctFeeCoti = $2,500 / $0.05 = 50,000 COTI
880
+ // fee = max(10, 50000) = 50000, min(50000, 3000) = 3000 COTI (cap)
881
+ const amount = ethers.parseEther("100000000");
882
+ const [fee] = await bridge.estimateDepositFee(amount);
883
+ console.log(` [Info] estimateDepositFee(100M COTI): fee=${ethers.formatEther(fee)} COTI (capped)`);
884
+ expect(fee).to.equal(ethers.parseEther("3000"));
885
+ });
886
+
887
+ it("Test 68: dynamic-fee: setDepositDynamicFee updates parameters", async function () {
888
+ const newFixed = ethers.parseEther("20");
889
+ const newPct = 1000n; // 0.1%
890
+ const newMax = ethers.parseEther("5000");
891
+
892
+ const tx = await bridge.setDepositDynamicFee(newFixed, newPct, newMax, { gasLimit: 2000000 });
893
+ await logTx(tx, "setDepositDynamicFee(20, 1000, 5000)", "setDepositDynamicFee", ["20", "1000", "5000"]);
894
+
895
+ expect(await bridge.depositFixedFee()).to.equal(newFixed);
896
+ expect(await bridge.depositPercentageBps()).to.equal(newPct);
897
+ expect(await bridge.depositMaxFee()).to.equal(newMax);
898
+
899
+ // Restore defaults
900
+ const restoreTx = await bridge.setDepositDynamicFee(ethers.parseEther("10"), 500n, ethers.parseEther("3000"), { gasLimit: 2000000 });
901
+ await logTx(restoreTx, "Restore deposit fee defaults", "setDepositDynamicFee", ["10", "500", "3000"]);
902
+ });
903
+
904
+ it("Test 69: dynamic-fee: setWithdrawDynamicFee updates parameters", async function () {
905
+ const newFixed = ethers.parseEther("5");
906
+ const newPct = 500n;
907
+ const newMax = ethers.parseEther("2000");
908
+
909
+ const tx = await bridge.setWithdrawDynamicFee(newFixed, newPct, newMax, { gasLimit: 2000000 });
910
+ await logTx(tx, "setWithdrawDynamicFee(5, 500, 2000)", "setWithdrawDynamicFee", ["5", "500", "2000"]);
911
+
912
+ expect(await bridge.withdrawFixedFee()).to.equal(newFixed);
913
+ expect(await bridge.withdrawPercentageBps()).to.equal(newPct);
914
+ expect(await bridge.withdrawMaxFee()).to.equal(newMax);
915
+
916
+ // Restore defaults
917
+ const restoreTx = await bridge.setWithdrawDynamicFee(ethers.parseEther("3"), 250n, ethers.parseEther("1500"), { gasLimit: 2000000 });
918
+ await logTx(restoreTx, "Restore withdraw fee defaults", "setWithdrawDynamicFee", ["3", "250", "1500"]);
919
+ });
920
+
921
+ it("Test 70: dynamic-fee: setDepositDynamicFee reverts if fixedFee > maxFee", async function () {
922
+ await expectRevert(bridge.setDepositDynamicFee(ethers.parseEther("5000"), 500n, ethers.parseEther("3000"), { gasLimit: 2000000 }), /InvalidFeeConfiguration|revert/i, " [Info] Correctly reverted: fixedFee > maxFee");
923
+ });
924
+
925
+ it("Test 71: dynamic-fee: setDepositDynamicFee reverts if maxFee is 0", async function () {
926
+ await expectRevert(bridge.setDepositDynamicFee(0n, 500n, 0n, { gasLimit: 2000000 }), /InvalidFeeConfiguration|revert/i, " [Info] Correctly reverted: maxFee == 0");
927
+ });
928
+
929
+ it("Test 72: dynamic-fee: setDepositDynamicFee reverts if percentageBps > MAX_FEE_UNITS", async function () {
930
+ await expectRevert(bridge.setDepositDynamicFee(ethers.parseEther("10"), 200000n, ethers.parseEther("3000"), { gasLimit: 2000000 }), /InvalidFee|revert/i, " [Info] Correctly reverted: percentageBps > MAX_FEE_UNITS");
931
+ });
932
+
933
+ it("Test 73: dynamic-fee: setPriceOracle reverts for zero address", async function () {
934
+ await expectRevert(bridge.setPriceOracle(ethers.ZeroAddress, { gasLimit: 2000000 }), /InvalidAddress|revert/i, " [Info] Correctly reverted: oracle = address(0)");
935
+ });
936
+
937
+ it("Test 74: dynamic-fee: setPriceOracle emits PriceOracleUpdated", async function () {
938
+ const oracleAddr = await addr(mockOracle);
939
+ // Set to a new address then restore
940
+ const newOracle = ethers.Wallet.createRandom().address;
941
+ const tx = await bridge.setPriceOracle(newOracle, { gasLimit: 2000000 });
942
+ await logTx(tx, "setPriceOracle to random address", "setPriceOracle", [newOracle]);
943
+ await expect(tx).to.emit(bridge, "PriceOracleUpdated");
944
+
945
+ // Restore original oracle
946
+ const restoreTx = await bridge.setPriceOracle(oracleAddr, { gasLimit: 2000000 });
947
+ await logTx(restoreTx, "Restore original oracle", "setPriceOracle", [oracleAddr]);
948
+ });
949
+
950
+ it("Test 75: dynamic-fee: totalUserLiability increases on deposit", async function () {
951
+ const liabilityBefore = await bridge.totalUserLiability();
952
+ const amount = ethers.parseEther("100");
953
+
954
+ const [fee, cotiLastUpdated] = await bridge.estimateDepositFee(amount);
955
+ const tx = await bridge["deposit(uint256,uint256)"](cotiLastUpdated, cotiLastUpdated, { value: amount, gasLimit: 12000000 });
956
+ await logTx(tx, "Deposit 100 COTI for liability tracking", "deposit", ["100"]);
957
+
958
+ const liabilityAfter = await bridge.totalUserLiability();
959
+ const netDeposit = amount - fee;
960
+ console.log(` [Info] Liability before=${ethers.formatEther(liabilityBefore)}, after=${ethers.formatEther(liabilityAfter)}, net=${ethers.formatEther(netDeposit)}`);
961
+ expect(liabilityAfter - liabilityBefore).to.equal(netDeposit);
962
+ });
963
+
964
+ it("Test 76: dynamic-fee: totalUserLiability decreases on withdraw", async function () {
965
+ const amount = ethers.parseEther("50");
966
+ const bridgeAddr = await addr(bridge);
967
+
968
+ await logTx(await privateCoti.connect(owner)["approve(address,uint256)"](bridgeAddr, amount, { gasLimit: 2000000 }), "Approve for liability withdraw test", "approve", [bridgeAddr, "50"]);
969
+
970
+ const liabilityBefore = await bridge.totalUserLiability();
971
+ const [fee, cotiLastUpdated] = await bridge.estimateWithdrawFee(amount);
972
+
973
+ const tx = await bridge["withdraw(uint256,uint256,uint256)"](amount, cotiLastUpdated, cotiLastUpdated, { gasLimit: 12000000 });
974
+ await logTx(tx, "Withdraw 50 COTI for liability tracking", "withdraw", ["50"]);
975
+
976
+ const liabilityAfter = await bridge.totalUserLiability();
977
+ const netWithdraw = amount - fee;
978
+ console.log(` [Info] Liability before=${ethers.formatEther(liabilityBefore)}, after=${ethers.formatEther(liabilityAfter)}, net=${ethers.formatEther(netWithdraw)}`);
979
+ expect(liabilityBefore - liabilityAfter).to.equal(netWithdraw);
980
+ });
981
+
982
+ it("Test 77: dynamic-fee: feeRecipient and rescueRecipient are set correctly", async function () {
983
+ const feeRecip = await bridge.feeRecipient();
984
+ const rescueRecip = await bridge.rescueRecipient();
985
+ console.log(` [Info] feeRecipient=${feeRecip}, rescueRecipient=${rescueRecip}`);
986
+ expect(feeRecip).to.equal(owner.address);
987
+ expect(rescueRecip).to.equal(owner.address);
988
+ });
989
+
990
+ it("Test 78: dynamic-fee: AccessControlEnumerable role member tracking", async function () {
991
+ const OPERATOR_ROLE = await bridge.OPERATOR_ROLE();
992
+ const operatorCount = await bridge.getRoleMemberCount(OPERATOR_ROLE);
993
+ console.log(` [Info] Operator count: ${operatorCount}`);
994
+ expect(operatorCount).to.be.gte(1n);
995
+
996
+ const firstOperator = await bridge.getRoleMember(OPERATOR_ROLE, 0);
997
+ console.log(` [Info] First operator: ${firstOperator}`);
998
+ expect(firstOperator).to.equal(owner.address);
999
+ });
1000
+
1001
+ it("Test 79: dynamic-fee: DynamicFeeUpdated event emitted on setDepositDynamicFee", async function () {
1002
+ const tx = await bridge.setDepositDynamicFee(ethers.parseEther("10"), 500n, ethers.parseEther("3000"), { gasLimit: 2000000 });
1003
+ await logTx(tx, "setDepositDynamicFee for event check", "setDepositDynamicFee", ["10", "500", "3000"]);
1004
+ await expect(tx).to.emit(bridge, "DynamicFeeUpdated");
1005
+ });
1006
+
1007
+ it("Test 80: dynamic-fee: receive() direct deposit works without timestamps", async function () {
1008
+ const bridgeAddr = await addr(bridge);
1009
+ const amount = ethers.parseEther("100");
1010
+ const feeBefore = await bridge.accumulatedCotiFees();
1011
+
1012
+ const tx = await owner.sendTransaction({ to: bridgeAddr, value: amount, gasLimit: 2000000 });
1013
+ await logTx(tx, "Direct COTI transfer (receive fallback) for fee check", "receive()", [ethers.formatEther(amount)]);
1014
+
1015
+ const feeAfter = await bridge.accumulatedCotiFees();
1016
+ const feeCharged = feeAfter - feeBefore;
1017
+ console.log(` [Info] Fee charged via receive(): ${ethers.formatEther(feeCharged)} COTI`);
1018
+ expect(feeCharged).to.be.gt(0n);
1019
+ });
1020
+
1021
+ it("Test 81: dynamic-fee: withdrawFees sends to feeRecipient", async function () {
1022
+ const fees = await bridge.accumulatedCotiFees();
1023
+ if (fees === 0n) {
1024
+ console.log(" [Info] No accumulated fees to withdraw, skipping");
1025
+ this.skip();
1026
+ return;
1027
+ }
1028
+ const withdrawAmount = fees / 2n > 0n ? fees / 2n : fees;
1029
+
1030
+ const tx = await bridge.withdrawFees(withdrawAmount, { gasLimit: 2000000 });
1031
+ await logTx(tx, `withdrawFees(${ethers.formatEther(withdrawAmount)})`, "withdrawFees", [ethers.formatEther(withdrawAmount)]);
1032
+
1033
+ await expect(tx).to.emit(bridge, "FeesWithdrawn");
1034
+ console.log(` [Info] Withdrew ${ethers.formatEther(withdrawAmount)} COTI fees to feeRecipient`);
1035
+ });
1036
+
1037
+ it("Test 82: dynamic-fee: setNativeCotiFee reverts on native bridge", async function () {
1038
+ await expectRevert(bridge.setNativeCotiFee(ethers.parseEther("1"), { gasLimit: 2000000 }), /NativeCotiFeeNotApplicable|revert/i, " [Info] Correctly reverted: NativeCotiFeeNotApplicable");
1039
+ });
1040
+ });
1041
+
1042
+ // ─────────────────────────────────────────────────────────────────────────
1043
+ // DYNAMIC FEE FEATURES - ERC20 BRIDGE
1044
+ // ─────────────────────────────────────────────────────────────────────────
1045
+
1046
+ describe("Dynamic Fee Features - ERC20 Bridge", function () {
1047
+ let publicToken, bridge;
1048
+ const UNIT = BigInt(10 ** 18);
1049
+ const COTI_FEE_BUFFER = ethers.parseEther("3100");
1050
+
1051
+ before(async function () {
1052
+ if (skipIfOnlyPrivateERC20.call(this)) return;
1053
+ const env = await setupERC20BridgeSuite("Dynamic Fee - ERC20", 10000n);
1054
+ publicToken = env.publicToken;
1055
+ bridge = env.bridge;
1056
+ });
1057
+
1058
+ it("Test 83: dynamic-fee-erc20: estimateDepositFee returns correct fee for ERC20", async function () {
1059
+ // 10 WETH at $2300 = $23,000 USD
1060
+ // pctFee = $23,000 * 500/1,000,000 = $11.50
1061
+ // pctFeeCoti = $11.50 / $0.05 = 230 COTI
1062
+ // fee = max(10, 230) = 230, min(230, 3000) = 230 COTI
1063
+ const amount = 10n * UNIT;
1064
+ const [fee, cotiLastUpdated, tokenLastUpdated, blockTimestamp] = await bridge.estimateDepositFee(amount);
1065
+ console.log(` [Info] estimateDepositFee(10 WETH): fee=${ethers.formatEther(fee)} COTI`);
1066
+ expect(fee).to.equal(ethers.parseEther("230"));
1067
+ expect(cotiLastUpdated).to.be.gt(0n);
1068
+ expect(tokenLastUpdated).to.be.gt(0n);
1069
+ expect(blockTimestamp).to.be.gt(0n);
1070
+ });
1071
+
1072
+ it("Test 84: dynamic-fee-erc20: estimateWithdrawFee returns correct fee for ERC20", async function () {
1073
+ // 10 WETH at $2300 = $23,000 USD
1074
+ // pctFee = $23,000 * 250/1,000,000 = $5.75
1075
+ // pctFeeCoti = $5.75 / $0.05 = 115 COTI
1076
+ // fee = max(3, 115) = 115, min(115, 1500) = 115 COTI
1077
+ const amount = 10n * UNIT;
1078
+ const [fee] = await bridge.estimateWithdrawFee(amount);
1079
+ console.log(` [Info] estimateWithdrawFee(10 WETH): fee=${ethers.formatEther(fee)} COTI`);
1080
+ expect(fee).to.equal(ethers.parseEther("115"));
1081
+ });
1082
+
1083
+ it("Test 85: dynamic-fee-erc20: Max fee cap applies for large ERC20 deposits", async function () {
1084
+ // 1000 WETH at $2300 = $2,300,000 USD
1085
+ // pctFee = $2,300,000 * 500/1,000,000 = $1,150
1086
+ // pctFeeCoti = $1,150 / $0.05 = 23,000 COTI
1087
+ // fee = max(10, 23000) = 23000, min(23000, 3000) = 3000 COTI (capped)
1088
+ const amount = 1000n * UNIT;
1089
+ const [fee] = await bridge.estimateDepositFee(amount);
1090
+ console.log(` [Info] estimateDepositFee(1000 WETH): fee=${ethers.formatEther(fee)} COTI (capped)`);
1091
+ expect(fee).to.equal(ethers.parseEther("3000"));
1092
+ });
1093
+
1094
+ it("Test 86: dynamic-fee-erc20: tokenSymbol returns correct oracle symbol", async function () {
1095
+ const symbol = await bridge.tokenSymbol();
1096
+ console.log(` [Info] tokenSymbol: ${symbol}`);
1097
+ expect(symbol).to.equal("ETH");
1098
+ });
1099
+
1100
+ it("Test 87: dynamic-fee-erc20: Full deposit collects COTI fee from msg.value", async function () {
1101
+ const amount = 10n * UNIT;
1102
+ const bridgeAddr = await addr(bridge);
1103
+ await logTx(await publicToken.approve(bridgeAddr, amount, { gasLimit: 2000000 }), "Approve WETH for deposit", "approve", [bridgeAddr, "10"]);
1104
+
1105
+ const feeBefore = await bridge.accumulatedCotiFees();
1106
+ const [fee, cotiLastUpdated, tokenLastUpdated] = await bridge.estimateDepositFee(amount);
1107
+ console.log(` [Info] Estimated fee: ${ethers.formatEther(fee)} COTI`);
1108
+
1109
+ const tx = await bridge["deposit(uint256,uint256,uint256)"](amount, cotiLastUpdated, tokenLastUpdated, { value: COTI_FEE_BUFFER, gasLimit: 12000000 });
1110
+ await logTx(tx, "Deposit 10 WETH with COTI fee", "deposit", ["10"]);
1111
+
1112
+ const feeAfter = await bridge.accumulatedCotiFees();
1113
+ const actualFee = feeAfter - feeBefore;
1114
+ console.log(` [Info] Actual COTI fee collected: ${ethers.formatEther(actualFee)}`);
1115
+ expect(actualFee).to.equal(fee);
1116
+ });
1117
+
1118
+ it("Test 88: dynamic-fee-erc20: Deposit reverts with insufficient COTI fee", async function () {
1119
+ const amount = 10n * UNIT;
1120
+ const bridgeAddr = await addr(bridge);
1121
+ await logTx(await publicToken.approve(bridgeAddr, amount, { gasLimit: 2000000 }), "Approve WETH for insufficient fee test", "approve", [bridgeAddr, "10"]);
1122
+
1123
+ const [fee, cotiLastUpdated, tokenLastUpdated] = await bridge.estimateDepositFee(amount);
1124
+
1125
+ try {
1126
+ // Send less than the required fee
1127
+ const insufficientFee = fee / 2n;
1128
+ const tx = await bridge["deposit(uint256,uint256,uint256)"](amount, cotiLastUpdated, tokenLastUpdated, { value: insufficientFee, gasLimit: 12000000 });
1129
+ await waitForReceiptWithRetry(tx);
1130
+ expect.fail("Expected revert but succeeded");
1131
+ } catch (error) {
1132
+ expect(error.message).to.match(/InsufficientCotiFee|revert/i);
1133
+ console.log(" [Info] Correctly reverted: InsufficientCotiFee");
1134
+ }
1135
+ });
1136
+ });
1137
+
1138
+ // ─────────────────────────────────────────────────────────────────────────
1139
+ // BLACKLIST & ACCESS CONTROL TESTS
1140
+ // ─────────────────────────────────────────────────────────────────────────
1141
+
1142
+ describe("Blacklist & Access Control", function () {
1143
+ let bridge;
1144
+
1145
+ before(async function () {
1146
+ if (skipIfOnlyPrivateERC20.call(this)) return;
1147
+ const env = await setupNativeBridgeEnv("Blacklist & Access Control");
1148
+ bridge = env.bridge;
1149
+ });
1150
+
1151
+ it("Test 89: blacklist: addToBlacklist blocks deposit", async function () {
1152
+ // Blacklist the owner address
1153
+ const tx = await bridge.addToBlacklist(owner.address, { gasLimit: 2000000 });
1154
+ await logTx(tx, "addToBlacklist(owner)", "addToBlacklist", [owner.address]);
1155
+ await expect(tx).to.emit(bridge, "Blacklisted");
1156
+
1157
+ expect(await bridge.blacklisted(owner.address)).to.equal(true);
1158
+
1159
+ // Try to deposit — should revert
1160
+ const [, cotiLastUpdated] = await bridge.estimateDepositFee(ethers.parseEther("100"));
1161
+ await expectRevert(bridge["deposit(uint256,uint256)"](cotiLastUpdated, cotiLastUpdated, { value: ethers.parseEther("100"), gasLimit: 12000000 }), /AddressBlacklisted|revert/i, " [Info] Correctly reverted: AddressBlacklisted on deposit");
1162
+ });
1163
+
1164
+ it("Test 90: blacklist: removeFromBlacklist restores access", async function () {
1165
+ const tx = await bridge.removeFromBlacklist(owner.address, { gasLimit: 2000000 });
1166
+ await logTx(tx, "removeFromBlacklist(owner)", "removeFromBlacklist", [owner.address]);
1167
+ await expect(tx).to.emit(bridge, "UnBlacklisted");
1168
+
1169
+ expect(await bridge.blacklisted(owner.address)).to.equal(false);
1170
+
1171
+ // Deposit should now work
1172
+ const [, cotiLastUpdated] = await bridge.estimateDepositFee(ethers.parseEther("100"));
1173
+ const depositTx = await bridge["deposit(uint256,uint256)"](cotiLastUpdated, cotiLastUpdated, { value: ethers.parseEther("100"), gasLimit: 12000000 });
1174
+ await logTx(depositTx, "Deposit after unblacklist", "deposit", ["100"]);
1175
+ await expect(depositTx).to.emit(bridge, "Deposit");
1176
+ });
1177
+
1178
+ it("Test 91: blacklist: addToBlacklist reverts for zero address", async function () {
1179
+ await expectRevert(bridge.addToBlacklist(ethers.ZeroAddress, { gasLimit: 2000000 }), /InvalidAddress|revert/i, " [Info] Correctly reverted: addToBlacklist(address(0))");
1180
+ });
1181
+
1182
+ it("Test 92: access-control: addOperator and removeOperator work correctly", async function () {
1183
+ const randomAddr = ethers.Wallet.createRandom().address;
1184
+
1185
+ const addTx = await bridge.addOperator(randomAddr, { gasLimit: 2000000 });
1186
+ await logTx(addTx, "addOperator(random)", "addOperator", [randomAddr]);
1187
+ await expect(addTx).to.emit(bridge, "OperatorAdded");
1188
+
1189
+ const isOp = await bridge.isOperator(randomAddr);
1190
+ expect(isOp).to.equal(true);
1191
+
1192
+ const removeTx = await bridge.removeOperator(randomAddr, { gasLimit: 2000000 });
1193
+ await logTx(removeTx, "removeOperator(random)", "removeOperator", [randomAddr]);
1194
+ await expect(removeTx).to.emit(bridge, "OperatorRemoved");
1195
+
1196
+ const isOpAfter = await bridge.isOperator(randomAddr);
1197
+ expect(isOpAfter).to.equal(false);
1198
+ });
1199
+
1200
+ it("Test 93: access-control: pause blocks deposits, unpause restores", async function () {
1201
+ const pauseTx = await bridge.pause({ gasLimit: 2000000 });
1202
+ await logTx(pauseTx, "pause()", "pause", []);
1203
+
1204
+ // Try deposit while paused
1205
+ const [, cotiLastUpdated] = await bridge.estimateDepositFee(ethers.parseEther("100"));
1206
+ await expectRevert(bridge["deposit(uint256,uint256)"](cotiLastUpdated, cotiLastUpdated, { value: ethers.parseEther("100"), gasLimit: 12000000 }), /Pausable|paused|revert/i, " [Info] Correctly reverted: deposit while paused");
1207
+
1208
+ // Unpause
1209
+ const unpauseTx = await bridge.unpause({ gasLimit: 2000000 });
1210
+ await logTx(unpauseTx, "unpause()", "unpause", []);
1211
+ });
1212
+
1213
+ it("Test 94: access-control: renounceOwnership is disabled", async function () {
1214
+ await expectRevert(bridge.renounceOwnership({ gasLimit: 2000000 }), /renounceOwnership disabled|revert/i, " [Info] Correctly reverted: renounceOwnership disabled");
1215
+ });
1216
+
1217
+ it("Test 95: access-control: setIsDepositEnabled toggles deposits", async function () {
1218
+ // Disable deposits
1219
+ const disableTx = await bridge.setIsDepositEnabled(false, { gasLimit: 2000000 });
1220
+ await logTx(disableTx, "setIsDepositEnabled(false)", "setIsDepositEnabled", ["false"]);
1221
+ expect(await bridge.isDepositEnabled()).to.equal(false);
1222
+
1223
+ // Try deposit — should revert
1224
+ const [, cotiLastUpdated] = await bridge.estimateDepositFee(ethers.parseEther("100"));
1225
+ await expectRevert(bridge["deposit(uint256,uint256)"](cotiLastUpdated, cotiLastUpdated, { value: ethers.parseEther("100"), gasLimit: 12000000 }), /DepositDisabled|revert/i, " [Info] Correctly reverted: DepositDisabled");
1226
+
1227
+ // Re-enable
1228
+ const enableTx = await bridge.setIsDepositEnabled(true, { gasLimit: 2000000 });
1229
+ await logTx(enableTx, "setIsDepositEnabled(true)", "setIsDepositEnabled", ["true"]);
1230
+ expect(await bridge.isDepositEnabled()).to.equal(true);
1231
+ });
1232
+
1233
+ it("Test 96: access-control: setLimits enforces deposit/withdraw bounds", async function () {
1234
+ const minDep = ethers.parseEther("50");
1235
+ const maxDep = ethers.parseEther("10000");
1236
+ const minWith = ethers.parseEther("10");
1237
+ const maxWith = ethers.parseEther("5000");
1238
+
1239
+ const tx = await bridge.setLimits(minDep, maxDep, minWith, maxWith, { gasLimit: 2000000 });
1240
+ await logTx(tx, "setLimits(50, 10000, 10, 5000)", "setLimits", ["50", "10000", "10", "5000"]);
1241
+ await expect(tx).to.emit(bridge, "LimitsUpdated");
1242
+
1243
+ expect(await bridge.minDepositAmount()).to.equal(minDep);
1244
+ expect(await bridge.maxDepositAmount()).to.equal(maxDep);
1245
+
1246
+ // Try deposit below minimum
1247
+ const [, cotiLastUpdated] = await bridge.estimateDepositFee(ethers.parseEther("10"));
1248
+ await expectRevert(bridge["deposit(uint256,uint256)"](cotiLastUpdated, cotiLastUpdated, { value: ethers.parseEther("10"), gasLimit: 12000000 }), /DepositBelowMinimum|revert/i, " [Info] Correctly reverted: DepositBelowMinimum");
1249
+
1250
+ // Restore defaults
1251
+ const restoreTx = await bridge.setLimits(1n, ethers.MaxUint256, 1n, ethers.MaxUint256, { gasLimit: 2000000 });
1252
+ await logTx(restoreTx, "Restore default limits", "setLimits", ["1", "max", "1", "max"]);
1253
+ });
1254
+ });
1255
+
1256
+
1257
+ // ─────────────────────────────────────────────────────────────────────────
1258
+ // FULL COVERAGE — NATIVE BRIDGE REVERT PATHS
1259
+ // ─────────────────────────────────────────────────────────────────────────
1260
+
1261
+ describe("Full Coverage - Native Bridge Reverts", function () {
1262
+ let bridge;
1263
+
1264
+ before(async function () {
1265
+ if (skipIfOnlyPrivateERC20.call(this)) return;
1266
+ const env = await setupNativeBridgeEnv("Full Coverage - Native Reverts");
1267
+ bridge = env.bridge;
1268
+ });
1269
+
1270
+ it("Test 97: revert: native deposit with zero value reverts AmountZero", async function () {
1271
+ const [, cotiLastUpdated] = await bridge.estimateDepositFee(ethers.parseEther("100"));
1272
+ await expectRevert(bridge["deposit(uint256,uint256)"](cotiLastUpdated, cotiLastUpdated, { value: 0n, gasLimit: 2000000 }), /AmountZero|revert/i, " [Info] Correctly reverted: deposit with value=0");
1273
+ });
1274
+
1275
+ it("Test 98: revert: native withdraw with amount=0 reverts AmountZero", async function () {
1276
+ const [, cotiLastUpdated] = await bridge.estimateWithdrawFee(ethers.parseEther("100"));
1277
+ await expectRevert(bridge["withdraw(uint256,uint256,uint256)"](0n, cotiLastUpdated, cotiLastUpdated, { gasLimit: 2000000 }), /AmountZero|revert/i, " [Info] Correctly reverted: withdraw with amount=0");
1278
+ });
1279
+
1280
+ it("Test 99: revert: withdrawFees(0) reverts AmountZero", async function () {
1281
+ await expectRevert(bridge.withdrawFees(0n, { gasLimit: 2000000 }), /AmountZero|revert/i, " [Info] Correctly reverted: withdrawFees(0)");
1282
+ });
1283
+
1284
+ it("Test 100: revert: withdrawFees exceeding accumulated reverts InsufficientAccumulatedFees", async function () {
1285
+ await expectRevert(bridge.withdrawFees(ethers.parseEther("999999"), { gasLimit: 2000000 }), /InsufficientAccumulatedFees|revert/i, " [Info] Correctly reverted: withdrawFees > accumulated");
1286
+ });
1287
+
1288
+ it("Test 101: revert: rescueNative(0) reverts AmountZero", async function () {
1289
+ await expectRevert(bridge.rescueNative(0n, { gasLimit: 2000000 }), /AmountZero|revert/i, " [Info] Correctly reverted: rescueNative(0)");
1290
+ });
1291
+
1292
+ it("Test 102: revert: rescueNative exceeding rescueable reverts ExceedsRescueableAmount", async function () {
1293
+ // First deposit to create some balance and fees
1294
+ const [, cotiLastUpdated] = await bridge.estimateDepositFee(ethers.parseEther("100"));
1295
+ await logTx(await bridge["deposit(uint256,uint256)"](cotiLastUpdated, cotiLastUpdated, { value: ethers.parseEther("100"), gasLimit: 12000000 }), "Deposit for rescue test", "deposit", ["100"]);
1296
+
1297
+ // Try to rescue more than balance minus fees
1298
+ const bal = await ethers.provider.getBalance(await addr(bridge));
1299
+ await expectRevert(bridge.rescueNative(bal, { gasLimit: 2000000 }), /ExceedsRescueableAmount|InsufficientEthBalance|revert/i, " [Info] Correctly reverted: rescueNative exceeds rescueable");
1300
+ });
1301
+
1302
+ it("Test 103: revert: receive() reverts when deposits disabled", async function () {
1303
+ await logTx(await bridge.setIsDepositEnabled(false, { gasLimit: 2000000 }), "Disable deposits for receive test", "setIsDepositEnabled", ["false"]);
1304
+
1305
+ try {
1306
+ const tx = await owner.sendTransaction({ to: await addr(bridge), value: ethers.parseEther("10"), gasLimit: 2000000 });
1307
+ await waitForReceiptWithRetry(tx);
1308
+ expect.fail("Expected revert");
1309
+ } catch (error) {
1310
+ expect(error.message).to.match(/DepositDisabled|revert/i);
1311
+ console.log(" [Info] Correctly reverted: receive() when deposits disabled");
1312
+ }
1313
+
1314
+ await logTx(await bridge.setIsDepositEnabled(true, { gasLimit: 2000000 }), "Re-enable deposits", "setIsDepositEnabled", ["true"]);
1315
+ });
1316
+
1317
+ it("Test 104: revert: setLimits with min > max reverts InvalidLimitConfiguration", async function () {
1318
+ await expectRevert(bridge.setLimits(ethers.parseEther("100"), ethers.parseEther("10"), 1n, ethers.MaxUint256, { gasLimit: 2000000 }), /InvalidLimitConfiguration|revert/i, " [Info] Correctly reverted: minDeposit > maxDeposit");
1319
+ });
1320
+
1321
+ it("Test 105: revert: setDepositFee exceeding MAX_FEE_UNITS reverts InvalidFee", async function () {
1322
+ await expectRevert(bridge.setDepositFee(200000n, { gasLimit: 2000000 }), /InvalidFee|revert/i, " [Info] Correctly reverted: setDepositFee > MAX_FEE_UNITS");
1323
+ });
1324
+
1325
+ it("Test 106: legacy: setDepositFee and setWithdrawFee update basis points", async function () {
1326
+ const tx1 = await bridge.setDepositFee(1000n, { gasLimit: 2000000 });
1327
+ await logTx(tx1, "setDepositFee(1000)", "setDepositFee", ["1000"]);
1328
+ expect(await bridge.depositFeeBasisPoints()).to.equal(1000n);
1329
+
1330
+ const tx2 = await bridge.setWithdrawFee(500n, { gasLimit: 2000000 });
1331
+ await logTx(tx2, "setWithdrawFee(500)", "setWithdrawFee", ["500"]);
1332
+ expect(await bridge.withdrawFeeBasisPoints()).to.equal(500n);
1333
+ });
1334
+
1335
+ it("Test 107: revert: setWithdrawFee exceeding MAX_FEE_UNITS reverts InvalidFee", async function () {
1336
+ await expectRevert(bridge.setWithdrawFee(200000n, { gasLimit: 2000000 }), /InvalidFee|revert/i, " [Info] Correctly reverted: setWithdrawFee > MAX_FEE_UNITS");
1337
+ });
1338
+
1339
+ it("Test 108: transferOwnership revokes old operators and grants to new owner", async function () {
1340
+ const newOwner = ethers.Wallet.createRandom().address;
1341
+ const OPERATOR_ROLE = await bridge.OPERATOR_ROLE();
1342
+
1343
+ // Check current operator count
1344
+ const opCountBefore = await bridge.getRoleMemberCount(OPERATOR_ROLE);
1345
+ console.log(` [Info] Operators before transfer: ${opCountBefore}`);
1346
+
1347
+ const tx = await bridge.transferOwnership(newOwner, { gasLimit: 2000000 });
1348
+ await logTx(tx, `transferOwnership to ${newOwner}`, "transferOwnership", [newOwner]);
1349
+
1350
+ // New owner should have operator role
1351
+ expect(await bridge.isOperator(newOwner)).to.equal(true);
1352
+ // Old owner should NOT have operator role
1353
+ expect(await bridge.isOperator(owner.address)).to.equal(false);
1354
+ // New owner is the owner
1355
+ expect(await bridge.owner()).to.equal(newOwner);
1356
+
1357
+ console.log(" [Info] Ownership transferred, old operators revoked, new owner has roles");
1358
+ // NOTE: We can't transfer back since we don't have the new owner's private key.
1359
+ // This bridge instance is now owned by a random address — subsequent tests use different bridges.
1360
+ });
1361
+ });
1362
+
1363
+ // ─────────────────────────────────────────────────────────────────────────
1364
+ // FULL COVERAGE — ERC20 BRIDGE REVERT PATHS
1365
+ // ─────────────────────────────────────────────────────────────────────────
1366
+
1367
+ describe("Full Coverage - ERC20 Bridge Reverts", function () {
1368
+ let publicToken, privateToken, bridge;
1369
+ const UNIT = BigInt(10 ** 18);
1370
+ const COTI_FEE_BUFFER = ethers.parseEther("3100");
1371
+
1372
+ before(async function () {
1373
+ if (skipIfOnlyPrivateERC20.call(this)) return;
1374
+ const env = await setupERC20BridgeSuite("Full Coverage - ERC20 Reverts", 10000n);
1375
+ publicToken = env.publicToken; privateToken = env.privateToken;
1376
+ bridge = env.bridge;
1377
+ });
1378
+
1379
+ it("Test 109: revert: ERC20 deposit with amount=0 reverts AmountZero", async function () {
1380
+ const [, cotiLastUpdated, tokenLastUpdated] = await bridge.estimateDepositFee(1n * UNIT);
1381
+ await expectRevert(bridge["deposit(uint256,uint256,uint256)"](0n, cotiLastUpdated, tokenLastUpdated, { value: COTI_FEE_BUFFER, gasLimit: 2000000 }), /AmountZero|revert/i, " [Info] Correctly reverted: ERC20 deposit amount=0");
1382
+ });
1383
+
1384
+ it("Test 110: revert: ERC20 withdraw with amount=0 reverts AmountZero", async function () {
1385
+ const [, cotiLastUpdated, tokenLastUpdated] = await bridge.estimateWithdrawFee(1n * UNIT);
1386
+ await expectRevert(bridge["withdraw(uint256,uint256,uint256)"](0n, cotiLastUpdated, tokenLastUpdated, { value: COTI_FEE_BUFFER, gasLimit: 2000000 }), /AmountZero|revert/i, " [Info] Correctly reverted: ERC20 withdraw amount=0");
1387
+ });
1388
+
1389
+ it("Test 111: revert: ERC20 deposit when deposits disabled reverts DepositDisabled", async function () {
1390
+ await logTx(await bridge.setIsDepositEnabled(false, { gasLimit: 2000000 }), "Disable deposits (ERC20)", "setIsDepositEnabled", ["false"]);
1391
+
1392
+ const amount = 1n * UNIT;
1393
+ const bridgeAddr = await addr(bridge);
1394
+ await logTx(await publicToken.approve(bridgeAddr, amount, { gasLimit: 2000000 }), "Approve for disabled deposit test", "approve", [bridgeAddr, "1"]);
1395
+ const [, cotiLastUpdated, tokenLastUpdated] = await bridge.estimateDepositFee(amount);
1396
+
1397
+ await expectRevert(bridge["deposit(uint256,uint256,uint256)"](amount, cotiLastUpdated, tokenLastUpdated, { value: COTI_FEE_BUFFER, gasLimit: 12000000 }), /DepositDisabled|revert/i, " [Info] Correctly reverted: ERC20 deposit when disabled");
1398
+
1399
+ await logTx(await bridge.setIsDepositEnabled(true, { gasLimit: 2000000 }), "Re-enable deposits (ERC20)", "setIsDepositEnabled", ["true"]);
1400
+ });
1401
+
1402
+ it("Test 112: revert: ERC20 withdraw with InsufficientBridgeLiquidity", async function () {
1403
+ // Try to withdraw more than the bridge holds (bridge has 0 tokens)
1404
+ const amount = 1000n * UNIT;
1405
+ const bridgeAddr = await addr(bridge);
1406
+ await logTx(await privateToken["approve(address,uint256)"](bridgeAddr, amount, { gasLimit: 2000000 }), "Approve for liquidity test", "approve", [bridgeAddr, "1000"]);
1407
+ const [, cotiLastUpdated, tokenLastUpdated] = await bridge.estimateWithdrawFee(amount);
1408
+
1409
+ await expectRevert(bridge["withdraw(uint256,uint256,uint256)"](amount, cotiLastUpdated, tokenLastUpdated, { value: COTI_FEE_BUFFER, gasLimit: 12000000 }), /InsufficientBridgeLiquidity|revert/i, " [Info] Correctly reverted: InsufficientBridgeLiquidity");
1410
+ });
1411
+
1412
+ it("Test 113: revert: rescueERC20 with private token reverts CannotRescueBridgeToken", async function () {
1413
+ const privAddr = await addr(privateToken);
1414
+ await expectRevert(bridge.rescueERC20(privAddr, 1n, { gasLimit: 2000000 }), /CannotRescueBridgeToken|revert/i, " [Info] Correctly reverted: rescueERC20(privateToken)");
1415
+ });
1416
+
1417
+ it("Test 114: revert: rescueERC20 with amount=0 reverts AmountZero", async function () {
1418
+ const pubAddr = await addr(publicToken);
1419
+ await expectRevert(bridge.rescueERC20(pubAddr, 0n, { gasLimit: 2000000 }), /AmountZero|revert/i, " [Info] Correctly reverted: rescueERC20 amount=0");
1420
+ });
1421
+
1422
+ it("Test 115: ERC20 setNativeCotiFee works (not native bridge)", async function () {
1423
+ const tx = await bridge.setNativeCotiFee(ethers.parseEther("5"), { gasLimit: 2000000 });
1424
+ await logTx(tx, "setNativeCotiFee(5) on ERC20 bridge", "setNativeCotiFee", ["5"]);
1425
+ expect(await bridge.nativeCotiFee()).to.equal(ethers.parseEther("5"));
1426
+ await expect(tx).to.emit(bridge, "NativeCotiFeeUpdated");
1427
+
1428
+ // Reset
1429
+ const resetTx = await bridge.setNativeCotiFee(0n, { gasLimit: 2000000 });
1430
+ await logTx(resetTx, "Reset nativeCotiFee to 0", "setNativeCotiFee", ["0"]);
1431
+ });
1432
+
1433
+ it("Test 116: ERC20 withdrawCotiFees happy path", async function () {
1434
+ // First deposit to accumulate some COTI fees
1435
+ const amount = 10n * UNIT;
1436
+ const bridgeAddr = await addr(bridge);
1437
+ await logTx(await publicToken.approve(bridgeAddr, amount, { gasLimit: 2000000 }), "Approve for withdrawCotiFees test", "approve", [bridgeAddr, "10"]);
1438
+ const [, cotiLastUpdated, tokenLastUpdated] = await bridge.estimateDepositFee(amount);
1439
+ await logTx(await bridge["deposit(uint256,uint256,uint256)"](amount, cotiLastUpdated, tokenLastUpdated, { value: COTI_FEE_BUFFER, gasLimit: 12000000 }), "Deposit for COTI fee accumulation", "deposit", ["10"]);
1440
+
1441
+ const fees = await bridge.accumulatedCotiFees();
1442
+ console.log(` [Info] Accumulated COTI fees: ${ethers.formatEther(fees)}`);
1443
+ expect(fees).to.be.gt(0n);
1444
+
1445
+ const withdrawAmount = fees / 2n;
1446
+ const tx = await bridge.withdrawCotiFees(withdrawAmount, { gasLimit: 2000000 });
1447
+ await logTx(tx, `withdrawCotiFees(${ethers.formatEther(withdrawAmount)})`, "withdrawCotiFees", [ethers.formatEther(withdrawAmount)]);
1448
+ await expect(tx).to.emit(bridge, "CotiFeesWithdrawn");
1449
+ });
1450
+
1451
+ it("Test 117: revert: withdrawCotiFees(0) reverts AmountZero", async function () {
1452
+ await expectRevert(bridge.withdrawCotiFees(0n, { gasLimit: 2000000 }), /AmountZero|revert/i, " [Info] Correctly reverted: withdrawCotiFees(0)");
1453
+ });
1454
+
1455
+ it("Test 118: revert: withdrawCotiFees exceeding accumulated reverts", async function () {
1456
+ await expectRevert(bridge.withdrawCotiFees(ethers.parseEther("999999"), { gasLimit: 2000000 }), /InsufficientAccumulatedFees|revert/i, " [Info] Correctly reverted: withdrawCotiFees > accumulated");
1457
+ });
1458
+
1459
+ it("Test 119: ERC20 totalUserLiability tracks deposit and withdraw", async function () {
1460
+ const amount = 5n * UNIT;
1461
+ const bridgeAddr = await addr(bridge);
1462
+
1463
+ // Deposit
1464
+ await logTx(await publicToken.approve(bridgeAddr, amount, { gasLimit: 2000000 }), "Approve for liability test", "approve", [bridgeAddr, "5"]);
1465
+ const liabilityBefore = await bridge.totalUserLiability();
1466
+ const [, cotiLastUpdated, tokenLastUpdated] = await bridge.estimateDepositFee(amount);
1467
+ await logTx(await bridge["deposit(uint256,uint256,uint256)"](amount, cotiLastUpdated, tokenLastUpdated, { value: COTI_FEE_BUFFER, gasLimit: 12000000 }), "Deposit for ERC20 liability test", "deposit", ["5"]);
1468
+ const liabilityAfterDeposit = await bridge.totalUserLiability();
1469
+ expect(liabilityAfterDeposit).to.be.gt(liabilityBefore);
1470
+ console.log(` [Info] Liability after deposit: ${ethers.formatEther(liabilityAfterDeposit)}`);
1471
+
1472
+ // Withdraw
1473
+ await logTx(await privateToken["approve(address,uint256)"](bridgeAddr, amount, { gasLimit: 2000000 }), "Approve private for liability withdraw", "approve", [bridgeAddr, "5"]);
1474
+ const [, cotiLastUpdated2, tokenLastUpdated2] = await bridge.estimateWithdrawFee(amount);
1475
+ await logTx(await bridge["withdraw(uint256,uint256,uint256)"](amount, cotiLastUpdated2, tokenLastUpdated2, { value: COTI_FEE_BUFFER, gasLimit: 12000000 }), "Withdraw for ERC20 liability test", "withdraw", ["5"]);
1476
+ const liabilityAfterWithdraw = await bridge.totalUserLiability();
1477
+ expect(liabilityAfterWithdraw).to.be.lt(liabilityAfterDeposit);
1478
+ console.log(` [Info] Liability after withdraw: ${ethers.formatEther(liabilityAfterWithdraw)}`);
1479
+ });
1480
+
1481
+ it("Test 120: revert: addOperator with zero address reverts InvalidAddress", async function () {
1482
+ await expectRevert(bridge.addOperator(ethers.ZeroAddress, { gasLimit: 2000000 }), /InvalidAddress|revert/i, " [Info] Correctly reverted: addOperator(address(0))");
1483
+ });
1484
+
1485
+ it("Test 121: revert: removeOperator with zero address reverts InvalidAddress", async function () {
1486
+ await expectRevert(bridge.removeOperator(ethers.ZeroAddress, { gasLimit: 2000000 }), /InvalidAddress|revert/i, " [Info] Correctly reverted: removeOperator(address(0))");
1487
+ });
1488
+
1489
+ it("Test 122: revert: removeFromBlacklist with zero address reverts InvalidAddress", async function () {
1490
+ await expectRevert(bridge.removeFromBlacklist(ethers.ZeroAddress, { gasLimit: 2000000 }), /InvalidAddress|revert/i, " [Info] Correctly reverted: removeFromBlacklist(address(0))");
1491
+ });
1492
+
1493
+ it("Test 123: revert: setWithdrawDynamicFee with fixedFee > maxFee reverts", async function () {
1494
+ await expectRevert(bridge.setWithdrawDynamicFee(ethers.parseEther("5000"), 250n, ethers.parseEther("1500"), { gasLimit: 2000000 }), /InvalidFeeConfiguration|revert/i, " [Info] Correctly reverted: setWithdrawDynamicFee fixedFee > maxFee");
1495
+ });
1496
+
1497
+ it("Test 124: revert: setWithdrawDynamicFee with maxFee=0 reverts", async function () {
1498
+ await expectRevert(bridge.setWithdrawDynamicFee(0n, 250n, 0n, { gasLimit: 2000000 }), /InvalidFeeConfiguration|revert/i, " [Info] Correctly reverted: setWithdrawDynamicFee maxFee=0");
1499
+ });
1500
+
1501
+ it("Test 125: revert: setWithdrawDynamicFee with pctBps > MAX_FEE_UNITS reverts", async function () {
1502
+ await expectRevert(bridge.setWithdrawDynamicFee(ethers.parseEther("3"), 200000n, ethers.parseEther("1500"), { gasLimit: 2000000 }), /InvalidFee|revert/i, " [Info] Correctly reverted: setWithdrawDynamicFee pctBps > MAX");
1503
+ });
1504
+ });
1505
+
1506
+ // ─────────────────────────────────────────────────────────────────────────
1507
+ // FULL COVERAGE — CotiPriceConsumer
1508
+ // ─────────────────────────────────────────────────────────────────────────
1509
+
1510
+ describe("Full Coverage - CotiPriceConsumer", function () {
1511
+ let oracle, mockRef;
1512
+
1513
+ before(async function () {
1514
+ if (skipIfOnlyPrivateERC20.call(this)) return;
1515
+
1516
+ // Deploy MockStdReference (Band Protocol mock)
1517
+ const MockRefFactory = await ethers.getContractFactory("MockStdReference");
1518
+ mockRef = await MockRefFactory.deploy({ gasLimit: 12000000 });
1519
+ await (mockRef.waitForDeployment ? mockRef.waitForDeployment() : mockRef.deployed());
1520
+
1521
+ // Set COTI price and ETH price
1522
+ await mockRef.setRate("COTI", ethers.parseEther("0.05"), { gasLimit: 2000000 });
1523
+ await mockRef.setRate("ETH", ethers.parseEther("2300"), { gasLimit: 2000000 });
1524
+ // Set lastUpdated to current block timestamp
1525
+ const currentBlock = await ethers.provider.getBlock("latest");
1526
+ await mockRef.setLastUpdatedBase("COTI", currentBlock.timestamp, { gasLimit: 2000000 });
1527
+ await mockRef.setLastUpdatedBase("ETH", currentBlock.timestamp, { gasLimit: 2000000 });
1528
+
1529
+ // Deploy real CotiPriceConsumer with MockStdReference
1530
+ const OracleFactory = await ethers.getContractFactory("CotiPriceConsumer");
1531
+ oracle = await OracleFactory.deploy(await addr(mockRef), 3600, { gasLimit: 12000000 });
1532
+ await (oracle.waitForDeployment ? oracle.waitForDeployment() : oracle.deployed());
1533
+
1534
+ await registerContract("CotiPriceConsumer", oracle, "Full Coverage - CotiPriceConsumer");
1535
+ await registerContract("MockStdReference", mockRef, "Full Coverage - CotiPriceConsumer");
1536
+ await new Promise(r => setTimeout(r, 5000));
1537
+ });
1538
+
1539
+ it("Test 126: oracle: owner is deployer", async function () {
1540
+ const oracleOwner = await oracle.owner();
1541
+ console.log(` [Info] Oracle owner: ${oracleOwner}`);
1542
+ expect(oracleOwner).to.equal(owner.address);
1543
+ });
1544
+
1545
+ it("Test 127: oracle: ref is immutable and set correctly", async function () {
1546
+ const refAddr = await oracle.ref();
1547
+ console.log(` [Info] Oracle ref: ${refAddr}`);
1548
+ expect(refAddr).to.equal(await addr(mockRef));
1549
+ });
1550
+
1551
+ it("Test 128: oracle: maxStaleness is set correctly", async function () {
1552
+ const staleness = await oracle.maxStaleness();
1553
+ console.log(` [Info] maxStaleness: ${staleness}`);
1554
+ expect(staleness).to.equal(3600n);
1555
+ });
1556
+
1557
+ it("Test 129: oracle: MIN_STALENESS constant is 3600", async function () {
1558
+ const minStaleness = await oracle.MIN_STALENESS();
1559
+ console.log(` [Info] MIN_STALENESS: ${minStaleness}`);
1560
+ expect(minStaleness).to.equal(3600n);
1561
+ });
1562
+
1563
+ it("Test 130: oracle: getPrice returns correct rate", async function () {
1564
+ const cotiPrice = await oracle.getPrice("COTI");
1565
+ console.log(` [Info] COTI price: ${ethers.formatEther(cotiPrice)}`);
1566
+ expect(cotiPrice).to.equal(ethers.parseEther("0.05"));
1567
+
1568
+ const ethPrice = await oracle.getPrice("ETH");
1569
+ console.log(` [Info] ETH price: ${ethers.formatEther(ethPrice)}`);
1570
+ expect(ethPrice).to.equal(ethers.parseEther("2300"));
1571
+ });
1572
+
1573
+ it("Test 131: oracle: getPriceWithMeta returns rate, lastUpdated, blockTimestamp", async function () {
1574
+ const [rate, lastUpdated, blockTimestamp] = await oracle.getPriceWithMeta("COTI");
1575
+ console.log(` [Info] getPriceWithMeta: rate=${ethers.formatEther(rate)}, lastUpdated=${lastUpdated}, blockTs=${blockTimestamp}`);
1576
+ expect(rate).to.equal(ethers.parseEther("0.05"));
1577
+ expect(lastUpdated).to.be.gt(0n);
1578
+ expect(blockTimestamp).to.be.gt(0n);
1579
+ });
1580
+
1581
+ it("Test 132: oracle: getPriceData returns full ReferenceData struct", async function () {
1582
+ const data = await oracle.getPriceData("COTI");
1583
+ console.log(` [Info] getPriceData: rate=${ethers.formatEther(data.rate)}, lastUpdatedBase=${data.lastUpdatedBase}`);
1584
+ expect(data.rate).to.equal(ethers.parseEther("0.05"));
1585
+ expect(data.lastUpdatedBase).to.be.gt(0n);
1586
+ expect(data.lastUpdatedQuote).to.be.gt(0n);
1587
+ });
1588
+
1589
+ it("Test 133: oracle: setMaxStaleness updates threshold and emits event", async function () {
1590
+ const tx = await oracle.setMaxStaleness(7200, { gasLimit: 2000000 });
1591
+ await logTx(tx, "setMaxStaleness(7200)", "setMaxStaleness", ["7200"]);
1592
+ await expect(tx).to.emit(oracle, "MaxStalenessUpdated");
1593
+ expect(await oracle.maxStaleness()).to.equal(7200n);
1594
+
1595
+ // Restore
1596
+ const restoreTx = await oracle.setMaxStaleness(3600, { gasLimit: 2000000 });
1597
+ await logTx(restoreTx, "Restore maxStaleness(3600)", "setMaxStaleness", ["3600"]);
1598
+ });
1599
+
1600
+ it("Test 134: oracle: setMaxStaleness reverts if below MIN_STALENESS", async function () {
1601
+ await expectRevert(oracle.setMaxStaleness(100, { gasLimit: 2000000 }), /StalenessTooLow|revert/i, " [Info] Correctly reverted: staleness < MIN_STALENESS");
1602
+ });
1603
+
1604
+ it("Test 135: oracle: getPrice reverts on stale data", async function () {
1605
+ // Set lastUpdated to a very old timestamp (2 hours ago)
1606
+ const currentBlock = await ethers.provider.getBlock("latest");
1607
+ const staleTimestamp = currentBlock.timestamp - 7200;
1608
+ await mockRef.setLastUpdatedBase("COTI", staleTimestamp, { gasLimit: 2000000 });
1609
+
1610
+ try {
1611
+ await oracle.getPrice("COTI");
1612
+ expect.fail("Expected revert");
1613
+ } catch (error) {
1614
+ expect(error.message).to.match(/StaleOracleData|revert/i);
1615
+ console.log(" [Info] Correctly reverted: StaleOracleData");
1616
+ }
1617
+
1618
+ // Restore fresh timestamp
1619
+ const freshBlock = await ethers.provider.getBlock("latest");
1620
+ await mockRef.setLastUpdatedBase("COTI", freshBlock.timestamp, { gasLimit: 2000000 });
1621
+ });
1622
+
1623
+ it("Test 136: oracle: constructor reverts with zero ref address", async function () {
1624
+ const OracleFactory = await ethers.getContractFactory("CotiPriceConsumer");
1625
+ try {
1626
+ const badOracle = await OracleFactory.deploy(ethers.ZeroAddress, 3600, { gasLimit: 12000000 });
1627
+ await (badOracle.waitForDeployment ? badOracle.waitForDeployment() : badOracle.deployed());
1628
+ expect.fail("Expected revert");
1629
+ } catch (error) {
1630
+ expect(error.message).to.match(/zero ref address|revert/i);
1631
+ console.log(" [Info] Correctly reverted: constructor with zero ref");
1632
+ }
1633
+ });
1634
+
1635
+ it("Test 137: oracle: constructor reverts with staleness below minimum", async function () {
1636
+ const OracleFactory = await ethers.getContractFactory("CotiPriceConsumer");
1637
+ try {
1638
+ const badOracle = await OracleFactory.deploy(await addr(mockRef), 100, { gasLimit: 12000000 });
1639
+ await (badOracle.waitForDeployment ? badOracle.waitForDeployment() : badOracle.deployed());
1640
+ expect.fail("Expected revert");
1641
+ } catch (error) {
1642
+ expect(error.message).to.match(/StalenessTooLow|revert/i);
1643
+ console.log(" [Info] Correctly reverted: constructor with staleness < MIN_STALENESS");
1644
+ }
1645
+ });
1646
+
1647
+ it("Test 138: oracle: setMaxStaleness reverts for non-owner", async function () {
1648
+ // user1 is not the owner
1649
+ if (user1.address === owner.address) {
1650
+ console.log(" [Info] Skipping: user1 === owner on single-signer testnet");
1651
+ this.skip();
1652
+ return;
1653
+ }
1654
+ await expectRevert(oracle.connect(user1).setMaxStaleness(7200, { gasLimit: 2000000 }), /caller is not the owner|revert/i, " [Info] Correctly reverted: non-owner setMaxStaleness");
1655
+ });
1656
+ });
1657
+
1658
+ after(function () {
1659
+ console.log("\n===========================================================");
1660
+ console.log(`TOTAL TESTS RUN: ${testCounter}`);
1661
+ console.log("===========================================================\n");
1662
+ });
1663
+ });